7 releases
Uses new Rust 2024
| 0.8.4 | Mar 6, 2026 |
|---|---|
| 0.8.3 | Mar 5, 2026 |
| 0.8.2 | Feb 23, 2026 |
| 0.7.8 | Jan 27, 2026 |
| 0.1.0 | Jan 23, 2026 |
#1147 in Build Utils
6,509 downloads per month
Used in 25 crates
(6 directly)
180KB
2K
SLoC
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:
- Compile-Time Validation - Catches configuration errors before runtime
- Schema Generation - Enables CLI-based JSON Schema file generation
- 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: Base structs get Serialize/Deserialize/JsonSchema 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:
- Base structs (
base = true):serde::Serialize,serde::Deserialize,schemars::JsonSchema - Nested structs (
base = ParentStruct):schemars::JsonSchemaonly
(direct Serialize/Deserialize is intentionally blocked)
Do NOT add Serialize/Deserialize manually for nested structs - direct serialization is prohibited.
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(orgts::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(orgts::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
- Scans source files for
#[struct_to_gts_schema]annotations - Extracts metadata (schema_id, description, properties)
- Maps Rust types to JSON Schema types
- 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 asrequiredin the generated schema- Generic type parameters (e.g.,
PinBaseEventV1<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: >s::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<>s::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
allOfwith$refto 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 = truerequires a single-segmentschema_idbase = ParentStructrequires a multi-segmentschema_idwhere the parent segment matchesParentStruct'sSCHEMA_ID
GTS ID Format
gts.<vendor>.<package>.<namespace>.<type>.v<MAJOR>[.<MINOR>]~
Examples:
gts.x.core.iam.user.v1~- IAM user schemagts.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(): Returnsserde_json::Valuewith$refinallOfgts_schema_with_refs_as_string(): Returns compact JSON stringgts_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:
- Path traversal prevention - Cannot write files outside the source repository
- File extension enforcement - Both macro and CLI validate
.jsonextension - Canonicalization - Resolves symbolic links to prevent escapes
License
Apache-2.0
See Also
Dependencies
~0.4–1.2MB
~26K SLoC