Repository navigation
feat(codegen): add identifier naming, UCP keyword stripping, and $defs ref rewriting - #81
Conversation
…s ref rewriting First PR in a stacked series introducing a two-layer code-generation pipeline to `ucp-schema` (replacing monolithic PR #73) so downstream SDKs (`python-sdk`, `js-sdk`) and service stub generators can consume compiled UCP schemas directly without custom preprocessing scripts: 1. [This PR] Foundational `codegen::normalizer` naming, UCP keyword stripping, and `#/$defs/` reference rewriting utilities. 2. Directional slicing (`CreateRequest`, `UpdateRequest`, `CompleteRequest`, Response) and directional `$ref` alignment. 3. Capability/extension reachability, `$defs` hoisting, extension composition, and the `generate_types()` library API. 4. `ucp-schema generate-types` CLI subcommand (`--capability`, `--extension`, `--schema-dir`) and self-contained `--profile` input. 5. Polymorphic union reification (`<Union>Base` ordered `anyOf` fallback per ucp#800 §2.4, inline variant hoisting, and subtype discovery). 6. `ucp-schema generate-openapi` CLI & library binding a profile's REST service schema (`rest.openapi.json`) to compiled capability types. Changes in this PR: - Promote `collect_schema_files` from `src/linter.rs` to `pub(crate)` in `src/loader.rs` (excluding `*.openapi.json` and `*.types.json` bundle files) so `linter` and `codegen` share a single directory walker. - Add PascalCase and `$defs` qualification helpers (`to_pascal_case`, `is_reverse_domain_name`, `is_generic_def_name`, `qualify_def_name`, `qualify_container_op_name`, and `ref_to_def_name`). - Add `strip_ucp_keywords` to strip UCP authoring annotations (`ucp_request`, `ucp_response`, `ucp_shared_request`, `x-ucp-*`, root `name`/`version`/ `requires`/`embedded`, `$schema`, `$id`) and prune dangling `required` entries while preserving instance-data payloads (`const`, `enum`, `default`, `examples`). - Add `rewrite_refs_to_defs` to rewrite self (`#`), local (`#/$defs/*`), and cross-file `$ref` pointers into the unified `#/$defs/<TypeName>` namespace.
| let base = if s.contains('.') && !s.ends_with(".json") { | ||
| capability_short_name(s) | ||
| } else { | ||
| s.to_string() | ||
| }; | ||
|
|
||
| base.split(['_', '-', ' ', '/']) | ||
| .filter(|part| !part.is_empty()) | ||
| .map(|part| { | ||
| let mixed = part.chars().any(|c| c.is_ascii_uppercase()) | ||
| && part.chars().any(|c| c.is_ascii_lowercase()); | ||
| let mut chars = part.chars(); | ||
| let Some(first) = chars.next() else { | ||
| return String::new(); | ||
| }; | ||
| let rest = if mixed { | ||
| chars.as_str().to_string() | ||
| } else { | ||
| chars.as_str().to_ascii_lowercase() | ||
| }; | ||
| format!("{}{rest}", first.to_ascii_uppercase()) | ||
| }) | ||
| .collect() |
There was a problem hiding this comment.
Two edge cases in to_pascal_case:
- Non-reverse-domain dotted keys: Using
s.contains('.') && !s.ends_with(".json")drops the prefix on 2-segment dotted$defskeys such as"checkout.complete_request"(which appears in PR feat(codegen): add directional schema slicing and $ref alignment #82's tests), collapsing it to"CompleteRequest"instead of"CheckoutCompleteRequest". Usingis_reverse_domain_name(s)here and including'.'in thebase.split(...)delimiters would preserve non-RDN dotted identifiers while still extractingcapability_short_namefor RDN capability names. - Idempotency on consecutive uppercase letters:
mixedchecksany(uppercase) && any(lowercase). If an identifier has a single-letter segment followed by another segment (e.g.,"a_b"->"AB") or an all-uppercase acronym, callingto_pascal_casea second time seesmixed == falseand lowercases the tail (to_pascal_case("AB")->"Ab").
There was a problem hiding this comment.
Fixed in 54bc773 — to_pascal_case now checks is_reverse_domain_name(s) before stripping a domain prefix, splits on '.', and treats delimiter-free strings starting with an uppercase ASCII letter as already PascalCase.
| pub fn ref_to_def_name(ref_str: &str, parent_name: Option<&str>) -> String { | ||
| if ref_str == "#" { | ||
| return parent_name.map_or_else(|| "Self".to_string(), to_pascal_case); | ||
| } |
There was a problem hiding this comment.
Nit: Consider checking if ref_str == "#" || ref_str == "#/" here so a root JSON Pointer "#/" doesn't fall through to line 117 (file_stem_to_pascal("")) and produce an empty "#/$defs/".
There was a problem hiding this comment.
Fixed in 54bc773 — ref_to_def_name now treats both "#" and "#/" as root self-references.
Implement Task 4 (PR 1d) of the ucp-schema codegen pipeline: - Wire Commands::GenerateTypes in src/bin/ucp-schema.rs with --profile, --capability, --extension, --schema-dir (alias --schema-local-base), --schema-remote-base, --output, and --pretty. - Implement CliExitCode for CodegenError and generalize write_json_output to <T: serde::Serialize + ?Sized>. - Add CLI integration tests in tests/generate_types_test.rs covering stdout pretty/compact output, file output, repeatable/comma-delimited flags, and error exit codes. Stacked PR Roadmap: - PR 1a (#81): Identifier qualification, UCP keyword stripping, $ref rewriting - PR 1b (#82): Directional schema slicing and $ref alignment - PR 1c (#83): Capability/extension reachability, composition, and generate_types() library API - PR 1d (this PR): CLI generate-types subcommand - PR 2: Self-contained .well-known/ucp --profile scoping - PR 3: Inline conditional variant hoisting and ordered anyOf union lowering - PR 4: OpenAPI 3.1 REST binding assembly (generate-openapi)
Implement Task 4 (PR 1d) of the ucp-schema codegen pipeline: - Wire Commands::GenerateTypes in src/bin/ucp-schema.rs with --profile, --capability, --extension, --schema-dir (alias --schema-local-base), --schema-remote-base, --output, and --pretty. - Implement CliExitCode for CodegenError and generalize write_json_output to <T: serde::Serialize + ?Sized>. - Add CLI integration tests in tests/generate_types_test.rs covering stdout pretty/compact output, file output, repeatable/comma-delimited flags, and error exit codes. Stacked PR Roadmap: - PR 1a (#81): Identifier qualification, UCP keyword stripping, $ref rewriting - PR 1b (#82): Directional schema slicing and $ref alignment - PR 1c (#83): Capability/extension reachability, composition, and generate_types() library API - PR 1d (this PR): CLI generate-types subcommand - PR 2: Self-contained .well-known/ucp --profile scoping - PR 3: Inline conditional variant hoisting and ordered anyOf union lowering - PR 4: OpenAPI 3.1 REST binding assembly (generate-openapi)
Implement Task 4 (PR 1d) of the ucp-schema codegen pipeline: - Wire Commands::GenerateTypes in src/bin/ucp-schema.rs with --profile, --capability, --extension, --schema-dir (alias --schema-local-base), --schema-remote-base, --output, and --pretty. - Implement CliExitCode for CodegenError and generalize write_json_output to <T: serde::Serialize + ?Sized>. - Add CLI integration tests in tests/generate_types_test.rs covering stdout pretty/compact output, file output, repeatable/comma-delimited flags, and error exit codes. Stacked PR Roadmap: - PR 1a (#81): Identifier qualification, UCP keyword stripping, $ref rewriting - PR 1b (#82): Directional schema slicing and $ref alignment - PR 1c (#83): Capability/extension reachability, composition, and generate_types() library API - PR 1d (this PR): CLI generate-types subcommand - PR 2: Self-contained .well-known/ucp --profile scoping - PR 3: Inline conditional variant hoisting and ordered anyOf union lowering - PR 4: OpenAPI 3.1 REST binding assembly (generate-openapi)
Implement Task 4 (PR 1d) of the ucp-schema codegen pipeline: - Wire Commands::GenerateTypes in src/bin/ucp-schema.rs with --profile, --capability, --extension, --schema-dir (alias --schema-local-base), --schema-remote-base, --output, and --pretty. - Implement CliExitCode for CodegenError and generalize write_json_output to <T: serde::Serialize + ?Sized>. - Add CLI integration tests in tests/generate_types_test.rs covering stdout pretty/compact output, file output, repeatable/comma-delimited flags, and error exit codes. Stacked PR Roadmap: - PR 1a (#81): Identifier qualification, UCP keyword stripping, $ref rewriting - PR 1b (#82): Directional schema slicing and $ref alignment - PR 1c (#83): Capability/extension reachability, composition, and generate_types() library API - PR 1d (this PR): CLI generate-types subcommand - PR 2: Self-contained .well-known/ucp --profile scoping - PR 3: Inline conditional variant hoisting and ordered anyOf union lowering - PR 4: OpenAPI 3.1 REST binding assembly (generate-openapi)
Implement Task 4 (PR 1d) of the ucp-schema codegen pipeline: - Wire Commands::GenerateTypes in src/bin/ucp-schema.rs with --profile, --capability, --extension, --schema-dir (alias --schema-local-base), --schema-remote-base, --output, and --pretty. - Implement CliExitCode for CodegenError and generalize write_json_output to <T: serde::Serialize + ?Sized>. - Add CLI integration tests in tests/generate_types_test.rs covering stdout pretty/compact output, file output, repeatable/comma-delimited flags, and error exit codes. Stacked PR Roadmap: - PR 1a (#81): Identifier qualification, UCP keyword stripping, $ref rewriting - PR 1b (#82): Directional schema slicing and $ref alignment - PR 1c (#83): Capability/extension reachability, composition, and generate_types() library API - PR 1d (this PR): CLI generate-types subcommand - PR 2: Self-contained .well-known/ucp --profile scoping - PR 3: Inline conditional variant hoisting and ordered anyOf union lowering - PR 4: OpenAPI 3.1 REST binding assembly (generate-openapi)
…ref, and prune vacuous anyOf stubs
Implement Task 4 (PR 1d) of the ucp-schema codegen pipeline: - Wire Commands::GenerateTypes in src/bin/ucp-schema.rs with --profile, --capability, --extension, --schema-dir (alias --schema-local-base), --schema-remote-base, --output, and --pretty. - Implement CliExitCode for CodegenError and generalize write_json_output to <T: serde::Serialize + ?Sized>. - Add CLI integration tests in tests/generate_types_test.rs covering stdout pretty/compact output, file output, repeatable/comma-delimited flags, and error exit codes. Stacked PR Roadmap: - PR 1a (#81): Identifier qualification, UCP keyword stripping, $ref rewriting - PR 1b (#82): Directional schema slicing and $ref alignment - PR 1c (#83): Capability/extension reachability, composition, and generate_types() library API - PR 1d (this PR): CLI generate-types subcommand - PR 2: Self-contained .well-known/ucp --profile scoping - PR 3: Inline conditional variant hoisting and ordered anyOf union lowering - PR 4: OpenAPI 3.1 REST binding assembly (generate-openapi)
…n in qualify_def_name
niranjanmanjunath
left a comment
There was a problem hiding this comment.
LGTM! Clean foundational PR for the codegen pipeline.
Verified that the fixes cleanly resolve all the edge cases from the earlier review:
- Separating
collect_json_files(for linting OpenAPI/OpenRPC specs) fromcollect_schema_files(for codegen) and preserving explicit single-file paths. - Multi-pass idempotency in
to_pascal_caseandref_to_def_name. - Top-down propagation of inherited properties/refs in
prune_dangling_requiredacross same-instance combinators (allOf/anyOf/oneOf/if/then/else).
Good unit test coverage across all of these cases as well. Approving!
Summary
Implements Task 1 (PR 1a) of the two-layer
ucp-schemacode-generation pipeline: foundational naming qualification, UCP keyword stripping, and#/$defs/reference rewriting utilities insrc/codegen/normalizer.rs.Changes
collect_schema_filesfromsrc/linter.rstopub(crate)insrc/loader.rs(excluding*.openapi.jsonand*.types.jsonbundle files) solinterandcodegenshare a single directory walker.$defsqualification helpers (to_pascal_case,is_reverse_domain_name,is_generic_def_name,qualify_def_name,qualify_container_op_name, andref_to_def_name).strip_ucp_keywordsto strip UCP authoring annotations (ucp_request,ucp_response,ucp_shared_request,x-ucp-*, rootname/version/requires/embedded,$schema,$id) and prune danglingrequiredentries while preserving instance-data payloads (const,enum,default,examples).rewrite_refs_to_defsto rewrite self (#), local (#/$defs/*), and cross-file$refpointers into the unified#/$defs/<TypeName>namespace.Stacked PR Series Roadmap
$refrewriting (src/codegen/normalizer.rs)$refalignment (src/codegen/normalizer.rs)generate_types()library API (src/codegen/{mod,reachability,hoist,compose}.rs)generate-typessubcommand (src/bin/ucp-schema.rs,tests/generate_types_test.rs).well-known/ucp--profilescoping (src/codegen/profile.rs)src/codegen/normalizer.rs,src/codegen/reify.rs)<Union>Base) (src/codegen/reify.rs)src/codegen/reify.rs)generate_models.sh,postprocess_models.py,src/ucp_sdk/models/)generate_models.sh,scripts/generate-zod-from-types.mjs,src/spec_generated.ts)ucp-schema generate-openapi,src/openapi/mod.rs)Test Plan
cargo fmt --checkcargo test --all-targetscargo clippy --all-targets -- -D warnings