type_bridge_schema/
schema_set.rs1use 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
9pub const SCHEMA_SET_V1_FORMAT: &str = "typebridge.schema-set/v1";
11pub const SCHEMA_DISCOVERY_V1: &str = "typebridge.schema-discovery/v1";
13
14#[derive(Clone, Debug, Eq, PartialEq)]
16pub struct SchemaDiscoveryVersion;
17
18impl SchemaDiscoveryVersion {
19 #[must_use]
21 pub const fn as_str(&self) -> &'static str {
22 SCHEMA_DISCOVERY_V1
23 }
24}
25
26#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct SchemaSetManifest {
29 sources: Vec<String>,
30}
31
32impl SchemaSetManifest {
33 #[must_use]
35 pub const fn format(&self) -> &'static str {
36 SCHEMA_SET_V1_FORMAT
37 }
38
39 #[must_use]
41 pub fn sources(&self) -> &[String] {
42 &self.sources
43 }
44}
45
46#[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 #[must_use]
95 pub fn path(&self) -> &Path {
96 &self.path
97 }
98
99 #[must_use]
101 pub fn source(&self) -> &str {
102 self.document.source()
103 }
104
105 #[must_use]
107 pub const fn fingerprint(&self) -> &DocumentFingerprint {
108 self.document.fingerprint()
109 }
110
111 #[must_use]
113 pub const fn root(&self) -> &YamlMapping {
114 self.document.root()
115 }
116
117 #[must_use]
119 pub fn comments(&self) -> &[SchemaComment] {
120 self.document.comments()
121 }
122
123 #[must_use]
125 pub const fn manifest(&self) -> &SchemaSetManifest {
126 &self.manifest
127 }
128
129 #[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}