Skip to content

GTS macro proposal#78

Open
asmith987 wants to merge 5 commits into
GlobalTypeSystem:mainfrom
asmith987:gts-macro-proposal
Open

GTS macro proposal#78
asmith987 wants to merge 5 commits into
GlobalTypeSystem:mainfrom
asmith987:gts-macro-proposal

Conversation

@asmith987

@asmith987 asmith987 commented Mar 11, 2026

Copy link
Copy Markdown

This PR contains a proposal to evolve #[struct_to_gts_schema] into a derive-based #[derive(GtsSchema)]` macro. The goal is to bring the macro into closer alignment with the GTS spec v0.8.0, particularly around identity field requirements.

The full proposal is at docs/001-macro-proposal.md. Some key changes:

  • Identity fields become optional - the specs defines schemas without GTS identity fields, but the current macro requires them. This change would solve struct_to_gts_schema: gts_type field blocks Deserialize for structs #72 .
  • x-gts-ref: "/$id vs gts.* - the spec distinguishes self-reference from cross-reference. The current macro generates gts.* for all GtsSchemaId fields. The new macro would use field annotations such as #[gts(type_field)] or #[gts(instance_id)] to enable this functionality
  • User control serde - removes the silent derive injection/removals. Nested serde blocking is still the default.
  • properties are auto-derived from struct fields - removes the manual properties = "field1,field2" parameter
  • extends = Parent replaces base = True / base = Parent - Clearer naming and if extends is omitted, the struct is considered root by default
  • Explicit over implicit - All derives are visibile in the struct definition, no hidden trait injections.

All schema output would be structurally identical.

The working implementation is at https://github.com/asmith987/gts-rust/tree/gts-macro-implementation

Relevant docs in the PR:

  • docs/001-macro-proposal.md - Proposal

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced #[derive(GtsSchema)] macro with #[gts(...)] attributes for schema configuration and validation.
    • Automatic constructor generation that populates identity fields.
    • Support for schema inheritance via extends with compile-time validation.
    • Enhanced JSON schema generation with improved identity-field handling.
  • Documentation

    • Added comprehensive migration guide for transitioning from the legacy schema macro.
    • Created architecture documentation for the new macro design.
    • Updated README with new macro usage patterns and examples.

@coderabbitai

coderabbitai Bot commented Mar 11, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@asmith987 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 8 minutes and 9 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 8 minutes and 9 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e391650b-c589-4a40-b430-9e889962bb18

📥 Commits

Reviewing files that changed from the base of the PR and between 7620bc1 and 034c776.

📒 Files selected for processing (68)
  • docs/001-gts-schema-derive-macro-adr.md
  • docs/002-macro-migration-implementation-plan.md
  • docs/002-struct-to-gts-schema-migration.md
  • gts-macros/README.md
  • gts-macros/src/gts_attrs.rs
  • gts-macros/src/gts_codegen.rs
  • gts-macros/src/gts_field_attrs.rs
  • gts-macros/src/gts_schema_derive.rs
  • gts-macros/src/gts_serde.rs
  • gts-macros/src/gts_validation.rs
  • gts-macros/src/lib.rs
  • gts-macros/tests/compile_fail/base_parent_mismatch.stderr
  • gts-macros/tests/compile_fail/non_gts_generic.stderr
  • gts-macros/tests/v2_compile_fail/both_type_and_instance.rs
  • gts-macros/tests/v2_compile_fail/both_type_and_instance.stderr
  • gts-macros/tests/v2_compile_fail/duplicate_instance_id.rs
  • gts-macros/tests/v2_compile_fail/duplicate_instance_id.stderr
  • gts-macros/tests/v2_compile_fail/duplicate_type_field.rs
  • gts-macros/tests/v2_compile_fail/duplicate_type_field.stderr
  • gts-macros/tests/v2_compile_fail/enum_not_supported.rs
  • gts-macros/tests/v2_compile_fail/enum_not_supported.stderr
  • gts-macros/tests/v2_compile_fail/extends_none_multi_segment.rs
  • gts-macros/tests/v2_compile_fail/extends_none_multi_segment.stderr
  • gts-macros/tests/v2_compile_fail/extends_parent_mismatch.rs
  • gts-macros/tests/v2_compile_fail/extends_parent_mismatch.stderr
  • gts-macros/tests/v2_compile_fail/extends_parent_no_generic.rs
  • gts-macros/tests/v2_compile_fail/extends_parent_no_generic.stderr
  • gts-macros/tests/v2_compile_fail/extends_single_segment.rs
  • gts-macros/tests/v2_compile_fail/extends_single_segment.stderr
  • gts-macros/tests/v2_compile_fail/identity_on_derived_struct.rs
  • gts-macros/tests/v2_compile_fail/identity_on_derived_struct.stderr
  • gts-macros/tests/v2_compile_fail/instance_id_wrong_type.rs
  • gts-macros/tests/v2_compile_fail/instance_id_wrong_type.stderr
  • gts-macros/tests/v2_compile_fail/invalid_gts_id.rs
  • gts-macros/tests/v2_compile_fail/invalid_gts_id.stderr
  • gts-macros/tests/v2_compile_fail/missing_description.rs
  • gts-macros/tests/v2_compile_fail/missing_description.stderr
  • gts-macros/tests/v2_compile_fail/missing_dir_path.rs
  • gts-macros/tests/v2_compile_fail/missing_dir_path.stderr
  • gts-macros/tests/v2_compile_fail/missing_identity_annotation.rs
  • gts-macros/tests/v2_compile_fail/missing_identity_annotation.stderr
  • gts-macros/tests/v2_compile_fail/missing_schema_id.rs
  • gts-macros/tests/v2_compile_fail/missing_schema_id.stderr
  • gts-macros/tests/v2_compile_fail/multiple_generics.rs
  • gts-macros/tests/v2_compile_fail/multiple_generics.stderr
  • gts-macros/tests/v2_compile_fail/nested_direct_serialize.rs
  • gts-macros/tests/v2_compile_fail/nested_direct_serialize.stderr
  • gts-macros/tests/v2_compile_fail/nested_direct_serialize_cfg_attr.rs
  • gts-macros/tests/v2_compile_fail/nested_direct_serialize_cfg_attr.stderr
  • gts-macros/tests/v2_compile_fail/root_multi_segment.rs
  • gts-macros/tests/v2_compile_fail/root_multi_segment.stderr
  • gts-macros/tests/v2_compile_fail/tuple_struct.rs
  • gts-macros/tests/v2_compile_fail/tuple_struct.stderr
  • gts-macros/tests/v2_compile_fail/type_field_wrong_type.rs
  • gts-macros/tests/v2_compile_fail/type_field_wrong_type.stderr
  • gts-macros/tests/v2_compile_fail/unknown_field_attr.rs
  • gts-macros/tests/v2_compile_fail/unknown_field_attr.stderr
  • gts-macros/tests/v2_compile_fail/unknown_gts_attr.rs
  • gts-macros/tests/v2_compile_fail/unknown_gts_attr.stderr
  • gts-macros/tests/v2_compile_fail/version_mismatch.rs
  • gts-macros/tests/v2_compile_fail/version_mismatch.stderr
  • gts-macros/tests/v2_compile_fail_tests.rs
  • gts-macros/tests/v2_inheritance_tests.rs
  • gts-macros/tests/v2_inheritance_tests_mixed.rs
  • gts-macros/tests/v2_integration_tests.rs
  • gts-macros/tests/v2_parity_tests.rs
  • gts-macros/tests/v2_serde_rename_tests.rs
  • gts-macros/tests/v2_serialization_tests.rs
📝 Walkthrough

Walkthrough

This pull request introduces a new #[derive(GtsSchema)] derive macro with #[gts(...)] helper attributes, replacing the legacy #[struct_to_gts_schema] macro. It includes comprehensive documentation, attribute parsing, compile-time validation, code generation for serialization/deserialization, and extensive test coverage.

Changes

Cohort / File(s) Summary
Submodule Reference
.gts-spec
Updated submodule commit pointer to incorporate upstream changes.
Architecture & Migration Documentation
docs/001-gts-schema-derive-macro-adr.md, docs/002-macro-migration-implementation-plan.md, docs/002-struct-to-gts-schema-migration.md
New ADR documents design of #[derive(GtsSchema)], phased migration plan from legacy macro, and migration guide for upgrading structs. Specifies schema ID format validation, inheritance via extends, identity-field enforcement, and JSON Schema generation rules.
README Refactoring
gts-macros/README.md
Substantially refactored to document new derive macro as primary API while retaining legacy macro documentation. Updates examples, attribute syntax, validation rules, and generated constructor behavior.
Struct-level Attribute Parsing
gts-macros/src/gts_attrs.rs
New module parsing top-level #[gts(...)] attributes (dir_path, schema_id, description, optional extends) into structured GtsAttrs with required-field validation and unknown-attribute error detection.
Field-level Attribute Parsing
gts-macros/src/gts_field_attrs.rs
New module parsing field-level #[gts(...)] directives (type_field, instance_id, skip) into FieldGtsAttrs, enforcing single directive per field and validating attribute names.
Compile-time Validation
gts-macros/src/gts_validation.rs
New module implementing comprehensive validation: struct shape (named only), schema ID format, version alignment between struct name and schema_id, segment count matching extends presence, generic parameter limits, identity-field type/mutual-exclusivity checks.
Code Generation (Core)
gts-macros/src/gts_codegen.rs
New module generating GtsSchema trait impl with schema constants, inheritance assertions, allOf+$ref composition, identity-field x-gts-ref overrides, runtime schema accessors, instance ID generation, and generated new(...) constructor auto-populating identity fields.
Serde Generation
gts-macros/src/gts_serde.rs
New module generating Serialize/Deserialize implementations for root types and GtsSerialize/GtsDeserialize for nested types, with GtsNoDirectSerialize/GtsNoDirectDeserialize guards for nested structs. Handles field renaming and generic parameter bounds.
Derive Macro Entry
gts-macros/src/gts_schema_derive.rs
New module orchestrating derive macro flow: parses struct/field attributes, validates, delegates to codegen and serde generation, aggregates results.
Macro Registration
gts-macros/src/lib.rs
Registers new #[derive(GtsSchema)] proc-macro, promotes helper functions to pub(crate) visibility for cross-module reuse, updates documentation to mark legacy field constants.
Compile-fail Test Fixtures (v2)
gts-macros/tests/v2_compile_fail/*.rs, gts-macros/tests/v2_compile_fail/*.stderr
Comprehensive test coverage for validation errors: missing/invalid attributes (dir_path, schema_id, description), struct shape constraints (enum, tuple struct), identity-field rules (duplicates, wrong types, mutually exclusive, missing on root), inheritance constraints (segment counts, parent schema-id matching, parent lacking generic), version mismatches, generics limits, and unknown attributes.
Compile-fail Test Runner
gts-macros/tests/v2_compile_fail_tests.rs
Integration entrypoint using trybuild to execute compile-fail test suite against v2_compile_fail/*.rs fixtures.
Integration Tests (Inheritance)
gts-macros/tests/v2_inheritance_tests.rs
Tests 2-level and 3-level extends hierarchies validating allOf/$ref structure, schema ID derivation, base schema ID resolution, instance ID generation, and unit-struct payload serialization.
Integration Tests (Core Functionality)
gts-macros/tests/v2_integration_tests.rs
Validates generated schema structure ($id, $schema, type, additionalProperties), required/optional field handling, x-gts-ref for identity fields, skip directive exclusion, GENERIC_FIELD detection, JSON Schema formats, and generated constructor behavior across non-generic, generic, instance-id, and derived variants.
Integration Tests (Parity & Serde)
gts-macros/tests/v2_parity_tests.rs, gts-macros/tests/v2_serialization_tests.rs, gts-macros/tests/v2_serde_rename_tests.rs
Parity tests compare old and new macro outputs ensuring schema/serde equivalence and verify new features (description, skip). Serialization tests validate JSON round-tripping, renamed fields, instance helpers, and constructor behavior. Serde rename tests confirm field renaming consistency across nested structures.
Legacy Test Fixes
gts-macros/tests/compile_fail/base_parent_mismatch.stderr, gts-macros/tests/compile_fail/non_gts_generic.stderr
Updated expected error codes (E0412 → E0425) for unresolved identifier diagnostics in legacy macro tests.

Sequence Diagram

sequenceDiagram
    participant User as User Code
    participant Parser as Attribute Parser
    participant Validator as Validator
    participant CodeGen as Code Generator
    participant Serde as Serde Generator
    participant Compiler as Rust Compiler

    User->>Parser: Invoke #[derive(GtsSchema)]
    Parser->>Parser: Parse #[gts(...)] attributes
    Parser->>Parser: Parse #[gts(...)] field attributes
    Parser->>Validator: Validate parsed attributes
    Validator->>Validator: Check struct shape
    Validator->>Validator: Validate schema-id format
    Validator->>Validator: Verify version alignment
    Validator->>Validator: Check segment counts
    Validator->>Validator: Validate identity fields
    alt Validation fails
        Validator->>Compiler: Emit compile error
        Compiler->>User: Error tokens
    else Validation succeeds
        Validator->>CodeGen: Pass validated attributes
        CodeGen->>CodeGen: Generate GtsSchema impl
        CodeGen->>CodeGen: Generate schema constants
        CodeGen->>CodeGen: Generate allOf composition
        CodeGen->>CodeGen: Generate constructors
        CodeGen->>Serde: Pass parsed structure
        Serde->>Serde: Generate Serde impls
        Serde->>Serde: Generate GtsSerialize/GtsDeserialize
        Serde->>Serde: Emit trait guards
        CodeGen->>User: Expanded token stream
        Serde->>User: Expanded token stream
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

The diff introduces substantial logic density across multiple new codegen modules (gts_codegen.rs at 792 lines, gts_serde.rs at 650 lines), heterogeneous changes spanning attribute parsing, validation, code generation, and 40+ test fixtures with mixed complexities. While organized into cohesive modules, each codegen/serde section involves intricate proc-macro token manipulation and serde implementation detail requiring careful reasoning per module. The extensive test suite partially offsets this complexity through coherent patterns, but validation logic and serde bounds interactions demand focused review.

Possibly related PRs

  • PR #63: Modifies JSON Schema generation to add "additionalProperties": false, which directly aligns with the schema output produced by the new GtsSchema codegen.
  • PR #62: Updates the same .gts-spec submodule pointer, indicating shared upstream dependency alignment.
  • PR #51: Introduces gts-id validation utilities that are directly integrated into the new derive macro's validation pipeline via gts_id::validate_gts_id.

Suggested reviewers

  • MikeFalcon77
  • Artifizer

🐰 A macro hops into view, with #[gts(...)]* and #[derive(...)]` too!
Inherits and validates, schemas spring anew,
Identity fields dance in allOf's grand brew—
From legacy past to this future so bright,
The derive macro leaps, and the schemas take flight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'GTS macro proposal' is vague and does not clearly summarize the main change. It refers to a macro but fails to convey the specific intent: introducing #[derive(GtsSchema)] to replace #[struct_to_gts_schema]. Revise the title to be more specific, e.g., 'Introduce #[derive(GtsSchema)] macro to replace struct_to_gts_schema' or 'Evolve GTS schema macro to derive-based approach'.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (3)
docs/001-macro-proposal.md (2)

126-174: Clear before/after migration example with helpful summary table.

The side-by-side comparison makes the transition immediately understandable. The table at lines 167-174 provides excellent quick-reference guidance for each change.

One minor observation: The "After" example (line 149) explicitly includes JsonSchema in the derives, but according to the ADR (lines 235-236 in the ADR file), JsonSchema is auto-added if not present. Consider adding a note clarifying that JsonSchema can be omitted and will be added automatically.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/001-macro-proposal.md` around lines 126 - 174, Add a brief clarifying
sentence to the "After" example explaining that JsonSchema in the derive list is
optional because the macro will auto-add JsonSchema when omitted per the ADR;
reference the example struct BaseEventV1 and the derive attributes (Serialize,
Deserialize, JsonSchema, GtsSchema) and mention the ADR lines about auto-adding
JsonSchema so readers know they can omit JsonSchema and rely on the macro to
inject it.

380-418: Well-planned modular architecture for extensibility.

The proposed module structure (lines 380-390) is a significant improvement over the current ~1,843-line single file. The two concrete examples (lines 392-417) demonstrate how the architecture accommodates future spec features:

  1. Adding a new field-level attribute requires changes to only 4 focused modules
  2. Schema traits (§9.7) can be added via the same parse-validate-generate pipeline

However, the code blocks at lines 400 and 418 are missing language specifiers. While these are pseudo-structure examples rather than actual code, adding language hints would improve rendering.

Minor: Add language specifiers to code blocks
-```
+```text
 gts-macros/src/
   lib.rs                  Entry points (current + updated macro)
   gts_schema_derive.rs    #[derive(GtsSchema)] orchestration

and

-```
+```json
 "x-gts-traits-schema": {
   "properties": {
     "topicRef": { "x-gts-ref": "gts.x.core.events.topic.v1~" },

As per coding guidelines, this addresses the markdownlint warnings for missing language specifiers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/001-macro-proposal.md` around lines 380 - 418, Add missing Markdown
language specifiers to the two fenced code blocks showing the directory layout
and the JSON trait example: change the first fence to use "text" (for the
gts-macros/src/ listing) and the second fence to use "json" (for the
"x-gts-traits-schema"/"x-gts-traits" example). Locate the blocks that contain
the "gts-macros/src/" directory listing and the JSON snippet starting with
"x-gts-traits-schema" and update their opening triple-backtick fences to ```text
and ```json respectively so markdownlint warnings are resolved and syntax
rendering improves.
docs/001-macro-alignment-implementation-plan.md (1)

60-82: Comprehensive compile-fail test coverage.

The 15 compile-fail tests cover all error cases for the new macro. Each test has a clear validation purpose and spec reference. However, the code block at line 78 is missing a language specifier.

Minor: Add language specifier
-| `unknown_gts_attr` | `#[gts(nonexistent)]` | Fail-fast on typos |
+| `unknown_gts_attr` | `#[gts(nonexistent)]` (markdown table, no code block) | Fail-fast on typos |

Actually, reviewing the context, this appears to be within a markdown table, not a code block. The static analysis warning at line 78 may be a false positive or referring to a different issue. No change needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/001-macro-alignment-implementation-plan.md` around lines 60 - 82, The
reviewer flagged a missing code-block language specifier around the table entry
near `nested_direct_serialize_cfg_attr`; inspect the markdown row containing
`nested_direct_serialize` / `nested_direct_serialize_cfg_attr` to confirm
whether it is truly a fenced code block or just table text; if it's just table
content, leave it unchanged and mark the static-analysis warning as a false
positive (or add a comment in the PR), but if there is an accidental fenced code
block, add the appropriate language specifier (e.g., ```rust) to that fence to
silence the warning.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@docs/001-macro-alignment-implementation-plan.md`:
- Around line 60-82: The reviewer flagged a missing code-block language
specifier around the table entry near `nested_direct_serialize_cfg_attr`;
inspect the markdown row containing `nested_direct_serialize` /
`nested_direct_serialize_cfg_attr` to confirm whether it is truly a fenced code
block or just table text; if it's just table content, leave it unchanged and
mark the static-analysis warning as a false positive (or add a comment in the
PR), but if there is an accidental fenced code block, add the appropriate
language specifier (e.g., ```rust) to that fence to silence the warning.

In `@docs/001-macro-proposal.md`:
- Around line 126-174: Add a brief clarifying sentence to the "After" example
explaining that JsonSchema in the derive list is optional because the macro will
auto-add JsonSchema when omitted per the ADR; reference the example struct
BaseEventV1 and the derive attributes (Serialize, Deserialize, JsonSchema,
GtsSchema) and mention the ADR lines about auto-adding JsonSchema so readers
know they can omit JsonSchema and rely on the macro to inject it.
- Around line 380-418: Add missing Markdown language specifiers to the two
fenced code blocks showing the directory layout and the JSON trait example:
change the first fence to use "text" (for the gts-macros/src/ listing) and the
second fence to use "json" (for the "x-gts-traits-schema"/"x-gts-traits"
example). Locate the blocks that contain the "gts-macros/src/" directory listing
and the JSON snippet starting with "x-gts-traits-schema" and update their
opening triple-backtick fences to ```text and ```json respectively so
markdownlint warnings are resolved and syntax rendering improves.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f4d4cf41-1e27-4748-96ae-3af4dd6c9af5

📥 Commits

Reviewing files that changed from the base of the PR and between 3c3a5db and 5376ad5.

📒 Files selected for processing (4)
  • .gts-spec
  • docs/001-macro-alignment-adr.md
  • docs/001-macro-alignment-implementation-plan.md
  • docs/001-macro-proposal.md

Comment thread docs/001-macro-alignment-adr.md Outdated
Comment thread docs/001-macro-alignment-adr.md Outdated
Comment thread docs/001-macro-alignment-adr.md Outdated
Comment thread docs/001-macro-alignment-adr.md Outdated
Comment thread docs/001-macro-alignment-adr.md Outdated
Comment thread docs/001-macro-alignment-adr.md Outdated
Comment thread docs/001-macro-alignment-adr.md Outdated
Comment thread docs/001-macro-alignment-adr.md Outdated
Comment thread docs/001-macro-alignment-adr.md Outdated
Comment thread docs/001-macro-alignment-adr.md Outdated
Comment thread docs/001-macro-alignment-adr.md Outdated
Comment thread docs/001-macro-alignment-adr.md Outdated
Comment thread docs/001-macro-alignment-adr.md Outdated
Comment thread docs/001-macro-proposal.md Outdated

The `#[struct_to_gts_schema]` macro has been the primary integration point between Rust structs and the Global Type System. It delivers compile-time validation, JSON Schema generation, and a runtime API from a single annotation. As the GTS specification has matured and usage has grown, several areas have emerged where the macro's assumptions can be brought into closer alignment with the spec.

This proposal evolves the macro from `#[struct_to_gts_schema]` to `#[derive(GtsSchema)]` with `#[gts(...)]` attributes. The goals are:

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.

  1. On one hand, I understand that #[derive(GtsSchema)] looks more Rust-idiomatic. On the other hand, it is not clear to me why we now need two separate annotations: #[derive(GtsSchema)] and #[gts(...)].
    Does #[derive(GtsSchema)] make sense without #[gts(...)], and vice versa? If they are expected to always be used together, maybe it's enough to have just #[gts(...)]?
  2. From my perspective, we should have two independent macros instead: gts_schema and gts_instance.
    This PR only improves the schema-related macros; instance-related functionality is out of scope here.
    I’m fine with that approach and will follow up with a separate PR for gts_instance once this one is merged (discussed with @Artifizer).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@aviator5 Regarding the points you mentioned:

  1. [gts(...)] is just a helper attribute declared by the derive macro. It provides configuration details for the derive macro to read. Similar to clap with #[derive(Parser)] + #[clap(...)]. We could revert back to using a attribute macro without derive and only use #[gts(...)], but then the struct is no longer the source of truth since the attribute macro is free to manipulate it as it pleases. The original #[struct_to_gts_schema] did this.
  2. When you say two independent macros, do you mean two derive macros or two procedural attribute macros? If you mean two attribute macros, wouldn't that be a little brittle since the first macro would run on source and the second would run on whatever was emitted by the first attribute macro?

@asmith987 asmith987 force-pushed the gts-macro-proposal branch from 7620bc1 to 2d5acfb Compare April 24, 2026 21:41
@asmith987 asmith987 requested a review from Artifizer April 24, 2026 21:44

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
gts-macros/src/lib.rs (2)

198-221: ⚠️ Potential issue | 🟠 Major

Parse #[serde(rename = ...)] structurally instead of slicing the token string.

The current implementation fails silently when rename is not the final attribute. For example, #[serde(rename = "type", alias = "kind")] extracts type", alias = "kind instead of type. The token string slicing assumes the closing quote is always at the end of the remainder, which breaks once additional comma-separated attributes follow.

Proposed fix
 pub(crate) fn get_serde_rename(field: &syn::Field) -> Option<String> {
     for attr in &field.attrs {
-        // Parse the serde attribute using a simpler approach
-        if attr.path().is_ident("serde")
-            && let Ok(meta) = attr.meta.require_list()
-        {
-            let tokens = meta.tokens.to_string();
-
-            // Look for rename = "value" pattern in the token string
-            if let Some(rename_start) = tokens.find("rename") {
-                let rename_part = &tokens[rename_start..];
-                if let Some(eq_pos) = rename_part.find('=') {
-                    let value_part = &rename_part[eq_pos + 1..].trim();
-                    // Extract the string value between quotes
-                    if value_part.starts_with('"') && value_part.ends_with('"') {
-                        let rename_value = &value_part[1..value_part.len() - 1];
-                        return Some(rename_value.to_owned());
-                    }
-                }
-            }
+        if !attr.path().is_ident("serde") {
+            continue;
+        }
+        let Ok(meta) = attr.meta.require_list() else {
+            continue;
+        };
+        let Ok(items) = meta.parse_args_with(
+            syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
+        ) else {
+            continue;
+        };
+
+        for item in items {
+            if let syn::Meta::NameValue(name_value) = item
+                && name_value.path.is_ident("rename")
+                && let syn::Expr::Lit(syn::ExprLit {
+                    lit: syn::Lit::Str(lit_str),
+                    ..
+                }) = name_value.value
+            {
+                return Some(lit_str.value());
+            }
         }
     }
     None
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gts-macros/src/lib.rs` around lines 198 - 221, get_serde_rename currently
slices the token string and fails when rename isn't the last serde key; instead
parse the attribute structurally: for each attr where
attr.path().is_ident("serde") use attr.parse_meta() (or
attr.meta.require_list()) to get a Meta::List and iterate its nested entries,
match each NestedMeta::Meta(Meta::NameValue) whose path is_ident("rename"), then
extract the value by matching the NameValue.lit as Lit::Str and returning its
value; update get_serde_rename to return None if no such NameValue Lit::Str is
found.

603-620: ⚠️ Potential issue | 🔴 Critical

Fix the where clause concatenation to prevent invalid Rust syntax.

The function concatenates an existing where clause with a new predicate using quote! { #existing #generic_ident: #bounds_tokens, }, which lacks a comma separator between predicates. For input like where P: Clone, this expands to where P: Clone T: ..., causing any #[derive(GtsSchema)] struct with an existing where clause to fail during macro expansion.

Replace the quote-based concatenation with proper manipulation of syn::WhereClause::predicates to ensure predicates are correctly separated:

Proposed fix
 pub(crate) fn build_where_clause(
     generics: &syn::Generics,
     where_clause: Option<&syn::WhereClause>,
     bounds: &str,
 ) -> proc_macro2::TokenStream {
-    if let Some(generic_param) = generics.type_params().next() {
-        let generic_ident = &generic_param.ident;
-        let bounds_tokens: proc_macro2::TokenStream =
-            bounds.parse().expect("Failed to parse bounds");
-        if let Some(existing) = where_clause {
-            quote! { `#existing` `#generic_ident`: `#bounds_tokens`, }
-        } else {
-            quote! { where `#generic_ident`: `#bounds_tokens` }
-        }
-    } else {
-        quote! { `#where_clause` }
-    }
+    let Some(generic_param) = generics.type_params().next() else {
+        return quote! { `#where_clause` };
+    };
+
+    let generic_ident = &generic_param.ident;
+    let bounds_tokens: proc_macro2::TokenStream =
+        bounds.parse().expect("Failed to parse bounds");
+
+    let mut clause = where_clause
+        .cloned()
+        .unwrap_or_else(|| syn::parse_quote!(where));
+    clause
+        .predicates
+        .push(syn::parse_quote!(`#generic_ident`: `#bounds_tokens`));
+
+    quote! { `#clause` }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gts-macros/src/lib.rs` around lines 603 - 620, The concatenation in
build_where_clause produces invalid Rust by inserting predicates without proper
separators; instead, parse the new predicate from the generic identifier and
bounds (use bounds.parse::<syn::WherePredicate>()) and if where_clause is Some,
clone it, push the parsed predicate into its .predicates
(syn::punctuated::Punctuated will preserve commas), then return the modified
where clause as a TokenStream; if where_clause is None, build a new
syn::WhereClause with the single parsed predicate and return that. Ensure you
reference build_where_clause, generics.type_params().next(), generic_ident,
bounds.parse(), and syn::WhereClause::predicates when locating and implementing
the change.
🧹 Nitpick comments (5)
gts-macros/src/gts_field_attrs.rs (2)

25-65: Minor: duplicate error message and docstring don't match all rejection paths.

The doc comment on Line 27 says "duplicate #[gts(...)] blocks", but the code at Lines 56–61 also rejects duplicates within a single block (e.g. #[gts(type_field, instance_id)] or #[gts(skip, skip)]). The error text "only one #[gts(...)] attribute per field is allowed" is then a bit misleading in the intra-block case — the user wrote one block.

Consider tightening both:

♻️ Proposed wording tweak
-    /// Parse all `#[gts(...)]` attributes on a single field.
-    ///
-    /// Returns an error for unknown attributes or duplicate `#[gts(...)]` blocks.
+    /// Parse all `#[gts(...)]` attributes on a single field.
+    ///
+    /// Returns an error for unknown directives or when more than one
+    /// `#[gts(...)]` directive is present on the same field (whether across
+    /// multiple `#[gts(...)]` blocks or within a single block).
     pub fn from_field(field: &syn::Field) -> syn::Result<Self> {
@@
-                if result.is_some() {
-                    return Err(syn::Error::new(
-                        ident.span(),
-                        "GtsSchema: only one #[gts(...)] attribute per field is allowed",
-                    ));
-                }
+                if result.is_some() {
+                    return Err(syn::Error::new(
+                        ident.span(),
+                        "GtsSchema: only one #[gts(...)] directive (type_field, instance_id, or skip) per field is allowed",
+                    ));
+                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gts-macros/src/gts_field_attrs.rs` around lines 25 - 65, The docstring and
duplicate-error text in from_field are inconsistent with the actual
duplicate-detection location inside attr.parse_nested_meta: update the doc
comment on from_field to say it rejects duplicate attributes both across
multiple #[gts(...)] blocks and within a single block, and change the syn::Error
message produced when result.is_some() (inside attr.parse_nested_meta) to
something like "GtsSchema: duplicate gts attribute for this field; only one of
type_field, instance_id, or skip is allowed" so it clearly covers intra-block
duplicates; refer to the from_field function, the attr.parse_nested_meta
closure, the result variable, and the GtsFieldAttr variants when making the text
changes.

36-65: Consider explicitly rejecting values on field directives with a dedicated error message.

Field-level directives (type_field, instance_id, skip) do not accept values, but when someone mistakenly writes #[gts(type_field = "x")], the resulting error comes from syn's parse_nested_meta parser rather than this module. This produces a generic error without the "GtsSchema:" prefix, which is less helpful than the module's custom error messages. No test case currently covers this edge case.

Either confirm the generic error is acceptable, or explicitly consume and reject the value with a tailored error (similar to how struct-level parsing handles = in gts_attrs.rs).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gts-macros/src/gts_field_attrs.rs` around lines 36 - 65, The parser for
field-level directives (inside attr.parse_nested_meta in gts_field_attrs.rs)
currently lets syn produce a generic error when a user supplies a value (e.g.,
#[gts(type_field = "x")]); update the nested-meta handling to explicitly reject
value tokens: inside the closure used by attr.parse_nested_meta, after
extracting ident and before returning Ok(()), check for any attached value
(e.g., meta.value or by using meta.parse_nested_meta to detect `=`/Lit`
patterns) and, if present, return a syn::Error with a GtsSchema-prefixed message
(similar wording to struct-level checks in gts_attrs.rs) indicating that
type_field/instance_id/skip do not accept values; ensure you reference
GtsFieldAttr and the existing result handling so behavior and
duplicate-attribute checks remain unchanged.
gts-macros/tests/v2_compile_fail/type_field_wrong_type.rs (1)

11-15: Fixture asserts an unintended failure ordering — consider hardening.

Both event_type: String and pub id: String are present, and neither is a GtsSchemaId / GtsInstanceId. The test relies on validate_field_gts_attrs reaching the is_type_gts_schema_id check before the "root struct must declare exactly one identity field" check (which would otherwise fire if type_field is silently dropped or refactored). Two independent failure paths can lead to brittle .stderr snapshots if the validation order is reordered later.

Consider adding a sibling fixture (or comment in the snapshot) that pins this to the type-mismatch error specifically — e.g., make id: GtsInstanceId so a regression that drops the is_type_gts_schema_id check still fails distinctly rather than masking as a "missing identity field" error.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gts-macros/tests/v2_compile_fail/type_field_wrong_type.rs` around lines 11 -
15, The test fixture EventV1 relies on validation ordering and can produce
brittle stderr snapshots; update the fixture so the type-mismatch failure is
unambiguous by making the identity field a GtsInstanceId/GtsSchemaId (e.g.,
change id to a GtsInstanceId) while keeping #[gts(type_field)] on event_type, so
validate_field_gts_attrs and its is_type_gts_schema_id check will produce the
type-mismatch error rather than triggering the "root struct must declare exactly
one identity field" path; reference EventV1, the #[gts(type_field)] attribute,
validate_field_gts_attrs, and is_type_gts_schema_id when making the change.
gts-macros/src/gts_attrs.rs (2)

44-90: Duplicate keys are silently overwritten.

#[gts(dir_path = "a", dir_path = "b")] currently parses without error and silently keeps the last value. This is a minor footgun and inconsistent with the strictness shown elsewhere (unknown keys are rejected). Consider erroring on duplicates so misconfigurations surface at compile time instead of producing a confusing schema artifact.

♻️ Proposed fix
             match key_str.as_str() {
                 "dir_path" => {
                     input.parse::<Token![=]>()?;
                     let value: LitStr = input.parse()?;
+                    if dir_path.is_some() {
+                        return Err(syn::Error::new_spanned(
+                            key,
+                            "GtsSchema: duplicate attribute 'dir_path' in #[gts(...)]",
+                        ));
+                    }
                     dir_path = Some(value.value());
                 }
                 "schema_id" => {
                     input.parse::<Token![=]>()?;
                     let value: LitStr = input.parse()?;
+                    if schema_id.is_some() {
+                        return Err(syn::Error::new_spanned(
+                            key,
+                            "GtsSchema: duplicate attribute 'schema_id' in #[gts(...)]",
+                        ));
+                    }
                     schema_id = Some(value.value());
                 }
                 "description" => {
                     input.parse::<Token![=]>()?;
                     let value: LitStr = input.parse()?;
+                    if description.is_some() {
+                        return Err(syn::Error::new_spanned(
+                            key,
+                            "GtsSchema: duplicate attribute 'description' in #[gts(...)]",
+                        ));
+                    }
                     description = Some(value.value());
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gts-macros/src/gts_attrs.rs` around lines 44 - 90, The parser currently
allows duplicate keys (e.g., dir_path, schema_id, description, extends) to
silently overwrite previous values; update the parsing loop in gts_attrs.rs (the
while !input.is_empty() block that parses key_str and sets dir_path, schema_id,
description, extends) to check whether the target Option is already Some before
assigning and, if so, return a syn::Error::new_spanned(key, "... duplicate
attribute ...") describing the duplicate; for extends treat the explicit "None"
vs an ident the same way but still error if extends was already provided. Ensure
each match arm for "dir_path", "schema_id", "description", and "extends"
performs this duplicate-check and returns an error instead of overwriting.

87-89: Comma between attributes is parsed as optional.

The loop only consumes a comma if present; #[gts(dir_path = "a" schema_id = "b")] would parse successfully. Consider requiring a comma (when !input.is_empty() after a value) so syntax stays consistent with typical attribute grammars and trybuild diagnostics for missing commas remain obvious.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gts-macros/src/gts_attrs.rs` around lines 87 - 89, The attribute parser
currently treats the comma separator as optional (the if input.peek(Token![,]) {
input.parse::<Token![,]>()? } block), allowing malformed attributes like
#[gts(dir_path = "a" schema_id = "b")]; change this so that after parsing a
value you check if !input.is_empty() and if so require a comma: if
input.peek(Token![,]) { input.parse::<Token![,]>()? } else { return
Err(syn::Error::new(input.span(), "expected `,` between attributes")); } —
update the loop that consumes separators (the block using input.peek and
input.parse::<Token![,]>()) to enforce the comma and return a syn::Error when
missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/002-struct-to-gts-schema-migration.md`:
- Line 307: Update the table row for "JsonSchema derive" to indicate it is NOT
auto-added by #[derive(GtsSchema)]; explicitly state that callers must still add
#[derive(JsonSchema)] themselves (the new test fixtures do this), and adjust the
description to clarify that #[derive(GtsSchema)] does not provide JsonSchema
required for schemars::schema_for! so leaving "Auto-added" would cause compile
failures.

In `@gts-macros/README.md`:
- Around line 58-64: The README example shows deriving Serialize and Deserialize
on the generic-root struct BaseEventV1<P: GtsSchema>, which conflicts with the
generated generic-root serde bridge; remove the explicit #[derive(Serialize,
Deserialize)] from BaseEventV1 (and the other occurrences noted around lines
~232-235) so the docs match the implementation that relies on the generated
serde bridge via GtsSchema/GtsSchema-derived code instead of user-written serde
derives; keep Debug/Clone/JsonSchema/GtsSchema derives and update the “Root
structs must derive…” guidance to not instruct adding Serialize/Deserialize for
generic-root types.

In `@gts-macros/src/gts_attrs.rs`:
- Around line 64-75: When validating collisions between multi-segment schema IDs
and the parsed extends value, improve the error emitted to explicitly mention
that extends = "None" is treated as the sentinel meaning "no extends" (the
sentinel is detected in the parsing branch where ident == "None" and the parsed
variable is set via extends = None), and add a diagnostic hint explaining that
if the user intended to reference a parent type they should use the parent's
actual type name (and that qualified paths like crate::events::Parent are not
accepted because parsing currently only accepts a syn::Ident). Update the error
message that currently reads like "schema_id has {count} segments but no
'extends' is specified" to include a note that "extends = None was parsed as the
reserved sentinel" and suggest using a real parent identifier or change the
attribute format to support paths.

In `@gts-macros/src/gts_codegen.rs`:
- Around line 113-128: The auto-populator currently always emits <#generic_ident
as ::gts::GtsSchema>::SCHEMA_ID which causes BaseEventV1::<()>::new(...) to
stamp an empty id; change the logic in the type_field_ident handling so when
info.generic_param_name is present but equals the sentinel for the unit case
(the empty inner schema id / ""), emit the root schema id expression (e.g.
::gts::gts::GtsSchema::SCHEMA_ID wrapped in ::gts::gts::GtsSchemaId::new)
instead of referencing the generic_ident; keep the existing branch that emits
<#generic_ident as ::gts::GtsSchema>::SCHEMA_ID for all other generic_param_name
values and still use Self::gts_schema_id().clone() for the non-generic branch so
the populator generation (the variable populator used in inits.push) produces
the correct code for the unit sentinel.
- Around line 288-300: The has_serde_skip function incorrectly uses substring
matching on meta.tokens; replace that with proper parsing of the serde attribute
list: call attr.meta.require_list(), iterate meta.nested and for each
NestedMeta::Meta(Meta::Path(path)) check path.is_ident("skip") ||
path.is_ident("skip_serializing") || path.is_ident("skip_deserializing") and
return true only for those exact idents; leave has_serde_skip signature and use
syn::NestedMeta / syn::Meta types so skip_serializing_if (and other name-value
or path entries) are not treated as matches.

In `@gts-macros/src/gts_serde.rs`:
- Around line 208-225: The generated visit_map currently treats every field as
required by unconditionally calling .ok_or_else(||
serde::de::Error::missing_field(`#field_names`)) on each `#field_idents`; update the
logic inside the visit_map (the block that initializes #(let mut `#field_idents`:
Option<_> = None;)* and then resolves them) to detect when a field is Option<T>
or has serde(default) and in those cases do not call missing_field but instead
leave the Option as None or call T::default() respectively; specifically, adjust
the resolution of `#field_idents` (the code that currently does .ok_or_else(...)?
for each `#field_names`) to conditionally use .unwrap_or(None) /
.unwrap_or_default() semantics for optional/defaulted fields, or alternatively
add a compile-time validation in the macro that rejects fields annotated with
Option<T> or #[serde(default)] to avoid diverging from standard serde behavior.
- Around line 439-495: The Deserialize impl and UnitStructVisitor in
gen_base_unit_struct_serde currently drop the struct generics (the code emits
"impl<'de> serde::Deserialize<'de> for `#struct_name` `#where_clause`" and uses
"type Value = `#struct_name`" / "Ok(`#struct_name`)"), causing a mismatch with the
Serialize impl which correctly uses `#impl_generics` / `#ty_generics` /
`#where_clause`; fix by threading the same impl_generics, ty_generics and
where_clause into the Deserialize emission (use impl `#impl_generics`
serde::Deserialize<'de> for `#struct_name` `#ty_generics` `#where_clause`, set
Visitor::Value = `#struct_name` `#ty_generics`, and return Ok(`#struct_name`
`#ty_generics`) in visit_map/visit_unit). Apply the same pattern to
gen_nested_unit_struct_serde so it also emits generic-aware impls (use the same
`#impl_generics/`#ty_generics/#where_clause there).
- Around line 98-103: The code appends a new where-predicate by interpolating an
existing syn::WhereClause (`where_clause` / `existing`) with `#existing
`#gp_ident`: ::gts::GtsSerialize,` which can produce invalid syntax when the
original clause has no trailing comma; update the logic in `ser_where` and the
other places in `build_where_clause_for_gts` to reconstruct the predicates list
instead of concatenating raw `#existing` tokens: walk or take
`existing.predicates`, extend it with a new `PredicateType` for `#gp_ident:
::gts::GtsSerialize`, and then emit the where clause using a punctuated pattern
like `where #(`#predicates`),*` (matching the approach used in
`build_visitor_where_clause`) so the resulting clause always has proper
punctuation.

In `@gts-macros/src/gts_validation.rs`:
- Around line 23-24: The validation only checks the number of generic type
parameters but not that there are corresponding struct fields using those
generic parameters; update validate_generics (and the other validation block
around gen_base_assertion) to iterate the struct's fields, count/collect which
fields' types are exactly the generic type idents declared on the item, and
ensure each generic parameter has at least one matching field (and that the
total distinct matched generic params equals the number of generic params). Use
the item/type parameter idents and field type paths to perform the match
(referencing validate_generics, validate_field_gts_attrs, and gen_base_assertion
to locate the checks) and return a clear error when a generic parameter has no
corresponding field. Ensure duplicate fields mapping to the same generic param
still counts as satisfying that param but validate all params are covered.

In `@gts-macros/tests/v2_compile_fail/extends_parent_no_generic.rs`:
- Around line 22-31: The test fails for the wrong reason: LeafTypeV1 is missing
#[derive(serde::Serialize)] and the generated const block unconditionally
instantiates a generic GtsSchema assoc (causing E0107) so the parent-generic
assertion never runs; fix by either (A) add #[derive(serde::Serialize)] to
LeafTypeV1 and change the const assertion logic that checks
PARENT_GENERIC_FIELD/is_none() to first verify the parent arity (i.e., only
evaluate <P as GtsSchema>::SCHEMA_ID or related consts when P is declared
generic) or (B) if you intend to accept arity mismatch failures, update the test
comment to state arity errors are an expected failure mode instead of asserting
the parent-generic-field behavior. Ensure references to LeafTypeV1, the const
block that reads PARENT_GENERIC_FIELD, and the <P as GtsSchema>::SCHEMA_ID
instantiation are the locations you change.

---

Outside diff comments:
In `@gts-macros/src/lib.rs`:
- Around line 198-221: get_serde_rename currently slices the token string and
fails when rename isn't the last serde key; instead parse the attribute
structurally: for each attr where attr.path().is_ident("serde") use
attr.parse_meta() (or attr.meta.require_list()) to get a Meta::List and iterate
its nested entries, match each NestedMeta::Meta(Meta::NameValue) whose path
is_ident("rename"), then extract the value by matching the NameValue.lit as
Lit::Str and returning its value; update get_serde_rename to return None if no
such NameValue Lit::Str is found.
- Around line 603-620: The concatenation in build_where_clause produces invalid
Rust by inserting predicates without proper separators; instead, parse the new
predicate from the generic identifier and bounds (use
bounds.parse::<syn::WherePredicate>()) and if where_clause is Some, clone it,
push the parsed predicate into its .predicates (syn::punctuated::Punctuated will
preserve commas), then return the modified where clause as a TokenStream; if
where_clause is None, build a new syn::WhereClause with the single parsed
predicate and return that. Ensure you reference build_where_clause,
generics.type_params().next(), generic_ident, bounds.parse(), and
syn::WhereClause::predicates when locating and implementing the change.

---

Nitpick comments:
In `@gts-macros/src/gts_attrs.rs`:
- Around line 44-90: The parser currently allows duplicate keys (e.g., dir_path,
schema_id, description, extends) to silently overwrite previous values; update
the parsing loop in gts_attrs.rs (the while !input.is_empty() block that parses
key_str and sets dir_path, schema_id, description, extends) to check whether the
target Option is already Some before assigning and, if so, return a
syn::Error::new_spanned(key, "... duplicate attribute ...") describing the
duplicate; for extends treat the explicit "None" vs an ident the same way but
still error if extends was already provided. Ensure each match arm for
"dir_path", "schema_id", "description", and "extends" performs this
duplicate-check and returns an error instead of overwriting.
- Around line 87-89: The attribute parser currently treats the comma separator
as optional (the if input.peek(Token![,]) { input.parse::<Token![,]>()? }
block), allowing malformed attributes like #[gts(dir_path = "a" schema_id =
"b")]; change this so that after parsing a value you check if !input.is_empty()
and if so require a comma: if input.peek(Token![,]) {
input.parse::<Token![,]>()? } else { return Err(syn::Error::new(input.span(),
"expected `,` between attributes")); } — update the loop that consumes
separators (the block using input.peek and input.parse::<Token![,]>()) to
enforce the comma and return a syn::Error when missing.

In `@gts-macros/src/gts_field_attrs.rs`:
- Around line 25-65: The docstring and duplicate-error text in from_field are
inconsistent with the actual duplicate-detection location inside
attr.parse_nested_meta: update the doc comment on from_field to say it rejects
duplicate attributes both across multiple #[gts(...)] blocks and within a single
block, and change the syn::Error message produced when result.is_some() (inside
attr.parse_nested_meta) to something like "GtsSchema: duplicate gts attribute
for this field; only one of type_field, instance_id, or skip is allowed" so it
clearly covers intra-block duplicates; refer to the from_field function, the
attr.parse_nested_meta closure, the result variable, and the GtsFieldAttr
variants when making the text changes.
- Around line 36-65: The parser for field-level directives (inside
attr.parse_nested_meta in gts_field_attrs.rs) currently lets syn produce a
generic error when a user supplies a value (e.g., #[gts(type_field = "x")]);
update the nested-meta handling to explicitly reject value tokens: inside the
closure used by attr.parse_nested_meta, after extracting ident and before
returning Ok(()), check for any attached value (e.g., meta.value or by using
meta.parse_nested_meta to detect `=`/Lit` patterns) and, if present, return a
syn::Error with a GtsSchema-prefixed message (similar wording to struct-level
checks in gts_attrs.rs) indicating that type_field/instance_id/skip do not
accept values; ensure you reference GtsFieldAttr and the existing result
handling so behavior and duplicate-attribute checks remain unchanged.

In `@gts-macros/tests/v2_compile_fail/type_field_wrong_type.rs`:
- Around line 11-15: The test fixture EventV1 relies on validation ordering and
can produce brittle stderr snapshots; update the fixture so the type-mismatch
failure is unambiguous by making the identity field a GtsInstanceId/GtsSchemaId
(e.g., change id to a GtsInstanceId) while keeping #[gts(type_field)] on
event_type, so validate_field_gts_attrs and its is_type_gts_schema_id check will
produce the type-mismatch error rather than triggering the "root struct must
declare exactly one identity field" path; reference EventV1, the
#[gts(type_field)] attribute, validate_field_gts_attrs, and
is_type_gts_schema_id when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d7c4c10a-6e30-486b-a5cc-b42cc54534d3

📥 Commits

Reviewing files that changed from the base of the PR and between 5376ad5 and 7620bc1.

📒 Files selected for processing (68)
  • docs/001-gts-schema-derive-macro-adr.md
  • docs/002-macro-migration-implementation-plan.md
  • docs/002-struct-to-gts-schema-migration.md
  • gts-macros/README.md
  • gts-macros/src/gts_attrs.rs
  • gts-macros/src/gts_codegen.rs
  • gts-macros/src/gts_field_attrs.rs
  • gts-macros/src/gts_schema_derive.rs
  • gts-macros/src/gts_serde.rs
  • gts-macros/src/gts_validation.rs
  • gts-macros/src/lib.rs
  • gts-macros/tests/compile_fail/base_parent_mismatch.stderr
  • gts-macros/tests/compile_fail/non_gts_generic.stderr
  • gts-macros/tests/v2_compile_fail/both_type_and_instance.rs
  • gts-macros/tests/v2_compile_fail/both_type_and_instance.stderr
  • gts-macros/tests/v2_compile_fail/duplicate_instance_id.rs
  • gts-macros/tests/v2_compile_fail/duplicate_instance_id.stderr
  • gts-macros/tests/v2_compile_fail/duplicate_type_field.rs
  • gts-macros/tests/v2_compile_fail/duplicate_type_field.stderr
  • gts-macros/tests/v2_compile_fail/enum_not_supported.rs
  • gts-macros/tests/v2_compile_fail/enum_not_supported.stderr
  • gts-macros/tests/v2_compile_fail/extends_none_multi_segment.rs
  • gts-macros/tests/v2_compile_fail/extends_none_multi_segment.stderr
  • gts-macros/tests/v2_compile_fail/extends_parent_mismatch.rs
  • gts-macros/tests/v2_compile_fail/extends_parent_mismatch.stderr
  • gts-macros/tests/v2_compile_fail/extends_parent_no_generic.rs
  • gts-macros/tests/v2_compile_fail/extends_parent_no_generic.stderr
  • gts-macros/tests/v2_compile_fail/extends_single_segment.rs
  • gts-macros/tests/v2_compile_fail/extends_single_segment.stderr
  • gts-macros/tests/v2_compile_fail/identity_on_derived_struct.rs
  • gts-macros/tests/v2_compile_fail/identity_on_derived_struct.stderr
  • gts-macros/tests/v2_compile_fail/instance_id_wrong_type.rs
  • gts-macros/tests/v2_compile_fail/instance_id_wrong_type.stderr
  • gts-macros/tests/v2_compile_fail/invalid_gts_id.rs
  • gts-macros/tests/v2_compile_fail/invalid_gts_id.stderr
  • gts-macros/tests/v2_compile_fail/missing_description.rs
  • gts-macros/tests/v2_compile_fail/missing_description.stderr
  • gts-macros/tests/v2_compile_fail/missing_dir_path.rs
  • gts-macros/tests/v2_compile_fail/missing_dir_path.stderr
  • gts-macros/tests/v2_compile_fail/missing_identity_annotation.rs
  • gts-macros/tests/v2_compile_fail/missing_identity_annotation.stderr
  • gts-macros/tests/v2_compile_fail/missing_schema_id.rs
  • gts-macros/tests/v2_compile_fail/missing_schema_id.stderr
  • gts-macros/tests/v2_compile_fail/multiple_generics.rs
  • gts-macros/tests/v2_compile_fail/multiple_generics.stderr
  • gts-macros/tests/v2_compile_fail/nested_direct_serialize.rs
  • gts-macros/tests/v2_compile_fail/nested_direct_serialize.stderr
  • gts-macros/tests/v2_compile_fail/nested_direct_serialize_cfg_attr.rs
  • gts-macros/tests/v2_compile_fail/nested_direct_serialize_cfg_attr.stderr
  • gts-macros/tests/v2_compile_fail/root_multi_segment.rs
  • gts-macros/tests/v2_compile_fail/root_multi_segment.stderr
  • gts-macros/tests/v2_compile_fail/tuple_struct.rs
  • gts-macros/tests/v2_compile_fail/tuple_struct.stderr
  • gts-macros/tests/v2_compile_fail/type_field_wrong_type.rs
  • gts-macros/tests/v2_compile_fail/type_field_wrong_type.stderr
  • gts-macros/tests/v2_compile_fail/unknown_field_attr.rs
  • gts-macros/tests/v2_compile_fail/unknown_field_attr.stderr
  • gts-macros/tests/v2_compile_fail/unknown_gts_attr.rs
  • gts-macros/tests/v2_compile_fail/unknown_gts_attr.stderr
  • gts-macros/tests/v2_compile_fail/version_mismatch.rs
  • gts-macros/tests/v2_compile_fail/version_mismatch.stderr
  • gts-macros/tests/v2_compile_fail_tests.rs
  • gts-macros/tests/v2_inheritance_tests.rs
  • gts-macros/tests/v2_inheritance_tests_mixed.rs
  • gts-macros/tests/v2_integration_tests.rs
  • gts-macros/tests/v2_parity_tests.rs
  • gts-macros/tests/v2_serde_rename_tests.rs
  • gts-macros/tests/v2_serialization_tests.rs
✅ Files skipped from review due to trivial changes (31)
  • gts-macros/tests/v2_compile_fail/enum_not_supported.stderr
  • gts-macros/tests/v2_compile_fail/instance_id_wrong_type.stderr
  • gts-macros/tests/v2_compile_fail/invalid_gts_id.stderr
  • gts-macros/tests/v2_compile_fail/tuple_struct.stderr
  • gts-macros/tests/v2_compile_fail/duplicate_type_field.stderr
  • gts-macros/tests/v2_compile_fail/duplicate_instance_id.stderr
  • gts-macros/tests/v2_compile_fail/missing_description.stderr
  • gts-macros/tests/v2_compile_fail/both_type_and_instance.stderr
  • gts-macros/tests/v2_compile_fail/extends_none_multi_segment.stderr
  • gts-macros/tests/v2_compile_fail/extends_parent_no_generic.stderr
  • gts-macros/tests/compile_fail/base_parent_mismatch.stderr
  • gts-macros/tests/v2_compile_fail/missing_identity_annotation.stderr
  • gts-macros/tests/v2_compile_fail/missing_dir_path.stderr
  • gts-macros/tests/v2_compile_fail/type_field_wrong_type.stderr
  • gts-macros/tests/v2_compile_fail/unknown_field_attr.stderr
  • gts-macros/tests/v2_compile_fail/multiple_generics.stderr
  • gts-macros/tests/v2_compile_fail/missing_schema_id.stderr
  • gts-macros/tests/v2_compile_fail/extends_single_segment.stderr
  • gts-macros/tests/v2_compile_fail/unknown_gts_attr.stderr
  • gts-macros/tests/v2_compile_fail/identity_on_derived_struct.stderr
  • gts-macros/tests/v2_compile_fail/version_mismatch.stderr
  • gts-macros/tests/v2_compile_fail/root_multi_segment.stderr
  • gts-macros/tests/v2_compile_fail/invalid_gts_id.rs
  • docs/002-macro-migration-implementation-plan.md
  • gts-macros/tests/v2_compile_fail/nested_direct_serialize.stderr
  • gts-macros/tests/v2_compile_fail/missing_schema_id.rs
  • gts-macros/tests/v2_compile_fail/extends_parent_mismatch.stderr
  • gts-macros/tests/compile_fail/non_gts_generic.stderr
  • gts-macros/tests/v2_compile_fail/nested_direct_serialize_cfg_attr.stderr
  • gts-macros/tests/v2_compile_fail/tuple_struct.rs
  • gts-macros/tests/v2_compile_fail/unknown_gts_attr.rs

Comment thread docs/002-struct-to-gts-schema-migration.md Outdated
Comment thread gts-macros/README.md Outdated
Comment thread gts-macros/src/gts_attrs.rs
Comment thread gts-macros/src/gts_codegen.rs
Comment thread gts-macros/src/gts_codegen.rs
Comment thread gts-macros/src/gts_serde.rs Outdated
Comment thread gts-macros/src/gts_serde.rs
Comment thread gts-macros/src/gts_serde.rs
Comment thread gts-macros/src/gts_validation.rs
Comment thread gts-macros/tests/v2_compile_fail/extends_parent_no_generic.rs
…ct/field attributes

Replaces the prior plan for a future attribute-macro rewrite with a shipped
derive macro. `#[derive(GtsSchema)]` + `#[gts(...)]` helper attributes live
alongside the existing `#[struct_to_gts_schema]` attribute macro; coexistence
is the steady state, deprecation and removal are out of scope.

Highlights:
- Struct-level `#[gts(dir_path, schema_id, description, extends)]` and
  field-level `#[gts(type_field | instance_id | skip)]` attribute grammar.
- Identity-field rule enforced at compile time: every root struct declares
  exactly one of `type_field` / `instance_id`; derived structs (`extends =
  Parent`) declare neither. `extends = None` is an equivalent explicit root
  marker.
- Generated `pub fn new(...)` constructor on every named-field struct.
  `type_field` auto-populated from `GtsSchemaId::new(<P as GtsSchema>::SCHEMA_ID)`
  (generic root) or `Self::gts_schema_id().clone()` (non-generic root);
  `instance_id` passed by the caller.
- Unconditional block on direct `Serialize` / `Deserialize` on derived
  structs via the `GtsNoDirectSerialize` / `GtsNoDirectDeserialize` marker
  traits. No opt-out attribute.
- Spec-correct `"x-gts-ref": "/$id"` on identity fields; generic
  `"x-gts-ref": "gts.*"` retained for other `GtsSchemaId` fields.
- `description` now emitted into runtime schemas via
  `gts_schema_with_refs()`.
- Per-field serde rename in the nested deserializer (replaces the prior
  `rename_all = "snake_case"` blanket).

Docs:
- New ADR (`docs/001-gts-schema-derive-macro-adr.md`).
- Migration guide (`docs/002-struct-to-gts-schema-migration.md`) describing
  the old/new diff, schema-output parity, compile-fail fixture mapping, and
  the coexistence stance.
- Implementation plan (`docs/002-macro-migration-implementation-plan.md`).
- Old `001-macro-proposal.md` / `001-macro-alignment-*.md` superseded and
  removed.
- `gts-macros/README.md` rewritten around the derive macro, with the old
  macro presented as legacy.

Tests (all suites green, ~104 tests + 24 trybuild fixtures):
- `v2_compile_fail/` — compile-time rejection of invalid configurations
  (missing identity, wrong field types, schema-ID format, inheritance
  mismatches, direct-serde on nested, etc.).
- `v2_integration_tests` — runtime API + schema output for base structs,
  incl. generated-constructor behavior.
- `v2_inheritance_tests` — multi-level inheritance chains.
- `v2_serialization_tests` — `Serialize` / `Deserialize` round-trips through
  the `GtsSerialize` bridge.
- `v2_serde_rename_tests` — per-field `#[serde(rename)]` handling.
- `v2_parity_tests` — identical output vs `#[struct_to_gts_schema]` on
  equivalent structs (with the `x-gts-ref: "/$id"` improvement noted
  above).
- `v2_inheritance_tests_mixed` — interop between old and new macros.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Andre Smith <andre.smith@acronis.com>
@asmith987 asmith987 force-pushed the gts-macro-proposal branch from 2d5acfb to 034c776 Compare April 24, 2026 22:26
@asmith987

Copy link
Copy Markdown
Author

@Artifizer I added the implementation of the macro as part of this PR. The implementation considers all of the comments that was made. The README is here. This PR does not include any CLI modifications, but that would be a follow up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants