use std::collections::BTreeMap;
use std::sync::Arc;
use gts::{GtsID, GtsIdSegment, GtsInstanceId, GtsSchemaId};
use serde_json::{Map, Value};
use uuid::Uuid;
use crate::error::TypesRegistryError;
pub type GtsTypeId = GtsSchemaId;
#[must_use]
pub fn is_type_schema_id(s: &str) -> bool {
s.ends_with('~')
}
#[derive(Debug, Clone, PartialEq)]
pub struct GtsTypeSchema {
pub type_uuid: Uuid,
pub type_id: GtsTypeId,
pub segments: Vec<GtsIdSegment>,
pub raw_schema: Value,
pub traits: Option<Value>,
pub traits_schema: Option<Value>,
pub parent: Option<Arc<GtsTypeSchema>>,
pub title: Option<String>,
pub description: Option<String>,
}
impl GtsTypeSchema {
pub fn try_new(
type_id: GtsTypeId,
raw_schema: Value,
description: Option<String>,
parent: Option<Arc<GtsTypeSchema>>,
) -> Result<Self, TypesRegistryError> {
if !is_type_schema_id(type_id.as_ref()) {
return Err(TypesRegistryError::invalid_gts_type_id(format!(
"{type_id} does not end with `~`",
)));
}
match (
parent.as_ref(),
Self::derive_parent_type_id(type_id.as_ref()),
) {
(Some(parent_schema), Some(expected)) if expected != parent_schema.type_id => {
return Err(TypesRegistryError::invalid_gts_type_id(format!(
"type-schema {type_id} expects parent {expected}, got {}",
parent_schema.type_id,
)));
}
(Some(_), None) => {
return Err(TypesRegistryError::invalid_gts_type_id(format!(
"root type-schema {type_id} cannot have a parent",
)));
}
(None, Some(expected)) => {
return Err(TypesRegistryError::invalid_gts_type_id(format!(
"derived type-schema {type_id} requires parent {expected}, got None",
)));
}
_ => {}
}
let parsed = GtsID::new(type_id.as_ref())
.map_err(|e| TypesRegistryError::invalid_gts_type_id(format!("{e}")))?;
let type_uuid = parsed.to_uuid();
let segments = parsed.gts_id_segments;
let traits = Self::extract_traits(&raw_schema);
let traits_schema = Self::extract_traits_schema(&raw_schema);
let title = Self::extract_title(&raw_schema);
Ok(Self {
type_uuid,
type_id,
segments,
raw_schema,
traits,
traits_schema,
parent,
title,
description,
})
}
#[must_use]
pub fn derive_parent_type_id(type_id: &str) -> Option<GtsTypeId> {
let trimmed = type_id.strip_suffix('~')?;
let last_tilde = trimmed.rfind('~')?;
Some(GtsTypeId::new(&type_id[..=last_tilde]))
}
#[must_use]
pub fn extract_traits(schema: &Value) -> Option<Value> {
schema.get("x-gts-traits").cloned()
}
#[must_use]
pub fn extract_traits_schema(schema: &Value) -> Option<Value> {
schema.get("x-gts-traits-schema").cloned()
}
#[must_use]
pub fn extract_allof_refs(schema: &Value) -> Vec<String> {
let Some(arr) = schema.get("allOf").and_then(|v| v.as_array()) else {
return Vec::new();
};
arr.iter()
.filter_map(|item| item.get("$ref").and_then(|r| r.as_str()))
.map(|r| r.strip_prefix("gts://").unwrap_or(r).to_owned())
.collect()
}
#[must_use]
pub fn extract_title(schema: &Value) -> Option<String> {
schema
.get("title")
.and_then(|v| v.as_str())
.map(ToOwned::to_owned)
}
#[must_use]
pub fn primary_segment(&self) -> Option<&GtsIdSegment> {
self.segments.first()
}
#[must_use]
pub fn vendor(&self) -> Option<&str> {
self.primary_segment().map(|s| s.vendor.as_str())
}
#[must_use]
pub fn ancestors(&self) -> AncestorIter<'_> {
AncestorIter {
current: Some(self),
}
}
#[must_use]
pub fn effective_schema(&self) -> Value {
merge_schema_with_parent(&self.raw_schema, self.parent.as_deref())
}
#[must_use]
pub fn effective_properties(&self) -> BTreeMap<String, Value> {
let mut out = self
.parent
.as_ref()
.map_or_else(BTreeMap::new, |p| p.effective_properties());
for (k, v) in collect_own_properties(&self.raw_schema) {
out.insert(k, v);
}
out
}
#[must_use]
pub fn effective_required(&self) -> Vec<String> {
let mut seen = std::collections::HashSet::new();
let mut out = Vec::new();
for ancestor in self.ancestors() {
for r in collect_own_required(&ancestor.raw_schema) {
if seen.insert(r.clone()) {
out.push(r);
}
}
}
out
}
#[must_use]
pub fn effective_traits(&self) -> Value {
let mut acc: Map<String, Value> = Map::new();
for s in self.ancestors() {
if let Some(Value::Object(traits)) = s.traits.as_ref() {
for (k, v) in traits {
acc.entry(k.clone()).or_insert_with(|| v.clone());
}
}
}
let chain: Vec<&GtsTypeSchema> = self.ancestors().collect();
for s in chain.iter().rev() {
let Some(traits_schema) = s.traits_schema.as_ref() else {
continue;
};
let Some(Value::Object(props)) = traits_schema.get("properties") else {
continue;
};
for (k, prop) in props {
if let Some(default) = prop.get("default") {
acc.entry(k.clone()).or_insert_with(|| default.clone());
}
}
}
if acc.is_empty() {
Value::Null
} else {
Value::Object(acc)
}
}
#[must_use]
pub fn effective_traits_schema(&self) -> Vec<Value> {
let mut out: Vec<Value> = self
.ancestors()
.filter_map(|s| s.traits_schema.clone())
.collect();
out.reverse();
out
}
}
pub struct AncestorIter<'a> {
current: Option<&'a GtsTypeSchema>,
}
impl<'a> Iterator for AncestorIter<'a> {
type Item = &'a GtsTypeSchema;
fn next(&mut self) -> Option<Self::Item> {
let curr = self.current.take()?;
self.current = curr.parent.as_deref();
Some(curr)
}
}
fn collect_own_properties(schema: &Value) -> BTreeMap<String, Value> {
let mut out = BTreeMap::new();
if let Some(props) = schema.get("properties").and_then(|v| v.as_object()) {
for (k, v) in props {
out.insert(k.clone(), v.clone());
}
}
if let Some(arr) = schema.get("allOf").and_then(|v| v.as_array()) {
for item in arr {
let is_pure_ref = item
.as_object()
.is_some_and(|m| m.len() == 1 && m.contains_key("$ref"));
if is_pure_ref {
continue;
}
if let Some(props) = item.get("properties").and_then(|v| v.as_object()) {
for (k, v) in props {
out.insert(k.clone(), v.clone());
}
}
}
}
out
}
fn collect_own_required(schema: &Value) -> Vec<String> {
let mut out = Vec::new();
if let Some(req) = schema.get("required").and_then(|v| v.as_array()) {
for r in req {
if let Some(s) = r.as_str() {
out.push(s.to_owned());
}
}
}
if let Some(arr) = schema.get("allOf").and_then(|v| v.as_array()) {
for item in arr {
let is_pure_ref = item
.as_object()
.is_some_and(|m| m.len() == 1 && m.contains_key("$ref"));
if is_pure_ref {
continue;
}
if let Some(req) = item.get("required").and_then(|v| v.as_array()) {
for r in req {
if let Some(s) = r.as_str() {
out.push(s.to_owned());
}
}
}
}
}
out
}
fn merge_schema_with_parent(schema: &Value, parent: Option<&GtsTypeSchema>) -> Value {
let Value::Object(map) = schema else {
return schema.clone();
};
let Some(parent) = parent else {
return Value::Object(map.clone());
};
let mut out = map.clone();
if let Some(Value::Array(items)) = out.get_mut("allOf").cloned().as_ref() {
let mut new_items = Vec::with_capacity(items.len());
for item in items {
let resolved = if let Some(obj) = item.as_object()
&& obj.len() == 1
&& let Some(ref_uri) = obj.get("$ref").and_then(|r| r.as_str())
&& {
let target = ref_uri.strip_prefix("gts://").unwrap_or(ref_uri);
parent.type_id == target
} {
let mut merged = parent.effective_schema();
if let Value::Object(ref mut m) = merged {
m.remove("$id");
m.remove("$schema");
}
merged
} else {
item.clone()
};
new_items.push(resolved);
}
out.insert("allOf".to_owned(), Value::Array(new_items));
}
Value::Object(out)
}
#[derive(Debug, Clone, PartialEq)]
pub struct GtsInstance {
pub uuid: Uuid,
pub id: GtsInstanceId,
pub segments: Vec<GtsIdSegment>,
pub object: Value,
pub type_schema: Arc<GtsTypeSchema>,
pub description: Option<String>,
}
impl GtsInstance {
pub fn try_new(
id: GtsInstanceId,
object: Value,
description: Option<String>,
type_schema: Arc<GtsTypeSchema>,
) -> Result<Self, TypesRegistryError> {
if is_type_schema_id(id.as_ref()) {
return Err(TypesRegistryError::invalid_gts_instance_id(format!(
"{id} ends with `~` (looks like a type-schema id)",
)));
}
let derived = Self::derive_type_id(id.as_ref()).ok_or_else(|| {
TypesRegistryError::invalid_gts_instance_id(format!(
"instance id {id} has no type-schema chain (no `~`)"
))
})?;
if derived != type_schema.type_id {
return Err(TypesRegistryError::invalid_gts_instance_id(format!(
"instance id {id} chain prefix {derived} does not match type-schema {0}",
type_schema.type_id
)));
}
let parsed = GtsID::new(id.as_ref())
.map_err(|e| TypesRegistryError::invalid_gts_instance_id(format!("{e}")))?;
let uuid = parsed.to_uuid();
let segments = parsed.gts_id_segments;
Ok(Self {
uuid,
id,
segments,
object,
type_schema,
description,
})
}
#[must_use]
pub fn type_id(&self) -> &GtsTypeId {
&self.type_schema.type_id
}
#[must_use]
pub fn derive_type_id(id: &str) -> Option<GtsTypeId> {
id.rfind('~').map(|i| GtsTypeId::new(&id[..=i]))
}
#[must_use]
pub fn primary_segment(&self) -> Option<&GtsIdSegment> {
self.segments.first()
}
#[must_use]
pub fn vendor(&self) -> Option<&str> {
self.primary_segment().map(|s| s.vendor.as_str())
}
}
#[derive(Debug, Clone)]
pub enum RegisterResult {
Ok {
gts_id: String,
},
Err {
gts_id: Option<String>,
error: TypesRegistryError,
},
}
impl RegisterResult {
#[must_use]
pub const fn is_ok(&self) -> bool {
matches!(self, Self::Ok { .. })
}
#[must_use]
pub const fn is_err(&self) -> bool {
matches!(self, Self::Err { .. })
}
pub fn as_result(&self) -> Result<&str, &TypesRegistryError> {
match self {
Self::Ok { gts_id } => Ok(gts_id),
Self::Err { error, .. } => Err(error),
}
}
pub fn into_result(self) -> Result<String, TypesRegistryError> {
match self {
Self::Ok { gts_id } => Ok(gts_id),
Self::Err { error, .. } => Err(error),
}
}
#[must_use]
pub fn ok(self) -> Option<String> {
match self {
Self::Ok { gts_id } => Some(gts_id),
Self::Err { .. } => None,
}
}
#[must_use]
pub fn err(self) -> Option<TypesRegistryError> {
match self {
Self::Ok { .. } => None,
Self::Err { error, .. } => Some(error),
}
}
pub fn ensure_all_ok(results: &[Self]) -> Result<(), TypesRegistryError> {
for result in results {
if let Self::Err { error, .. } = result {
return Err(error.clone());
}
}
Ok(())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RegisterSummary {
pub succeeded: usize,
pub failed: usize,
}
impl RegisterSummary {
#[must_use]
pub fn from_results(results: &[RegisterResult]) -> Self {
let succeeded = results.iter().filter(|r| r.is_ok()).count();
let failed = results.len() - succeeded;
Self { succeeded, failed }
}
#[must_use]
pub const fn all_succeeded(&self) -> bool {
self.failed == 0
}
#[must_use]
pub const fn all_failed(&self) -> bool {
self.succeeded == 0
}
#[must_use]
pub const fn total(&self) -> usize {
self.succeeded + self.failed
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TypeSchemaQuery {
pub pattern: Option<String>,
}
impl TypeSchemaQuery {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_pattern(mut self, pattern: impl Into<String>) -> Self {
self.pattern = Some(pattern.into());
self
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.pattern.is_none()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct InstanceQuery {
pub pattern: Option<String>,
}
impl InstanceQuery {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_pattern(mut self, pattern: impl Into<String>) -> Self {
self.pattern = Some(pattern.into());
self
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.pattern.is_none()
}
}
#[cfg(test)]
#[path = "models_tests.rs"]
mod tests;