Skip to content

Commit 47b4cf5

Browse files
dmealingclaude
andcommitted
test(codegen-csharp): cover long/double/bool scalar arrays + enum strict-map + Extract(opts) overload
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 72c497c commit 47b4cf5

1 file changed

Lines changed: 91 additions & 1 deletion

File tree

server/csharp/MetaObjects.Codegen.Tests/ExtractorCodegenTests.cs

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ public sealed class ExtractorCodegenTests
3737
// • lines : REQUIRED array-of-objects Line{ sku (required), qty:int }
3838
// • tags : REQUIRED scalar-array string[]
3939
// • scores : REQUIRED scalar-array int[] (kind-typed mirror element regression guard)
40+
// • quantities: REQUIRED scalar-array long[] (StrictArg long .Value unwrap)
41+
// • weights : REQUIRED scalar-array double[] (StrictArg double .Value unwrap)
42+
// • flagsArr: REQUIRED scalar-array bool[] (StrictArg bool .Value unwrap)
43+
// • priority: REQUIRED enum (strict-map `m.priority!` run-proof)
4044
// • note : scalar (not @required)
4145
// • shipTo : single nested Customer (not @required)
4246
private const string Model = """
@@ -54,6 +58,10 @@ public sealed class ExtractorCodegenTests
5458
{ "field.object": { "name": "lines", "isArray": true, "@required": true, "@objectRef": "Line" } },
5559
{ "field.string": { "name": "tags", "isArray": true, "@required": true } },
5660
{ "field.int": { "name": "scores", "isArray": true, "@required": true } },
61+
{ "field.long": { "name": "quantities", "isArray": true, "@required": true } },
62+
{ "field.double": { "name": "weights", "isArray": true, "@required": true } },
63+
{ "field.boolean":{ "name": "flagsArr", "isArray": true, "@required": true } },
64+
{ "field.enum": { "name": "priority", "@required": true, "@values": ["LOW", "HIGH"] } },
5765
{ "field.string": { "name": "note" } },
5866
{ "field.object": { "name": "shipTo", "@objectRef": "Customer" } }
5967
]}},
@@ -134,6 +142,10 @@ public void Generated_extract_populates_strict_graph_from_dirty_text()
134142
" \"lines\": [ { \"sku\": \"A\", \"qty\": 2 }, { \"sku\": \"B\", \"qty\": 5 } ]," +
135143
" \"tags\": [\"x\", \"y\"]," +
136144
" \"scores\": [3, 7]," +
145+
" \"quantities\": [10, 9000000000]," +
146+
" \"weights\": [1.5, 2.25]," +
147+
" \"flagsArr\": [true, false, true]," +
148+
" \"priority\": \"HIGH\"," +
137149
" \"shipTo\": { \"name\": \"Grace\" }, }\n```";
138150

139151
var order = extract.Invoke(null, new object?[] { orderMo, dirty })!;
@@ -162,11 +174,88 @@ public void Generated_extract_populates_strict_graph_from_dirty_text()
162174
var scores = ((IEnumerable)scoresProp.GetValue(order)!).Cast<object>().ToList();
163175
Assert.Equal(new object[] { 3, 7 }, scores.ToArray());
164176

177+
// required NON-STRING scalar-array (long[]) — proves the StrictArg `x!.Value` long unwrap
178+
// narrows the kind-typed mirror (IReadOnlyList<long?>?) to the strict long element. The
179+
// CS8619-as-error compile gate would have failed had the element type mismatched.
180+
var quantitiesProp = order.GetType().GetProperty("quantities")!;
181+
Assert.Equal(typeof(IReadOnlyList<long>), quantitiesProp.PropertyType);
182+
var quantities = ((IEnumerable)quantitiesProp.GetValue(order)!).Cast<object>().ToList();
183+
Assert.Equal(new object[] { 10L, 9000000000L }, quantities.ToArray());
184+
Assert.IsType<long>(quantities[0]);
185+
186+
// required NON-STRING scalar-array (double[]) — proves the double `x!.Value` unwrap.
187+
var weightsProp = order.GetType().GetProperty("weights")!;
188+
Assert.Equal(typeof(IReadOnlyList<double>), weightsProp.PropertyType);
189+
var weights = ((IEnumerable)weightsProp.GetValue(order)!).Cast<object>().ToList();
190+
Assert.Equal(new object[] { 1.5d, 2.25d }, weights.ToArray());
191+
Assert.IsType<double>(weights[0]);
192+
193+
// required NON-STRING scalar-array (bool[]) — proves the bool `x!.Value` unwrap.
194+
var flagsProp = order.GetType().GetProperty("flagsArr")!;
195+
Assert.Equal(typeof(IReadOnlyList<bool>), flagsProp.PropertyType);
196+
var flags = ((IEnumerable)flagsProp.GetValue(order)!).Cast<object>().ToList();
197+
Assert.Equal(new object[] { true, false, true }, flags.ToArray());
198+
Assert.IsType<bool>(flags[0]);
199+
200+
// required ENUM through the strict tier — the StrictArg enum branch (`m.priority!`,
201+
// string-backed) populates the strict record from dirty input. NOTE the strict property
202+
// type is `object` (not `string`): PayloadCodegen's scalar map has no enum entry, so an enum
203+
// field falls through to the `object` default — but the value is the string-backed member.
204+
var priorityProp = order.GetType().GetProperty("priority")!;
205+
Assert.Equal(typeof(object), priorityProp.PropertyType);
206+
Assert.Equal("HIGH", priorityProp.GetValue(order));
207+
165208
// non-@required single nested, present -> populates.
166209
var shipTo = order.GetType().GetProperty("shipTo")!.GetValue(order)!;
167210
Assert.Equal("Grace", shipTo.GetType().GetProperty("name")!.GetValue(shipTo));
168211
}
169212

213+
[Fact]
214+
public void Generated_extract_opts_overload_populates_same_strict_graph()
215+
{
216+
var root = Load(Model);
217+
var asm = Compile(root);
218+
219+
var extractorType = asm.GetType("Acme.Generated.OrderExtractor")!;
220+
var optsType = typeof(MetaObjects.Render.Extract.ExtractOptions);
221+
222+
// The 3-arg overload: Extract(MetaObject, string, ExtractOptions).
223+
var extractWithOpts = extractorType.GetMethod("Extract",
224+
new[] { typeof(MetaObject), typeof(string), optsType })!;
225+
Assert.NotNull(extractWithOpts);
226+
227+
MetaObject orderMo = root.FindObject("Order")!;
228+
229+
// NOTE: shipTo is included because PayloadCodegen types EVERY field (even non-@required ones)
230+
// as `required` non-nullable, so the strict mapper maps shipTo as required — omitting it would
231+
// pass null to ToStrict_Customer (the documented C#-specific optionality, see the file header).
232+
const string dirty =
233+
"Here:\n```json\n" +
234+
"{ \"orderId\": \"A-200\"," +
235+
" \"customer\": { \"name\": \"Ada\" }," +
236+
" \"lines\": [ { \"sku\": \"A\", \"qty\": 3 } ]," +
237+
" \"tags\": [\"x\"]," +
238+
" \"scores\": [9]," +
239+
" \"quantities\": [42]," +
240+
" \"weights\": [3.5]," +
241+
" \"flagsArr\": [false]," +
242+
" \"priority\": \"HIGH\"," +
243+
" \"shipTo\": { \"name\": \"Grace\" } }\n```";
244+
245+
var opts = optsType.GetMethod("Defaults")!.Invoke(null, null);
246+
var order = extractWithOpts.Invoke(null, new object?[] { orderMo, dirty, opts })!;
247+
248+
// Same populated strict graph as the 2-arg path, via the opts overload.
249+
Assert.Equal("A-200", order.GetType().GetProperty("orderId")!.GetValue(order));
250+
var customer = order.GetType().GetProperty("customer")!.GetValue(order)!;
251+
Assert.Equal("Ada", customer.GetType().GetProperty("name")!.GetValue(customer));
252+
253+
var quantities = ((IEnumerable)order.GetType().GetProperty("quantities")!.GetValue(order)!)
254+
.Cast<object>().ToList();
255+
Assert.Equal(new object[] { 42L }, quantities.ToArray());
256+
Assert.Equal("HIGH", order.GetType().GetProperty("priority")!.GetValue(order));
257+
}
258+
170259
[Fact]
171260
public void Generated_extract_throws_on_lost_required()
172261
{
@@ -195,7 +284,8 @@ public void Re_exposed_extract_never_throws_on_clean_input()
195284

196285
const string clean =
197286
"{ \"orderId\": \"A-7\", \"customer\": { \"name\": \"Ada\" }," +
198-
" \"lines\": [ { \"sku\": \"A\", \"qty\": 1 } ], \"tags\": [\"a\"], \"scores\": [1] }";
287+
" \"lines\": [ { \"sku\": \"A\", \"qty\": 1 } ], \"tags\": [\"a\"], \"scores\": [1]," +
288+
" \"quantities\": [1], \"weights\": [1.0], \"flagsArr\": [true], \"priority\": \"LOW\" }";
199289

200290
var result = extractLenient.Invoke(null, new object?[] { orderMo, clean })!;
201291
var report = result.GetType().GetProperty("Report")!.GetValue(result)!;

0 commit comments

Comments
 (0)