1use std::collections::BTreeMap;
2
3use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory, DiagnosticCode};
4use type_bridge_contract::schema::{
5 DocumentFingerprint, DocumentId, SchemaDiagnostic, SchemaDiagnostics,
6 SchemaDocumentSetFingerprint, SourceSpan,
7};
8
9use crate::schema_set::SchemaSetManifestDocument;
10
11pub const DEFAULT_MAX_DOCUMENTS: usize = 4_096;
13pub const DEFAULT_MAX_AGGREGATE_BYTES: usize = 64 * 1024 * 1024;
15pub const DEFAULT_MAX_DOCUMENT_BYTES: usize = 16 * 1024 * 1024;
17pub const DEFAULT_MAX_DEPTH: usize = 64;
19pub const DEFAULT_MAX_NODES: usize = 65_536;
21pub const DEFAULT_MAX_SCALAR_SOURCE_BYTES: usize = DEFAULT_MAX_DOCUMENT_BYTES;
27
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30pub struct SchemaParseLimits {
31 max_documents: usize,
32 max_aggregate_bytes: usize,
33 max_document_bytes: usize,
34 max_depth: usize,
35 max_nodes: usize,
36 max_scalar_source_bytes: usize,
37}
38
39impl SchemaParseLimits {
40 #[must_use]
42 pub const fn new(
43 max_documents: usize,
44 max_aggregate_bytes: usize,
45 max_document_bytes: usize,
46 max_depth: usize,
47 max_nodes: usize,
48 max_scalar_source_bytes: usize,
49 ) -> Self {
50 Self {
51 max_documents,
52 max_aggregate_bytes,
53 max_document_bytes,
54 max_depth,
55 max_nodes,
56 max_scalar_source_bytes,
57 }
58 }
59
60 #[must_use]
62 pub const fn max_documents(self) -> usize {
63 self.max_documents
64 }
65
66 #[must_use]
68 pub const fn max_aggregate_bytes(self) -> usize {
69 self.max_aggregate_bytes
70 }
71
72 #[must_use]
74 pub const fn max_document_bytes(self) -> usize {
75 self.max_document_bytes
76 }
77
78 #[must_use]
80 pub const fn max_depth(self) -> usize {
81 self.max_depth
82 }
83
84 #[must_use]
86 pub const fn max_nodes(self) -> usize {
87 self.max_nodes
88 }
89
90 #[must_use]
92 pub const fn max_scalar_source_bytes(self) -> usize {
93 self.max_scalar_source_bytes
94 }
95
96 #[must_use]
102 pub const fn max_scalar_bytes(self) -> usize {
103 self.max_scalar_source_bytes()
104 }
105}
106
107impl Default for SchemaParseLimits {
108 fn default() -> Self {
109 Self::new(
110 DEFAULT_MAX_DOCUMENTS,
111 DEFAULT_MAX_AGGREGATE_BYTES,
112 DEFAULT_MAX_DOCUMENT_BYTES,
113 DEFAULT_MAX_DEPTH,
114 DEFAULT_MAX_NODES,
115 DEFAULT_MAX_SCALAR_SOURCE_BYTES,
116 )
117 }
118}
119
120#[derive(Clone, Copy, Debug, Eq, PartialEq)]
122pub enum YamlScalarStyle {
123 Plain,
125 SingleQuoted,
127 DoubleQuoted,
129 Literal,
131 Folded,
133}
134
135#[derive(Clone, Copy, Debug, Eq, PartialEq)]
137pub enum YamlCollectionStyle {
138 Block,
140 Flow,
142}
143
144#[derive(Clone, Copy, Debug, Eq, PartialEq)]
146pub enum CommentPlacement {
147 Above,
149 Right,
151 Free,
153 Last,
155}
156
157#[derive(Clone, Debug, Eq, PartialEq)]
159pub struct SchemaComment {
160 text: String,
161 placement: CommentPlacement,
162 span: SourceSpan,
163}
164
165impl SchemaComment {
166 pub(crate) fn new(text: String, placement: CommentPlacement, span: SourceSpan) -> Self {
167 Self {
168 text,
169 placement,
170 span,
171 }
172 }
173
174 #[must_use]
176 pub fn text(&self) -> &str {
177 &self.text
178 }
179
180 #[must_use]
182 pub const fn placement(&self) -> CommentPlacement {
183 self.placement
184 }
185
186 #[must_use]
188 pub const fn span(&self) -> &SourceSpan {
189 &self.span
190 }
191}
192
193#[derive(Clone, Debug, Eq, PartialEq)]
195pub struct YamlScalar {
196 value: String,
197 raw: String,
198 style: YamlScalarStyle,
199 span: SourceSpan,
200}
201
202impl YamlScalar {
203 pub(crate) fn new(
204 value: String,
205 raw: String,
206 style: YamlScalarStyle,
207 span: SourceSpan,
208 ) -> Self {
209 Self {
210 value,
211 raw,
212 style,
213 span,
214 }
215 }
216
217 #[must_use]
219 pub fn value(&self) -> &str {
220 &self.value
221 }
222
223 #[must_use]
225 pub fn raw(&self) -> &str {
226 &self.raw
227 }
228
229 #[must_use]
231 pub const fn style(&self) -> YamlScalarStyle {
232 self.style
233 }
234
235 #[must_use]
237 pub const fn span(&self) -> &SourceSpan {
238 &self.span
239 }
240}
241
242#[derive(Clone, Debug, Eq, PartialEq)]
244pub struct YamlMappingEntry {
245 key: YamlScalar,
246 value: YamlNode,
247}
248
249impl YamlMappingEntry {
250 pub(crate) const fn new(key: YamlScalar, value: YamlNode) -> Self {
251 Self { key, value }
252 }
253
254 #[must_use]
256 pub const fn key(&self) -> &YamlScalar {
257 &self.key
258 }
259
260 #[must_use]
262 pub const fn value(&self) -> &YamlNode {
263 &self.value
264 }
265}
266
267#[derive(Clone, Debug, Eq, PartialEq)]
269pub struct YamlMapping {
270 entries: Vec<YamlMappingEntry>,
271 style: YamlCollectionStyle,
272 span: SourceSpan,
273}
274
275impl YamlMapping {
276 pub(crate) const fn new(
277 entries: Vec<YamlMappingEntry>,
278 style: YamlCollectionStyle,
279 span: SourceSpan,
280 ) -> Self {
281 Self {
282 entries,
283 style,
284 span,
285 }
286 }
287
288 #[must_use]
290 pub fn entries(&self) -> &[YamlMappingEntry] {
291 &self.entries
292 }
293
294 #[must_use]
296 pub const fn style(&self) -> YamlCollectionStyle {
297 self.style
298 }
299
300 #[must_use]
302 pub const fn span(&self) -> &SourceSpan {
303 &self.span
304 }
305}
306
307#[derive(Clone, Debug, Eq, PartialEq)]
309pub struct YamlSequence {
310 items: Vec<YamlNode>,
311 style: YamlCollectionStyle,
312 span: SourceSpan,
313}
314
315impl YamlSequence {
316 pub(crate) const fn new(
317 items: Vec<YamlNode>,
318 style: YamlCollectionStyle,
319 span: SourceSpan,
320 ) -> Self {
321 Self { items, style, span }
322 }
323
324 #[must_use]
326 pub fn items(&self) -> &[YamlNode] {
327 &self.items
328 }
329
330 #[must_use]
332 pub const fn style(&self) -> YamlCollectionStyle {
333 self.style
334 }
335
336 #[must_use]
338 pub const fn span(&self) -> &SourceSpan {
339 &self.span
340 }
341}
342
343#[derive(Clone, Debug, Eq, PartialEq)]
345pub enum YamlNode {
346 Scalar(YamlScalar),
348 Sequence(YamlSequence),
350 Mapping(YamlMapping),
352}
353
354impl YamlNode {
355 #[must_use]
357 pub const fn span(&self) -> &SourceSpan {
358 match self {
359 Self::Scalar(value) => value.span(),
360 Self::Sequence(value) => value.span(),
361 Self::Mapping(value) => value.span(),
362 }
363 }
364
365 #[must_use]
367 pub const fn as_mapping(&self) -> Option<&YamlMapping> {
368 match self {
369 Self::Mapping(value) => Some(value),
370 Self::Scalar(_) | Self::Sequence(_) => None,
371 }
372 }
373
374 #[must_use]
376 pub const fn as_sequence(&self) -> Option<&YamlSequence> {
377 match self {
378 Self::Sequence(value) => Some(value),
379 Self::Scalar(_) | Self::Mapping(_) => None,
380 }
381 }
382
383 #[must_use]
385 pub const fn as_scalar(&self) -> Option<&YamlScalar> {
386 match self {
387 Self::Scalar(value) => Some(value),
388 Self::Sequence(_) | Self::Mapping(_) => None,
389 }
390 }
391}
392
393#[derive(Clone, Debug, Eq, PartialEq)]
395pub struct SchemaDocument {
396 id: DocumentId,
397 source: String,
398 fingerprint: DocumentFingerprint,
399 root: YamlMapping,
400 comments: Vec<SchemaComment>,
401}
402
403impl SchemaDocument {
404 pub(crate) const fn new(
405 id: DocumentId,
406 source: String,
407 fingerprint: DocumentFingerprint,
408 root: YamlMapping,
409 comments: Vec<SchemaComment>,
410 ) -> Self {
411 Self {
412 id,
413 source,
414 fingerprint,
415 root,
416 comments,
417 }
418 }
419
420 pub fn parse(id: DocumentId, source: impl Into<String>) -> Result<Self, SchemaDiagnostics> {
422 Self::parse_with_limits(id, source, SchemaParseLimits::default())
423 }
424
425 pub fn parse_with_limits(
427 id: DocumentId,
428 source: impl Into<String>,
429 limits: SchemaParseLimits,
430 ) -> Result<Self, SchemaDiagnostics> {
431 crate::yaml::parse_document_with_limits(id, source.into(), limits)
432 }
433
434 #[must_use]
436 pub const fn id(&self) -> &DocumentId {
437 &self.id
438 }
439
440 #[must_use]
442 pub fn source(&self) -> &str {
443 &self.source
444 }
445
446 #[must_use]
448 pub const fn fingerprint(&self) -> &DocumentFingerprint {
449 &self.fingerprint
450 }
451
452 #[must_use]
454 pub const fn root(&self) -> &YamlMapping {
455 &self.root
456 }
457
458 #[must_use]
460 pub fn comments(&self) -> &[SchemaComment] {
461 &self.comments
462 }
463}
464
465#[derive(Clone, Debug, Default, Eq, PartialEq)]
467pub struct SchemaDocumentSet {
468 documents: BTreeMap<DocumentId, SchemaDocument>,
469 manifest: Option<SchemaSetManifestDocument>,
470}
471
472impl SchemaDocumentSet {
473 pub fn parse<I, S>(sources: I) -> Result<Self, SchemaDiagnostics>
475 where
476 I: IntoIterator<Item = (DocumentId, S)>,
477 S: Into<String>,
478 {
479 Self::parse_with_limits(sources, SchemaParseLimits::default())
480 }
481
482 pub fn parse_with_limits<I, S>(
484 sources: I,
485 limits: SchemaParseLimits,
486 ) -> Result<Self, SchemaDiagnostics>
487 where
488 I: IntoIterator<Item = (DocumentId, S)>,
489 S: Into<String>,
490 {
491 let mut documents: BTreeMap<DocumentId, SchemaDocument> = BTreeMap::new();
492 let mut aggregate_bytes = 0usize;
493
494 for (id, source) in sources {
495 if documents.len() >= limits.max_documents() {
496 return Err(resource_diagnostic(
497 "schema_document_count_limit",
498 format!(
499 "schema document count exceeds the limit of {}",
500 limits.max_documents()
501 ),
502 None,
503 ));
504 }
505
506 let source = source.into();
507 aggregate_bytes = aggregate_bytes.checked_add(source.len()).ok_or_else(|| {
508 resource_diagnostic(
509 "schema_aggregate_size_limit",
510 "schema aggregate source size overflowed",
511 None,
512 )
513 })?;
514 if aggregate_bytes > limits.max_aggregate_bytes() {
515 return Err(resource_diagnostic(
516 "schema_aggregate_size_limit",
517 format!(
518 "schema aggregate source size exceeds the limit of {} bytes",
519 limits.max_aggregate_bytes()
520 ),
521 None,
522 ));
523 }
524
525 if let Some(existing) = documents.get(&id) {
526 return Err(crate::yaml::diagnostic_with_related(
527 DiagnosticCategory::InvalidContract,
528 "duplicate_schema_document",
529 format!("schema document identifier `{}` is duplicated", id.as_str()),
530 existing.root().span().clone(),
531 existing.root().span().clone(),
532 "first document with this identifier",
533 ));
534 }
535
536 let document = SchemaDocument::parse_with_limits(id.clone(), source, limits)?;
537 documents.insert(id, document);
538 }
539
540 Ok(Self {
541 documents,
542 manifest: None,
543 })
544 }
545
546 pub(crate) fn attach_manifest(&mut self, manifest: SchemaSetManifestDocument) {
547 self.manifest = Some(manifest);
548 }
549
550 #[must_use]
552 pub const fn manifest(&self) -> Option<&SchemaSetManifestDocument> {
553 self.manifest.as_ref()
554 }
555
556 pub fn fingerprint(&self) -> Result<SchemaDocumentSetFingerprint, SchemaDiagnostics> {
558 let mut canonical = Vec::new();
559 canonical.extend_from_slice(
560 &u64::try_from(self.documents.len())
561 .expect("schema document count is bounded below u64::MAX")
562 .to_be_bytes(),
563 );
564 for (id, document) in &self.documents {
565 canonical.extend_from_slice(
566 &u64::try_from(id.as_str().len())
567 .expect("document identifier length is bounded below u64::MAX")
568 .to_be_bytes(),
569 );
570 canonical.extend_from_slice(id.as_str().as_bytes());
571 canonical.extend_from_slice(&document.fingerprint().as_fingerprint().digest().bytes());
572 }
573 SchemaDocumentSetFingerprint::compute(&canonical)
574 .map_err(|error| SchemaDiagnostics::one(SchemaDiagnostic::new(error, None)))
575 }
576
577 #[must_use]
579 pub fn len(&self) -> usize {
580 self.documents.len()
581 }
582
583 #[must_use]
585 pub fn is_empty(&self) -> bool {
586 self.documents.is_empty()
587 }
588
589 #[must_use]
591 pub fn get(&self, id: &DocumentId) -> Option<&SchemaDocument> {
592 self.documents.get(id)
593 }
594
595 pub fn iter(&self) -> impl ExactSizeIterator<Item = (&DocumentId, &SchemaDocument)> {
597 self.documents.iter()
598 }
599}
600
601pub(crate) fn resource_diagnostic(
602 code: &'static str,
603 message: impl Into<String>,
604 primary: Option<SourceSpan>,
605) -> SchemaDiagnostics {
606 let diagnostic = Diagnostic::new(
607 DiagnosticCategory::ResourceLimit,
608 DiagnosticCode::new(code).expect("static schema diagnostic code is valid"),
609 message,
610 );
611 SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, primary))
612}