1use crate::schema::graph::{SchemaGraph, reachable_defs};
16use crate::schema::metadata::{MetadataEnvelope, Role};
17use crate::schema::schema_type::{
18 BinaryRestrictions, DiscriminatorRule, NumericBound, NumericRestrictions, PathDirection,
19 PathKind, QuantityValue, SchemaType, TextRestrictions, UrlRestrictions,
20};
21use crate::schema::validation::validate_graph;
22use crate::schema::{FromSchema, FromSchemaError, IntoSchema, SchemaBuilder, SchemaValue, TypeId};
23use serde::{Deserialize, Serialize};
24use std::collections::HashSet;
25
26const FINGERPRINT_DOMAIN: &str = "golem-schema-fingerprint";
27const FORMAT_VERSION: u64 = 1;
28
29#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
31#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
32#[cfg_attr(feature = "full", desert(transparent))]
33#[serde(transparent)]
34pub struct SchemaFingerprintV1(pub [u8; 32]);
35
36impl SchemaFingerprintV1 {
37 pub const FORMAT_VERSION: u8 = 1;
38
39 pub fn as_bytes(&self) -> &[u8; 32] {
40 &self.0
41 }
42
43 pub fn to_hex(self) -> String {
44 blake3::Hash::from_bytes(self.0).to_hex().to_string()
45 }
46}
47
48impl std::fmt::Display for SchemaFingerprintV1 {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.write_str(&blake3::Hash::from_bytes(self.0).to_hex())
51 }
52}
53
54impl IntoSchema for SchemaFingerprintV1 {
55 fn type_id() -> TypeId {
56 TypeId::new("golem_schema.SchemaFingerprintV1")
57 }
58
59 fn register_in(builder: &mut SchemaBuilder) -> SchemaType {
60 <Vec<u8> as IntoSchema>::register_in(builder)
61 }
62
63 fn to_value(&self) -> SchemaValue {
64 self.0.to_vec().to_value()
65 }
66}
67
68impl FromSchema for SchemaFingerprintV1 {
69 fn from_value(value: &SchemaValue) -> Result<Self, FromSchemaError> {
70 let bytes = Vec::<u8>::from_value(value)?;
71 let bytes: [u8; 32] = bytes.try_into().map_err(|bytes: Vec<u8>| {
72 FromSchemaError::custom(format!(
73 "schema fingerprint must have 32 bytes, got {}",
74 bytes.len()
75 ))
76 })?;
77 Ok(Self(bytes))
78 }
79}
80
81#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
82pub enum SchemaFingerprintError {
83 #[error("invalid stream element schema: {0}")]
84 InvalidSchema(String),
85 #[error("duplicate value `{value}` in set-valued schema field `{field}`")]
86 DuplicateSetValue { field: &'static str, value: String },
87}
88
89pub fn schema_fingerprint_v1(
95 graph: &SchemaGraph,
96 element: Option<&SchemaType>,
97) -> Result<SchemaFingerprintV1, SchemaFingerprintError> {
98 let bytes = canonical_schema_bytes_v1(graph, element)?;
99 Ok(SchemaFingerprintV1(*blake3::hash(&bytes).as_bytes()))
100}
101
102pub fn resolve_stream_element_schema_v1(
108 graph: &SchemaGraph,
109 fingerprint: SchemaFingerprintV1,
110) -> Result<Option<SchemaGraph>, SchemaFingerprintError> {
111 let mut elements = Vec::new();
112 collect_stream_elements(&graph.root, &mut elements);
113 for definition in &graph.defs {
114 collect_stream_elements(&definition.body, &mut elements);
115 }
116 for element in elements {
117 if schema_fingerprint_v1(graph, element)? == fingerprint {
118 let root = element.cloned().unwrap_or_else(synthetic_unit);
119 return Ok(Some(SchemaGraph {
120 defs: reachable_defs(graph, &root),
121 root,
122 }));
123 }
124 }
125 Ok(None)
126}
127
128fn collect_stream_elements<'a>(ty: &'a SchemaType, elements: &mut Vec<Option<&'a SchemaType>>) {
129 match ty {
130 SchemaType::Record { fields, .. } => {
131 for field in fields {
132 collect_stream_elements(&field.body, elements);
133 }
134 }
135 SchemaType::Variant { cases, .. } => {
136 for case in cases {
137 if let Some(payload) = &case.payload {
138 collect_stream_elements(payload, elements);
139 }
140 }
141 }
142 SchemaType::Tuple {
143 elements: tuple_elements,
144 ..
145 } => {
146 for element in tuple_elements {
147 collect_stream_elements(element, elements);
148 }
149 }
150 SchemaType::List { element, .. } | SchemaType::FixedList { element, .. } => {
151 collect_stream_elements(element, elements);
152 }
153 SchemaType::Map { key, value, .. } => {
154 collect_stream_elements(key, elements);
155 collect_stream_elements(value, elements);
156 }
157 SchemaType::Option { inner, .. } => collect_stream_elements(inner, elements),
158 SchemaType::Result { spec, .. } => {
159 if let Some(ok) = &spec.ok {
160 collect_stream_elements(ok, elements);
161 }
162 if let Some(err) = &spec.err {
163 collect_stream_elements(err, elements);
164 }
165 }
166 SchemaType::Union { spec, .. } => {
167 for branch in &spec.branches {
168 collect_stream_elements(&branch.body, elements);
169 }
170 }
171 SchemaType::Future { inner, .. } => {
172 if let Some(inner) = inner {
173 collect_stream_elements(inner, elements);
174 }
175 }
176 SchemaType::Stream { inner, .. } => {
177 elements.push(inner.as_deref());
178 if let Some(inner) = inner {
179 collect_stream_elements(inner, elements);
180 }
181 }
182 SchemaType::Secret { spec, .. } => collect_stream_elements(&spec.inner, elements),
183 SchemaType::Ref { .. }
184 | SchemaType::Bool { .. }
185 | SchemaType::S8 { .. }
186 | SchemaType::S16 { .. }
187 | SchemaType::S32 { .. }
188 | SchemaType::S64 { .. }
189 | SchemaType::U8 { .. }
190 | SchemaType::U16 { .. }
191 | SchemaType::U32 { .. }
192 | SchemaType::U64 { .. }
193 | SchemaType::F32 { .. }
194 | SchemaType::F64 { .. }
195 | SchemaType::Char { .. }
196 | SchemaType::String { .. }
197 | SchemaType::Enum { .. }
198 | SchemaType::Flags { .. }
199 | SchemaType::Text { .. }
200 | SchemaType::Binary { .. }
201 | SchemaType::Path { .. }
202 | SchemaType::Url { .. }
203 | SchemaType::Uuid { .. }
204 | SchemaType::Datetime { .. }
205 | SchemaType::Duration { .. }
206 | SchemaType::Quantity { .. }
207 | SchemaType::QuotaToken { .. }
208 | SchemaType::PermissionCard { .. } => {}
209 }
210}
211
212fn canonical_schema_bytes_v1(
213 graph: &SchemaGraph,
214 element: Option<&SchemaType>,
215) -> Result<Vec<u8>, SchemaFingerprintError> {
216 let root = element.cloned().unwrap_or_else(synthetic_unit);
217 let is_synthetic_unit = element.is_none();
218
219 let mut seen = HashSet::new();
220 for def in &graph.defs {
221 if !seen.insert(def.id.as_str()) {
222 return Err(SchemaFingerprintError::InvalidSchema(format!(
223 "duplicate type id `{}`",
224 def.id
225 )));
226 }
227 }
228
229 let mut defs = reachable_defs(graph, &root);
230 defs.sort_by(|left, right| {
231 left.id
232 .as_str()
233 .as_bytes()
234 .cmp(right.id.as_str().as_bytes())
235 });
236 let projected = SchemaGraph {
237 defs: defs.clone(),
238 root: root.clone(),
239 };
240 if let Err(errors) = validate_graph(&projected) {
241 return Err(SchemaFingerprintError::InvalidSchema(
242 errors
243 .into_iter()
244 .map(|error| error.to_string())
245 .collect::<Vec<_>>()
246 .join("; "),
247 ));
248 }
249
250 let mut encoder = CborEncoder::default();
251 encoder.array(4);
252 encoder.text(FINGERPRINT_DOMAIN);
253 encoder.unsigned(FORMAT_VERSION);
254 if is_synthetic_unit {
255 encoder.array(2);
256 encoder.unsigned(0);
257 encode_metadata(&mut encoder, &MetadataEnvelope::default())?;
258 } else {
259 encode_type(&mut encoder, &root)?;
260 }
261 encoder.array(defs.len());
262 for def in defs {
263 encoder.array(3);
264 encoder.text(def.id.as_str());
265 encoder.optional_text(def.name.as_deref());
266 encode_type(&mut encoder, &def.body)?;
267 }
268
269 Ok(encoder.bytes)
270}
271
272fn synthetic_unit() -> SchemaType {
273 SchemaType::Tuple {
274 elements: Vec::new(),
275 metadata: MetadataEnvelope::default(),
276 }
277}
278
279fn encode_type(
280 encoder: &mut CborEncoder,
281 schema: &SchemaType,
282) -> Result<(), SchemaFingerprintError> {
283 match schema {
284 SchemaType::Ref { id, metadata } => {
285 encoder.array(3);
286 encoder.unsigned(1);
287 encoder.text(id.as_str());
288 encode_metadata(encoder, metadata)?;
289 }
290 SchemaType::Bool { metadata } => encode_leaf(encoder, 2, metadata)?,
291 SchemaType::S8 {
292 restrictions,
293 metadata,
294 } => encode_numeric(encoder, 3, restrictions.as_ref(), metadata)?,
295 SchemaType::S16 {
296 restrictions,
297 metadata,
298 } => encode_numeric(encoder, 4, restrictions.as_ref(), metadata)?,
299 SchemaType::S32 {
300 restrictions,
301 metadata,
302 } => encode_numeric(encoder, 5, restrictions.as_ref(), metadata)?,
303 SchemaType::S64 {
304 restrictions,
305 metadata,
306 } => encode_numeric(encoder, 6, restrictions.as_ref(), metadata)?,
307 SchemaType::U8 {
308 restrictions,
309 metadata,
310 } => encode_numeric(encoder, 7, restrictions.as_ref(), metadata)?,
311 SchemaType::U16 {
312 restrictions,
313 metadata,
314 } => encode_numeric(encoder, 8, restrictions.as_ref(), metadata)?,
315 SchemaType::U32 {
316 restrictions,
317 metadata,
318 } => encode_numeric(encoder, 9, restrictions.as_ref(), metadata)?,
319 SchemaType::U64 {
320 restrictions,
321 metadata,
322 } => encode_numeric(encoder, 10, restrictions.as_ref(), metadata)?,
323 SchemaType::F32 {
324 restrictions,
325 metadata,
326 } => encode_numeric(encoder, 11, restrictions.as_ref(), metadata)?,
327 SchemaType::F64 {
328 restrictions,
329 metadata,
330 } => encode_numeric(encoder, 12, restrictions.as_ref(), metadata)?,
331 SchemaType::Char { metadata } => encode_leaf(encoder, 13, metadata)?,
332 SchemaType::String { metadata } => encode_leaf(encoder, 14, metadata)?,
333 SchemaType::Record { fields, metadata } => {
334 encoder.array(3);
335 encoder.unsigned(15);
336 encoder.array(fields.len());
337 for field in fields {
338 encoder.array(3);
339 encoder.text(&field.name);
340 encode_type(encoder, &field.body)?;
341 encode_metadata(encoder, &field.metadata)?;
342 }
343 encode_metadata(encoder, metadata)?;
344 }
345 SchemaType::Variant { cases, metadata } => {
346 encoder.array(3);
347 encoder.unsigned(16);
348 encoder.array(cases.len());
349 for case in cases {
350 encoder.array(3);
351 encoder.text(&case.name);
352 encode_optional_type(encoder, case.payload.as_ref())?;
353 encode_metadata(encoder, &case.metadata)?;
354 }
355 encode_metadata(encoder, metadata)?;
356 }
357 SchemaType::Enum { cases, metadata } => {
358 encode_names(encoder, 17, cases, metadata)?;
359 }
360 SchemaType::Flags { flags, metadata } => {
361 encode_names(encoder, 18, flags, metadata)?;
362 }
363 SchemaType::Tuple { elements, metadata } => {
364 encoder.array(3);
365 encoder.unsigned(19);
366 encoder.array(elements.len());
367 for element in elements {
368 encode_type(encoder, element)?;
369 }
370 encode_metadata(encoder, metadata)?;
371 }
372 SchemaType::List { element, metadata } => {
373 encode_unary(encoder, 20, element, metadata)?;
374 }
375 SchemaType::FixedList {
376 element,
377 length,
378 metadata,
379 } => {
380 encoder.array(4);
381 encoder.unsigned(21);
382 encode_type(encoder, element)?;
383 encoder.unsigned(u64::from(*length));
384 encode_metadata(encoder, metadata)?;
385 }
386 SchemaType::Map {
387 key,
388 value,
389 metadata,
390 } => {
391 encoder.array(4);
392 encoder.unsigned(22);
393 encode_type(encoder, key)?;
394 encode_type(encoder, value)?;
395 encode_metadata(encoder, metadata)?;
396 }
397 SchemaType::Option { inner, metadata } => {
398 encode_unary(encoder, 23, inner, metadata)?;
399 }
400 SchemaType::Result { spec, metadata } => {
401 encoder.array(4);
402 encoder.unsigned(24);
403 encode_optional_type(encoder, spec.ok.as_deref())?;
404 encode_optional_type(encoder, spec.err.as_deref())?;
405 encode_metadata(encoder, metadata)?;
406 }
407 SchemaType::Text {
408 restrictions,
409 metadata,
410 } => encode_text(encoder, restrictions, metadata)?,
411 SchemaType::Binary {
412 restrictions,
413 metadata,
414 } => encode_binary(encoder, restrictions, metadata)?,
415 SchemaType::Path { spec, metadata } => {
416 encoder.array(6);
417 encoder.unsigned(27);
418 encoder.unsigned(match spec.direction {
419 PathDirection::Input => 0,
420 PathDirection::Output => 1,
421 PathDirection::InOut => 2,
422 });
423 encoder.unsigned(match spec.kind {
424 PathKind::File => 0,
425 PathKind::Directory => 1,
426 PathKind::Any => 2,
427 });
428 encode_optional_set(
429 encoder,
430 "path.allowed_mime_types",
431 spec.allowed_mime_types.as_ref(),
432 )?;
433 encode_optional_set(
434 encoder,
435 "path.allowed_extensions",
436 spec.allowed_extensions.as_ref(),
437 )?;
438 encode_metadata(encoder, metadata)?;
439 }
440 SchemaType::Url {
441 restrictions,
442 metadata,
443 } => encode_url(encoder, restrictions, metadata)?,
444 SchemaType::Uuid { metadata } => encode_leaf(encoder, 38, metadata)?,
445 SchemaType::Datetime { metadata } => encode_leaf(encoder, 29, metadata)?,
446 SchemaType::Duration { metadata } => encode_leaf(encoder, 30, metadata)?,
447 SchemaType::Quantity { spec, metadata } => {
448 encoder.array(6);
449 encoder.unsigned(31);
450 encoder.text(&spec.base_unit);
451 encoder.array(spec.allowed_suffixes.len());
452 for suffix in &spec.allowed_suffixes {
453 encoder.text(suffix);
454 }
455 encode_optional_quantity(encoder, spec.min.as_ref());
456 encode_optional_quantity(encoder, spec.max.as_ref());
457 encode_metadata(encoder, metadata)?;
458 }
459 SchemaType::Union { spec, metadata } => {
460 encoder.array(3);
461 encoder.unsigned(32);
462 encoder.array(spec.branches.len());
463 for branch in &spec.branches {
464 encoder.array(4);
465 encoder.text(&branch.tag);
466 encode_type(encoder, &branch.body)?;
467 encode_discriminator(encoder, &branch.discriminator);
468 encode_metadata(encoder, &branch.metadata)?;
469 }
470 encode_metadata(encoder, metadata)?;
471 }
472 SchemaType::Secret { spec, metadata } => {
473 encoder.array(4);
474 encoder.unsigned(33);
475 encode_type(encoder, &spec.inner)?;
476 encoder.optional_text(spec.category.as_deref());
477 encode_metadata(encoder, metadata)?;
478 }
479 SchemaType::QuotaToken { spec, metadata } => {
480 encoder.array(3);
481 encoder.unsigned(34);
482 encoder.optional_text(spec.resource_name.as_deref());
483 encode_metadata(encoder, metadata)?;
484 }
485 SchemaType::Future { inner, metadata } => {
486 encoder.array(3);
487 encoder.unsigned(35);
488 encode_optional_type(encoder, inner.as_deref())?;
489 encode_metadata(encoder, metadata)?;
490 }
491 SchemaType::Stream { inner, metadata } => {
492 encoder.array(3);
493 encoder.unsigned(36);
494 encode_optional_type(encoder, inner.as_deref())?;
495 encode_metadata(encoder, metadata)?;
496 }
497 SchemaType::PermissionCard { spec, metadata } => {
498 encoder.array(3);
499 encoder.unsigned(37);
500 encoder.boolean(spec.polymorphic);
501 encode_metadata(encoder, metadata)?;
502 }
503 }
504 Ok(())
505}
506
507fn encode_leaf(
508 encoder: &mut CborEncoder,
509 tag: u64,
510 metadata: &MetadataEnvelope,
511) -> Result<(), SchemaFingerprintError> {
512 encoder.array(2);
513 encoder.unsigned(tag);
514 encode_metadata(encoder, metadata)
515}
516
517fn encode_numeric(
518 encoder: &mut CborEncoder,
519 tag: u64,
520 restrictions: Option<&NumericRestrictions>,
521 metadata: &MetadataEnvelope,
522) -> Result<(), SchemaFingerprintError> {
523 encoder.array(3);
524 encoder.unsigned(tag);
525 match restrictions {
526 None => encoder.null(),
527 Some(restrictions) => {
528 encoder.array(3);
529 encode_optional_bound(encoder, restrictions.min);
530 encode_optional_bound(encoder, restrictions.max);
531 encoder.optional_text(restrictions.unit.as_deref().filter(|unit| !unit.is_empty()));
532 }
533 }
534 encode_metadata(encoder, metadata)
535}
536
537fn encode_optional_bound(encoder: &mut CborEncoder, bound: Option<NumericBound>) {
538 match bound {
539 None => encoder.null(),
540 Some(NumericBound::Signed(value)) => {
541 encoder.array(2);
542 encoder.unsigned(0);
543 encoder.signed(value);
544 }
545 Some(NumericBound::Unsigned(value)) => {
546 encoder.array(2);
547 encoder.unsigned(1);
548 encoder.unsigned(value);
549 }
550 Some(NumericBound::FloatBits(bits)) => {
551 encoder.array(2);
552 encoder.unsigned(2);
553 encoder.unsigned(if f64::from_bits(bits) == 0.0 { 0 } else { bits });
554 }
555 }
556}
557
558fn encode_names(
559 encoder: &mut CborEncoder,
560 tag: u64,
561 names: &[String],
562 metadata: &MetadataEnvelope,
563) -> Result<(), SchemaFingerprintError> {
564 encoder.array(3);
565 encoder.unsigned(tag);
566 encoder.array(names.len());
567 for name in names {
568 encoder.text(name);
569 }
570 encode_metadata(encoder, metadata)
571}
572
573fn encode_unary(
574 encoder: &mut CborEncoder,
575 tag: u64,
576 inner: &SchemaType,
577 metadata: &MetadataEnvelope,
578) -> Result<(), SchemaFingerprintError> {
579 encoder.array(3);
580 encoder.unsigned(tag);
581 encode_type(encoder, inner)?;
582 encode_metadata(encoder, metadata)
583}
584
585fn encode_optional_type(
586 encoder: &mut CborEncoder,
587 schema: Option<&SchemaType>,
588) -> Result<(), SchemaFingerprintError> {
589 match schema {
590 Some(schema) => encode_type(encoder, schema),
591 None => {
592 encoder.null();
593 Ok(())
594 }
595 }
596}
597
598fn encode_text(
599 encoder: &mut CborEncoder,
600 restrictions: &TextRestrictions,
601 metadata: &MetadataEnvelope,
602) -> Result<(), SchemaFingerprintError> {
603 encoder.array(6);
604 encoder.unsigned(25);
605 encode_optional_set(encoder, "text.languages", restrictions.languages.as_ref())?;
606 encoder.optional_u32(restrictions.min_length);
607 encoder.optional_u32(restrictions.max_length);
608 encoder.optional_text(restrictions.regex.as_deref());
609 encode_metadata(encoder, metadata)
610}
611
612fn encode_binary(
613 encoder: &mut CborEncoder,
614 restrictions: &BinaryRestrictions,
615 metadata: &MetadataEnvelope,
616) -> Result<(), SchemaFingerprintError> {
617 encoder.array(5);
618 encoder.unsigned(26);
619 encode_optional_set(
620 encoder,
621 "binary.mime_types",
622 restrictions.mime_types.as_ref(),
623 )?;
624 encoder.optional_u32(restrictions.min_bytes);
625 encoder.optional_u32(restrictions.max_bytes);
626 encode_metadata(encoder, metadata)
627}
628
629fn encode_url(
630 encoder: &mut CborEncoder,
631 restrictions: &UrlRestrictions,
632 metadata: &MetadataEnvelope,
633) -> Result<(), SchemaFingerprintError> {
634 encoder.array(4);
635 encoder.unsigned(28);
636 encode_optional_set(
637 encoder,
638 "url.allowed_schemes",
639 restrictions.allowed_schemes.as_ref(),
640 )?;
641 encode_optional_set(
642 encoder,
643 "url.allowed_hosts",
644 restrictions.allowed_hosts.as_ref(),
645 )?;
646 encode_metadata(encoder, metadata)
647}
648
649fn encode_optional_set(
650 encoder: &mut CborEncoder,
651 field: &'static str,
652 values: Option<&Vec<String>>,
653) -> Result<(), SchemaFingerprintError> {
654 let Some(values) = values else {
655 encoder.null();
656 return Ok(());
657 };
658 let mut values: Vec<&str> = values.iter().map(String::as_str).collect();
659 values.sort_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
660 for duplicate in values.windows(2) {
661 if duplicate[0] == duplicate[1] {
662 return Err(SchemaFingerprintError::DuplicateSetValue {
663 field,
664 value: duplicate[0].to_string(),
665 });
666 }
667 }
668 encoder.array(values.len());
669 for value in values {
670 encoder.text(value);
671 }
672 Ok(())
673}
674
675fn encode_optional_quantity(encoder: &mut CborEncoder, value: Option<&QuantityValue>) {
676 match value {
677 None => encoder.null(),
678 Some(value) => {
679 encoder.array(3);
680 encoder.signed(value.mantissa);
681 encoder.signed(i64::from(value.scale));
682 encoder.text(&value.unit);
683 }
684 }
685}
686
687fn encode_discriminator(encoder: &mut CborEncoder, discriminator: &DiscriminatorRule) {
688 match discriminator {
689 DiscriminatorRule::Prefix { prefix } => {
690 encoder.array(2);
691 encoder.unsigned(0);
692 encoder.text(prefix);
693 }
694 DiscriminatorRule::Suffix { suffix } => {
695 encoder.array(2);
696 encoder.unsigned(1);
697 encoder.text(suffix);
698 }
699 DiscriminatorRule::Contains { substring } => {
700 encoder.array(2);
701 encoder.unsigned(2);
702 encoder.text(substring);
703 }
704 DiscriminatorRule::Regex { regex } => {
705 encoder.array(2);
706 encoder.unsigned(3);
707 encoder.text(regex);
708 }
709 DiscriminatorRule::FieldEquals(field) => {
710 encoder.array(3);
711 encoder.unsigned(4);
712 encoder.text(&field.field_name);
713 encoder.optional_text(field.literal.as_deref());
714 }
715 DiscriminatorRule::FieldAbsent { field_name } => {
716 encoder.array(2);
717 encoder.unsigned(5);
718 encoder.text(field_name);
719 }
720 }
721}
722
723fn encode_metadata(
724 encoder: &mut CborEncoder,
725 metadata: &MetadataEnvelope,
726) -> Result<(), SchemaFingerprintError> {
727 encoder.array(5);
728 encoder.optional_text(metadata.doc.as_deref());
729 encode_set(encoder, "metadata.aliases", &metadata.aliases)?;
730 encoder.array(metadata.examples.len());
731 for example in &metadata.examples {
732 encoder.text(example);
733 }
734 encoder.optional_text(metadata.deprecated.as_deref());
735 match &metadata.role {
736 None => encoder.null(),
737 Some(Role::Multimodal) => {
738 encoder.array(1);
739 encoder.unsigned(0);
740 }
741 Some(Role::UnstructuredText) => {
742 encoder.array(1);
743 encoder.unsigned(1);
744 }
745 Some(Role::UnstructuredBinary) => {
746 encoder.array(1);
747 encoder.unsigned(2);
748 }
749 Some(Role::Other(role)) => {
750 encoder.array(2);
751 encoder.unsigned(3);
752 encoder.text(role);
753 }
754 }
755 Ok(())
756}
757
758fn encode_set(
759 encoder: &mut CborEncoder,
760 field: &'static str,
761 values: &[String],
762) -> Result<(), SchemaFingerprintError> {
763 let mut values: Vec<&str> = values.iter().map(String::as_str).collect();
764 values.sort_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
765 for duplicate in values.windows(2) {
766 if duplicate[0] == duplicate[1] {
767 return Err(SchemaFingerprintError::DuplicateSetValue {
768 field,
769 value: duplicate[0].to_string(),
770 });
771 }
772 }
773 encoder.array(values.len());
774 for value in values {
775 encoder.text(value);
776 }
777 Ok(())
778}
779
780#[derive(Default)]
781struct CborEncoder {
782 bytes: Vec<u8>,
783}
784
785impl CborEncoder {
786 fn unsigned(&mut self, value: u64) {
787 self.major(0, value);
788 }
789
790 fn signed(&mut self, value: i64) {
791 if value >= 0 {
792 self.unsigned(value as u64);
793 } else {
794 self.major(1, (-1i128 - i128::from(value)) as u64);
795 }
796 }
797
798 fn array(&mut self, length: usize) {
799 self.major(4, length as u64);
800 }
801
802 fn text(&mut self, value: &str) {
803 self.major(3, value.len() as u64);
804 self.bytes.extend_from_slice(value.as_bytes());
805 }
806
807 fn optional_text(&mut self, value: Option<&str>) {
808 match value {
809 Some(value) => self.text(value),
810 None => self.null(),
811 }
812 }
813
814 fn optional_u32(&mut self, value: Option<u32>) {
815 match value {
816 Some(value) => self.unsigned(u64::from(value)),
817 None => self.null(),
818 }
819 }
820
821 fn boolean(&mut self, value: bool) {
822 self.bytes.push(if value { 0xf5 } else { 0xf4 });
823 }
824
825 fn null(&mut self) {
826 self.bytes.push(0xf6);
827 }
828
829 fn major(&mut self, major: u8, value: u64) {
830 let prefix = major << 5;
831 match value {
832 0..=23 => self.bytes.push(prefix | value as u8),
833 24..=0xff => self.bytes.extend_from_slice(&[prefix | 24, value as u8]),
834 0x100..=0xffff => {
835 self.bytes.push(prefix | 25);
836 self.bytes.extend_from_slice(&(value as u16).to_be_bytes());
837 }
838 0x1_0000..=0xffff_ffff => {
839 self.bytes.push(prefix | 26);
840 self.bytes.extend_from_slice(&(value as u32).to_be_bytes());
841 }
842 _ => {
843 self.bytes.push(prefix | 27);
844 self.bytes.extend_from_slice(&value.to_be_bytes());
845 }
846 }
847 }
848}
849
850#[cfg(test)]
851mod tests {
852 use super::{
853 SchemaFingerprintError, SchemaFingerprintV1, canonical_schema_bytes_v1,
854 resolve_stream_element_schema_v1, schema_fingerprint_v1,
855 };
856 use crate::schema::{
857 MetadataEnvelope, NamedFieldType, PermissionCardSpec, SchemaGraph, SchemaType,
858 SchemaTypeDef, TypeId,
859 };
860 #[cfg(feature = "regex")]
861 use crate::schema::{Role, TextRestrictions};
862 use test_r::test;
863
864 #[test]
865 fn v1_golden_vectors() {
866 let graph = SchemaGraph::empty();
867 assert_eq!(canonical_schema_bytes_v1(&graph, None).unwrap().len(), 37);
868 assert_eq!(
869 schema_fingerprint_v1(&graph, None).unwrap().to_hex(),
870 "b50494cf0f33961c703d5f6e6af3d3159e528c4d09c1d801172cdf8f022dcafa"
871 );
872 assert_eq!(
873 canonical_schema_bytes_v1(&graph, Some(&SchemaType::string()))
874 .unwrap()
875 .len(),
876 37
877 );
878 assert_eq!(
879 schema_fingerprint_v1(&graph, Some(&SchemaType::string()))
880 .unwrap()
881 .to_hex(),
882 "61c50c0a3c6ffd63529621ada78afc0d4d8e5fe691f8b0993035f847c660a307"
883 );
884 assert_eq!(
885 canonical_schema_bytes_v1(&graph, Some(&SchemaType::list(SchemaType::string())))
886 .unwrap()
887 .len(),
888 45
889 );
890 assert_eq!(
891 schema_fingerprint_v1(&graph, Some(&SchemaType::list(SchemaType::string())))
892 .unwrap()
893 .to_hex(),
894 "4939707f8ef97e9d4b31b568332eaf5a3011f2be7c358f7546966fadfb9416d4"
895 );
896
897 let node_id = TypeId::new("example.node");
898 let recursive = SchemaGraph {
899 root: SchemaType::ref_to(node_id.clone()),
900 defs: vec![SchemaTypeDef {
901 id: node_id.clone(),
902 name: Some("Node".to_string()),
903 body: SchemaType::record(vec![
904 NamedFieldType {
905 name: "value".to_string(),
906 body: SchemaType::string(),
907 metadata: MetadataEnvelope::default(),
908 },
909 NamedFieldType {
910 name: "next".to_string(),
911 body: SchemaType::option(SchemaType::ref_to(node_id)),
912 metadata: MetadataEnvelope::default(),
913 },
914 ]),
915 }],
916 };
917 assert_eq!(
918 canonical_schema_bytes_v1(&recursive, Some(&recursive.root))
919 .unwrap()
920 .len(),
921 140
922 );
923 assert_eq!(
924 schema_fingerprint_v1(&recursive, Some(&recursive.root))
925 .unwrap()
926 .to_hex(),
927 "3931585d2d02a2b7d5c99e3da1082ac8fe904c535e2700bd45e29a95ff2399fa"
928 );
929 }
930
931 #[test]
932 #[cfg(feature = "regex")]
933 fn v1_regex_golden_vector() {
934 let constrained = SchemaType::Text {
935 restrictions: TextRestrictions {
936 languages: Some(vec!["fr".to_string(), "en".to_string()]),
937 min_length: Some(1),
938 max_length: Some(64),
939 regex: Some("^[a-z]+$".to_string()),
940 },
941 metadata: MetadataEnvelope {
942 doc: Some("text".to_string()),
943 aliases: vec!["z".to_string(), "a".to_string()],
944 examples: vec!["\"hello\"".to_string()],
945 deprecated: Some("use-v2".to_string()),
946 role: Some(Role::Other("prompt".to_string())),
947 },
948 };
949 let constrained_bytes =
950 canonical_schema_bytes_v1(&SchemaGraph::empty(), Some(&constrained)).unwrap();
951 let constrained_hex = constrained_bytes
952 .iter()
953 .map(|byte| format!("{byte:02x}"))
954 .collect::<String>();
955 assert_eq!(
956 constrained_hex,
957 "847818676f6c656d2d736368656d612d66696e6765727072696e74018618198262656e626672011840685e5b612d7a5d2b24856474657874826161617a81672268656c6c6f22667573652d763282036670726f6d707480"
958 );
959 assert_eq!(constrained_bytes.len(), 87);
960 assert_eq!(
961 blake3::hash(&constrained_bytes).to_hex().as_str(),
962 "b985cdb5445862be90e8dca06bbfa9c46b50cf40edc84ed34205bb3a214c5bb0"
963 );
964 }
965
966 #[test]
967 fn v1_permission_card_golden_vector() {
968 let permission_card = SchemaType::permission_card(PermissionCardSpec { polymorphic: true });
969 let permission_card_bytes =
970 canonical_schema_bytes_v1(&SchemaGraph::empty(), Some(&permission_card)).unwrap();
971 assert_eq!(
972 permission_card_bytes
973 .iter()
974 .map(|byte| format!("{byte:02x}"))
975 .collect::<String>(),
976 "847818676f6c656d2d736368656d612d66696e6765727072696e7401831825f585f68080f6f680"
977 );
978 assert_eq!(permission_card_bytes.len(), 39);
979 assert_eq!(
980 blake3::hash(&permission_card_bytes).to_hex().as_str(),
981 "b7d3c09af5db4e56b527051f561689f451dfdb21213cd70aabd11618e244da8b"
982 );
983 }
984
985 #[test]
986 fn sorts_set_values_and_rejects_duplicates() {
987 let graph = SchemaGraph::empty();
988 let left = SchemaType::Text {
989 restrictions: crate::schema::TextRestrictions {
990 languages: Some(vec!["fr".into(), "en".into()]),
991 ..Default::default()
992 },
993 metadata: MetadataEnvelope::default(),
994 };
995 let right = SchemaType::Text {
996 restrictions: crate::schema::TextRestrictions {
997 languages: Some(vec!["en".into(), "fr".into()]),
998 ..Default::default()
999 },
1000 metadata: MetadataEnvelope::default(),
1001 };
1002 assert_eq!(
1003 schema_fingerprint_v1(&graph, Some(&left)).unwrap(),
1004 schema_fingerprint_v1(&graph, Some(&right)).unwrap()
1005 );
1006
1007 let duplicate = SchemaType::Text {
1008 restrictions: crate::schema::TextRestrictions {
1009 languages: Some(vec!["en".into(), "en".into()]),
1010 ..Default::default()
1011 },
1012 metadata: MetadataEnvelope::default(),
1013 };
1014 assert!(matches!(
1015 schema_fingerprint_v1(&graph, Some(&duplicate)),
1016 Err(SchemaFingerprintError::DuplicateSetValue { .. })
1017 ));
1018 }
1019
1020 #[test]
1021 fn resolves_stream_element_fingerprints_to_self_contained_schema_graphs() {
1022 let node_id = TypeId::new("example.node");
1023 let node_ref = SchemaType::ref_to(node_id.clone());
1024 let graph = SchemaGraph {
1025 root: SchemaType::record(vec![
1026 NamedFieldType {
1027 name: "nodes".to_string(),
1028 body: SchemaType::stream(Some(node_ref.clone())),
1029 metadata: MetadataEnvelope::default(),
1030 },
1031 NamedFieldType {
1032 name: "signals".to_string(),
1033 body: SchemaType::stream(None),
1034 metadata: MetadataEnvelope::default(),
1035 },
1036 ]),
1037 defs: vec![SchemaTypeDef {
1038 id: node_id.clone(),
1039 name: Some("Node".to_string()),
1040 body: SchemaType::record(vec![NamedFieldType {
1041 name: "next".to_string(),
1042 body: SchemaType::option(SchemaType::ref_to(node_id)),
1043 metadata: MetadataEnvelope::default(),
1044 }]),
1045 }],
1046 };
1047
1048 let node_fingerprint = schema_fingerprint_v1(&graph, Some(&node_ref)).unwrap();
1049 let resolved = resolve_stream_element_schema_v1(&graph, node_fingerprint)
1050 .unwrap()
1051 .unwrap();
1052 assert_eq!(resolved.root, node_ref);
1053 assert_eq!(resolved.defs, graph.defs);
1054
1055 let unit_fingerprint = schema_fingerprint_v1(&graph, None).unwrap();
1056 let resolved_unit = resolve_stream_element_schema_v1(&graph, unit_fingerprint)
1057 .unwrap()
1058 .unwrap();
1059 assert_eq!(
1060 resolved_unit,
1061 SchemaGraph::anonymous(SchemaType::tuple(Vec::new()))
1062 );
1063
1064 assert!(
1065 resolve_stream_element_schema_v1(&graph, SchemaFingerprintV1([0xff; 32]))
1066 .unwrap()
1067 .is_none()
1068 );
1069 }
1070}