From e905b3082f4d1e0eec21fabc7d58a137c1e2a945 Mon Sep 17 00:00:00 2001 From: Andre Smith Date: Wed, 4 Mar 2026 14:45:18 -0700 Subject: [PATCH] fix(gts-macros): make GTS type/id field optional and fix deserialization Signed-off-by: Andre Smith --- gts-macros/README.md | 83 +++++++----- gts-macros/src/lib.rs | 126 +++++++++++++----- .../compile_fail/base_parent_mismatch.stderr | 20 +-- .../base_parent_single_segment.stderr | 6 - .../base_struct_both_id_and_type.stderr | 2 +- .../compile_fail/base_struct_missing_id.rs | 19 --- .../base_struct_missing_id.stderr | 5 - .../base_struct_wrong_gts_type.rs | 20 --- .../base_struct_wrong_gts_type.stderr | 5 - .../compile_fail/base_struct_wrong_id_type.rs | 20 --- .../base_struct_wrong_id_type.stderr | 5 - .../base_true_multi_segment.stderr | 2 +- .../gts_field_not_in_properties.rs | 19 +++ .../gts_field_not_in_properties.stderr | 13 ++ .../tests/compile_fail/non_gts_generic.stderr | 95 +++++++++++-- .../version_mismatch_major.stderr | 2 +- ...rsion_mismatch_minor_missing_schema.stderr | 2 +- ...rsion_mismatch_minor_missing_struct.stderr | 2 +- gts-macros/tests/inheritance_tests.rs | 123 ++++++++++++++++- 19 files changed, 400 insertions(+), 169 deletions(-) delete mode 100644 gts-macros/tests/compile_fail/base_struct_missing_id.rs delete mode 100644 gts-macros/tests/compile_fail/base_struct_missing_id.stderr delete mode 100644 gts-macros/tests/compile_fail/base_struct_wrong_gts_type.rs delete mode 100644 gts-macros/tests/compile_fail/base_struct_wrong_gts_type.stderr delete mode 100644 gts-macros/tests/compile_fail/base_struct_wrong_id_type.rs delete mode 100644 gts-macros/tests/compile_fail/base_struct_wrong_id_type.stderr create mode 100644 gts-macros/tests/compile_fail/gts_field_not_in_properties.rs create mode 100644 gts-macros/tests/compile_fail/gts_field_not_in_properties.stderr diff --git a/gts-macros/README.md b/gts-macros/README.md index d3bee97..4e252d0 100644 --- a/gts-macros/README.md +++ b/gts-macros/README.md @@ -112,7 +112,7 @@ pub struct MyStructV1 { ... } | **Property existence** | Every property in the list must exist as a field in the struct | | **Struct type** | Only structs with named fields are supported (no tuple structs) | | **Generic type constraints** | Generic type parameters must implement `GtsSchema` (only `()` or other GTS structs allowed) | -| **Base struct field validation** | Base structs (`base = true`) must have either ID fields OR GTS Type fields, but not both (see below) | +| **Base struct field validation** | Base structs (`base = true`) may optionally have ID fields OR GTS Type fields in `properties`, but not both. GTS fields must be in `properties` if present (see below) | ### Compile Error Examples @@ -193,7 +193,21 @@ error[E0277]: the trait bound `MyStruct: GtsSchema` is not satisfied | ^^^^^^^^^^^^^^^^^^^^^ the trait `GtsSchema` is not implemented for `MyStruct` ``` -**Base struct field validation - ID fields:** +**Base struct without ID or type field (simplest form):** +```rust +#[struct_to_gts_schema( + dir_path = "schemas", + base = true, + schema_id = "gts.x.core.errors.quota_failure.v1~", + description = "Quota failure with one or more violations", + properties = "violations" +)] +pub struct QuotaFailureV1 { + pub violations: Vec, // ✅ No type/id field needed +} +``` + +**Base struct with ID field in properties:** ```rust use gts::gts::GtsInstanceId; @@ -205,13 +219,13 @@ use gts::gts::GtsInstanceId; properties = "id,name" )] pub struct BaseEventTopicV1

{ - pub id: GtsInstanceId, // ✅ Valid ID field + pub id: GtsInstanceId, // ✅ Valid ID field (in properties) pub name: String, pub payload: P, } ``` -**Base struct field validation - GTS Type fields:** +**Base struct with GTS Type field in properties:** ```rust use gts::gts::GtsSchemaId; @@ -223,61 +237,70 @@ use gts::gts::GtsSchemaId; properties = "r#type,name" )] pub struct BaseEventV1

{ - pub id: Uuid, // Event UUID - pub r#type: GtsSchemaId, // Event Type - ✅ Valid GTS Type field + pub id: Uuid, // Event UUID (not a GTS field) + pub r#type: GtsSchemaId, // Event Type - ✅ Valid GTS Type field (in properties) pub name: String, pub payload: P, } ``` -**Invalid base struct - both ID and GTS Type fields:** +**GTS type/id field NOT in properties (compile error):** ```rust #[struct_to_gts_schema( dir_path = "schemas", base = true, - schema_id = "gts.x.core.events.topic.v1~", - description = "Invalid base with both ID and type", - properties = "id,r#type,name" // ❌ Error! Both ID and GTS Type fields + schema_id = "gts.x.core.errors.rate_limit.v1~", + description = "Rate limit error", + properties = "retry_after" )] -pub struct BaseEventV1

{ - pub id: GtsInstanceId, // Event topic ID field - pub r#type: GtsSchemaId, // Event type (schema) ID field - ❌ Cannot have both! +pub struct RateLimitErrorV1 { + pub gts_type: GtsSchemaId, // ❌ Error! Must be in properties or removed + pub retry_after: u64, +} +``` +``` +error: struct_to_gts_schema: Field `gts_type` has type `GtsSchemaId` but is not listed + in `properties`. Either add `gts_type` to the `properties` list, or remove it + from the struct (use `RateLimitErrorV1::gts_schema_id()` or + `RateLimitErrorV1::SCHEMA_ID` to access the schema ID at runtime). ``` -**Invalid base struct - wrong GTS Type field type:** +**Invalid base struct - both ID and GTS Type fields in properties:** ```rust #[struct_to_gts_schema( dir_path = "schemas", base = true, - schema_id = "gts.x.core.events.type.v1~", - description = "Base event with wrong type field", - properties = "r#type,name" + schema_id = "gts.x.core.events.topic.v1~", + description = "Invalid base with both ID and type", + properties = "id,r#type,name" // ❌ Error! Both ID and GTS Type fields in properties )] pub struct BaseEventV1

{ - pub id: Uuid, // Event UUID - pub r#type: String, // Event type (schema) - ❌ Should be GtsSchemaId - pub name: String, - pub payload: P, -} -``` -``` -error: struct_to_gts_schema: Base structs with GTS Type fields must have at least one GTS Type field (type, gts_type, gtsType, or schema) of type GtsSchemaId + pub id: GtsInstanceId, // Event topic ID field + pub r#type: GtsSchemaId, // Event type (schema) ID field - ❌ Cannot have both! ``` ### Base Struct Field Validation Rules -Base structs (`base = true`) must follow **exactly one** of these patterns: +Base structs (`base = true`) may **optionally** include ID or GTS Type fields. When present in `properties`, they are validated: + +#### Option 1: No ID/Type Field +- The simplest form — no ID or type field needed +- Schema ID is always available via `Self::gts_schema_id()` through the `GtsSchema` trait +- Use case: Simple data structs, error types, configuration objects -#### Option 1: ID Fields +#### Option 2: ID Fields (in `properties`) - **Supported field names**: `$id`, `id`, `gts_id`, `gtsId` - **Required type**: `GtsInstanceId` (or `gts::GtsInstanceId`) - **Use case**: Instance-based identification -#### Option 2: GTS Type Fields +#### Option 3: GTS Type Fields (in `properties`) - **Supported field names**: `type`, `r#type`, `gts_type`, `gtsType`, `schema` - **Supported serde renames**: Fields with `#[serde(rename = "type")]`, `#[serde(rename = "gts_type")]`, `#[serde(rename = "gtsType")]`, or `#[serde(rename = "schema")]` - **Required type**: `GtsSchemaId` (or `gts::GtsSchemaId`) -- **Use case**: Schema-based identification +- **Use case**: Schema-based identification / type discriminator + +#### GTS Fields Must Be in Properties +If a GTS type/id field (e.g., `gts_type: GtsSchemaId` or `id: GtsInstanceId`) is present on the struct, it **must** be listed in `properties`. If you don't need the field in JSON, simply remove it from the struct — the schema ID is always accessible via `Self::gts_schema_id()` or `Self::SCHEMA_ID`. **Serde rename example:** ```rust @@ -298,7 +321,7 @@ pub struct BaseEventV1

{ } ``` -**Important**: Base structs cannot have both ID fields AND GTS Type fields. They must choose one approach. +**Important**: Base structs cannot have both ID fields AND GTS Type fields in `properties`. They must choose one approach (or use neither). --- diff --git a/gts-macros/src/lib.rs b/gts-macros/src/lib.rs index d6030cb..3f83f80 100644 --- a/gts-macros/src/lib.rs +++ b/gts-macros/src/lib.rs @@ -235,55 +235,53 @@ fn validate_base_struct_fields( return Ok(()); } - // Check for presence of ID and GTS Type fields (including serde renames) - let has_id_field = fields.iter().any(|f| field_name_matches(f, ID_FIELD_NAMES)); - - let has_type_field = fields.iter().any(|f| { - field_name_matches(f, TYPE_FIELD_NAMES) || has_matching_serde_rename(f, SERDE_TYPE_RENAMES) - }); - - if !has_id_field && !has_type_field { - return Err(syn::Error::new_spanned( - &input.ident, - format!( - "struct_to_gts_schema: Base structs must have either an ID field (one of: {}) OR a GTS Type field (one of: {}), but not both.", - ID_FIELD_NAMES.join(", "), - TYPE_FIELD_NAMES.join(", ") - ), - )); - } + let property_names: Vec = args + .properties + .split(',') + .map(|s| s.trim().to_owned()) + .filter(|s| !s.is_empty()) + .collect(); - // Validate field types - validate_field_types(input, fields) + // Only validate type/id fields that are listed in properties. + // Fields not in properties are treated as regular fields (no GTS validation). + validate_field_types(input, fields, &property_names) } -/// Validate that field types are correct for ID and GTS Type fields +/// Validate that field types are correct for ID and GTS Type fields. +/// Only validates fields that are listed in the `properties` list — fields not in +/// properties are treated as regular fields and not subject to GTS type validation. fn validate_field_types( input: &syn::DeriveInput, fields: &syn::punctuated::Punctuated, + property_names: &[String], ) -> Result<(), syn::Error> { + // Only consider fields that are in the properties list + let is_in_properties = |field: &syn::Field| -> bool { + field + .ident + .as_ref() + .is_some_and(|ident| property_names.contains(&ident.to_string())) + }; + let has_valid_id_field = fields.iter().any(|field| { - field_name_matches(field, ID_FIELD_NAMES) && is_type_gts_instance_id(&field.ty) + is_in_properties(field) + && field_name_matches(field, ID_FIELD_NAMES) + && is_type_gts_instance_id(&field.ty) }); let has_valid_type_field = fields.iter().any(|field| { let is_type_field = field_name_matches(field, TYPE_FIELD_NAMES) || has_matching_serde_rename(field, SERDE_TYPE_RENAMES); - is_type_field && is_type_gts_schema_id(&field.ty) + is_in_properties(field) && is_type_field && is_type_gts_schema_id(&field.ty) }); - // Enforce "either/or but not both" logic + // Enforce "either/or but not both" logic (only for valid GTS fields in properties). + // Fields with recognized names but non-GTS types (e.g., `id: Uuid`) are treated as + // regular fields and don't trigger this check. if has_valid_id_field && has_valid_type_field { return Err(syn::Error::new_spanned( &input.ident, - "struct_to_gts_schema: Base structs must have either an ID field (one of: $id, id, gts_id, or gtsId) of type GtsInstanceId OR a GTS Type field (one of: type, gts_type, gtsType, or schema) of type GtsSchemaId, but not both. Found both valid ID and GTS Type fields.", - )); - } - - if !has_valid_id_field && !has_valid_type_field { - return Err(syn::Error::new_spanned( - &input.ident, - "struct_to_gts_schema: Base structs must have either an ID field (one of: $id, id, gts_id, or gtsId) of type GtsInstanceId OR a GTS Type field (one of: type, gts_type, gtsType, or schema) of type GtsSchemaId", + "struct_to_gts_schema: Base structs cannot have both an ID field (GtsInstanceId) and a GTS Type field (GtsSchemaId) in properties. Found both valid ID and GTS Type fields.", )); } @@ -590,6 +588,65 @@ fn add_gts_serde_attrs(input: &mut syn::DeriveInput, base: &BaseAttr) { } } +/// Validate that GTS type/id fields (e.g., `gts_type: GtsSchemaId`, `id: GtsInstanceId`) are +/// listed in `properties` when present on a base struct. +/// +/// If a struct has a recognized GTS field but doesn't list it in `properties`, it's likely +/// a mistake — the macro already provides `SCHEMA_ID` and `gts_schema_id()` for accessing +/// the schema ID at runtime. Fields not in properties would be invisible to serde, so there's +/// no reason to declare them on the struct. +fn validate_gts_fields_in_properties( + input: &syn::DeriveInput, + base: &BaseAttr, + properties: &str, +) -> Result<(), syn::Error> { + if !matches!(base, BaseAttr::IsBase) { + return Ok(()); + } + + let property_names: Vec = properties + .split(',') + .map(|s| s.trim().to_owned()) + .filter(|s| !s.is_empty()) + .collect(); + + if let syn::Data::Struct(ref data_struct) = input.data + && let syn::Fields::Named(ref fields) = data_struct.fields + { + for field in &fields.named { + let Some(ident) = &field.ident else { + continue; + }; + let field_name = ident.to_string(); + + let is_gts_id_field = + field_name_matches(field, ID_FIELD_NAMES) && is_type_gts_instance_id(&field.ty); + let is_gts_type_field = (field_name_matches(field, TYPE_FIELD_NAMES) + || has_matching_serde_rename(field, SERDE_TYPE_RENAMES)) + && is_type_gts_schema_id(&field.ty); + + if (is_gts_id_field || is_gts_type_field) && !property_names.contains(&field_name) { + let type_desc = if is_gts_type_field { + "GtsSchemaId" + } else { + "GtsInstanceId" + }; + return Err(syn::Error::new_spanned( + ident, + format!( + "struct_to_gts_schema: Field `{field_name}` has type `{type_desc}` but is not listed in `properties`. \ + Either add `{field_name}` to the `properties` list, or remove it from the struct \ + (use `{}::gts_schema_id()` or `{}::SCHEMA_ID` to access the schema ID at runtime).", + input.ident, input.ident + ), + )); + } + } + } + + Ok(()) +} + /// Build a custom where clause with additional trait bounds on generic params fn build_where_clause( generics: &syn::Generics, @@ -953,6 +1010,13 @@ pub fn struct_to_gts_schema(attr: TokenStream, item: TokenStream) -> TokenStream // For base structs with generic fields, add serde attributes for GtsSerialize/GtsDeserialize add_gts_serde_attrs(&mut modified_input, &args.base); + // Validate that GTS type/id fields are listed in properties when present + if let Err(err) = + validate_gts_fields_in_properties(&modified_input, &args.base, &args.properties) + { + return err.to_compile_error().into(); + } + // Validate base attribute consistency with schema_id segments if let Err(err) = validate_base_segments(&input, &args.base, &args.schema_id) { return err.to_compile_error().into(); diff --git a/gts-macros/tests/compile_fail/base_parent_mismatch.stderr b/gts-macros/tests/compile_fail/base_parent_mismatch.stderr index 67fd712..df112e0 100644 --- a/gts-macros/tests/compile_fail/base_parent_mismatch.stderr +++ b/gts-macros/tests/compile_fail/base_parent_mismatch.stderr @@ -1,11 +1,11 @@ -error: struct_to_gts_schema: Base structs must have either an ID field (one of: $id, id, gts_id, or gtsId) of type GtsInstanceId OR a GTS Type field (one of: type, gts_type, gtsType, or schema) of type GtsSchemaId - --> tests/compile_fail/base_parent_mismatch.rs:15:12 +error[E0080]: evaluation panicked: struct_to_gts_schema: Base struct 'BaseEventV1' schema ID must match parent segment 'gts.x.wrong.parent.type.v1~' from schema_id + --> tests/compile_fail/base_parent_mismatch.rs:23:1 | -15 | pub struct BaseEventV1

{ - | ^^^^^^^^^^^ - -error[E0412]: cannot find type `BaseEventV1` in this scope - --> tests/compile_fail/base_parent_mismatch.rs:25:12 - | -25 | base = BaseEventV1, - | ^^^^^^^^^^^ not found in this scope +23 | / #[struct_to_gts_schema( +24 | | dir_path = "schemas", +25 | | base = BaseEventV1, +26 | | schema_id = "gts.x.wrong.parent.type.v1~x.core.audit.event.v1~", +27 | | description = "This should fail", +28 | | properties = "user_id" +29 | | )] + | |__^ evaluation of `_::_` failed here diff --git a/gts-macros/tests/compile_fail/base_parent_single_segment.stderr b/gts-macros/tests/compile_fail/base_parent_single_segment.stderr index af66f91..a0921b7 100644 --- a/gts-macros/tests/compile_fail/base_parent_single_segment.stderr +++ b/gts-macros/tests/compile_fail/base_parent_single_segment.stderr @@ -1,9 +1,3 @@ -error: struct_to_gts_schema: Base structs must have either an ID field (one of: $id, id, gts_id, or gtsId) of type GtsInstanceId OR a GTS Type field (one of: type, gts_type, gtsType, or schema) of type GtsSchemaId - --> tests/compile_fail/base_parent_single_segment.rs:15:12 - | -15 | pub struct BaseEventV1

{ - | ^^^^^^^^^^^ - error: struct_to_gts_schema: 'base' specifies a parent struct but schema_id 'gts.x.core.audit.event.v1~' has only 1 segment. A child type must have at least 2 segments. Either use 'base = true' or add parent segment to schema_id. --> tests/compile_fail/base_parent_single_segment.rs:29:12 | diff --git a/gts-macros/tests/compile_fail/base_struct_both_id_and_type.stderr b/gts-macros/tests/compile_fail/base_struct_both_id_and_type.stderr index f0ae1ac..03e9a77 100644 --- a/gts-macros/tests/compile_fail/base_struct_both_id_and_type.stderr +++ b/gts-macros/tests/compile_fail/base_struct_both_id_and_type.stderr @@ -1,4 +1,4 @@ -error: struct_to_gts_schema: Base structs must have either an ID field (one of: $id, id, gts_id, or gtsId) of type GtsInstanceId OR a GTS Type field (one of: type, gts_type, gtsType, or schema) of type GtsSchemaId, but not both. Found both valid ID and GTS Type fields. +error: struct_to_gts_schema: Base structs cannot have both an ID field (GtsInstanceId) and a GTS Type field (GtsSchemaId) in properties. Found both valid ID and GTS Type fields. --> tests/compile_fail/base_struct_both_id_and_type.rs:14:12 | 14 | pub struct TopicV1BothIdAndTypeV1

{ diff --git a/gts-macros/tests/compile_fail/base_struct_missing_id.rs b/gts-macros/tests/compile_fail/base_struct_missing_id.rs deleted file mode 100644 index 314e8aa..0000000 --- a/gts-macros/tests/compile_fail/base_struct_missing_id.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Test: Base struct missing required ID property (id, gts_id, or gtsId) - -use gts_macros::struct_to_gts_schema; - -#[struct_to_gts_schema( - dir_path = "schemas", - base = true, - schema_id = "gts.x.core.events.topic.v1~", - description = "Base topic type definition", - properties = "name,description" -)] -#[derive(Debug)] -pub struct TopicV1

{ - pub name: String, - pub description: Option, - pub config: P, -} - -fn main() {} diff --git a/gts-macros/tests/compile_fail/base_struct_missing_id.stderr b/gts-macros/tests/compile_fail/base_struct_missing_id.stderr deleted file mode 100644 index 172980d..0000000 --- a/gts-macros/tests/compile_fail/base_struct_missing_id.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: struct_to_gts_schema: Base structs must have either an ID field (one of: $id, id, gts_id, gtsId) OR a GTS Type field (one of: type, r#type, gts_type, gtsType, schema), but not both. - --> tests/compile_fail/base_struct_missing_id.rs:13:12 - | -13 | pub struct TopicV1

{ - | ^^^^^^^ diff --git a/gts-macros/tests/compile_fail/base_struct_wrong_gts_type.rs b/gts-macros/tests/compile_fail/base_struct_wrong_gts_type.rs deleted file mode 100644 index fbd3ede..0000000 --- a/gts-macros/tests/compile_fail/base_struct_wrong_gts_type.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! Test: Base struct with wrong GTS Type field type should fail compilation - -use gts_macros::struct_to_gts_schema; - -#[struct_to_gts_schema( - dir_path = "schemas", - base = true, - schema_id = "gts.x.core.events.topic.v1~", - description = "Base topic type definition with wrong GTS Type", - properties = "r#type,name,description" -)] -#[derive(Debug)] -pub struct TopicV1WrongGtsType

{ - pub r#type: String, // This should be GtsSchemaId, not String - pub name: String, - pub description: Option, - pub config: P, -} - -fn main() {} diff --git a/gts-macros/tests/compile_fail/base_struct_wrong_gts_type.stderr b/gts-macros/tests/compile_fail/base_struct_wrong_gts_type.stderr deleted file mode 100644 index 5371696..0000000 --- a/gts-macros/tests/compile_fail/base_struct_wrong_gts_type.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: struct_to_gts_schema: Base structs must have either an ID field (one of: $id, id, gts_id, or gtsId) of type GtsInstanceId OR a GTS Type field (one of: type, gts_type, gtsType, or schema) of type GtsSchemaId - --> tests/compile_fail/base_struct_wrong_gts_type.rs:13:12 - | -13 | pub struct TopicV1WrongGtsType

{ - | ^^^^^^^^^^^^^^^^^^^ diff --git a/gts-macros/tests/compile_fail/base_struct_wrong_id_type.rs b/gts-macros/tests/compile_fail/base_struct_wrong_id_type.rs deleted file mode 100644 index afae8db..0000000 --- a/gts-macros/tests/compile_fail/base_struct_wrong_id_type.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! Test: Base struct with wrong ID field type should fail compilation - -use gts_macros::struct_to_gts_schema; - -#[struct_to_gts_schema( - dir_path = "schemas", - base = true, - schema_id = "gts.x.core.events.topic.v1~", - description = "Base topic type definition with wrong ID type", - properties = "id,name,description" -)] -#[derive(Debug)] -pub struct TopicV1WrongIdType

{ - pub id: String, // This should be GtsInstanceId, not String - pub name: String, - pub description: Option, - pub config: P, -} - -fn main() {} diff --git a/gts-macros/tests/compile_fail/base_struct_wrong_id_type.stderr b/gts-macros/tests/compile_fail/base_struct_wrong_id_type.stderr deleted file mode 100644 index 27fbd15..0000000 --- a/gts-macros/tests/compile_fail/base_struct_wrong_id_type.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: struct_to_gts_schema: Base structs must have either an ID field (one of: $id, id, gts_id, or gtsId) of type GtsInstanceId OR a GTS Type field (one of: type, gts_type, gtsType, or schema) of type GtsSchemaId - --> tests/compile_fail/base_struct_wrong_id_type.rs:13:12 - | -13 | pub struct TopicV1WrongIdType

{ - | ^^^^^^^^^^^^^^^^^^ diff --git a/gts-macros/tests/compile_fail/base_true_multi_segment.stderr b/gts-macros/tests/compile_fail/base_true_multi_segment.stderr index b162ff9..464fb22 100644 --- a/gts-macros/tests/compile_fail/base_true_multi_segment.stderr +++ b/gts-macros/tests/compile_fail/base_true_multi_segment.stderr @@ -1,4 +1,4 @@ -error: struct_to_gts_schema: Base structs must have either an ID field (one of: $id, id, gts_id, or gtsId) of type GtsInstanceId OR a GTS Type field (one of: type, gts_type, gtsType, or schema) of type GtsSchemaId +error: struct_to_gts_schema: 'base = true' but schema_id 'gts.x.core.events.type.v1~x.core.audit.event.v1~' has 2 segments. A base type must have exactly 1 segment (no parent). Either use 'base = ParentStruct' or fix the schema_id. --> tests/compile_fail/base_true_multi_segment.rs:13:12 | 13 | pub struct InvalidBaseV1 { diff --git a/gts-macros/tests/compile_fail/gts_field_not_in_properties.rs b/gts-macros/tests/compile_fail/gts_field_not_in_properties.rs new file mode 100644 index 0000000..9113d46 --- /dev/null +++ b/gts-macros/tests/compile_fail/gts_field_not_in_properties.rs @@ -0,0 +1,19 @@ +//! Test: GTS type/id field present on struct but not listed in properties should fail + +use gts_macros::struct_to_gts_schema; +use gts::gts::GtsSchemaId; + +#[struct_to_gts_schema( + dir_path = "schemas", + base = true, + schema_id = "gts.x.core.errors.rate_limit.v1~", + description = "Rate limit error with gts_type not in properties", + properties = "retry_after" +)] +#[derive(Debug)] +pub struct RateLimitErrorV1 { + pub gts_type: GtsSchemaId, + pub retry_after: u64, +} + +fn main() {} diff --git a/gts-macros/tests/compile_fail/gts_field_not_in_properties.stderr b/gts-macros/tests/compile_fail/gts_field_not_in_properties.stderr new file mode 100644 index 0000000..dd2cbd5 --- /dev/null +++ b/gts-macros/tests/compile_fail/gts_field_not_in_properties.stderr @@ -0,0 +1,13 @@ +error: struct_to_gts_schema: Field `gts_type` has type `GtsSchemaId` but is not listed in `properties`. Either add `gts_type` to the `properties` list, or remove it from the struct (use `RateLimitErrorV1::gts_schema_id()` or `RateLimitErrorV1::SCHEMA_ID` to access the schema ID at runtime). + --> tests/compile_fail/gts_field_not_in_properties.rs:15:9 + | +15 | pub gts_type: GtsSchemaId, + | ^^^^^^^^ + +warning: unused import: `gts::gts::GtsSchemaId` + --> tests/compile_fail/gts_field_not_in_properties.rs:4:5 + | +4 | use gts::gts::GtsSchemaId; + | ^^^^^^^^^^^^^^^^^^^^^ + | + = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default diff --git a/gts-macros/tests/compile_fail/non_gts_generic.stderr b/gts-macros/tests/compile_fail/non_gts_generic.stderr index 4b62aa0..7b9dac3 100644 --- a/gts-macros/tests/compile_fail/non_gts_generic.stderr +++ b/gts-macros/tests/compile_fail/non_gts_generic.stderr @@ -1,17 +1,88 @@ -error: struct_to_gts_schema: Base structs must have either an ID field (one of: $id, id, gts_id, or gtsId) of type GtsInstanceId OR a GTS Type field (one of: type, gts_type, gtsType, or schema) of type GtsSchemaId - --> tests/compile_fail/non_gts_generic.rs:17:12 - | -17 | pub struct BaseEventV1

{ - | ^^^^^^^^^^^ - -error[E0412]: cannot find type `BaseEventV1` in this scope +error[E0277]: the trait bound `MyStruct: GtsSchema` is not satisfied --> tests/compile_fail/non_gts_generic.rs:31:17 | 31 | let _event: BaseEventV1 = BaseEventV1 { - | ^^^^^^^^^^^ not found in this scope + | ^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound + | +help: the trait `GtsSchema` is not implemented for `MyStruct` + --> tests/compile_fail/non_gts_generic.rs:24:1 + | +24 | pub struct MyStruct { + | ^^^^^^^^^^^^^^^^^^^ +help: the following other types implement trait `GtsSchema` + --> tests/compile_fail/non_gts_generic.rs:9:1 + | + 9 | / #[struct_to_gts_schema( +10 | | dir_path = "schemas", +11 | | base = true, +12 | | schema_id = "gts.x.core.events.type.v1~", +13 | | description = "Base event type", +14 | | properties = "id,payload" +15 | | )] + | |__^ `BaseEventV1

` + | + ::: $WORKSPACE/gts/src/schema.rs + | + | impl GtsSchema for () { + | ^^^^^^^^^^^^^^^^^^^^^ `()` +note: required by a bound in `BaseEventV1` + --> tests/compile_fail/non_gts_generic.rs:9:1 + | + 9 | / #[struct_to_gts_schema( +10 | | dir_path = "schemas", +11 | | base = true, +12 | | schema_id = "gts.x.core.events.type.v1~", +13 | | description = "Base event type", +14 | | properties = "id,payload" +15 | | )] + | |__^ required by this bound in `BaseEventV1` +16 | #[derive(Debug)] +17 | pub struct BaseEventV1

{ + | ----------- required by a bound in this struct + = note: this error originates in the attribute macro `struct_to_gts_schema` (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0422]: cannot find struct, variant or union type `BaseEventV1` in this scope - --> tests/compile_fail/non_gts_generic.rs:31:41 +error[E0277]: the trait bound `MyStruct: GtsSchema` is not satisfied + --> tests/compile_fail/non_gts_generic.rs:33:18 | -31 | let _event: BaseEventV1 = BaseEventV1 { - | ^^^^^^^^^^^ not found in this scope +33 | payload: MyStruct { + | __________________^ +34 | | some_id: "123".to_string(), +35 | | }, + | |_________^ unsatisfied trait bound + | +help: the trait `GtsSchema` is not implemented for `MyStruct` + --> tests/compile_fail/non_gts_generic.rs:24:1 + | +24 | pub struct MyStruct { + | ^^^^^^^^^^^^^^^^^^^ +help: the following other types implement trait `GtsSchema` + --> tests/compile_fail/non_gts_generic.rs:9:1 + | + 9 | / #[struct_to_gts_schema( +10 | | dir_path = "schemas", +11 | | base = true, +12 | | schema_id = "gts.x.core.events.type.v1~", +13 | | description = "Base event type", +14 | | properties = "id,payload" +15 | | )] + | |__^ `BaseEventV1

` + | + ::: $WORKSPACE/gts/src/schema.rs + | + | impl GtsSchema for () { + | ^^^^^^^^^^^^^^^^^^^^^ `()` +note: required by a bound in `BaseEventV1` + --> tests/compile_fail/non_gts_generic.rs:9:1 + | + 9 | / #[struct_to_gts_schema( +10 | | dir_path = "schemas", +11 | | base = true, +12 | | schema_id = "gts.x.core.events.type.v1~", +13 | | description = "Base event type", +14 | | properties = "id,payload" +15 | | )] + | |__^ required by this bound in `BaseEventV1` +16 | #[derive(Debug)] +17 | pub struct BaseEventV1

{ + | ----------- required by a bound in this struct + = note: this error originates in the attribute macro `struct_to_gts_schema` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/gts-macros/tests/compile_fail/version_mismatch_major.stderr b/gts-macros/tests/compile_fail/version_mismatch_major.stderr index 8524a2e..7407a27 100644 --- a/gts-macros/tests/compile_fail/version_mismatch_major.stderr +++ b/gts-macros/tests/compile_fail/version_mismatch_major.stderr @@ -1,4 +1,4 @@ -error: struct_to_gts_schema: Base structs must have either an ID field (one of: $id, id, gts_id, or gtsId) of type GtsInstanceId OR a GTS Type field (one of: type, gts_type, gtsType, or schema) of type GtsSchemaId +error: struct_to_gts_schema: Version mismatch between struct name and schema_id. Struct 'BaseEventV2' has version suffix 'V2' but schema_id 'gts.x.core.events.type.v1~' has version 'v1'. The versions must match exactly (e.g., BaseEventV1 with v1~, or BaseEventV2_0 with v2.0~) --> tests/compile_fail/version_mismatch_major.rs:13:12 | 13 | pub struct BaseEventV2 { diff --git a/gts-macros/tests/compile_fail/version_mismatch_minor_missing_schema.stderr b/gts-macros/tests/compile_fail/version_mismatch_minor_missing_schema.stderr index d38d9c6..7f30b7b 100644 --- a/gts-macros/tests/compile_fail/version_mismatch_minor_missing_schema.stderr +++ b/gts-macros/tests/compile_fail/version_mismatch_minor_missing_schema.stderr @@ -1,4 +1,4 @@ -error: struct_to_gts_schema: Base structs must have either an ID field (one of: $id, id, gts_id, or gtsId) of type GtsInstanceId OR a GTS Type field (one of: type, gts_type, gtsType, or schema) of type GtsSchemaId +error: struct_to_gts_schema: Version mismatch between struct name and schema_id. Struct 'BaseEventV3_0' has version suffix 'V3_0' but schema_id 'gts.x.core.events.type.v3~' has version 'v3'. The versions must match exactly (e.g., BaseEventV1 with v1~, or BaseEventV2_0 with v2.0~) --> tests/compile_fail/version_mismatch_minor_missing_schema.rs:13:12 | 13 | pub struct BaseEventV3_0 { diff --git a/gts-macros/tests/compile_fail/version_mismatch_minor_missing_struct.stderr b/gts-macros/tests/compile_fail/version_mismatch_minor_missing_struct.stderr index d871cba..75b4996 100644 --- a/gts-macros/tests/compile_fail/version_mismatch_minor_missing_struct.stderr +++ b/gts-macros/tests/compile_fail/version_mismatch_minor_missing_struct.stderr @@ -1,4 +1,4 @@ -error: struct_to_gts_schema: Base structs must have either an ID field (one of: $id, id, gts_id, or gtsId) of type GtsInstanceId OR a GTS Type field (one of: type, gts_type, gtsType, or schema) of type GtsSchemaId +error: struct_to_gts_schema: Version mismatch between struct name and schema_id. Struct 'BaseEventV2' has version suffix 'V2' but schema_id 'gts.x.core.events.type.v2.2~' has version 'v2.2'. The versions must match exactly (e.g., BaseEventV1 with v1~, or BaseEventV2_0 with v2.0~) --> tests/compile_fail/version_mismatch_minor_missing_struct.rs:13:12 | 13 | pub struct BaseEventV2 { diff --git a/gts-macros/tests/inheritance_tests.rs b/gts-macros/tests/inheritance_tests.rs index 8ce351a..ac4c892 100644 --- a/gts-macros/tests/inheritance_tests.rs +++ b/gts-macros/tests/inheritance_tests.rs @@ -166,7 +166,7 @@ Chained inheritance w/o new attributes base = true, schema_id = "gts.x.core.events.topic.v1~", description = "Base topic type definition", - properties = "name,description" + properties = "id,name,description" )] #[derive(Debug)] pub struct TopicV1

{ @@ -227,6 +227,38 @@ The macro automatically generates: No more manual schema implementation needed! ============================================================ */ +/* ============================================================ +Issue #72: Base struct without type/id field +============================================================ */ + +#[struct_to_gts_schema( + dir_path = "schemas", + base = true, + schema_id = "gts.x.core.errors.quota_failure.v1~", + description = "Quota failure with one or more violations", + properties = "violations" +)] +#[derive(Debug, Clone)] +pub struct QuotaFailureV1 { + pub violations: Vec, +} + +/* ============================================================ +Issue #72: Base struct without gts_type field +============================================================ */ + +#[struct_to_gts_schema( + dir_path = "schemas", + base = true, + schema_id = "gts.x.core.errors.rate_limit.v1~", + description = "Rate limit error without gts_type field", + properties = "retry_after" +)] +#[derive(Debug, Clone)] +pub struct RateLimitErrorV1 { + pub retry_after: u64, +} + /* ============================================================ Demo ============================================================ */ @@ -2754,4 +2786,93 @@ mod tests { "inner_data.properties should have 'content_value'" ); } + + /* ============================================================ + Issue #72: gts_type field blocks Deserialize + ============================================================ */ + + #[test] + fn test_base_struct_without_type_or_id_field() { + // QuotaFailureV1 has no type/id field at all — should compile and work + let quota = QuotaFailureV1 { + violations: vec!["exceeded".to_string()], + }; + + // Serialization should work + let json = serde_json::to_string("a).unwrap(); + assert!(json.contains("exceeded")); + + // Round-trip deserialization should work + let deserialized: QuotaFailureV1 = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.violations, vec!["exceeded".to_string()]); + + // Schema ID should still be accessible via trait + assert_eq!( + QuotaFailureV1::gts_schema_id().as_ref(), + "gts.x.core.errors.quota_failure.v1~" + ); + } + + #[test] + fn test_base_struct_without_gts_type_field() { + // RateLimitErrorV1 has no gts_type field — schema ID is accessed via generated methods + let error = RateLimitErrorV1 { retry_after: 30 }; + + // Serialization works normally + let json_value = serde_json::to_value(&error).unwrap(); + assert_eq!(json_value["retry_after"], 30); + assert!(json_value.get("gts_type").is_none()); + + // Deserialization works normally + let json_str = r#"{"retry_after": 60}"#; + let deserialized: RateLimitErrorV1 = serde_json::from_str(json_str).unwrap(); + assert_eq!(deserialized.retry_after, 60); + + // Schema ID is still accessible via the generated method + assert_eq!( + RateLimitErrorV1::gts_schema_id().as_ref(), + "gts.x.core.errors.rate_limit.v1~" + ); + } + + #[test] + fn test_gts_type_in_properties_preserved() { + // TopicV1WithGtsTypeV1 has gts_type IN properties — should be serialized normally + let topic = TopicV1WithGtsTypeV1:: { + gts_type: GtsSchemaId::new("gts.x.core.events.topic.v1~"), + name: "orders".to_string(), + description: None, + config: OrderTopicConfigV1, + }; + + let json_value = serde_json::to_value(&topic).unwrap(); + assert!( + json_value.get("gts_type").is_some(), + "gts_type should be present when listed in properties" + ); + assert_eq!(json_value["gts_type"], "gts.x.core.events.topic.v1~"); + } + + #[test] + fn test_id_uuid_field_not_treated_as_gts_id() { + // BaseEventV1 has `id: Uuid` which should NOT be treated as a GTS ID field. + // It should compile and serialize normally without serde skip. + let event = BaseEventV1:: { + event_type: SimplePayloadV1::gts_schema_id().clone(), + id: uuid::Uuid::new_v4(), + tenant_id: uuid::Uuid::new_v4(), + sequence_id: 1, + payload: SimplePayloadV1 { + message: "test".to_string(), + severity: 5, + }, + }; + + let json_value = serde_json::to_value(&event).unwrap(); + // id should be present (it's a regular Uuid field, not GtsInstanceId) + assert!( + json_value.get("id").is_some(), + "id: Uuid should be serialized normally" + ); + } }