gts-macros

Crates.iogts-macros
lib.rsgts-macros
version0.1.0
created_at2026-01-23 19:58:37.321507+00
updated_at2026-01-23 19:58:37.321507+00
descriptionProcedural macros for GTS schema generation
homepage
repositoryhttps://github.com/GlobalTypeSystem/gts-rust
max_upload_size
id2065372
size321,659
Artifizer (Artifizer)

documentation

README

GTS Macros

Procedural macros for GTS (Global Type System) schema generation from Rust structs.

Overview

The #[struct_to_gts_schema] attribute macro serves three purposes:

  1. Compile-Time Validation - Catches configuration errors before runtime
  2. Schema Generation - Enables CLI-based JSON Schema file generation
  3. Runtime API - Provides schema access, instance ID generation, and schema composition capabilities at runtime

Installation

Add to your Cargo.toml:

[dependencies]
gts-macros = { path = "path/to/gts-rust/gts-macros" }
serde = { version = "1.0", features = ["derive"] }

Quick Start

use gts_macros::struct_to_gts_schema;
use uuid::Uuid;
use gts::gts::{GtsInstanceId, GtsSchemaId};

// Base event type (root of the hierarchy)
// Note: #[derive(Serialize, Deserialize, JsonSchema)] is added automatically!
#[derive(Debug)]
#[struct_to_gts_schema(
    dir_path = "schemas",
    base = true,
    schema_id = "gts.x.core.events.type.v1~",
    description = "Base event type with common fields",
    properties = "id,tenant_id,payload"
)]
pub struct BaseEventV1<P> {
    pub id: Uuid,
    pub r#type: GtsSchemaId,
    pub tenant_id: Uuid,
    pub payload: P,
}

// Audit event that inherits from BaseEventV1
#[derive(Debug)]
#[struct_to_gts_schema(
    dir_path = "schemas",
    base = BaseEventV1,
    schema_id = "gts.x.core.events.type.v1~x.core.audit.event.v1~",
    description = "Audit event with user context",
    properties = "user_id,action"
)]
pub struct AuditEventV1 {
    pub user_id: Uuid,
    pub action: String,
}

// Runtime usage:
fn example() {
    // Access schema IDs
    let base_id = BaseEventV1::<()>::gts_schema_id();
    let audit_id = AuditEventV1::gts_schema_id();

    // Get schemas as JSON
    let base_schema = BaseEventV1::<()>::gts_schema_with_refs_as_string_pretty();
    let audit_schema = AuditEventV1::gts_schema_with_refs_as_string_pretty();

    // Generate instance IDs
    let event_id = AuditEventV1::gts_make_instance_id("evt-12345.v1");
    assert_eq!(event_id.as_ref(), "gts.x.core.events.type.v1~x.core.audit.event.v1~evt-12345.v1");
}

Purpose 1: Compile-Time Validation

The macro validates your annotations at compile time, catching errors early.

Automatic Derives

The macro automatically adds these derives to your struct:

  • serde::Serialize
  • serde::Deserialize
  • schemars::JsonSchema

Do NOT add these derives manually - they will conflict. You can add other derives like Debug, Clone, etc.

// ✅ Correct - only add Debug, Clone, etc.
#[derive(Debug, Clone)]
#[struct_to_gts_schema(...)]
pub struct MyStructV1 { ... }

// ❌ Wrong - will cause duplicate implementation errors
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[struct_to_gts_schema(...)]
pub struct MyStructV1 { ... }

What Gets Validated

Check Description
Required parameters All of dir_path, base, schema_id, description, properties must be present
Base consistency base = true requires single-segment schema_id; base = Parent requires multi-segment
Parent schema match When base = Parent, Parent's SCHEMA_ID must match the parent segment in schema_id
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)

Compile Error Examples

Missing property:

#[struct_to_gts_schema(
    dir_path = "schemas",
    base = true,
    schema_id = "gts.x.core.events.type.v1~",
    description = "Base event",
    properties = "id,nonexistent"  // ❌ Error!
)]
pub struct BaseEventV1<P> {
    pub id: Uuid,
    pub r#type: GtsSchemaId,
    pub payload: P,
}
error: struct_to_gts_schema: Property 'nonexistent' not found in struct.
       Available fields: ["id", "payload"]

Base mismatch (base = true with multi-segment schema_id):

#[struct_to_gts_schema(
    dir_path = "schemas",
    base = true,  // ❌ Error! base = true requires single-segment
    schema_id = "gts.x.core.events.type.v1~x.core.audit.event.v1~",
    description = "Audit event",
    properties = "user_id"
)]
pub struct AuditEventV1 { /* ... */ }
error: struct_to_gts_schema: base = true requires single-segment schema_id,
       but found 2 segments

Parent schema ID mismatch:

#[struct_to_gts_schema(
    dir_path = "schemas",
    base = WrongParent,  // ❌ Error! Parent's SCHEMA_ID doesn't match
    schema_id = "gts.x.core.events.type.v1~x.core.audit.event.v1~",
    description = "Audit event",
    properties = "user_id"
)]
pub struct AuditEventV1 { /* ... */ }
error: struct_to_gts_schema: Base struct 'WrongParent' schema ID must match
       parent segment 'gts.x.core.events.type.v1~' from schema_id

Tuple struct:

#[struct_to_gts_schema(/* ... */)]
pub struct Data(String);  // ❌ Tuple struct not supported
error: struct_to_gts_schema: Only structs with named fields are supported

Non-GTS struct as generic argument:

// Regular struct without struct_to_gts_schema
pub struct MyStruct { pub some_id: String }

// Using it as generic argument fails
let event: BaseEventV1<MyStruct> = BaseEventV1 { /* ... */ };  // ❌ Error!
error[E0277]: the trait bound `MyStruct: GtsSchema` is not satisfied
  --> src/main.rs:10:17
   |
10 |     let event: BaseEventV1<MyStruct> = BaseEventV1 { ... };
   |                ^^^^^^^^^^^^^^^^^^^^^ the trait `GtsSchema` is not implemented for `MyStruct`

Base struct field validation - ID fields:

use gts::gts::GtsInstanceId;

#[struct_to_gts_schema(
    dir_path = "schemas",
    base = true,
    schema_id = "gts.x.core.events.topic.v1~",
    description = "Base event with ID field",
    properties = "id,name"
)]
pub struct BaseEventTopicV1<P> {
    pub id: GtsInstanceId,  // ✅ Valid ID field
    pub name: String,
    pub payload: P,
}

Base struct field validation - GTS Type fields:

use gts::gts::GtsSchemaId;

#[struct_to_gts_schema(
    dir_path = "schemas",
    base = true,
    schema_id = "gts.x.core.events.type.v1~",
    description = "Base event with type field",
    properties = "r#type,name"
)]
pub struct BaseEventV1<P> {
    pub id: Uuid,             // Event UUID
    pub r#type: GtsSchemaId,  // Event Type - ✅ Valid GTS Type field
    pub name: String,
    pub payload: P,
}

Invalid base struct - both ID and GTS Type fields:

#[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
)]
pub struct BaseEventV1<P> {
    pub id: GtsInstanceId,     // Event topic ID field
    pub r#type: GtsSchemaId,   // Event type (schema) ID field - ❌ Cannot have both!

Invalid base struct - wrong GTS Type field type:

#[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"
)]
pub struct BaseEventV1<P> {
    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

Base Struct Field Validation Rules

Base structs (base = true) must follow exactly one of these patterns:

Option 1: ID Fields

  • Supported field names: $id, id, gts_id, gtsId
  • Required type: GtsInstanceId (or gts::GtsInstanceId)
  • Use case: Instance-based identification

Option 2: GTS Type Fields

  • 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

Serde rename example:

#[struct_to_gts_schema(
    dir_path = "schemas",
    base = true,
    schema_id = "gts.x.core.events.type.v1~",
    description = "Base event with serde(rename = \"type\")",
    properties = "event_type,id,tenant_id,sequence_id,payload"
)]
pub struct BaseEventV1<P> {
    #[serde(rename = "type")]
    pub event_type: GtsSchemaId,  // ✅ Valid - renamed to "type"
    pub id: Uuid,
    pub tenant_id: Uuid,
    pub sequence_id: u64,
    pub payload: P,
}

Important: Base structs cannot have both ID fields AND GTS Type fields. They must choose one approach.


Purpose 2: Schema Generation

Generate JSON Schema files using the GTS CLI tool.

Generate Schemas

# Using paths from macro (relative to source files)
gts generate-from-rust --source src/

# Override output directory
gts generate-from-rust --source src/ --output schemas/

# Exclude specific directories (can be used multiple times)
gts generate-from-rust --source . --exclude "tests/*" --exclude "examples/*"

# Using cargo
cargo run --bin gts -- generate-from-rust --source src/

Excluding Files

The CLI provides multiple ways to exclude files from scanning:

1. --exclude option (supports glob patterns):

gts generate-from-rust --source . --exclude "tests/*" --exclude "benches/*"

2. Auto-ignored directories: The following directories are automatically skipped:

  • compile_fail/ - trybuild compile-fail tests

3. // gts:ignore directive: Add this comment at the top of any .rs file:

// gts:ignore
//! This file will be skipped by the CLI

use gts_macros::struct_to_gts_schema;
// ...

What the CLI Does

  1. Scans source files for #[struct_to_gts_schema] annotations
  2. Extracts metadata (schema_id, description, properties)
  3. Maps Rust types to JSON Schema types
  4. Generates valid JSON Schema files at the specified dir_path/<schema_id>.schema.json

Generated Schema Examples

Base event type (schemas/gts.x.core.events.type.v1~.schema.json):

{
  "$id": "gts://gts.x.core.events.type.v1~",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "BaseEventV1",
  "type": "object",
  "description": "Base event type with common fields",
  "properties": {
    "id": { "type": "string", "format": "uuid" },
    "tenant_id": { "type": "string", "format": "uuid" },
    "payload": { "type": "object" }
  },
  "required": ["id", "tenant_id", "payload"]
}

Inherited audit event (schemas/gts.x.core.events.type.v1~x.core.audit.event.v1~.schema.json):

{
  "$id": "gts://gts.x.core.events.type.v1~x.core.audit.event.v1~",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "AuditEventV1",
  "type": "object",
  "description": "Audit event with user context",
  "allOf": [
    { "$ref": "gts://gts.x.core.events.type.v1~" },
    {
      "properties": {
        "payload": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "user_id": { "type": "string", "format": "uuid" },
            "action": { "type": "string" }
          },
          "required": ["user_id", "action"]
        }
      }
    }
  ]
}

Type Mapping

The CLI automatically maps Rust types to JSON Schema types:

Rust Type JSON Schema Type Format Required
String, &str string - Yes
i8-i128, u8-u128 integer - Yes
f32, f64 number - Yes
bool boolean - Yes
Vec<T> array - Yes
Option<T> Same as T - No
Uuid string uuid Yes
DateTime, NaiveDateTime string date-time Yes
NaiveDate string date Yes
HashMap<K,V>, BTreeMap<K,V> object - Yes
GtsInstanceId string gts-instance-id Yes
GtsSchemaId string gts-schema-id Yes

Notes:

  • Option<T> fields are not marked as required in the generated schema
  • Generic type parameters (e.g., P in BaseEventV1<P>) are mapped to {"type": "object"} placeholders

Purpose 3: Runtime API

The macro generates associated constants, methods, and implements the GtsSchema trait for runtime use.

Getting Schema IDs

Get the struct's GTS schema ID:

// Using gts_schema_id() - returns &'static GtsSchemaId
let schema_id: &gts::gts::GtsSchemaId = AuditEventV1::gts_schema_id();
println!("Schema ID: {}", schema_id.as_ref());
// Output: gts.x.core.events.type.v1~x.core.audit.event.v1~

// For generic structs, use () as type parameter
let base_id = BaseEventV1::<()>::gts_schema_id();
println!("Base schema ID: {}", base_id.as_ref());
// Output: gts.x.core.events.type.v1~

Get the parent (base) schema ID:

// Using gts_base_schema_id() - returns Option<&'static GtsSchemaId>
let parent_id: Option<&gts::gts::GtsSchemaId> = AuditEventV1::gts_base_schema_id();
match parent_id {
    Some(id) => println!("Parent schema ID: {}", id.as_ref()),
    // Output: gts.x.core.events.type.v1~
    None => println!("This is a base struct (no parent)"),
}

// Base structs return None
assert!(BaseEventV1::<()>::gts_base_schema_id().is_none());

// Child structs return Some(&GtsSchemaId)
assert_eq!(
    AuditEventV1::gts_base_schema_id().map(|id| id.as_ref()),
    Some("gts.x.core.events.type.v1~")
);

Getting Schemas

Get the struct's JSON schema with references:

use gts::GtsSchema;

// Using gts_schema_with_refs() - returns serde_json::Value
let schema_value = AuditEventV1::gts_schema_with_refs();
println!("Schema $id: {}", schema_value["$id"]);
// Output: gts://gts.x.core.events.type.v1~x.core.audit.event.v1~

// Using gts_schema_with_refs_as_string() - returns compact JSON String
let schema_json = AuditEventV1::gts_schema_with_refs_as_string();
println!("Schema JSON: {}", schema_json);

// Using gts_schema_with_refs_as_string_pretty() - returns pretty-printed JSON String
let schema_pretty = AuditEventV1::gts_schema_with_refs_as_string_pretty();
println!("Pretty schema:\n{}", schema_pretty);

// For generic structs, the type parameter doesn't affect the schema
// Both generate identical schemas:
let schema1 = BaseEventV1::<()>::gts_schema_with_refs();
let schema2 = BaseEventV1::<AuditEventV1>::gts_schema_with_refs();
assert_eq!(schema1, schema2);  // OK. Identical schemas

Schema structure:

  • Base structs (single-segment schema_id): Direct properties, no allOf

    {
      "$id": "gts://gts.x.core.events.type.v1~",
      "type": "object",
      "additionalProperties": false,
      "properties": { /* ... */ }
    }
    
  • Child structs (multi-segment schema_id): Uses allOf with $ref to parent

    {
      "$id": "gts://gts.x.core.events.type.v1~x.core.audit.event.v1~",
      "type": "object",
      "allOf": [
        { "$ref": "gts://gts.x.core.events.type.v1~" },
        { "properties": { /* child-specific properties */ } }
      ]
    }
    

Serialization & Deserialization

Important: Nested payload structs (like AuditPayloadV1, PlaceOrderDataV1) should never be serialized/deserialized alone. Always serialize/deserialize the complete event hierarchy starting from the base event.

Serialize complete event instances to JSON:

use serde::{Serialize, Deserialize};
use uuid::Uuid;

// Create a complete event with nested payloads
// BaseEventV1 -> AuditPayloadV1 -> PlaceOrderDataV1
let event = BaseEventV1 {
    event_type: PlaceOrderDataV1::gts_schema_id().clone(),
    id: Uuid::new_v4(),
    tenant_id: Uuid::new_v4(),
    sequence_id: 42,
    payload: AuditPayloadV1 {
        user_agent: "Mozilla/5.0".to_string(),
        user_id: Uuid::new_v4(),
        ip_address: "192.168.1.1".to_string(),
        data: PlaceOrderDataV1 {
            order_id: Uuid::new_v4(),
            product_id: Uuid::new_v4(),
        },
    },
};

// Using GTS instance methods (recommended):
// gts_instance_json() - returns serde_json::Value
let json_value = event.gts_instance_json();
println!("Value: {:?}", json_value);

// gts_instance_json_as_string() - returns compact JSON String
let json_string = event.gts_instance_json_as_string();
println!("JSON: {}", json_string);

// gts_instance_json_as_string_pretty() - returns pretty-printed JSON String
let json_pretty = event.gts_instance_json_as_string_pretty();
println!("Pretty JSON:\n{}", json_pretty);

// You can also use serde_json directly:
let json_string = serde_json::to_string(&event).unwrap();
let json_pretty = serde_json::to_string_pretty(&event).unwrap();
let json_value = serde_json::to_value(&event).unwrap();

Deserialize JSON to complete event instances:

// Deserialize from JSON string
let json_str = r#"{
    "type": "gts.x.core.events.type.v1~x.core.audit.event.v1~x.marketplace.orders.purchase.v1~",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "tenant_id": "660e8400-e29b-41d4-a716-446655440000",
    "sequence_id": 42,
    "payload": {
        "user_agent": "Mozilla/5.0",
        "user_id": "770e8400-e29b-41d4-a716-446655440000",
        "ip_address": "192.168.1.1",
        "data": {
            "order_id": "880e8400-e29b-41d4-a716-446655440000",
            "product_id": "990e8400-e29b-41d4-a716-446655440000"
        }
    }
}"#;

let event: BaseEventV1<AuditPayloadV1<PlaceOrderDataV1>> =
    serde_json::from_str(json_str).unwrap();
println!("Deserialized: {:?}", event);

// Deserialize from serde_json::Value
let json_value = serde_json::json!({
    "type": "gts.x.core.events.type.v1~x.core.audit.event.v1~x.marketplace.orders.purchase.v1~",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "tenant_id": "660e8400-e29b-41d4-a716-446655440000",
    "sequence_id": 42,
    "payload": {
        "user_agent": "Mozilla/5.0",
        "user_id": "770e8400-e29b-41d4-a716-446655440000",
        "ip_address": "192.168.1.1",
        "data": {
            "order_id": "880e8400-e29b-41d4-a716-446655440000",
            "product_id": "990e8400-e29b-41d4-a716-446655440000"
        }
    }
});

let event: BaseEventV1<AuditPayloadV1<PlaceOrderDataV1>> =
    serde_json::from_value(json_value).unwrap();
println!("Deserialized: {:?}", event);

// Deserialize from reader (file, network stream, etc.)
use std::fs::File;
let file = File::open("event.json").unwrap();
let event: BaseEventV1<AuditPayloadV1<PlaceOrderDataV1>> =
    serde_json::from_reader(file).unwrap();

Working with different payload types:

// You can use different payload combinations with the same base event
type OrderEvent = BaseEventV1<AuditPayloadV1<PlaceOrderDataV1>>;
type SimpleEvent = BaseEventV1<()>;

// Create and serialize different event types
let order_event: OrderEvent = BaseEventV1 {
    event_type: PlaceOrderDataV1::gts_schema_id().clone(),
    id: Uuid::new_v4(),
    tenant_id: Uuid::new_v4(),
    sequence_id: 1,
    payload: AuditPayloadV1 {
        user_agent: "Mozilla/5.0".to_string(),
        user_id: Uuid::new_v4(),
        ip_address: "192.168.1.1".to_string(),
        data: PlaceOrderDataV1 {
            order_id: Uuid::new_v4(),
            product_id: Uuid::new_v4(),
        },
    },
};

let json = serde_json::to_string_pretty(&order_event).unwrap();
println!("Order event JSON:\n{}", json);

// Deserialize back to the correct type
let deserialized: OrderEvent = serde_json::from_str(&json).unwrap();

Generating Instance IDs

Generate instance IDs by appending a segment to the schema ID:

// Generate event instance ID
let topic_id = AuditEventTopicV1::gts_make_instance_id("x.core.audit.topic.v1");
assert_eq!(
    topic_id.as_ref(),
    "gts.x.core.events.topic.v1~x.core.audit.topic.v1"
);

// Generate base event instance ID
let base_id = BaseEventTopicV1::<()>::gts_make_instance_id("a.b.c.topic.v1");
assert_eq!(base_id.as_ref(), "gts.x.core.events.topic.v1~a.b.c.topic.v1");

// Convert to String when needed
let id_string: String = topic_id.clone().into();

// Use as map key (implements Hash, Eq, Clone)
use std::collections::HashMap;
let mut events: HashMap<gts::GtsInstanceId, String> = HashMap::new();
events.insert(event_id, "processed".to_owned());

// Serialize/deserialize instance IDs
let id_json = serde_json::to_string(&event_id).unwrap();
let id_back: gts::GtsInstanceId = serde_json::from_str(&id_json).unwrap();

Schema Composition & Inheritance (GtsSchema Trait)

The macro automatically implements the GtsSchema trait, enabling runtime schema composition for nested generic types:

use gts::GtsSchema;

// Get composed schema for nested type
let schema = BaseEventV1::<AuditPayloadV1<PlaceOrderDataV1>>::gts_schema_with_refs_allof();

// The schema will have proper nesting:
// - payload field contains AuditPayloadV1's schema
// - payload.data field contains PlaceOrderDataV1's schema
// - All with additionalProperties: false for type safety

Generic Field Type Safety: Generic fields (fields that accept nested types) automatically have additionalProperties: false set. This ensures:

  • ✅ Only properly nested inherited structs can be used as values
  • ✅ No arbitrary extra properties can be added to generic fields
  • ✅ Type safety is enforced at the JSON Schema level

Complete Runtime API Reference

API Type Description
gts_schema_id() &'static GtsSchemaId Get the struct's GTS schema ID
gts_base_schema_id() Option<&'static GtsSchemaId> Get parent schema ID (None for base structs)
gts_schema_with_refs() serde_json::Value Get schema as JSON value with $ref
gts_schema_with_refs_as_string() String Get schema as compact JSON string
gts_schema_with_refs_as_string_pretty() String Get schema as pretty-printed JSON string
gts_instance_json(&self) serde_json::Value Serialize instance to JSON value
gts_instance_json_as_string(&self) String Serialize instance to compact JSON string
gts_instance_json_as_string_pretty(&self) String Serialize instance to pretty-printed JSON string
gts_make_instance_id(segment) GtsInstanceId Generate instance ID by appending segment

Macro Parameters

All parameters are required (5 total):

Parameter Description Example
dir_path Output directory for generated schema "schemas"
base Inheritance declaration (see below) true or ParentStruct
schema_id GTS identifier "gts.x.app.entities.user.v1~"
description Human-readable description "User entity"
properties Comma-separated field list "id,email,name"

The base Attribute

The base attribute explicitly declares the struct's position in the inheritance hierarchy:

Value Meaning Schema ID Requirement
base = true This is a root/base type (no parent) Single-segment (e.g., gts.x.core.events.type.v1~)
base = ParentStruct This inherits from ParentStruct Multi-segment (e.g., gts.x.core.events.type.v1~x.core.audit.event.v1~)

Compile-time validation: The macro validates that:

  • base = true requires a single-segment schema_id
  • base = ParentStruct requires a multi-segment schema_id where the parent segment matches ParentStruct's SCHEMA_ID

GTS ID Format

gts.<vendor>.<package>.<namespace>.<type>.v<MAJOR>[.<MINOR>]~

Examples:

  • gts.x.core.iam.user.v1~ - IAM user schema
  • gts.x.commerce.orders.order.v1.0~ - Order schema with minor version

Complete Example

Define Event Type Hierarchy

// src/events.rs
use gts_macros::struct_to_gts_schema;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

// Base event type - the root of all events
#[derive(Debug, Serialize, Deserialize)]
#[struct_to_gts_schema(
    dir_path = "schemas",
    base = true,
    schema_id = "gts.x.core.events.type.v1~",
    description = "Base event type with common fields",
    properties = "id,tenant_id,timestamp,payload"
)]
pub struct BaseEventV1<P> {
    pub id: Uuid,
    pub r#type: GtsSchemaId,
    pub tenant_id: Uuid,
    pub timestamp: String,
    pub payload: P,
}

// Audit event - extends BaseEventV1 with user context
#[derive(Debug, Serialize, Deserialize)]
#[struct_to_gts_schema(
    dir_path = "schemas",
    base = BaseEventV1,
    schema_id = "gts.x.core.events.type.v1~x.core.audit.event.v1~",
    description = "Audit event with user tracking",
    properties = "user_id,ip_address,action"
)]
pub struct AuditEventV1<D> {
    pub user_id: Uuid,
    pub ip_address: String,
    pub action: D,
}

// Order placed event - extends AuditEventV1 for order actions
#[derive(Debug, Serialize, Deserialize)]
#[struct_to_gts_schema(
    dir_path = "schemas",
    base = AuditEventV1,
    schema_id = "gts.x.core.events.type.v1~x.core.audit.event.v1~x.shop.orders.placed.v1~",
    description = "Order placement event",
    properties = "order_id,total"
)]
pub struct OrderPlacedV1 {
    pub order_id: Uuid,
    pub total: f64,
}

Generate Schemas

gts generate-from-rust --source src/
# Output:
#   Generated schema: gts.x.core.events.type.v1~ @ schemas/...
#   Generated schema: gts.x.core.events.type.v1~x.core.audit.event.v1~ @ schemas/...
#   Generated schema: gts.x.core.events.type.v1~x.core.audit.event.v1~x.shop.orders.placed.v1~ @ schemas/...

Use at Runtime

fn main() {
    // Access schemas at any level
    println!("Base event schema: {}", BaseEventV1::<()>::gts_schema_with_refs_as_string_pretty());
    println!("Audit event schema: {}", AuditEventV1::<()>::gts_schema_with_refs_as_string_pretty());
    println!("Order placed schema: {}", OrderPlacedV1::gts_schema_with_refs_as_string_pretty());

    // Generate instance IDs
    let event_id = OrderPlacedV1::gts_make_instance_id("evt-12345.v1");
    println!("Event ID: {}", event_id);
    // Output: gts.x.core.events.type.v1~x.core.audit.event.v1~x.shop.orders.placed.v1~evt-12345.v1

    // Use as HashMap key
    use std::collections::HashMap;
    let mut events: HashMap<gts::GtsInstanceId, String> = HashMap::new();
    events.insert(event_id, "processed".to_owned());
}

Schema Inheritance & Compile-Time Guarantees

The macro supports explicit inheritance declaration through the base attribute and provides compile-time validation to ensure parent-child relationships are correct.

Inheritance Example

See tests/inheritance_tests.rs for a complete working example:

// Base event type (base = true, single-segment schema_id)
#[struct_to_gts_schema(
    dir_path = "schemas",
    base = true,
    schema_id = "gts.x.core.events.type.v1~",
    description = "Base event type definition",
    properties = "event_type,id,tenant_id,sequence_id,payload"
)]
pub struct BaseEventV1<P> {
    #[serde(rename = "type")]
    pub event_type: GtsSchemaId,
    pub id: Uuid,
    pub tenant_id: Uuid,
    pub sequence_id: u64,
    pub payload: P,
}

// Extends BaseEventV1 (base = ParentStruct, multi-segment schema_id)
#[struct_to_gts_schema(
    dir_path = "schemas",
    base = BaseEventV1,
    schema_id = "gts.x.core.events.type.v1~x.core.audit.event.v1~",
    description = "Audit event with user context",
    properties = "user_agent,user_id,ip_address,data"
)]
pub struct AuditPayloadV1<D> {
    pub user_agent: String,
    pub user_id: Uuid,
    pub ip_address: String,
    pub data: D,
}

// Extends AuditPayloadV1 (3-level inheritance chain)
#[struct_to_gts_schema(
    dir_path = "schemas",
    base = AuditPayloadV1,
    schema_id = "gts.x.core.events.type.v1~x.core.audit.event.v1~x.marketplace.orders.purchase.v1~",
    description = "Order placement audit event",
    properties = "order_id,product_id"
)]
pub struct PlaceOrderDataV1 {
    pub order_id: Uuid,
    pub product_id: Uuid,
}

Generated Schemas

Single-segment schema (no inheritance):

{
  "$id": "gts://gts.x.core.events.type.v1~",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "BaseEventV1",
  "type": "object",
  "description": "Base event type definition",
  "properties": { /* direct properties */ },
  "required": [ /* required fields */ ]
}

Multi-segment schema (with inheritance):

{
  "$id": "gts://gts.x.core.events.type.v1~x.core.audit.event.v1~",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "AuditPayloadV1",
  "type": "object",
  "description": "Audit event with user context",
  "allOf": [
    { "$ref": "gts://gts.x.core.events.type.v1~" },
    {
      "properties": {
        "payload": {
          "type": "object",
          "additionalProperties": false,
          "properties": { /* child-specific properties */ },
          "required": [ /* child-specific required fields */ ]
        }
      }
    }
  ]
}

Important: Generic fields (fields that accept nested types) automatically have additionalProperties: false set. This ensures that only properly nested inherited structs can be used, preventing arbitrary extra properties from being added to generic fields.

Compile-Time Guarantees

The macro validates your configuration at compile time, preventing runtime errors:

✅ Guaranteed ❌ Prevented
All required attributes exist Missing dir_path, base, schema_id, description, or properties
Base attribute consistency base = true with multi-segment schema_id, or base = Parent with single-segment
Parent schema ID match base = Parent where Parent's SCHEMA_ID doesn't match the parent segment
Properties exist in struct Referencing non-existent fields in properties list
Valid struct types Tuple structs, unit structs, enums
Single generic parameter Multiple type generics (prevents inheritance ambiguity)
Valid GTS ID format Malformed schema identifiers
Memory efficiency No unnecessary allocations in generated constants
Strict generic field validation Generic fields have additionalProperties: false to ensure only nested inherited structs are allowed
GTS-only generic arguments Using non-GTS structs as generic type parameters (see below)

Generic Type Parameter Constraints

The macro automatically adds a GtsSchema trait bound to all generic type parameters. This ensures that only valid GTS types can be used as generic arguments:

// ✅ Allowed: () is a valid GTS type (terminates the chain)
let event: BaseEventV1<()> = BaseEventV1 { /* ... */ };

// ✅ Allowed: AuditPayloadV1 has struct_to_gts_schema applied
let event: BaseEventV1<AuditPayloadV1<()>> = BaseEventV1 { /* ... */ };

// ❌ Compile error: MyStruct does not implement GtsSchema
pub struct MyStruct { pub some_id: String }
let event: BaseEventV1<MyStruct> = BaseEventV1 { /* ... */ };
// error: the trait bound `MyStruct: GtsSchema` is not satisfied

This prevents accidental use of arbitrary structs that haven't been properly annotated with struct_to_gts_schema, ensuring type safety across the entire GTS inheritance chain.

Generic Fields and additionalProperties

When a struct has a generic type parameter (e.g., BaseEventV1<P> with field payload: P), the generated schema sets additionalProperties: false on that field's schema. This ensures:

  • ✅ Only properly nested inherited structs can be used as values
  • ✅ No arbitrary extra properties can be added to generic fields
  • ✅ Type safety is enforced at the JSON Schema level

Example:

{
  "properties": {
    "payload": {
      "type": "object",
      "additionalProperties": false,
      "properties": { /* nested schema */ }
    }
  }
}

Schema Generation Methods

The macro generates methods for runtime schema access:

  • gts_schema_with_refs(): Returns serde_json::Value with $ref in allOf
  • gts_schema_with_refs_as_string(): Returns compact JSON string
  • gts_schema_with_refs_as_string_pretty(): Returns pretty-printed JSON string
// Get schema as JSON value
let schema_value = AuditEventV1::<()>::gts_schema_with_refs();

// Get schema as string (compact or pretty)
let schema_compact = AuditEventV1::<()>::gts_schema_with_refs_as_string();
let schema_pretty = AuditEventV1::<()>::gts_schema_with_refs_as_string_pretty();

// Schema IDs use LazyLock for efficient one-time initialization
let schema_id = AuditEventV1::gts_schema_id();
let parent_id = AuditEventV1::gts_base_schema_id();

Security Features

The CLI includes security checks:

  1. Path traversal prevention - Cannot write files outside the source repository
  2. File extension enforcement - Both macro and CLI validate .json extension
  3. Canonicalization - Resolves symbolic links to prevent escapes

License

Apache-2.0

See Also

Commit count: 163

cargo fmt