Skip to main content

type_bridge_schema/
schema_set.rs

1use std::path::{Path, PathBuf};
2
3use type_bridge_contract::diagnostic::DiagnosticCategory;
4use type_bridge_contract::schema::{DocumentFingerprint, DocumentId, SchemaDiagnostics};
5
6use crate::diagnostic::diagnostic;
7use crate::{SchemaComment, SchemaDocument, SchemaParseLimits, YamlMapping, YamlNode};
8
9/// The only supported schema-set manifest format in V1.
10pub const SCHEMA_SET_V1_FORMAT: &str = "typebridge.schema-set/v1";
11/// The frozen Phase 2 source-discovery algorithm identifier.
12pub const SCHEMA_DISCOVERY_V1: &str = "typebridge.schema-discovery/v1";
13
14/// A closed identifier for the source-discovery algorithm used by a snapshot.
15#[derive(Clone, Debug, Eq, PartialEq)]
16pub struct SchemaDiscoveryVersion;
17
18impl SchemaDiscoveryVersion {
19    /// Returns the frozen V1 discovery identifier.
20    #[must_use]
21    pub const fn as_str(&self) -> &'static str {
22        SCHEMA_DISCOVERY_V1
23    }
24}
25
26/// Validated semantic fields from a `typebridge.schema-set/v1` manifest.
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct SchemaSetManifest {
29    sources: Vec<String>,
30}
31
32impl SchemaSetManifest {
33    /// Returns the exact supported format identifier.
34    #[must_use]
35    pub const fn format(&self) -> &'static str {
36        SCHEMA_SET_V1_FORMAT
37    }
38
39    /// Returns source patterns in manifest order and spelling.
40    #[must_use]
41    pub fn sources(&self) -> &[String] {
42        &self.sources
43    }
44}
45
46/// A validated schema-set manifest retaining its exact source presentation.
47#[derive(Clone, Debug, Eq, PartialEq)]
48pub struct SchemaSetManifestDocument {
49    path: PathBuf,
50    document: SchemaDocument,
51    manifest: SchemaSetManifest,
52}
53
54impl SchemaSetManifestDocument {
55    pub(crate) fn parse(
56        path: PathBuf,
57        source: String,
58        limits: SchemaParseLimits,
59    ) -> Result<Self, SchemaDiagnostics> {
60        let document_id = DocumentId::new("schema-set-manifest").map_err(|error| {
61            diagnostic(
62                DiagnosticCategory::Integrity,
63                "invalid_schema_manifest_document_id",
64                error.message(),
65                None,
66            )
67        })?;
68        let document = match SchemaDocument::parse_with_limits(document_id, source, limits) {
69            Err(error)
70                if error.iter().next().is_some_and(|item| {
71                    item.diagnostic().code().as_str() == "yaml_root_not_mapping"
72                }) =>
73            {
74                let primary = error.iter().next().and_then(|item| item.primary()).cloned();
75                return Err(diagnostic(
76                    DiagnosticCategory::InvalidContract,
77                    "schema_set_root_not_mapping",
78                    "the schema-set manifest root must be a mapping",
79                    primary,
80                ));
81            }
82            Err(error) => return Err(error),
83            Ok(document) => document,
84        };
85        let manifest = parse_manifest(document.root())?;
86        Ok(Self {
87            path,
88            document,
89            manifest,
90        })
91    }
92
93    /// Returns the canonical manifest path captured by discovery.
94    #[must_use]
95    pub fn path(&self) -> &Path {
96        &self.path
97    }
98
99    /// Returns the source exactly as captured.
100    #[must_use]
101    pub fn source(&self) -> &str {
102        self.document.source()
103    }
104
105    /// Returns the exact-source document fingerprint.
106    #[must_use]
107    pub const fn fingerprint(&self) -> &DocumentFingerprint {
108        self.document.fingerprint()
109    }
110
111    /// Returns the retained lossless YAML root.
112    #[must_use]
113    pub const fn root(&self) -> &YamlMapping {
114        self.document.root()
115    }
116
117    /// Returns retained comments in source order.
118    #[must_use]
119    pub fn comments(&self) -> &[SchemaComment] {
120        self.document.comments()
121    }
122
123    /// Returns the validated manifest fields.
124    #[must_use]
125    pub const fn manifest(&self) -> &SchemaSetManifest {
126        &self.manifest
127    }
128
129    /// Returns source patterns in manifest order and spelling.
130    #[must_use]
131    pub fn sources(&self) -> &[String] {
132        self.manifest.sources()
133    }
134}
135
136fn parse_manifest(root: &YamlMapping) -> Result<SchemaSetManifest, SchemaDiagnostics> {
137    let mut format: Option<&YamlNode> = None;
138    let mut sources: Option<&YamlNode> = None;
139    for entry in root.entries() {
140        match entry.key().value() {
141            "format" => format = Some(entry.value()),
142            "sources" => sources = Some(entry.value()),
143            key => {
144                return Err(diagnostic(
145                    DiagnosticCategory::InvalidContract,
146                    "unknown_schema_set_key",
147                    format!("schema-set manifest key `{key}` is not supported in V1"),
148                    Some(entry.key().span().clone()),
149                ));
150            }
151        }
152    }
153
154    let format = format.ok_or_else(|| {
155        diagnostic(
156            DiagnosticCategory::InvalidContract,
157            "schema_set_format_missing",
158            "schema-set manifest requires `format`",
159            Some(root.span().clone()),
160        )
161    })?;
162    let format = format.as_scalar().ok_or_else(|| {
163        diagnostic(
164            DiagnosticCategory::UnsupportedCapability,
165            "unsupported_schema_set_format",
166            "schema-set manifest format must be `typebridge.schema-set/v1`",
167            Some(format.span().clone()),
168        )
169    })?;
170    if format.value() != SCHEMA_SET_V1_FORMAT {
171        return Err(diagnostic(
172            DiagnosticCategory::UnsupportedCapability,
173            "unsupported_schema_set_format",
174            format!(
175                "schema-set manifest format `{}` is not supported",
176                format.value()
177            ),
178            Some(format.span().clone()),
179        ));
180    }
181
182    let sources = sources.ok_or_else(|| {
183        diagnostic(
184            DiagnosticCategory::InvalidContract,
185            "schema_set_sources_missing",
186            "schema-set manifest requires `sources`",
187            Some(root.span().clone()),
188        )
189    })?;
190    let sequence = sources.as_sequence().ok_or_else(|| {
191        diagnostic(
192            DiagnosticCategory::InvalidContract,
193            "schema_set_sources_not_sequence",
194            "schema-set manifest `sources` must be a sequence",
195            Some(sources.span().clone()),
196        )
197    })?;
198    if sequence.items().is_empty() {
199        return Err(diagnostic(
200            DiagnosticCategory::InvalidContract,
201            "empty_schema_source_patterns",
202            "schema-set manifest must select at least one source pattern",
203            Some(sequence.span().clone()),
204        ));
205    }
206    let mut parsed_sources = Vec::with_capacity(sequence.items().len());
207    for item in sequence.items() {
208        let scalar = item.as_scalar().ok_or_else(|| {
209            diagnostic(
210                DiagnosticCategory::InvalidContract,
211                "schema_set_source_not_string",
212                "every schema-set source pattern must be a string",
213                Some(item.span().clone()),
214            )
215        })?;
216        parsed_sources.push(scalar.value().to_owned());
217    }
218    Ok(SchemaSetManifest {
219        sources: parsed_sources,
220    })
221}