Skip to content

Commit c5b767e

Browse files
authored
Merge pull request #386 from metaobjectsdev/fix/unify-route-collection-spelling
One collection-URL rule in every port: the entity name snake_cased, then pluralized
2 parents f5e2bf5 + 0feaf9b commit c5b767e

49 files changed

Lines changed: 1132 additions & 332 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,39 @@ edit (two registered `description` strings) and was ruled a hold, as 1.0.4's was
1515

1616
### Changed
1717

18+
- **BREAKING (generated routes): the REST collection URL is now the entity name
19+
`snake_case`d then pluralized, in every port.** One rule replaces five. The ports
20+
agreed only on single regular words — `Author` → `authors` under every old rule, and
21+
every collection base in every corpus was such a word, so all five lanes were green
22+
while `OrderSummary` was served at four different URLs: `/order_summaries` (TS
23+
entity), `/order-summaries` (TS projection), `/ordersummaries` (C#) and
24+
`/ordersummarys` (Java, Kotlin, Python). Kotlin's own comment asserted all four ports
25+
shared one trivial rule; only Java matched it.
26+
27+
**Who is affected.** If every entity and projection name in your model is a single
28+
regular word, nothing moves — `authors`, `posts`, `tags`, `persons`, `accounts`,
29+
`auths`, `documents`, `orders` are byte-identical under the old and new rules. If any
30+
name is multi-word or takes an irregular plural, **its collection URL changes** and
31+
clients calling it must follow. Regenerate (`meta gen`, `dotnet meta gen`,
32+
`mvn metaobjects:generate`, `metaobjects gen`) and diff your routes. This is a
33+
reference-template output change — codegen is opt-in and generated code is yours
34+
(`docs/compatibility-policy.md`) — so it ships here rather than waiting for a major.
35+
36+
The rule: `s`/`x`/`z`/`ch`/`sh` takes `es`, a consonant before `y` becomes `ies` (a
37+
vowel does not — `days`), and a run of capitals stays together until the final one
38+
that begins a word (`HTTPServer` → `http_servers`). It derives from the entity NAME,
39+
never the physical `@table`. TypeScript's deliberate entity-vs-projection split is
40+
collapsed into it, so `OrderSummary` is `/order_summaries` either way.
41+
42+
It is gated rather than merely documented: `fixtures/api-contract-conformance/m2m/`
43+
declares `PostCategory` — multi-word AND ending consonant+`y`, so one entity
44+
separates every spelling the ports used to produce — and asserts that both retired
45+
spellings 404, on each port's hand-rolled reference lane AND its generated lane. The
46+
JVM ports now share ONE implementation (`RouteNaming` in `codegen-base`) instead of
47+
two hand-maintained copies; Python's second private copy of the rule in
48+
`m2m_codegen` is gone the same way. The acronym case is pinned by unit test per port,
49+
since no corpus entity carries one.
50+
1851
- **`meta types <construct> --detail` no longer drops rows from a query that has already
1952
narrowed.** `meta types field.enum --detail` printed "20 of 22 shown — narrow with
2053
QUERY/--type/--kind or raise --limit", having dropped `@values` and `@xmlText`. Both halves

‎agent-context/skills/metaobjects-runtime-ui/SKILL.md‎

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -86,39 +86,39 @@ the verbs, filters, sort/pagination, and wire format are uniform.
8686
`apiPrefix` (default `/api`, set in project config) flows to both the server routes
8787
and the client fetch URLs.
8888

89-
The `<entity>` collection segment is NOT currently uniform across ports — a known
90-
divergence, not a subtlety. Each port composes it with its own rule, and no rule
91-
is more correct than another:
89+
The `<entity>` collection segment is the **entity name `snake_case`d and then
90+
pluralized** — one rule, identical in all five ports, and derived from the NAME,
91+
never from the physical `@table`:
9292

93-
| Port | `<entity>` segment rule | `OrderSummary` → | Decided by |
94-
|---|---|---|---|
95-
| TypeScript (entity) | snake_case, then pluralize (underscores) | `order_summaries` | `resourcePath` in `codegen-ts`'s `entity-ui-descriptor.ts` |
96-
| TypeScript (`source.rdb` projection) | pluralize → snake_case → hyphens | `order-summaries` | same function, projection branch |
97-
| C# | pluralize, then lowercase — no separator | `ordersummaries` | `CSharpNaming.RoutePath` (`MetaObjects.Codegen`) |
98-
| Java | lowercase + `"s"` | `ordersummarys` | `SpringNaming.pluralLowercase` (`codegen-spring`) |
99-
| Kotlin | lowercase + `"s"` | `ordersummarys` | `KotlinNaming.pluralLowercase` (`codegen-kotlin`) |
100-
| Python | lowercase + `"s"` | `ordersummarys` | `plural_lowercase` (`apidocs/naming.py`) |
101-
102-
A single-word name hides all of this (`Author` → `authors` under every rule
103-
above); a MULTI-word name must be checked against the serving port's rule before
104-
assuming a path — a wrong guess is a silent 404, not a build error. The trap is
105-
cross-port by construction: the generated web-client hooks and grids build their
106-
fetch URLs from the TypeScript `$path`, so for a multi-word name a React/TanStack
107-
client calls a path a C#, Java, Kotlin, or Python backend does not mount.
108-
109-
**TypeScript additionally splits entity from projection, deliberately**: the two
110-
TypeScript rows compose in a different ORDER as well as a different separator,
111-
and both spellings are already mounted, so neither may be "tidied" into the
112-
other — unifying them would be a breaking route rename for existing projection
113-
consumers. The split is deliberate and grandfathered, not an oversight.
114-
115-
These segment spellings are slated for UNIFICATION in a separate follow-up
116-
change — one rule across ports, a breaking route rename for the non-TypeScript
117-
ports — which will collapse this table back to a single rule. Until it lands,
118-
this section deliberately documents today's shipped behaviour: a doc describing
119-
an unshipped state is worse than one describing a messy shipped one. Read the
120-
deciding function for the port you are wiring; do not guess a path and do not
121-
"fix" one port's spelling to match another's.
93+
| Name | Segment | Rule |
94+
|---|---|---|
95+
| `Author` | `authors` | a single regular word takes `s` |
96+
| `PostCategory` | `post_categories` | multi-word: the capitals carry the word boundary |
97+
| `Address` | `addresses` | ending `s`/`x`/`z`/`ch`/`sh` takes `es` |
98+
| `Category` | `categories` | consonant + `y` becomes `ies` |
99+
| `Day` | `days` | a VOWEL before the `y` does not |
100+
| `HTTPServer` | `http_servers` | a run of capitals stays together until the final one that begins a word |
101+
102+
An `object.projection` uses the same rule, so `OrderSummary` is at
103+
`/order_summaries` either way. The generated web-client hooks and grids build
104+
their fetch URLs from the TypeScript `$path`, and every backend now mounts that
105+
same spelling, so a React/TanStack client works against any port's server.
106+
107+
**This is a change, and it was a breaking one.** Each port used to spell the
108+
segment differently and they agreed only on single regular words — which was
109+
every collection base in the corpus, so all five lanes were green while
110+
`OrderSummary` was served at four different URLs: `/order_summaries` (TS entity),
111+
`/order-summaries` (TS projection), `/ordersummaries` (C#) and `/ordersummarys`
112+
(Java, Kotlin, Python). A project whose entity names are all single regular words
113+
saw nothing move. Any multi-word or irregular-plural name had its collection URL
114+
renamed, and clients had to follow.
115+
116+
The rule is gated, not just documented: `fixtures/api-contract-conformance/m2m/`
117+
declares `PostCategory` — multi-word AND ending consonant+`y`, so it separates
118+
every spelling the ports used to produce — and asserts both retired spellings
119+
404, on each port's reference AND generated lane. The JVM ports share one
120+
implementation (`RouteNaming` in `codegen-base`); the acronym case is pinned by
121+
unit test in each port, since no corpus entity carries one.
122122

123123
| Verb | Path | Purpose |
124124
|---|---|---|

‎docs/CONFORMANCE.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ regenerate with `ls -d fixtures/<corpus>/*/ | wc -l` for directory-shaped corpor
3434
| [`fixtures/extract-conformance/`](../fixtures/extract-conformance/) | 33 | ✓ | ✓ | inherits the shared JVM engine | ✓ | ✓ |
3535
| [`fixtures/output-prompt-conformance/`](../fixtures/output-prompt-conformance/) | 14 | ✓ | ✓ | ✓ | ✓ | ✓ |
3636
| [`fixtures/persistence-conformance/`](../fixtures/persistence-conformance/) | 33 (27 query + 6 migration) | all 33 | 27 query (migrations TS-only, ADR-0015) | 27 query (via Exposed) | 27 query | 27 query |
37-
| [`fixtures/api-contract-conformance/`](../fixtures/api-contract-conformance/) | 49 (28 core + 9 tph + 8 m2m + 2 jsonb + 2 write-through) | ✓ (Fastify reference + generated lane) | ✓ (embedded HTTP + JDBC) | ✓ (embedded HTTP + Exposed) | ✓ (HttpListener + Npgsql) | ✓ (FastAPI + pg8000) |
37+
| [`fixtures/api-contract-conformance/`](../fixtures/api-contract-conformance/) | 50 (28 core + 9 tph + 9 m2m + 2 jsonb + 2 write-through) | ✓ (Fastify reference + generated lane) | ✓ (embedded HTTP + JDBC) | ✓ (embedded HTTP + Exposed) | ✓ (HttpListener + Npgsql) | ✓ (FastAPI + pg8000) |
3838
| [`fixtures/validation-conformance/`](../fixtures/validation-conformance/) | 16 cases | ✓ | ✓ | ✓ | ✓ | ✓ |
3939
| [`fixtures/registry-conformance/`](../fixtures/registry-conformance/) | 1 canonical manifest | ✓ (reference emitter) | ✓ | ✓ | ✓ | ✓ |
4040
| [`fixtures/object-model-conformance/`](../fixtures/object-model-conformance/) | 1 shared metadata fixture (per-port scenarios) | ✓ | ✓ | ✓ | ✓ | ✓ |
@@ -226,9 +226,9 @@ All 31 fixtures → [features/migrations-and-drift.md](features/migrations-and-d
226226
- `migrations/*` (6) → [features/migrations-and-drift.md](features/migrations-and-drift.md) (schema migration section)
227227
- `queries/*` (27) → [features/source-kinds.md](features/source-kinds.md) (query semantics against `source.rdb`)
228228

229-
### `fixtures/api-contract-conformance/` (49)
229+
### `fixtures/api-contract-conformance/` (50)
230230

231-
All 49 scenarios → [features/api-contract.md](features/api-contract.md) (cross-port
231+
All 50 scenarios → [features/api-contract.md](features/api-contract.md) (cross-port
232232
REST API URL grammar + JSON wire format). Verifies every backend's emitted CRUD
233233
routes answer identically over HTTP — list / get / create / patch+put / delete,
234234
plus pagination (`limit`/`offset`), sort (`sort=field:dir`), the `withCount=1`
@@ -328,7 +328,7 @@ Phase 1a is TypeScript + Python only; those three ports arrive in Phase 2.
328328
## Orphaned fixtures (tested but not yet documented)
329329

330330
The fixtures in the nine corpora mapped above (metamodel 329 + yaml 16 + verify 31
331-
+ render 15 + persistence 33 + api-contract 49 + source-resolution 25 + scope 10 +
331+
+ render 15 + persistence 33 + api-contract 50 + source-resolution 25 + scope 10 +
332332
dependency 23) each map to a feature doc. None are orphaned today. The remaining
333333
corpora in the totals table gate tooling contracts (registry manifests, provider
334334
composition, agent context, docs emit) rather than user-facing metamodel behaviour,

‎docs/features/api-contract.md‎

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,36 @@ app's job — see the policy section below.
7171
| `PUT` | `/<apiPrefix>/<entity>/:id` | Update (replace) — optional; same body shape as `PATCH` |
7272
| `DELETE` | `/<apiPrefix>/<entity>/:id` | Delete |
7373

74-
`<entity>` is lowercased + pluralized per the codegen's pluralization
75-
helper (so `Author` → `authors`). Generated TS hooks read `$path` from
76-
the entity-constants file, so the client and the server agree on the
77-
path segment without hand-coordination.
74+
#### The `<entity>` segment
75+
76+
`<entity>` is the **entity name** `snake_case`d and then pluralized. One rule, the
77+
same in all five ports, and derived from the NAME — never from the physical
78+
`@table`:
79+
80+
| Name | Segment | Why |
81+
|---|---|---|
82+
| `Author` | `authors` | a single regular word takes `s` |
83+
| `PostCategory` | `post_categories` | multi-word: the capitals carry the word boundary |
84+
| `Address` | `addresses` | ending `s`/`x`/`z`/`ch`/`sh` takes `es` |
85+
| `Category` | `categories` | consonant + `y` becomes `ies` |
86+
| `Day` | `days` | a VOWEL before the `y` does not |
87+
| `HTTPServer` | `http_servers` | a run of capitals stays together until the final one that begins a word |
88+
89+
The same rule serves an `object.projection`, so `OrderSummary` is at
90+
`/order_summaries` whether it is an entity or a projection.
91+
92+
Generated TS hooks read `$path` from the entity-constants file, so the client and
93+
the server agree on the path segment without hand-coordination.
94+
95+
> **This changed.** Each port used to spell this differently, and they only agreed
96+
> on single regular words like `Author` — which is every collection base the
97+
> corpus had, so every lane was green while `OrderSummary` was served at four
98+
> different URLs: `/order_summaries` (TS entity), `/order-summaries` (TS
99+
> projection), `/ordersummaries` (C#) and `/ordersummarys` (Java, Kotlin,
100+
> Python). If your entity names are all single regular words, nothing moves. If
101+
> any is multi-word or takes an irregular plural, **its collection URL changes**
102+
> and clients must follow. `fixtures/api-contract-conformance/m2m/`'s
103+
> `PostCategory` now gates it in every port, on both lanes.
78104
79105
### Filter operators (9)
80106

‎examples/advanced-modeling/.metaobjects/.gen-state/.hashes.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"src/generated/ProgramDescriptionPayload.ts": "dbd946676f6bd5f08b1fdf14fde91d063a3fafe093dd9fe35ad77e983291c7aa",
1919
"src/generated/ProgramSummary.queries.ts": "ce0757c98cfee05d5e4056025da0695a5a59cd79a9bd258d04ef420ce571629c",
2020
"src/generated/ProgramSummary.routes.ts": "a32bfbad5e32fafa331e26916c7b7112910cabbdf1b99969172d8d434bb58037",
21-
"src/generated/ProgramSummary.ts": "7c3028f57760caad20083db6a6ad648c6509a54839d0e12b9ad8af29c395cb8b",
21+
"src/generated/ProgramSummary.ts": "9c6b35ac32e31746a0ed5942066c3ead0e83d15978d612f61f9aaf21839f3fb8",
2222
"src/generated/Purchase.form.tsx": "d0cc5d82e13477ed1c1e9636212dbc5cf368a2e90a42bb2b65ff688b2a0453d5",
2323
"src/generated/Purchase.queries.ts": "d347ddf3960fd1725e421de798d11f17bc58606a3a7753112db1d9d0a8f1e96c",
2424
"src/generated/Purchase.routes.ts": "fd93a50609205bce5172f633de91d6ec30fed5f72f3a40beb7216390be340c5d",

‎examples/advanced-modeling/src/generated/ProgramSummary.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export type ProgramSummary = z.infer<typeof ProgramSummarySchema>;
3333
export const ProgramSummary = {
3434
$entity: "ProgramSummary",
3535
$view: "v_program_summary",
36-
$path: "/program-summaries",
36+
$path: "/program_summaries",
3737
id: { name: "id", label: "Id", view: "text", dbCol: "id" },
3838
title: { name: "title", label: "Title", view: "text", dbCol: "title" },
3939
authorName: {

0 commit comments

Comments
 (0)