Skip to content

Commit e94eb18

Browse files
committed
no-mistakes(review): Apply wire setting to M:N targets and FromDirectory
1 parent 6d38625 commit e94eb18

15 files changed

Lines changed: 192 additions & 16 deletions

File tree

‎agent-context/skills/metaobjects-codegen/references/wire-format.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ The generated input converters stay in every mode.
7878

7979
- From 1.0.x, upgrade to 1.1.2 or later. With no setting, C#, Java and Kotlin keep their 1.0.x
8080
output. In Python and TypeScript, a decimal keeps its scale (`"12.50"`).
81+
- In Python this is a change: a 1.0.x router wrote a decimal as a JSON number, a float that can
82+
lose digits (`12.5`). It now writes a string, Pydantic's `str(Decimal)`, which is the
83+
Python-normal form. There is no float option, by decision. Update a client that read the
84+
number to read the string (`"decimals": "string"` gives the canonical `"12.5"`).
8185
- A project already on 1.1.0 or 1.1.1 whose clients now expect strings and omitted members
8286
declares `"wire": { "decimals": "string", "nullMembers": "omit" }` before it moves to 1.1.2.
8387
- Every port's codegen drift gate (`meta verify --codegen`, `dotnet meta verify --codegen`,

‎fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/wire-format.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ The generated input converters stay in every mode.
7878

7979
- From 1.0.x, upgrade to 1.1.2 or later. With no setting, C#, Java and Kotlin keep their 1.0.x
8080
output. In Python and TypeScript, a decimal keeps its scale (`"12.50"`).
81+
- In Python this is a change: a 1.0.x router wrote a decimal as a JSON number, a float that can
82+
lose digits (`12.5`). It now writes a string, Pydantic's `str(Decimal)`, which is the
83+
Python-normal form. There is no float option, by decision. Update a client that read the
84+
number to read the string (`"decimals": "string"` gives the canonical `"12.5"`).
8185
- A project already on 1.1.0 or 1.1.1 whose clients now expect strings and omitted members
8286
declares `"wire": { "decimals": "string", "nullMembers": "omit" }` before it moves to 1.1.2.
8387
- Every port's codegen drift gate (`meta verify --codegen`, `dotnet meta verify --codegen`,

‎fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/wire-format.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ The generated input converters stay in every mode.
7878

7979
- From 1.0.x, upgrade to 1.1.2 or later. With no setting, C#, Java and Kotlin keep their 1.0.x
8080
output. In Python and TypeScript, a decimal keeps its scale (`"12.50"`).
81+
- In Python this is a change: a 1.0.x router wrote a decimal as a JSON number, a float that can
82+
lose digits (`12.5`). It now writes a string, Pydantic's `str(Decimal)`, which is the
83+
Python-normal form. There is no float option, by decision. Update a client that read the
84+
number to read the string (`"decimals": "string"` gives the canonical `"12.5"`).
8185
- A project already on 1.1.0 or 1.1.1 whose clients now expect strings and omitted members
8286
declares `"wire": { "decimals": "string", "nullMembers": "omit" }` before it moves to 1.1.2.
8387
- Every port's codegen drift gate (`meta verify --codegen`, `dotnet meta verify --codegen`,

‎fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/wire-format.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ The generated input converters stay in every mode.
7878

7979
- From 1.0.x, upgrade to 1.1.2 or later. With no setting, C#, Java and Kotlin keep their 1.0.x
8080
output. In Python and TypeScript, a decimal keeps its scale (`"12.50"`).
81+
- In Python this is a change: a 1.0.x router wrote a decimal as a JSON number, a float that can
82+
lose digits (`12.5`). It now writes a string, Pydantic's `str(Decimal)`, which is the
83+
Python-normal form. There is no float option, by decision. Update a client that read the
84+
number to read the string (`"decimals": "string"` gives the canonical `"12.5"`).
8185
- A project already on 1.1.0 or 1.1.1 whose clients now expect strings and omitted members
8286
declares `"wire": { "decimals": "string", "nullMembers": "omit" }` before it moves to 1.1.2.
8387
- Every port's codegen drift gate (`meta verify --codegen`, `dotnet meta verify --codegen`,

‎fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/wire-format.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ The generated input converters stay in every mode.
7878

7979
- From 1.0.x, upgrade to 1.1.2 or later. With no setting, C#, Java and Kotlin keep their 1.0.x
8080
output. In Python and TypeScript, a decimal keeps its scale (`"12.50"`).
81+
- In Python this is a change: a 1.0.x router wrote a decimal as a JSON number, a float that can
82+
lose digits (`12.5`). It now writes a string, Pydantic's `str(Decimal)`, which is the
83+
Python-normal form. There is no float option, by decision. Update a client that read the
84+
number to read the string (`"decimals": "string"` gives the canonical `"12.5"`).
8185
- A project already on 1.1.0 or 1.1.1 whose clients now expect strings and omitted members
8286
declares `"wire": { "decimals": "string", "nullMembers": "omit" }` before it moves to 1.1.2.
8387
- Every port's codegen drift gate (`meta verify --codegen`, `dotnet meta verify --codegen`,

‎fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/wire-format.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ The generated input converters stay in every mode.
7878

7979
- From 1.0.x, upgrade to 1.1.2 or later. With no setting, C#, Java and Kotlin keep their 1.0.x
8080
output. In Python and TypeScript, a decimal keeps its scale (`"12.50"`).
81+
- In Python this is a change: a 1.0.x router wrote a decimal as a JSON number, a float that can
82+
lose digits (`12.5`). It now writes a string, Pydantic's `str(Decimal)`, which is the
83+
Python-normal form. There is no float option, by decision. Update a client that read the
84+
number to read the string (`"decimals": "string"` gives the canonical `"12.5"`).
8185
- A project already on 1.1.0 or 1.1.1 whose clients now expect strings and omitted members
8286
declares `"wire": { "decimals": "string", "nullMembers": "omit" }` before it moves to 1.1.2.
8387
- Every port's codegen drift gate (`meta verify --codegen`, `dotnet meta verify --codegen`,

‎server/csharp/MetaObjects.Conformance.Tests/MetadataLocationTests.cs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,24 @@ public void FromDirectory_with_a_registry_applies_the_projects_naming()
120120
Assert.Equal(new PhysicalNaming(ColumnNamingStrategy.Literal, TableNamingRule.Literal), result.Root.PhysicalNaming);
121121
}
122122

123+
[Fact]
124+
public void FromDirectory_with_a_registry_applies_the_projects_wire()
125+
{
126+
WriteConfig("""{ "schema_version": 1, "wire": { "decimals": "string", "nullMembers": "omit" } }""");
127+
128+
var result = MetaObjects.Loader.MetaDataLoader.FromDirectory(
129+
MetaDir, MetaObjects.Provider.ComposeRegistry(MetaObjects.CoreTypes.LibraryProviders), libraries: ["iam"], strict: true);
130+
131+
Assert.Equal(new WireFormat(DecimalWireForm.String, NullMemberForm.Omit), result.Root.WireFormat);
132+
}
133+
123134
[Fact]
124135
public void FromDirectory_without_a_project_config_keeps_the_default()
125136
{
126137
var result = MetaObjects.Loader.MetaDataLoader.FromDirectory(
127138
MetaDir, MetaObjects.Provider.ComposeRegistry(MetaObjects.CoreTypes.LibraryProviders), libraries: ["iam"]);
128139

129140
Assert.Equal(PhysicalNaming.Default, result.Root.PhysicalNaming);
141+
Assert.Equal(WireFormat.Default, result.Root.WireFormat);
130142
}
131143
}

‎server/csharp/MetaObjects/Loader/MetaDataLoader.cs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,11 @@ public static LoadResult FromDirectory(string directory, TypeRegistry registry,
155155
// ADR-0058: the project's `naming` applies to a directory load too, as the JVM
156156
// fromDirectory does. Without this, a host that composes its own registry (the
157157
// documented way to add a provider) silently got snake_case whatever the project says.
158-
if (Config.MetadataLocation.ConfigFor(directory) is { } cfg) result.Root.PhysicalNaming = cfg.Naming;
158+
if (Config.MetadataLocation.ConfigFor(directory) is { } cfg)
159+
{
160+
result.Root.PhysicalNaming = cfg.Naming;
161+
result.Root.WireFormat = cfg.Wire;
162+
}
159163
return result;
160164
}
161165

‎server/python/src/metaobjects/codegen/generators/router_generator.py‎

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,8 @@ def _field_shape(f: MetaField, seen: frozenset[str]) -> dict[str, Any] | None:
285285
return {"m": shape} if f.sub_type == fc.FIELD_SUBTYPE_MAP else shape
286286

287287

288-
def _wire_lines(entities: list[MetaObject], wire: WireFormat) -> list[str]:
289-
"""The ``_WIRE`` constant every response is written through: the project's ``wire``
288+
def _wire_lines(entities: list[MetaObject], wire: WireFormat, name: str = "_WIRE") -> list[str]:
289+
"""The *name* constant a response of *entities* is written through: the project's ``wire``
290290
setting (ADR-0019 Amendment 2) and, when ``nullMembers`` is not ``native``, the declared
291291
members of each value-object field of *entities* (the first declaration of a name wins)."""
292292
shapes: dict[str, Any] = {}
@@ -298,7 +298,26 @@ def _wire_lines(entities: list[MetaObject], wire: WireFormat) -> list[str]:
298298
args = f'decimals="{wire.decimals}", null_members="{wire.null_members}"'
299299
if shapes:
300300
args += f", shapes={shapes!r}"
301-
return ["", "", f"_WIRE = WireOutput({args})"]
301+
return ["", "", f"{name} = WireOutput({args})"]
302+
303+
304+
def _m2m_wire_name(target: str) -> str:
305+
"""The wire constant an M:N traversal onto *target* writes the target's rows through."""
306+
return f"_WIRE_{_snake_case(target).upper()}"
307+
308+
309+
def _m2m_wire_lines(
310+
descriptors: list[M2mDescriptor], object_index: dict[str, MetaObject], wire: WireFormat
311+
) -> list[str]:
312+
"""One :func:`_m2m_wire_name` constant per distinct M:N target, shaped by the TARGET's
313+
value-object fields (and its TPH subtypes' when the target is a discriminator base)."""
314+
lines: list[str] = []
315+
for name in dict.fromkeys(d.target_entity for d in descriptors):
316+
target = next(o for o in object_index.values() if o.name == name)
317+
plan = tph_plan_for(target, object_index)
318+
entities = [target, *(st.entity for st in plan.subtypes)] if plan is not None else [target]
319+
lines.extend(_wire_lines(entities, wire, _m2m_wire_name(name)))
320+
return lines
302321

303322

304323
def _decimal_limits_lines(entities: list[MetaObject]) -> list[str]:
@@ -877,7 +896,8 @@ def _emit_m2m_route(
877896
f" repo: Annotated[{repo_class}, Depends(get_repository)],",
878897
") -> list[Any]:",
879898
f" return render_wire(repo.find_related_{d.relation_name}"
880-
f"({pk_param}{_target_subtype_arg(d, {d.target_discriminator})}), _WIRE)",
899+
f"({pk_param}{_target_subtype_arg(d, {d.target_discriminator})}), "
900+
f"{_m2m_wire_name(d.target_entity)})",
881901
]
882902

883903
def _emit_tph_m2m_route(
@@ -934,7 +954,8 @@ def _emit_tph_m2m_route(
934954
]
935955
lines.append(
936956
f" return render_wire(repo.find_related_{d.relation_name}"
937-
f"({subtype_expr}, {pk_param}{_target_subtype_arg(d, target_discs)}), _WIRE)"
957+
f"({subtype_expr}, {pk_param}{_target_subtype_arg(d, target_discs)}), "
958+
f"{_m2m_wire_name(d.target_entity)})"
938959
)
939960
return lines
940961

@@ -1159,6 +1180,11 @@ def _render_tph_router(
11591180
parts.append(f"_DECIMAL_FIELDS: frozenset[str] = {decimal_set_body}")
11601181
parts.extend(_decimal_limits_lines([entity, *(st.entity for st in plan.subtypes)]))
11611182
parts.extend(_wire_lines([entity, *(st.entity for st in plan.subtypes)], wire))
1183+
parts.extend(_m2m_wire_lines(
1184+
[*base_m2m, *(d for st in plan.subtypes for d in sub_m2m[st.entity.name])],
1185+
object_index,
1186+
wire,
1187+
))
11621188
parts.append("")
11631189
parts.append("")
11641190
parts.extend(_JSON_BODY_LINES)
@@ -1536,6 +1562,7 @@ def render_router(
15361562
parts.append(f"_DECIMAL_FIELDS: frozenset[str] = {decimal_set_body}")
15371563
parts.extend(_decimal_limits_lines([entity]))
15381564
parts.extend(_wire_lines([entity], wire))
1565+
parts.extend(_m2m_wire_lines(m2m, object_index or {}, wire))
15391566
parts.append("")
15401567
parts.append("")
15411568
parts.extend(_JSON_BODY_LINES)

‎server/python/src/metaobjects/codegen/runtime/decimal_wire.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,8 +291,8 @@ class WireOutput:
291291

292292

293293
def render_wire(value: Any, wire: WireOutput) -> Any:
294-
"""A response *value* (a row, a list of rows, the ``{"rows", "total"}`` envelope, a
295-
Pydantic model) in the form *wire* asks for. Input rules do not depend on *wire*."""
294+
"""A response *value* (a row, a list of rows, a Pydantic model) in the form *wire* asks
295+
for. Input rules do not depend on *wire*."""
296296
if wire.null_members != "native" and wire.shapes:
297297
value = _shape_members(value, wire)
298298
return to_wire(value) if wire.decimals == "string" else to_native(value)
@@ -310,7 +310,7 @@ def _shape_members(value: Any, wire: WireOutput) -> Any:
310310
return value
311311
out: dict[str, Any] = {}
312312
for k, v in value.items():
313-
out[k] = _shape_vo(v, wire.shapes[k], wire.null_members) if k in wire.shapes else _shape_members(v, wire)
313+
out[k] = _shape_vo(v, wire.shapes[k], wire.null_members) if k in wire.shapes else v
314314
return out
315315

316316

0 commit comments

Comments
 (0)