Skip to main content

types_registry_sdk/
models.rs

1//! Public models for the `types-registry` module.
2//!
3//! These are transport-agnostic data structures that define the contract
4//! between the `types-registry` module and its consumers.
5
6use std::collections::BTreeMap;
7use std::sync::Arc;
8
9use gts::{GtsID, GtsIdSegment, GtsInstanceId, GtsSchemaId};
10use serde_json::{Map, Value};
11use uuid::Uuid;
12
13use crate::error::TypesRegistryError;
14
15/// SDK-facing alias for a GTS type-schema identifier.
16///
17/// Backed by [`gts::GtsSchemaId`] from the `gts` crate. Exposed under the
18/// `GtsTypeId` name to keep the SDK's vocabulary consistent — within the
19/// types-registry SDK, schemas are *type-schemas* and their identifiers are
20/// *type ids*.
21pub type GtsTypeId = GtsSchemaId;
22
23/// Returns `true` if `s` is shaped like a type-schema GTS id (ends with `~`).
24///
25/// Type-schema ids and instance ids are lexically distinct in GTS: type-schema
26/// ids end with `~`, instance ids do not. Centralizing the predicate here so
27/// that callers don't sprinkle raw `ends_with('~')` checks across kind-aware
28/// code (`local_client`, mocks, etc.). Pure string predicate — does not parse
29/// or otherwise validate the id.
30///
31// TODO(#1752): drop this helper once `GtsSchemaId::try_new` /
32// `GtsInstanceId::try_new` land upstream in `gts-rust`. Callers should
33// consume `&GtsTypeId` / `&GtsInstanceId` directly and the kind invariant
34// becomes a type-system property instead of a runtime predicate.
35#[must_use]
36pub fn is_type_schema_id(s: &str) -> bool {
37    s.ends_with('~')
38}
39
40/// A registered GTS type-schema (type definition).
41///
42/// In addition to the common fields, the schema-specific extensions
43/// `x-gts-traits-schema` and `x-gts-traits` are extracted into top-level
44/// fields, and the GTS chain parent is pre-resolved into [`Self::parent`]
45/// (Arc-shared, deduplicated by the registry's local-client cache).
46///
47/// Use [`Self::effective_schema`], [`Self::effective_properties`],
48/// [`Self::effective_required`], [`Self::effective_traits`] to inspect the schema
49/// across the inheritance chain without manual walking.
50///
51/// `x-gts-final` / `x-gts-abstract` modifiers are intentionally not surfaced
52/// here yet — support will be added later.
53#[derive(Debug, Clone, PartialEq)]
54pub struct GtsTypeSchema {
55    /// Deterministic UUID v5 derived from the type id.
56    pub type_uuid: Uuid,
57
58    /// The full GTS type identifier. Always ends with `~`.
59    pub type_id: GtsTypeId,
60
61    /// All parsed segments from the GTS ID.
62    pub segments: Vec<GtsIdSegment>,
63
64    /// This type-schema's own raw JSON Schema body.
65    ///
66    /// `allOf[].$ref` references are kept verbatim — use [`Self::effective_schema`]
67    /// to obtain a representation with the parent inlined.
68    pub raw_schema: Value,
69
70    /// This type-schema's own `x-gts-traits` values, if present.
71    pub traits: Option<Value>,
72
73    /// This type-schema's own `x-gts-traits-schema`, if present.
74    pub traits_schema: Option<Value>,
75
76    /// Resolved parent type-schema in the inheritance chain.
77    ///
78    /// `None` for root type-schemas (no parent in the chain) or when the parent
79    /// hasn't been resolved by the producer.
80    pub parent: Option<Arc<GtsTypeSchema>>,
81
82    /// Optional human-readable title (`title` field of the JSON Schema).
83    pub title: Option<String>,
84
85    /// Optional human-readable description.
86    pub description: Option<String>,
87}
88
89impl GtsTypeSchema {
90    /// Constructs a `GtsTypeSchema` from its canonical inputs.
91    ///
92    /// `type_uuid` and `segments` are derived from `type_id` via gts-rust's
93    /// canonical parser — there is only one source of truth (the id string).
94    /// `traits` / `traits_schema` / `title` are extracted from `raw_schema`.
95    /// `parent` is pre-resolved by the caller (typically the local client
96    /// via its type-schema cache); presence/absence of `parent` is enforced
97    /// against the chain shape of `type_id` — a derived id MUST carry its
98    /// parent, a root id MUST NOT — so that
99    /// [`ancestors`](Self::ancestors) / [`effective_schema`](Self::effective_schema)
100    /// always observe a complete chain. A mismatched parent (chain-prefix
101    /// disagreement) is also rejected.
102    ///
103    /// # Errors
104    ///
105    /// Returns [`InvalidGtsTypeId`](TypesRegistryError::InvalidGtsTypeId)
106    /// in any of these cases:
107    /// - `type_id` does not end with `~` (looks like an instance id);
108    /// - `type_id` does not parse as a valid GTS identifier;
109    /// - `parent` is `Some(_)` but its `type_id` does not match the chain
110    ///   prefix derived from this `type_id`;
111    /// - `parent` is `Some(_)` but this `type_id` is a root (no chain prefix
112    ///   exists, so the schema cannot have a parent);
113    /// - `parent` is `None` but this `type_id` is derived (its chain prefix
114    ///   is non-empty, so the schema requires its parent to be passed in).
115    pub fn try_new(
116        type_id: GtsTypeId,
117        raw_schema: Value,
118        description: Option<String>,
119        parent: Option<Arc<GtsTypeSchema>>,
120    ) -> Result<Self, TypesRegistryError> {
121        if !is_type_schema_id(type_id.as_ref()) {
122            return Err(TypesRegistryError::invalid_gts_type_id(format!(
123                "{type_id} does not end with `~`",
124            )));
125        }
126        match (
127            parent.as_ref(),
128            Self::derive_parent_type_id(type_id.as_ref()),
129        ) {
130            (Some(parent_schema), Some(expected)) if expected != parent_schema.type_id => {
131                return Err(TypesRegistryError::invalid_gts_type_id(format!(
132                    "type-schema {type_id} expects parent {expected}, got {}",
133                    parent_schema.type_id,
134                )));
135            }
136            (Some(_), None) => {
137                return Err(TypesRegistryError::invalid_gts_type_id(format!(
138                    "root type-schema {type_id} cannot have a parent",
139                )));
140            }
141            (None, Some(expected)) => {
142                return Err(TypesRegistryError::invalid_gts_type_id(format!(
143                    "derived type-schema {type_id} requires parent {expected}, got None",
144                )));
145            }
146            // (Some, Some) where prefixes match  →  ok
147            // (None, None) — root with no parent  →  ok
148            _ => {}
149        }
150        let parsed = GtsID::new(type_id.as_ref())
151            .map_err(|e| TypesRegistryError::invalid_gts_type_id(format!("{e}")))?;
152        let type_uuid = parsed.to_uuid();
153        let segments = parsed.gts_id_segments;
154        let traits = Self::extract_traits(&raw_schema);
155        let traits_schema = Self::extract_traits_schema(&raw_schema);
156        let title = Self::extract_title(&raw_schema);
157        Ok(Self {
158            type_uuid,
159            type_id,
160            segments,
161            raw_schema,
162            traits,
163            traits_schema,
164            parent,
165            title,
166            description,
167        })
168    }
169
170    /// Derives the GTS parent's `type_id` by stripping the last `~`-segment.
171    ///
172    /// Mirrors gts-rust's chain semantics: for a chained type id like
173    /// `gts.x.core.events.type.v1~x.commerce.orders.order.v1.0~`, the parent
174    /// is `gts.x.core.events.type.v1~`. Returns `None` for root (single-segment)
175    /// type-schemas or for ids that don't end with `~`.
176    #[must_use]
177    pub fn derive_parent_type_id(type_id: &str) -> Option<GtsTypeId> {
178        let trimmed = type_id.strip_suffix('~')?;
179        let last_tilde = trimmed.rfind('~')?;
180        Some(GtsTypeId::new(&type_id[..=last_tilde]))
181    }
182
183    /// Reads `x-gts-traits` from the top level of a schema value.
184    #[must_use]
185    pub fn extract_traits(schema: &Value) -> Option<Value> {
186        schema.get("x-gts-traits").cloned()
187    }
188
189    /// Reads `x-gts-traits-schema` from the top level of a schema value.
190    #[must_use]
191    pub fn extract_traits_schema(schema: &Value) -> Option<Value> {
192        schema.get("x-gts-traits-schema").cloned()
193    }
194
195    /// Collects parent GTS IDs from `allOf[].$ref` (with `gts://` prefix stripped).
196    #[must_use]
197    pub fn extract_allof_refs(schema: &Value) -> Vec<String> {
198        let Some(arr) = schema.get("allOf").and_then(|v| v.as_array()) else {
199            return Vec::new();
200        };
201        arr.iter()
202            .filter_map(|item| item.get("$ref").and_then(|r| r.as_str()))
203            .map(|r| r.strip_prefix("gts://").unwrap_or(r).to_owned())
204            .collect()
205    }
206
207    /// Reads the optional `title` field.
208    #[must_use]
209    pub fn extract_title(schema: &Value) -> Option<String> {
210        schema
211            .get("title")
212            .and_then(|v| v.as_str())
213            .map(ToOwned::to_owned)
214    }
215
216    /// Returns the primary segment (first segment in the chain).
217    #[must_use]
218    pub fn primary_segment(&self) -> Option<&GtsIdSegment> {
219        self.segments.first()
220    }
221
222    /// Returns the vendor from the primary segment.
223    #[must_use]
224    pub fn vendor(&self) -> Option<&str> {
225        self.primary_segment().map(|s| s.vendor.as_str())
226    }
227
228    /// Iteration over the inheritance chain (this schema first, then parent,
229    /// then grandparent, ...). Linear walk via [`Self::parent`].
230    #[must_use]
231    pub fn ancestors(&self) -> AncestorIter<'_> {
232        AncestorIter {
233            current: Some(self),
234        }
235    }
236
237    /// Returns this schema's body with the GTS parent's `$ref` inlined where
238    /// it appears in `allOf` (parent body expanded in place). The shape of
239    /// the JSON Schema is preserved — `allOf`, `oneOf`, `anyOf`, `enum`, etc.
240    /// stay valid.
241    ///
242    /// Non-parent `allOf[].$ref` items (mixin references) are left as-is.
243    #[must_use]
244    pub fn effective_schema(&self) -> Value {
245        merge_schema_with_parent(&self.raw_schema, self.parent.as_deref())
246    }
247
248    /// Properties merged across the full chain. This schema wins on key
249    /// collisions; parent fills in inherited keys.
250    #[must_use]
251    pub fn effective_properties(&self) -> BTreeMap<String, Value> {
252        let mut out = self
253            .parent
254            .as_ref()
255            .map_or_else(BTreeMap::new, |p| p.effective_properties());
256        for (k, v) in collect_own_properties(&self.raw_schema) {
257            out.insert(k, v);
258        }
259        out
260    }
261
262    /// `required` field merged across the full chain (de-duplicated, order
263    /// preserved by first occurrence in pre-order walk).
264    #[must_use]
265    pub fn effective_required(&self) -> Vec<String> {
266        let mut seen = std::collections::HashSet::new();
267        let mut out = Vec::new();
268        for ancestor in self.ancestors() {
269            for r in collect_own_required(&ancestor.raw_schema) {
270                if seen.insert(r.clone()) {
271                    out.push(r);
272                }
273            }
274        }
275        out
276    }
277
278    /// Trait values merged across the chain.
279    ///
280    /// Resolution order (priority high → low):
281    /// 1. Declared `x-gts-traits` values from `self` and ancestors —
282    ///    rightmost wins, so a leaf's value overrides any parent's.
283    /// 2. Defaults from `x-gts-traits-schema.properties[*].default`
284    ///    declared anywhere in the chain. When two levels both declare a
285    ///    default for the same property, the **deepest** (closest to base)
286    ///    wins — mirroring gts-rust's locking rule that descendants cannot
287    ///    redefine an ancestor's default during schema-trait validation.
288    ///
289    /// Returns `Value::Null` only when neither declared traits nor
290    /// schema-declared defaults exist anywhere in the chain.
291    // TODO(#1723): replace with gts-rust's resolve_schema(...).effective_traits
292    // once that helper is exposed publicly.
293    #[must_use]
294    pub fn effective_traits(&self) -> Value {
295        let mut acc: Map<String, Value> = Map::new();
296        // Phase 1: declared traits. Walk own → ancestors and only insert
297        // when the key is absent so own (rightmost) wins over ancestors.
298        for s in self.ancestors() {
299            if let Some(Value::Object(traits)) = s.traits.as_ref() {
300                for (k, v) in traits {
301                    acc.entry(k.clone()).or_insert_with(|| v.clone());
302                }
303            }
304        }
305        // Phase 2: defaults from x-gts-traits-schema. Walk from deepest
306        // base to leaf so the **earliest** default wins on a given key,
307        // matching the locking semantics gts-rust enforces during
308        // validation. `or_insert_with` on the already-populated map means
309        // declared values still beat defaults.
310        let chain: Vec<&GtsTypeSchema> = self.ancestors().collect();
311        for s in chain.iter().rev() {
312            let Some(traits_schema) = s.traits_schema.as_ref() else {
313                continue;
314            };
315            let Some(Value::Object(props)) = traits_schema.get("properties") else {
316                continue;
317            };
318            for (k, prop) in props {
319                if let Some(default) = prop.get("default") {
320                    acc.entry(k.clone()).or_insert_with(|| default.clone());
321                }
322            }
323        }
324        if acc.is_empty() {
325            Value::Null
326        } else {
327            Value::Object(acc)
328        }
329    }
330
331    /// All `x-gts-traits-schema` blocks collected across the chain, ordered
332    /// from deepest base to this schema. Use to compose the effective trait
333    /// schema (e.g. via `allOf`) when validating trait values.
334    #[must_use]
335    pub fn effective_traits_schema(&self) -> Vec<Value> {
336        // Pre-order is self → ancestors; reverse to get deepest-base-first.
337        let mut out: Vec<Value> = self
338            .ancestors()
339            .filter_map(|s| s.traits_schema.clone())
340            .collect();
341        out.reverse();
342        out
343    }
344}
345
346/// Iterator over a type-schema's inheritance chain (self first, then each
347/// ancestor by following [`GtsTypeSchema::parent`]).
348pub struct AncestorIter<'a> {
349    current: Option<&'a GtsTypeSchema>,
350}
351
352impl<'a> Iterator for AncestorIter<'a> {
353    type Item = &'a GtsTypeSchema;
354
355    fn next(&mut self) -> Option<Self::Item> {
356        let curr = self.current.take()?;
357        self.current = curr.parent.as_deref();
358        Some(curr)
359    }
360}
361
362fn collect_own_properties(schema: &Value) -> BTreeMap<String, Value> {
363    let mut out = BTreeMap::new();
364    // Top-level properties.
365    if let Some(props) = schema.get("properties").and_then(|v| v.as_object()) {
366        for (k, v) in props {
367            out.insert(k.clone(), v.clone());
368        }
369    }
370    // Properties declared inside allOf branches that are NOT pure $refs
371    // (the parent's body comes from `self.parent`). Inline overlays count as "own".
372    if let Some(arr) = schema.get("allOf").and_then(|v| v.as_array()) {
373        for item in arr {
374            // Skip pure-$ref entries (resolved via `parent`).
375            let is_pure_ref = item
376                .as_object()
377                .is_some_and(|m| m.len() == 1 && m.contains_key("$ref"));
378            if is_pure_ref {
379                continue;
380            }
381            if let Some(props) = item.get("properties").and_then(|v| v.as_object()) {
382                for (k, v) in props {
383                    out.insert(k.clone(), v.clone());
384                }
385            }
386        }
387    }
388    out
389}
390
391fn collect_own_required(schema: &Value) -> Vec<String> {
392    let mut out = Vec::new();
393    if let Some(req) = schema.get("required").and_then(|v| v.as_array()) {
394        for r in req {
395            if let Some(s) = r.as_str() {
396                out.push(s.to_owned());
397            }
398        }
399    }
400    if let Some(arr) = schema.get("allOf").and_then(|v| v.as_array()) {
401        for item in arr {
402            let is_pure_ref = item
403                .as_object()
404                .is_some_and(|m| m.len() == 1 && m.contains_key("$ref"));
405            if is_pure_ref {
406                continue;
407            }
408            if let Some(req) = item.get("required").and_then(|v| v.as_array()) {
409                for r in req {
410                    if let Some(s) = r.as_str() {
411                        out.push(s.to_owned());
412                    }
413                }
414            }
415        }
416    }
417    out
418}
419
420/// Returns `schema` with the entry `allOf[i] = {$ref: gts://parent.type_id}`
421/// replaced by the merged body of the GTS parent. Other `allOf` entries
422/// (non-ref overlays, mixin `$ref`s pointing elsewhere) are left as-is.
423/// `$id` and `$schema` are stripped from the inlined parent to keep the
424/// merged document a valid composite schema.
425fn merge_schema_with_parent(schema: &Value, parent: Option<&GtsTypeSchema>) -> Value {
426    let Value::Object(map) = schema else {
427        return schema.clone();
428    };
429    let Some(parent) = parent else {
430        return Value::Object(map.clone());
431    };
432    let mut out = map.clone();
433
434    if let Some(Value::Array(items)) = out.get_mut("allOf").cloned().as_ref() {
435        let mut new_items = Vec::with_capacity(items.len());
436        for item in items {
437            let resolved = if let Some(obj) = item.as_object()
438                && obj.len() == 1
439                && let Some(ref_uri) = obj.get("$ref").and_then(|r| r.as_str())
440                && {
441                    let target = ref_uri.strip_prefix("gts://").unwrap_or(ref_uri);
442                    parent.type_id == target
443                } {
444                let mut merged = parent.effective_schema();
445                if let Value::Object(ref mut m) = merged {
446                    m.remove("$id");
447                    m.remove("$schema");
448                }
449                merged
450            } else {
451                item.clone()
452            };
453            new_items.push(resolved);
454        }
455        out.insert("allOf".to_owned(), Value::Array(new_items));
456    }
457
458    Value::Object(out)
459}
460
461/// A registered GTS instance.
462///
463/// The instance carries an `Arc`-shared reference to its [`GtsTypeSchema`],
464/// pre-resolved by the registry's local client (with full ancestor chain
465/// already linked). Inspect via `instance.type_schema.effective_*` directly.
466#[derive(Debug, Clone, PartialEq)]
467pub struct GtsInstance {
468    /// Deterministic UUID v5 derived from the GTS ID.
469    pub uuid: Uuid,
470
471    /// The full GTS instance identifier. Never ends with `~`.
472    pub id: GtsInstanceId,
473
474    /// All parsed segments from the GTS ID.
475    pub segments: Vec<GtsIdSegment>,
476
477    /// The full instance object (raw `Value`).
478    pub object: Value,
479
480    /// Resolved type-schema this instance conforms to (Arc-shared with the
481    /// registry's cache).
482    pub type_schema: Arc<GtsTypeSchema>,
483
484    /// Optional description of the entity.
485    pub description: Option<String>,
486}
487
488impl GtsInstance {
489    /// Constructs a `GtsInstance` from its canonical inputs plus a
490    /// pre-resolved type-schema reference.
491    ///
492    /// `uuid` and `segments` are derived from `id` via gts-rust's canonical
493    /// parser — there is only one source of truth (the id string). `id` must
494    /// NOT end with `~` and must contain at least one `~`. The passed
495    /// `type_schema.type_id` is verified to match the chain prefix derived
496    /// from `id` (everything up to and including the last `~`) so a
497    /// mismatched type-schema can't silently mislabel the instance.
498    ///
499    /// # Errors
500    ///
501    /// Returns [`InvalidGtsInstanceId`](TypesRegistryError::InvalidGtsInstanceId)
502    /// in any of these cases:
503    /// - `id` ends with `~` (looks like a type-schema id);
504    /// - `id` contains no `~` at all (no type-schema chain prefix);
505    /// - `id` does not parse as a valid GTS identifier;
506    /// - `type_schema.type_id` does not match the chain prefix derived from `id`.
507    pub fn try_new(
508        id: GtsInstanceId,
509        object: Value,
510        description: Option<String>,
511        type_schema: Arc<GtsTypeSchema>,
512    ) -> Result<Self, TypesRegistryError> {
513        if is_type_schema_id(id.as_ref()) {
514            return Err(TypesRegistryError::invalid_gts_instance_id(format!(
515                "{id} ends with `~` (looks like a type-schema id)",
516            )));
517        }
518        let derived = Self::derive_type_id(id.as_ref()).ok_or_else(|| {
519            TypesRegistryError::invalid_gts_instance_id(format!(
520                "instance id {id} has no type-schema chain (no `~`)"
521            ))
522        })?;
523        if derived != type_schema.type_id {
524            return Err(TypesRegistryError::invalid_gts_instance_id(format!(
525                "instance id {id} chain prefix {derived} does not match type-schema {0}",
526                type_schema.type_id
527            )));
528        }
529        let parsed = GtsID::new(id.as_ref())
530            .map_err(|e| TypesRegistryError::invalid_gts_instance_id(format!("{e}")))?;
531        let uuid = parsed.to_uuid();
532        let segments = parsed.gts_id_segments;
533        Ok(Self {
534            uuid,
535            id,
536            segments,
537            object,
538            type_schema,
539            description,
540        })
541    }
542
543    /// `type_id` of the type-schema this instance conforms to. Always ends with `~`.
544    #[must_use]
545    pub fn type_id(&self) -> &GtsTypeId {
546        &self.type_schema.type_id
547    }
548
549    /// Derives the type-schema (parent type) GTS ID from an instance `id`.
550    ///
551    /// Returns everything up to and including the last `~`. `None` when the
552    /// `id` contains no `~`.
553    #[must_use]
554    pub fn derive_type_id(id: &str) -> Option<GtsTypeId> {
555        id.rfind('~').map(|i| GtsTypeId::new(&id[..=i]))
556    }
557
558    /// Returns the primary segment (first segment in the chain).
559    #[must_use]
560    pub fn primary_segment(&self) -> Option<&GtsIdSegment> {
561        self.segments.first()
562    }
563
564    /// Returns the vendor from the primary segment.
565    #[must_use]
566    pub fn vendor(&self) -> Option<&str> {
567        self.primary_segment().map(|s| s.vendor.as_str())
568    }
569}
570
571/// Result of registering a single GTS entity in a batch operation.
572///
573/// Successful registration carries only the canonical (server-normalized)
574/// GTS id of the persisted entity. Callers that need a typed view of the
575/// registered entity should follow up with [`TypesRegistryClient::get_type_schema`]
576/// / [`TypesRegistryClient::get_instance`] — keeping registration's
577/// responsibility narrow ("did it persist?") and reads' responsibility narrow
578/// ("give me the resolved typed value").
579#[derive(Debug, Clone)]
580pub enum RegisterResult {
581    /// Successfully registered.
582    Ok {
583        /// The canonical GTS id of the registered entity.
584        gts_id: String,
585    },
586    /// Failed to register.
587    Err {
588        /// The GTS ID that was attempted, if it could be extracted from the input.
589        gts_id: Option<String>,
590        /// The error that occurred during registration.
591        error: TypesRegistryError,
592    },
593}
594
595impl RegisterResult {
596    /// Returns `true` if the registration was successful.
597    #[must_use]
598    pub const fn is_ok(&self) -> bool {
599        matches!(self, Self::Ok { .. })
600    }
601
602    /// Returns `true` if the registration failed.
603    #[must_use]
604    pub const fn is_err(&self) -> bool {
605        matches!(self, Self::Err { .. })
606    }
607
608    /// Converts to `Result<&str, &TypesRegistryError>` — the success arm
609    /// borrows the canonical `gts_id`.
610    ///
611    /// # Errors
612    ///
613    /// Returns `Err` with a reference to the error if this is a failed registration.
614    pub fn as_result(&self) -> Result<&str, &TypesRegistryError> {
615        match self {
616            Self::Ok { gts_id } => Ok(gts_id),
617            Self::Err { error, .. } => Err(error),
618        }
619    }
620
621    /// Converts into `Result<String, TypesRegistryError>` — the success arm
622    /// owns the canonical `gts_id`.
623    ///
624    /// # Errors
625    ///
626    /// Returns `Err` with the error if this is a failed registration.
627    pub fn into_result(self) -> Result<String, TypesRegistryError> {
628        match self {
629            Self::Ok { gts_id } => Ok(gts_id),
630            Self::Err { error, .. } => Err(error),
631        }
632    }
633
634    /// Returns the registered `gts_id` if successful, `None` otherwise.
635    #[must_use]
636    pub fn ok(self) -> Option<String> {
637        match self {
638            Self::Ok { gts_id } => Some(gts_id),
639            Self::Err { .. } => None,
640        }
641    }
642
643    /// Returns the error if failed, `None` otherwise.
644    #[must_use]
645    pub fn err(self) -> Option<TypesRegistryError> {
646        match self {
647            Self::Ok { .. } => None,
648            Self::Err { error, .. } => Some(error),
649        }
650    }
651
652    /// Returns `Ok(())` if all results are successful, or the first error.
653    ///
654    /// # Errors
655    ///
656    /// Returns the first `TypesRegistryError` encountered in `results`.
657    pub fn ensure_all_ok(results: &[Self]) -> Result<(), TypesRegistryError> {
658        for result in results {
659            if let Self::Err { error, .. } = result {
660                return Err(error.clone());
661            }
662        }
663        Ok(())
664    }
665}
666
667/// Summary of a batch registration operation.
668#[derive(Debug, Clone, Default, PartialEq, Eq)]
669pub struct RegisterSummary {
670    /// Number of successfully registered entities.
671    pub succeeded: usize,
672    /// Number of failed registrations.
673    pub failed: usize,
674}
675
676impl RegisterSummary {
677    /// Creates a new summary from a slice of register results.
678    #[must_use]
679    pub fn from_results(results: &[RegisterResult]) -> Self {
680        let succeeded = results.iter().filter(|r| r.is_ok()).count();
681        let failed = results.len() - succeeded;
682        Self { succeeded, failed }
683    }
684
685    /// Returns `true` if all registrations succeeded.
686    #[must_use]
687    pub const fn all_succeeded(&self) -> bool {
688        self.failed == 0
689    }
690
691    /// Returns `true` if all registrations failed.
692    #[must_use]
693    pub const fn all_failed(&self) -> bool {
694        self.succeeded == 0
695    }
696
697    /// Returns the total number of items processed.
698    #[must_use]
699    pub const fn total(&self) -> usize {
700        self.succeeded + self.failed
701    }
702}
703
704/// Query parameters for listing GTS type-schemas.
705#[derive(Debug, Clone, Default, PartialEq, Eq)]
706pub struct TypeSchemaQuery {
707    /// Optional GTS wildcard pattern (e.g. `gts.acme.*`).
708    pub pattern: Option<String>,
709}
710
711impl TypeSchemaQuery {
712    #[must_use]
713    pub fn new() -> Self {
714        Self::default()
715    }
716
717    #[must_use]
718    pub fn with_pattern(mut self, pattern: impl Into<String>) -> Self {
719        self.pattern = Some(pattern.into());
720        self
721    }
722
723    #[must_use]
724    pub fn is_empty(&self) -> bool {
725        self.pattern.is_none()
726    }
727}
728
729/// Query parameters for listing GTS instances.
730#[derive(Debug, Clone, Default, PartialEq, Eq)]
731pub struct InstanceQuery {
732    /// Optional GTS wildcard pattern (e.g. `gts.acme.events.user.v1~*`).
733    pub pattern: Option<String>,
734}
735
736impl InstanceQuery {
737    #[must_use]
738    pub fn new() -> Self {
739        Self::default()
740    }
741
742    #[must_use]
743    pub fn with_pattern(mut self, pattern: impl Into<String>) -> Self {
744        self.pattern = Some(pattern.into());
745        self
746    }
747
748    #[must_use]
749    pub fn is_empty(&self) -> bool {
750        self.pattern.is_none()
751    }
752}
753
754#[cfg(test)]
755#[path = "models_tests.rs"]
756mod tests;