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;