Skip to content

Commit 65b6a67

Browse files
authored
Merge pull request #21 from beheshty/feature/comprehensive-list-type
Feature/comprehensive list type
2 parents 8a4d647 + ace08cb commit 65b6a67

4 files changed

Lines changed: 94 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.1.0] - 2025-08-10
9+
### Changed
10+
- Enhanced the type inference logic for JSON arrays. The source generator now analyzes **all** objects within an array to create a single, comprehensive class for the list items. This replaces the previous behavior of only inspecting the first item, ensuring that properties from all objects are correctly captured.
11+
12+
## [2.0.1] - 2025-08-04
13+
### Added
14+
- Added `Shipped.md` and `Unshipped.md` files to track analyzer diagnostics and releases. These files follow the official Roslyn analyzer release tracking format.
15+
16+
### Notes
17+
- This is a documentation-only update with no functional code changes.
18+
819
## [2.0.1] - 2025-08-04
920
### Added
1021
- Added `Shipped.md` and `Unshipped.md` files to track analyzer diagnostics and releases.

src/SetSharp/ModelBuilder/ConfigurationModelBuilder.cs

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,29 +57,49 @@ string HandleNestedObject(Dictionary<string, object> obj)
5757

5858
private string InferListType(string parentSectionPath, string key, List<object> list)
5959
{
60-
if (list.Count == 0) return "ImmutableList<object>";
60+
if (list.Count == 0)
61+
{
62+
return "ImmutableList<object>";
63+
}
6164

62-
string HandleListItemObject(Dictionary<string, object> obj)
65+
var objectItems = list.OfType<Dictionary<string, object>>().ToList();
66+
67+
// If the list contains no objects (e.g., a list of strings or ints),
68+
// infer the type from the first element as a fallback.
69+
if (objectItems.Count == 0)
6370
{
64-
var sectionPath = string.IsNullOrEmpty(parentSectionPath) ? key : $"{parentSectionPath}:{key}";
65-
var classNameKey = $"{key}Item";
66-
return CreateNestedClass(sectionPath, classNameKey, obj, isFromCollection: true);
71+
return InferSimpleListType(list[0]);
6772
}
6873

69-
// Infer list type from the first item
70-
var firstItem = list[0];
71-
string listTypeName = firstItem switch
74+
var mergedObject = new Dictionary<string, object>();
75+
foreach (var item in objectItems)
76+
{
77+
foreach (var prop in item)
78+
{
79+
mergedObject[prop.Key] = prop.Value;
80+
}
81+
}
82+
83+
var sectionPath = string.IsNullOrEmpty(parentSectionPath) ? key : $"{parentSectionPath}:{key}";
84+
var classNameKey = $"{key}Item";
85+
var listTypeName = CreateNestedClass(sectionPath, classNameKey, mergedObject, isFromCollection: true);
86+
87+
return $"ImmutableList<{listTypeName}>";
88+
}
89+
90+
// Helper for simple list types (string, int, etc.)
91+
private string InferSimpleListType(object item)
92+
{
93+
string typeName = item switch
7294
{
7395
string => "string",
7496
int => "int",
7597
long => "long",
7698
double => "double",
7799
bool => "bool",
78-
Dictionary<string, object> obj => HandleListItemObject(obj),
79100
_ => "object"
80101
};
81-
82-
return $"ImmutableList<{listTypeName}>";
102+
return $"ImmutableList<{typeName}>";
83103
}
84104

85105
private string CreateNestedClass(string sectionPath, string classNameKey, Dictionary<string, object> obj, bool isFromCollection = false)

src/SetSharp/SetSharp.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
1111

1212
<PackageId>SetSharp</PackageId>
13-
<Version>2.0.1</Version>
13+
<Version>2.1.0</Version>
1414
<PackageIcon>icon.png</PackageIcon>
1515
<Authors>Amirhossein Beheshti</Authors>
1616
<Description>Generates strongly typed settings classes from appsettings.json using Source Generators.</Description>

tests/SetSharp.Tests/ModelBuilder/ConfigurationModelBuilderTests.cs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public void BuildFrom_WithFlatPrimitives_ShouldCreateModelCorrectly()
1414
{
1515
{ "ConnectionString", "Server=.;Database=Test;"},
1616
{ "Timeout", 30 },
17-
{ "MaxRetries", 9223372036854775807L },
17+
{ "MaxRetries", 9223372036854775807L },
1818
{ "EnableLogging", true },
1919
{ "DefaultThreshold", 0.95 }
2020
};
@@ -240,5 +240,55 @@ public void BuildFrom_WithEmptyDictionary_ShouldReturnRootWithNoProperties()
240240
Assert.Equal("RootOptions", rootModel.ClassName);
241241
Assert.Empty(rootModel.Properties);
242242
}
243+
244+
[Fact]
245+
public void BuildFrom_WithListOfObjectsHavingDifferentProperties_ShouldCreateComprehensiveClass()
246+
{
247+
// Arrange
248+
var builder = new ConfigurationModelBuilder();
249+
var root = new Dictionary<string, object>
250+
{
251+
{ "Products", new List<object> {
252+
// First item only has Name and Id
253+
new Dictionary<string, object> {
254+
{ "Id", 1 },
255+
{ "Name", "Gadget" }
256+
},
257+
// Second item adds a 'Price' property
258+
new Dictionary<string, object> {
259+
{ "Id", 2 },
260+
{ "Name", "Widget" },
261+
{ "Price", 99.99 }
262+
},
263+
// Third item adds an 'InStock' property
264+
new Dictionary<string, object> {
265+
{ "Id", 3 },
266+
{ "Name", "Doodad" },
267+
{ "InStock", true }
268+
}
269+
}}
270+
};
271+
272+
// Act
273+
var result = builder.BuildFrom(root);
274+
275+
// Assert
276+
Assert.Equal(2, result.Count);
277+
278+
var rootModel = result.First(c => c.ClassName == "RootOptions");
279+
var listProperty = Assert.Single(rootModel.Properties);
280+
Assert.Equal("Products", listProperty.PropertyName);
281+
Assert.Equal("ImmutableList<ProductsItemOptions>", listProperty.PropertyType);
282+
283+
var productItemModel = result.First(c => c.ClassName == "ProductsItemOptions");
284+
Assert.Equal("Products", productItemModel.SectionPath);
285+
286+
Assert.Equal(4, productItemModel.Properties.Count);
287+
Assert.Contains(productItemModel.Properties, p => p.PropertyName == "Id" && p.PropertyType == "int");
288+
Assert.Contains(productItemModel.Properties, p => p.PropertyName == "Name" && p.PropertyType == "string");
289+
Assert.Contains(productItemModel.Properties, p => p.PropertyName == "Price" && p.PropertyType == "double");
290+
Assert.Contains(productItemModel.Properties, p => p.PropertyName == "InStock" && p.PropertyType == "bool");
291+
}
243292
}
293+
244294
}

0 commit comments

Comments
 (0)