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