Skip to content

feat(codegen): add identifier naming, UCP keyword stripping, and $defs ref rewriting - #81

Merged
gsmith85 merged 6 commits into
mainfrom
feat/codegen-normalizer
Oct 13, 2026
Merged

gsmith85 merged 6 commits into
mainfrom
feat/codegen-normalizer

Conversation

@gsmith85

@gsmith85 gsmith85 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Implements Task 1 (PR 1a) of the two-layer ucp-schema code-generation pipeline: foundational naming qualification, UCP keyword stripping, and #/$defs/ reference rewriting utilities in src/codegen/normalizer.rs.

Changes

  • 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.

Stacked PR Series Roadmap

  1. PR 1a (this PR): Identifier qualification, UCP keyword stripping, $ref rewriting (src/codegen/normalizer.rs)
  2. PR 1b (feat(codegen): add directional schema slicing and $ref alignment #82): Directional schema slicing & $ref alignment (src/codegen/normalizer.rs)
  3. PR 1c (feat(codegen): add capability reachability, composition, and generate_types() API #83): Capability/extension reachability, composition & generate_types() library API (src/codegen/{mod,reachability,hoist,compose}.rs)
  4. PR 1d (feat(cli): add generate-types subcommand #84): CLI generate-types subcommand (src/bin/ucp-schema.rs, tests/generate_types_test.rs)
  5. PR 2 (feat(codegen): add self-contained .well-known/ucp profile scoping for generate-types #85): Self-contained .well-known/ucp --profile scoping (src/codegen/profile.rs)
  6. PR 3a (feat(codegen): add AST shape normalizers and inline conditional variant hoisting #86): Codegen AST shape normalizers & inline conditional variant hoisting (src/codegen/normalizer.rs, src/codegen/reify.rs)
  7. PR 3b (feat(codegen): lower open string discriminator unions into ordered anyOf with <Union>Base #87): Open string discriminator union lowering (<Union>Base) (src/codegen/reify.rs)
  8. PR 3c (feat(codegen): discover and register decentralized extended subtypes into open unions #88): Cross-file extended subtype discovery & registration (src/codegen/reify.rs)
  9. PR 4a (python-sdk#101): Python SDK direct JSON Schema codegen (generate_models.sh, postprocess_models.py, src/ucp_sdk/models/)
  10. PR 4b (js-sdk#87): JS/TS SDK direct JSON Schema codegen (generate_models.sh, scripts/generate-zod-from-types.mjs, src/spec_generated.ts)
  11. PR 5 (feat(openapi): add profile-driven OpenAPI 3.1 service and type binding (generate-openapi) #89): OpenAPI 3.1 REST service & type binding (ucp-schema generate-openapi, src/openapi/mod.rs)

Test Plan

  • cargo fmt --check
  • cargo test --all-targets
  • cargo clippy --all-targets -- -D warnings

…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.

@damaz91 damaz91 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for splitting PR #73 into a clean, reviewable stack, @gsmith85! Left a few inline comments on collect_schema_files (interaction with ucp-schema lint), prune_dangling_required on nested anyOf/oneOf/then branches (e.g., in service.json), and rewrite_refs_to_defs / to_pascal_case idempotency.

Comment thread src/loader.rs Outdated
Comment thread src/codegen/normalizer.rs Outdated
Comment on lines +13 to +35
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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two edge cases in to_pascal_case:

  1. Non-reverse-domain dotted keys: Using s.contains('.') && !s.ends_with(".json") drops the prefix on 2-segment dotted $defs keys 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". Using is_reverse_domain_name(s) here and including '.' in the base.split(...) delimiters would preserve non-RDN dotted identifiers while still extracting capability_short_name for RDN capability names.
  2. Idempotency on consecutive uppercase letters: mixed checks any(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, calling to_pascal_case a second time sees mixed == false and lowercases the tail (to_pascal_case("AB") -> "Ab").

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/codegen/normalizer.rs
Comment thread src/codegen/normalizer.rs
Comment on lines +100 to +103
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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 54bc773 — ref_to_def_name now treats both "#" and "#/" as root self-references.

Comment thread src/codegen/normalizer.rs
gsmith85 added a commit that referenced this pull request Oct 1, 2026
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)
gsmith85 added a commit that referenced this pull request Oct 1, 2026
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)
gsmith85 added a commit that referenced this pull request Oct 1, 2026
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)
gsmith85 added a commit that referenced this pull request Oct 1, 2026
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)
gsmith85 added a commit that referenced this pull request Oct 2, 2026
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)
gsmith85 added a commit that referenced this pull request Oct 2, 2026
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)

@niranjanmanjunath niranjanmanjunath left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) from collect_schema_files (for codegen) and preserving explicit single-file paths.
  • Multi-pass idempotency in to_pascal_case and ref_to_def_name .
  • Top-down propagation of inherited properties/refs in prune_dangling_required across same-instance combinators (allOf/anyOf/oneOf/if/then/else).

Good unit test coverage across all of these cases as well. Approving!

@damaz91 damaz91 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @gsmith85 - LGTM

@gsmith85
gsmith85 merged commit 26a92a5 into main Oct 13, 2026
27 checks passed
@gsmith85
gsmith85 deleted the feat/codegen-normalizer branch October 13, 2026 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants