toolkit_gts/lib.rs
1#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
2//! `ToolKit` GTS integration.
3//!
4//! This crate bridges Rust types to the [Global Type System] (GTS) used by
5//! Gears. It provides three things:
6//!
7//! 1. **Link-time inventory** of GTS Type Schemas and well-known Instances —
8//! collectors populated at link time via the `inventory` crate. Any crate
9//! in the process that uses the macros below contributes to the same
10//! global inventory. `types-registry` consumes the inventory at startup,
11//! so there is no per-gear registration code for entries known at
12//! compile time.
13//! 2. **Thin proc-macro wrappers** that delegate to upstream `gts-macros`
14//! and additionally submit the corresponding inventory entry:
15//! - `#[gts_type_schema(...)]` — wraps `gts_macros::struct_to_gts_schema`.
16//! - `gts_instance!` — wraps `gts_macros::gts_instance!`.
17//! - `gts_instance_raw!` — wraps `gts_macros::gts_instance_raw!`.
18//! 3. **Platform base types** — a small shipped set of GTS base Type Schemas
19//! ([`PluginV1`], [`AuthzPermissionV1`]) used across the platform.
20//!
21//! ## Adding a new entry
22//!
23//! For a new platform base Type Schema, put a `#[gts_type_schema(...)]`-annotated
24//! struct into this crate and add `mod your_gear;` below. For a Type Schema
25//! or Instance owned by a specific gear, use the same macros from that
26//! gear's crate — the inventory is process-global, so entries land in
27//! `types-registry` regardless of which crate declares them.
28//!
29//! [Global Type System]: https://github.com/hypernetix/gts-spec
30
31pub mod permission;
32pub mod plugin;
33
34pub use permission::AuthzPermissionV1;
35pub use plugin::PluginV1;
36
37// Re-export GTS primitives used by the wrapper macros and downstream
38// callers. Keeps `toolkit_gts::*` self-sufficient at the top level.
39pub use gts::{GTS_ID_PREFIX, GTS_ID_URI_PREFIX, GtsId, GtsInstanceId, GtsSchema};
40
41// Re-export `inventory` so the macro expansions can emit
42// `<crate>::inventory::submit!` without requiring consumer crates to add
43// `inventory` as a direct dep.
44#[doc(hidden)]
45pub use inventory;
46
47// Re-export the companion proc-macros so consumers need only one crate dep.
48pub use gts_macros::GtsTraitsSchema;
49pub use toolkit_gts_macros::{gts_id, gts_instance, gts_instance_raw, gts_type_schema, gts_uri};
50
51/// Hidden re-exports used by the `cf-gears-toolkit-gts-macros` proc-macro
52/// expansions to reach the upstream construction macros without forcing
53/// consumers to take a direct dependency on `gts-macros`.
54#[doc(hidden)]
55pub mod __private {
56 pub use ::gts_macros::{
57 gts_id as upstream_gts_id, gts_instance as upstream_gts_instance,
58 gts_instance_raw as upstream_gts_instance_raw,
59 struct_to_gts_schema as upstream_struct_to_gts_schema,
60 };
61}
62
63/// Registration record for a GTS Type Schema contributed to the process-wide
64/// inventory.
65///
66/// Each `#[gts_type_schema(...)]`-annotated type submits one of these via
67/// `inventory::submit!` at macro-expansion time. The `schema_fn` lazily
68/// invokes the macro-generated accessor (`gts_schema_with_refs_as_string`)
69/// to produce the JSON Schema document on demand.
70#[derive(Clone)]
71pub struct InventoryTypeSchema {
72 /// GTS Type Identifier (e.g. `gts.cf.toolkit.authz.permission.v1~`).
73 pub type_id: &'static str,
74 /// Lazy accessor returning the GTS Type Schema as a JSON string.
75 pub schema_fn: fn() -> String,
76}
77
78/// Registration record for a well-known GTS Instance contributed to the
79/// process-wide inventory.
80///
81/// Submitted by the `gts_instance! { ... }` macro. `type_id` is derived at
82/// macro-expansion time from the last `~` in the full instance id.
83#[derive(Clone)]
84pub struct InventoryInstance {
85 /// GTS Type Identifier the Instance conforms to (prefix of the full
86 /// Instance Identifier up to and including the last `~`).
87 pub type_id: &'static str,
88 /// Full GTS Instance Identifier.
89 pub instance_id: &'static str,
90 /// Lazy accessor returning the Instance payload as JSON (with `id`
91 /// auto-injected by the macro).
92 ///
93 /// Two emission paths populate this:
94 /// - `gts_instance! { ... }` (typed) expands to
95 /// `serde_json::to_value(&Struct { ... }).expect(...)`, which panics
96 /// only if a custom `Serialize` impl in the struct's transitive
97 /// field types fails (e.g., a map with non-string keys). In practice
98 /// our base types — `AuthzPermissionV1`, `PluginV1<P>` — use only
99 /// primitive / string / `GtsInstanceId` fields, so the panic path is
100 /// unreachable.
101 /// - `gts_instance_raw! { ... }` expands to `serde_json::json!(...)`
102 /// on a literal JSON object, which cannot panic at runtime.
103 pub payload_fn: fn() -> serde_json::Value,
104}
105
106inventory::collect!(InventoryTypeSchema);
107inventory::collect!(InventoryInstance);
108
109/// Returns every GTS Type Schema declared via `#[gts_type_schema(...)]` in
110/// any crate linked into the current process.
111///
112/// Source of truth for each Type Schema is its Rust struct via the
113/// macro-generated `gts_schema_with_refs_as_string` accessor — no
114/// hand-written JSON.
115///
116/// # Errors
117///
118/// Returns an error if any registered Type Schema accessor produces invalid
119/// JSON. This should be impossible with a correctly-applied `#[gts_type_schema]`
120/// macro and signals a macro regression.
121pub fn all_inventory_type_schemas() -> anyhow::Result<Vec<serde_json::Value>> {
122 let mut out = Vec::new();
123 for entry in inventory::iter::<InventoryTypeSchema> {
124 let schema_str = (entry.schema_fn)();
125 let value: serde_json::Value = serde_json::from_str(&schema_str).map_err(|e| {
126 anyhow::anyhow!(
127 "invalid GTS Type Schema JSON emitted by GTS type {}: {e}",
128 entry.type_id
129 )
130 })?;
131 out.push(value);
132 }
133 Ok(out)
134}
135
136/// Returns every well-known GTS Instance declared via `gts_instance!` in
137/// any crate linked into the current process.
138///
139/// # Errors
140///
141/// Currently never returns `Err`. Typed `gts_instance!` declarations use
142/// `serde_json::to_value` internally, whose only failure mode is a custom
143/// `Serialize` impl that fails — practically unreachable for our base
144/// types (primitive / string / `GtsInstanceId` fields only). Raw
145/// declarations use `serde_json::json!` on a literal object, which cannot
146/// fail. The `Result` return type is kept for symmetry with
147/// [`all_inventory_type_schemas`] and to leave room for surfacing
148/// per-instance serialization errors without an API break if a future
149/// base type ever introduces a fallible `Serialize` path.
150pub fn all_inventory_instances() -> anyhow::Result<Vec<serde_json::Value>> {
151 Ok(inventory::iter::<InventoryInstance>
152 .into_iter()
153 .map(|entry| (entry.payload_fn)())
154 .collect())
155}
156
157#[cfg(test)]
158mod tests {
159 use super::{
160 InventoryInstance, InventoryTypeSchema, all_inventory_instances, all_inventory_type_schemas,
161 };
162
163 #[test]
164 fn platform_base_schemas_are_registered_and_valid() {
165 let schemas = all_inventory_type_schemas().expect("schemas collect cleanly");
166
167 // Both platform base types shipped by this crate must be present.
168 let ids: Vec<&str> = inventory::iter::<InventoryTypeSchema>
169 .into_iter()
170 .map(|e| e.type_id)
171 .collect();
172 assert!(
173 ids.contains(&crate::gts_id!("cf.toolkit.plugins.plugin.v1~")),
174 "PluginV1 not registered; got ids: {ids:?}"
175 );
176 assert!(
177 ids.contains(&crate::gts_id!("cf.toolkit.authz.permission.v1~")),
178 "AuthzPermissionV1 not registered; got ids: {ids:?}"
179 );
180 assert_eq!(
181 schemas.len(),
182 ids.len(),
183 "iter vs aggregated count mismatch (did all entries collect cleanly?)"
184 );
185
186 for (idx, s) in schemas.iter().enumerate() {
187 assert!(s.is_object(), "schema #{idx} is not a JSON object: {s}");
188 assert!(s.get("$id").is_some(), "schema #{idx} missing $id: {s}");
189 assert!(
190 s.get("type").is_some(),
191 "schema #{idx} missing top-level type: {s}"
192 );
193 }
194 }
195
196 #[test]
197 fn inventory_instances_registry_is_consistent() {
198 // No instances ship from this crate, but the collector path must
199 // still run. (External crates may contribute instances; this crate
200 // only checks self-consistency.)
201 let instances = all_inventory_instances().expect("instances collect cleanly");
202 let ids: Vec<&str> = inventory::iter::<InventoryInstance>
203 .into_iter()
204 .map(|e| e.instance_id)
205 .collect();
206 assert_eq!(
207 instances.len(),
208 ids.len(),
209 "iter vs aggregated count mismatch"
210 );
211 }
212}