1use crate::schema::graph::SchemaGraph;
19use crate::schema::metadata::{MetadataEnvelope, TypeId};
20use crate::schema::schema_type::{
21 BinaryRestrictions, DiscriminatorRule, NumericBound, NumericRestrictions, PathSpec,
22 PermissionCardSpec, QuantitySpec, QuantityValue, QuotaTokenSpec, ResultSpec, SchemaType,
23 SecretSpec, TextRestrictions, UnionBranch, UnionSpec, UrlRestrictions, VariantCaseType,
24};
25use serde_json::{Map, Number, Value};
26use std::collections::{HashMap, HashSet};
27
28const JSON_SCHEMA_DRAFT: &str = "https://json-schema.org/draft/2020-12/schema";
29const MIME_TYPE_PATTERN: &str = "^[A-Za-z0-9!#$&^_.+\\-]+\\/[A-Za-z0-9!#$&^_.+\\-]+$";
30const BASE64URL_PATTERN: &str =
31 "^(?:[A-Za-z0-9_-]{4})*(?:[A-Za-z0-9_-][AQgw]|[A-Za-z0-9_-]{2}[AEIMQUYcgkosw048])?$";
32
33#[derive(Clone, Copy, Debug)]
38pub struct JsonSchemaConfig {
39 pub include_draft_marker: bool,
41 host_managed: HostManagedSchemaPolicy,
42 unsupported: UnsupportedLeafSchemaPolicy,
43}
44
45#[derive(Clone, Copy, Debug)]
46enum HostManagedSchemaPolicy {
47 TrustedSnapshot,
48 Reject,
49 Redact,
50}
51
52#[derive(Clone, Copy, Debug)]
53enum UnsupportedLeafSchemaPolicy {
54 Placeholder,
55 Reject,
56}
57
58impl JsonSchemaConfig {
59 pub const CANONICAL: Self = Self {
62 include_draft_marker: true,
63 host_managed: HostManagedSchemaPolicy::TrustedSnapshot,
64 unsupported: UnsupportedLeafSchemaPolicy::Placeholder,
65 };
66
67 pub const WITHOUT_DRAFT_MARKER: Self = Self {
70 include_draft_marker: false,
71 host_managed: HostManagedSchemaPolicy::TrustedSnapshot,
72 unsupported: UnsupportedLeafSchemaPolicy::Placeholder,
73 };
74
75 pub(crate) const EXTERNAL_INPUT: Self = Self {
76 include_draft_marker: false,
77 host_managed: HostManagedSchemaPolicy::Reject,
78 unsupported: UnsupportedLeafSchemaPolicy::Placeholder,
79 };
80
81 pub(crate) const EXTERNAL_OUTPUT: Self = Self {
82 include_draft_marker: false,
83 host_managed: HostManagedSchemaPolicy::Redact,
84 unsupported: UnsupportedLeafSchemaPolicy::Placeholder,
85 };
86
87 pub const REFLECTION: Self = Self {
90 include_draft_marker: true,
91 host_managed: HostManagedSchemaPolicy::Reject,
92 unsupported: UnsupportedLeafSchemaPolicy::Reject,
93 };
94}
95
96pub fn to_json_schema(graph: &SchemaGraph, ty: &SchemaType) -> Value {
100 to_json_schema_with_config(graph, ty, JsonSchemaConfig::CANONICAL)
101}
102
103pub fn to_json_schema_with_config(
112 graph: &SchemaGraph,
113 ty: &SchemaType,
114 config: JsonSchemaConfig,
115) -> Value {
116 let table = build_branch_name_table(graph, ty);
117 let mut root = render_type(graph, ty, true, &table, config);
118 let mut defs = render_defs(graph, &table, config);
119 add_union_branch_defs(graph, ty, &mut defs, &table, config);
120 if !defs.is_empty() {
121 if let Some(obj) = root.as_object_mut() {
122 obj.insert("$defs".to_string(), Value::Object(defs));
123 } else {
124 let mut wrapper = Map::new();
125 wrapper.insert("$defs".to_string(), Value::Object(defs));
126 wrapper.insert("allOf".to_string(), Value::Array(vec![root.clone()]));
127 root = Value::Object(wrapper);
128 }
129 }
130 if config.include_draft_marker
131 && let Some(obj) = root.as_object_mut()
132 {
133 let mut with_schema = Map::with_capacity(obj.len() + 1);
136 with_schema.insert(
137 "$schema".to_string(),
138 Value::String(JSON_SCHEMA_DRAFT.to_string()),
139 );
140 for (k, v) in obj.iter() {
141 with_schema.insert(k.clone(), v.clone());
142 }
143 return Value::Object(with_schema);
144 }
145 root
146}
147
148pub fn to_external_input_json_schema(
151 graph: &SchemaGraph,
152 ty: &SchemaType,
153 include_draft_marker: bool,
154) -> Value {
155 to_json_schema_with_config(
156 graph,
157 ty,
158 JsonSchemaConfig {
159 include_draft_marker,
160 ..JsonSchemaConfig::EXTERNAL_INPUT
161 },
162 )
163}
164
165pub fn to_external_output_json_schema(
168 graph: &SchemaGraph,
169 ty: &SchemaType,
170 include_draft_marker: bool,
171) -> Value {
172 to_json_schema_with_config(
173 graph,
174 ty,
175 JsonSchemaConfig {
176 include_draft_marker,
177 ..JsonSchemaConfig::EXTERNAL_OUTPUT
178 },
179 )
180}
181
182pub fn to_reflection_json_schema(
186 graph: &SchemaGraph,
187 ty: &SchemaType,
188 include_draft_marker: bool,
189) -> Value {
190 to_json_schema_with_config(
191 graph,
192 ty,
193 JsonSchemaConfig {
194 include_draft_marker,
195 ..JsonSchemaConfig::REFLECTION
196 },
197 )
198}
199
200fn resolves_to_option(graph: &SchemaGraph, ty: &SchemaType) -> bool {
203 let mut current = ty;
204 let mut visited: HashSet<TypeId> = HashSet::new();
205 loop {
206 match current {
207 SchemaType::Option { .. } => return true,
208 SchemaType::Ref { id, .. } => {
209 if !visited.insert(id.clone()) {
210 return false;
211 }
212 match graph.lookup(id) {
213 Some(def) => current = &def.body,
214 None => return false,
215 }
216 }
217 _ => return false,
218 }
219 }
220}
221
222pub(super) fn render_defs(
229 graph: &SchemaGraph,
230 table: &BranchNameTable,
231 config: JsonSchemaConfig,
232) -> Map<String, Value> {
233 let mut defs = Map::new();
234 for def in &graph.defs {
235 let mut body = render_type(graph, &def.body, false, table, config);
239 if let Some(name) = &def.name
240 && let Some(obj) = body.as_object_mut()
241 {
242 obj.entry("title").or_insert(Value::String(name.clone()));
243 }
244 defs.insert(def.id.0.clone(), body);
245 }
246 defs
247}
248
249pub(super) fn add_union_branch_defs(
252 graph: &SchemaGraph,
253 root_ty: &SchemaType,
254 defs: &mut Map<String, Value>,
255 table: &BranchNameTable,
256 config: JsonSchemaConfig,
257) {
258 let mut emitted = HashSet::new();
259 collect_union_branch_defs(graph, root_ty, defs, &mut emitted, table, config);
260 for def in &graph.defs {
261 collect_union_branch_defs(graph, &def.body, defs, &mut emitted, table, config);
262 }
263}
264
265fn collect_union_branch_defs(
266 graph: &SchemaGraph,
267 ty: &SchemaType,
268 defs: &mut Map<String, Value>,
269 emitted: &mut HashSet<String>,
270 table: &BranchNameTable,
271 config: JsonSchemaConfig,
272) {
273 match ty {
274 SchemaType::Union { spec, .. } => {
275 for branch in spec.branches.iter() {
276 let key = table.name_for(branch).to_string();
277 if emitted.insert(key.clone()) {
278 let mut body = render_type(graph, &branch.body, false, table, config);
279 attach_metadata(&mut body, &branch.metadata);
280 defs.insert(
281 key,
282 obj_inline([(
283 "allOf",
284 Value::Array(vec![
285 body,
286 discriminator_constraint(&branch.discriminator),
287 ]),
288 )]),
289 );
290 }
291 collect_union_branch_defs(graph, &branch.body, defs, emitted, table, config);
292 }
293 }
294 SchemaType::Record { fields, .. } => {
295 for f in fields {
296 collect_union_branch_defs(graph, &f.body, defs, emitted, table, config);
297 }
298 }
299 SchemaType::Variant { cases, .. } => {
300 for case in cases {
301 if let Some(p) = &case.payload {
302 collect_union_branch_defs(graph, p, defs, emitted, table, config);
303 }
304 }
305 }
306 SchemaType::Tuple { elements, .. } => {
307 for e in elements {
308 collect_union_branch_defs(graph, e, defs, emitted, table, config);
309 }
310 }
311 SchemaType::List { element, .. }
312 | SchemaType::FixedList { element, .. }
313 | SchemaType::Option { inner: element, .. } => {
314 collect_union_branch_defs(graph, element, defs, emitted, table, config);
315 }
316 SchemaType::Map { key, value, .. } => {
317 collect_union_branch_defs(graph, key, defs, emitted, table, config);
318 collect_union_branch_defs(graph, value, defs, emitted, table, config);
319 }
320 SchemaType::Result { spec, .. } => {
321 if let Some(t) = &spec.ok {
322 collect_union_branch_defs(graph, t, defs, emitted, table, config);
323 }
324 if let Some(t) = &spec.err {
325 collect_union_branch_defs(graph, t, defs, emitted, table, config);
326 }
327 }
328 SchemaType::Future { inner, .. } | SchemaType::Stream { inner, .. } => {
329 if let Some(t) = inner {
330 collect_union_branch_defs(graph, t, defs, emitted, table, config);
331 }
332 }
333 _ => {}
334 }
335}
336
337pub(super) struct BranchNameTable {
356 names: HashMap<String, String>,
357}
358
359impl BranchNameTable {
360 pub(super) fn name_for(&self, branch: &UnionBranch) -> &str {
361 let key = canonical_branch_key(branch);
362 self.names.get(&key).map(String::as_str).expect(
363 "BranchNameTable must contain every union branch reachable from the render root \
364 — `build_branch_name_table` is the source of truth for this invariant",
365 )
366 }
367}
368
369pub(super) fn build_branch_name_table(
370 graph: &SchemaGraph,
371 root_ty: &SchemaType,
372) -> BranchNameTable {
373 let mut collector = BranchCollector::default();
374 collector.walk_type(root_ty);
375 for def in &graph.defs {
376 collector.path.clear();
380 let seg = def.name.clone().unwrap_or_else(|| def.id.0.clone());
381 collector.path.push(seg);
382 collector.walk_type(&def.body);
383 }
384 collector.path.clear();
385 let taken: HashSet<String> = graph.defs.iter().map(|d| d.id.0.clone()).collect();
388 collector.into_table(taken)
389}
390
391fn canonical_branch_key(branch: &UnionBranch) -> String {
394 let bytes = serde_json::to_vec(branch).expect("UnionBranch serializes deterministically");
395 let hex = blake3::hash(&bytes).to_hex();
396 hex.as_str()[..32].to_string()
399}
400
401#[derive(Default)]
402struct BranchCollector {
403 keys: Vec<String>,
407 occurrences: HashMap<String, Occurrence>,
408 path: Vec<String>,
412}
413
414struct Occurrence {
415 tag: String,
416 path: Vec<String>,
419}
420
421impl BranchCollector {
422 fn record(&mut self, branch: &UnionBranch) {
423 let key = canonical_branch_key(branch);
424 if let std::collections::hash_map::Entry::Vacant(slot) = self.occurrences.entry(key.clone())
425 {
426 slot.insert(Occurrence {
427 tag: branch.tag.clone(),
428 path: self.path.clone(),
429 });
430 self.keys.push(key);
431 }
432 }
433
434 fn walk_type(&mut self, ty: &SchemaType) {
442 match ty {
443 SchemaType::Union { spec, .. } => {
444 for branch in &spec.branches {
445 self.record(branch);
446 self.path.push(branch.tag.clone());
447 self.walk_type(&branch.body);
448 self.path.pop();
449 }
450 }
451 SchemaType::Record { fields, .. } => {
452 for f in fields {
453 self.path.push(f.name.clone());
454 self.walk_type(&f.body);
455 self.path.pop();
456 }
457 }
458 SchemaType::Variant { cases, .. } => {
459 for case in cases {
460 if let Some(p) = &case.payload {
461 self.path.push(case.name.clone());
462 self.walk_type(p);
463 self.path.pop();
464 }
465 }
466 }
467 SchemaType::Tuple { elements, .. } => {
468 for (i, e) in elements.iter().enumerate() {
469 self.path.push(format!("item{i}"));
470 self.walk_type(e);
471 self.path.pop();
472 }
473 }
474 SchemaType::List { element, .. } | SchemaType::FixedList { element, .. } => {
475 self.path.push("item".to_string());
476 self.walk_type(element);
477 self.path.pop();
478 }
479 SchemaType::Option { inner, .. } => {
480 self.path.push("inner".to_string());
481 self.walk_type(inner);
482 self.path.pop();
483 }
484 SchemaType::Map { key, value, .. } => {
485 self.path.push("key".to_string());
486 self.walk_type(key);
487 self.path.pop();
488 self.path.push("value".to_string());
489 self.walk_type(value);
490 self.path.pop();
491 }
492 SchemaType::Result { spec, .. } => {
493 if let Some(t) = &spec.ok {
494 self.path.push("ok".to_string());
495 self.walk_type(t);
496 self.path.pop();
497 }
498 if let Some(t) = &spec.err {
499 self.path.push("err".to_string());
500 self.walk_type(t);
501 self.path.pop();
502 }
503 }
504 SchemaType::Future { inner, .. } | SchemaType::Stream { inner, .. } => {
505 if let Some(t) = inner {
506 self.path.push("inner".to_string());
507 self.walk_type(t);
508 self.path.pop();
509 }
510 }
511 _ => {}
512 }
513 }
514
515 fn into_table(self, mut taken: HashSet<String>) -> BranchNameTable {
516 let mut groups: Vec<(String, Vec<String>)> = Vec::new();
519 for key in &self.keys {
520 let occ = &self.occurrences[key];
521 let preferred = sanitise_to_upper_camel(&occ.tag);
522 match groups.iter_mut().find(|(name, _)| name == &preferred) {
523 Some((_, members)) => members.push(key.clone()),
524 None => groups.push((preferred, vec![key.clone()])),
525 }
526 }
527
528 let mut names = HashMap::<String, String>::new();
529 for (preferred, members) in groups {
530 if members.len() == 1 && !taken.contains(&preferred) {
531 let only = members.into_iter().next().unwrap();
534 taken.insert(preferred.clone());
535 names.insert(only, preferred);
536 } else {
537 for key in members {
541 let occ = &self.occurrences[&key];
542 let assigned = disambiguate(&preferred, &occ.path, &taken, &key);
543 taken.insert(assigned.clone());
544 names.insert(key, assigned);
545 }
546 }
547 }
548
549 BranchNameTable { names }
550 }
551}
552
553fn disambiguate(base: &str, path: &[String], taken: &HashSet<String>, canonical: &str) -> String {
557 let mut candidate = base.to_string();
558 for seg in path.iter().rev() {
559 let seg_camel = sanitise_to_upper_camel(seg);
560 if seg_camel.is_empty() {
561 continue;
562 }
563 candidate = format!("{seg_camel}{candidate}");
564 if !taken.contains(&candidate) {
565 return candidate;
566 }
567 }
568 let suffix_len = 6.min(canonical.len());
569 format!("{base}_{}", &canonical[..suffix_len])
570}
571
572fn sanitise_to_upper_camel(s: &str) -> String {
577 let mut out = String::with_capacity(s.len());
578 let mut upper_next = true;
579 for ch in s.chars() {
580 if ch.is_ascii_alphanumeric() {
581 if upper_next {
582 for u in ch.to_uppercase() {
583 out.push(u);
584 }
585 upper_next = false;
586 } else {
587 out.push(ch);
588 }
589 } else {
590 upper_next = true;
591 }
592 }
593 if out.is_empty() {
594 return "Branch".to_string();
595 }
596 if out.starts_with(|c: char| c.is_ascii_digit()) {
597 format!("_{out}")
598 } else {
599 out
600 }
601}
602
603fn discriminator_constraint(rule: &DiscriminatorRule) -> Value {
604 let mut obj = Map::new();
605 match rule {
606 DiscriminatorRule::Prefix { prefix } => {
607 obj.insert(
608 "pattern".to_string(),
609 Value::String(format!("^{}", regex_escape(prefix))),
610 );
611 }
612 DiscriminatorRule::Suffix { suffix } => {
613 obj.insert(
614 "pattern".to_string(),
615 Value::String(format!("{}$", regex_escape(suffix))),
616 );
617 }
618 DiscriminatorRule::Contains { substring } => {
619 obj.insert(
620 "pattern".to_string(),
621 Value::String(regex_escape(substring)),
622 );
623 }
624 DiscriminatorRule::Regex { regex } => {
625 obj.insert("pattern".to_string(), Value::String(regex.clone()));
626 }
627 DiscriminatorRule::FieldEquals(disc) => {
628 obj.insert(
631 "required".to_string(),
632 Value::Array(vec![Value::String(disc.field_name.clone())]),
633 );
634 if let Some(lit) = &disc.literal {
635 let props = obj
636 .entry("properties")
637 .or_insert_with(|| Value::Object(Map::new()))
638 .as_object_mut()
639 .expect("properties is object");
640 let field = props
641 .entry(disc.field_name.clone())
642 .or_insert_with(|| obj_inline([("type", Value::String("string".to_string()))]));
643 if let Some(field_obj) = field.as_object_mut() {
644 field_obj.insert("const".to_string(), Value::String(lit.clone()));
645 }
646 }
647 }
648 DiscriminatorRule::FieldAbsent { field_name } => {
649 let not = obj_inline([(
651 "required",
652 Value::Array(vec![Value::String(field_name.clone())]),
653 )]);
654 obj.insert("not".to_string(), not);
655 }
656 }
657 Value::Object(obj)
658}
659
660pub(super) fn render_type(
661 graph: &SchemaGraph,
662 ty: &SchemaType,
663 root: bool,
664 table: &BranchNameTable,
665 config: JsonSchemaConfig,
666) -> Value {
667 let mut rendered = match ty {
668 SchemaType::Ref { id, .. } => obj([("$ref", Value::String(ref_pointer(id, root)))]),
669
670 SchemaType::Bool { .. } => obj([("type", Value::String("boolean".to_string()))]),
671 SchemaType::S8 { restrictions, .. } => {
672 signed_integer_schema(i8::MIN as i64, i8::MAX as i64, restrictions.as_ref())
673 }
674 SchemaType::S16 { restrictions, .. } => {
675 signed_integer_schema(i16::MIN as i64, i16::MAX as i64, restrictions.as_ref())
676 }
677 SchemaType::S32 { restrictions, .. } => {
678 signed_integer_schema(i32::MIN as i64, i32::MAX as i64, restrictions.as_ref())
679 }
680 SchemaType::S64 { restrictions, .. } => signed_64_schema(restrictions.as_ref()),
681 SchemaType::U8 { restrictions, .. } => {
682 unsigned_integer_schema(0, u8::MAX as u64, restrictions.as_ref())
683 }
684 SchemaType::U16 { restrictions, .. } => {
685 unsigned_integer_schema(0, u16::MAX as u64, restrictions.as_ref())
686 }
687 SchemaType::U32 { restrictions, .. } => {
688 unsigned_integer_schema(0, u32::MAX as u64, restrictions.as_ref())
689 }
690 SchemaType::U64 { restrictions, .. } => unsigned_64_schema(restrictions.as_ref()),
691 SchemaType::F32 { restrictions, .. } => {
692 float_schema(-(f32::MAX as f64), f32::MAX as f64, restrictions.as_ref())
693 }
694 SchemaType::F64 { restrictions, .. } => {
695 float_schema(-f64::MAX, f64::MAX, restrictions.as_ref())
696 }
697 SchemaType::Char { .. } => obj([
698 ("type", Value::String("string".to_string())),
699 ("minLength", Value::Number(1.into())),
700 ("maxLength", Value::Number(1.into())),
701 ]),
702 SchemaType::String { .. } => obj([("type", Value::String("string".to_string()))]),
703
704 SchemaType::Record { fields, .. } => {
705 let mut props = Map::new();
706 let mut required = Vec::with_capacity(fields.len());
707 for field in fields {
708 let mut field_schema = render_type(graph, &field.body, false, table, config);
709 attach_metadata(&mut field_schema, &field.metadata);
710 props.insert(field.name.clone(), field_schema);
711 if !resolves_to_option(graph, &field.body) {
715 required.push(Value::String(field.name.clone()));
716 }
717 }
718 obj([
719 ("type", Value::String("object".to_string())),
720 ("properties", Value::Object(props)),
721 ("required", Value::Array(required)),
722 ("additionalProperties", Value::Bool(false)),
723 ])
724 }
725
726 SchemaType::Variant { cases, .. } => {
727 Value::Object(variant_schema(graph, cases, table, config))
728 }
729
730 SchemaType::Enum { cases, .. } => obj([
731 ("type", Value::String("string".to_string())),
732 (
733 "enum",
734 Value::Array(cases.iter().cloned().map(Value::String).collect()),
735 ),
736 ]),
737
738 SchemaType::Flags { flags, .. } => obj([
739 ("type", Value::String("array".to_string())),
740 (
741 "items",
742 obj([
743 ("type", Value::String("string".to_string())),
744 (
745 "enum",
746 Value::Array(flags.iter().cloned().map(Value::String).collect()),
747 ),
748 ]),
749 ),
750 ("uniqueItems", Value::Bool(true)),
751 ]),
752
753 SchemaType::Tuple { elements, .. } => {
754 if elements.is_empty() {
755 obj([
759 ("type", Value::String("array".to_string())),
760 ("minItems", Value::Number(0u64.into())),
761 ("maxItems", Value::Number(0u64.into())),
762 ])
763 } else {
764 obj([
765 ("type", Value::String("array".to_string())),
766 (
767 "prefixItems",
768 Value::Array(
769 elements
770 .iter()
771 .map(|e| render_type(graph, e, false, table, config))
772 .collect(),
773 ),
774 ),
775 ("items", Value::Bool(false)),
776 ("minItems", Value::Number((elements.len() as u64).into())),
777 ])
778 }
779 }
780
781 SchemaType::List { element, .. } => obj([
782 ("type", Value::String("array".to_string())),
783 ("items", render_type(graph, element, false, table, config)),
784 ]),
785
786 SchemaType::FixedList {
787 element, length, ..
788 } => obj([
789 ("type", Value::String("array".to_string())),
790 ("items", render_type(graph, element, false, table, config)),
791 ("minItems", Value::Number((*length).into())),
792 ("maxItems", Value::Number((*length).into())),
793 ]),
794
795 SchemaType::Map { key, value, .. } => {
796 let pair = obj([
797 ("type", Value::String("array".to_string())),
798 (
799 "prefixItems",
800 Value::Array(vec![
801 render_type(graph, key, false, table, config),
802 render_type(graph, value, false, table, config),
803 ]),
804 ),
805 ("items", Value::Bool(false)),
806 ("minItems", Value::Number(2.into())),
807 ("maxItems", Value::Number(2.into())),
808 ]);
809 obj([
810 ("type", Value::String("array".to_string())),
811 ("items", pair),
812 ])
813 }
814
815 SchemaType::Option { inner, .. } => obj([(
816 "oneOf",
817 Value::Array(vec![
818 obj([("type", Value::String("null".to_string()))]),
819 render_type(graph, inner, false, table, config),
820 ]),
821 )]),
822
823 SchemaType::Result { spec, .. } => Value::Object(result_schema(graph, spec, table, config)),
824
825 SchemaType::Text { restrictions, .. } => Value::Object(text_schema(restrictions)),
826 SchemaType::Binary { restrictions, .. } => Value::Object(binary_schema(restrictions)),
827 SchemaType::Path { spec, .. } => Value::Object(path_schema(spec)),
828 SchemaType::Url { restrictions, .. } => Value::Object(url_schema(restrictions)),
829 SchemaType::Uuid { .. } => obj([
830 ("type", Value::String("string".to_string())),
831 ("format", Value::String("uuid".to_string())),
832 ]),
833 SchemaType::Datetime { .. } => obj([
834 ("type", Value::String("string".to_string())),
835 ("format", Value::String("date-time".to_string())),
836 ]),
837 SchemaType::Duration { .. } => obj([
838 ("type", Value::String("object".to_string())),
839 ("properties", obj([("nanoseconds", signed_64_schema(None))])),
840 (
841 "required",
842 Value::Array(vec![Value::String("nanoseconds".to_string())]),
843 ),
844 ("additionalProperties", Value::Bool(false)),
845 (
846 "title",
847 Value::String("Duration in nanoseconds".to_string()),
848 ),
849 ]),
850 SchemaType::Quantity { spec, .. } => Value::Object(quantity_schema(spec)),
851
852 SchemaType::Union { spec, .. } => Value::Object(union_schema(graph, spec, table, config)),
853
854 SchemaType::Secret { spec, .. } => {
855 host_managed_schema(config.host_managed, "secret", || {
856 Value::Object(secret_schema(spec))
857 })
858 }
859 SchemaType::QuotaToken { spec, .. } => {
860 host_managed_schema(config.host_managed, "quota-token", || {
861 Value::Object(quota_token_schema(spec))
862 })
863 }
864 SchemaType::PermissionCard { spec, .. } => {
865 host_managed_schema(config.host_managed, "permission-card", || {
866 Value::Object(permission_card_schema(spec))
867 })
868 }
869
870 SchemaType::Future { .. } | SchemaType::Stream { .. } => match config.unsupported {
871 UnsupportedLeafSchemaPolicy::Placeholder => obj([
872 ("type", Value::String("null".to_string())),
873 (
874 "description",
875 Value::String("WASI P3 placeholder".to_string()),
876 ),
877 ]),
878 UnsupportedLeafSchemaPolicy::Reject => obj([("not", Value::Object(Map::new()))]),
879 },
880 };
881
882 attach_metadata(&mut rendered, ty.metadata());
887 rendered
888}
889
890fn host_managed_schema(
891 policy: HostManagedSchemaPolicy,
892 kind: &str,
893 trusted: impl FnOnce() -> Value,
894) -> Value {
895 match policy {
896 HostManagedSchemaPolicy::TrustedSnapshot => trusted(),
897 HostManagedSchemaPolicy::Reject => obj([
898 ("not", Value::Object(Map::new())),
899 (
900 "description",
901 Value::String(format!(
902 "Host-managed {kind} capabilities cannot be supplied externally"
903 )),
904 ),
905 ]),
906 HostManagedSchemaPolicy::Redact => obj([
907 ("type", Value::String("string".to_string())),
908 ("const", Value::String(format!("<redacted: {kind}>"))),
909 (
910 "description",
911 Value::String(format!(
912 "Host-managed {kind} capability values are redacted"
913 )),
914 ),
915 ]),
916 }
917}
918
919fn ref_pointer(id: &TypeId, _root: bool) -> String {
920 ref_to_def_key(&id.0)
921}
922
923pub(super) fn ref_to_def_key(key: &str) -> String {
928 format!("#/$defs/{}", escape_pointer_token(key))
929}
930
931fn signed_integer_schema(min: i64, max: i64, restrictions: Option<&NumericRestrictions>) -> Value {
932 let minimum = restrictions
933 .and_then(|value| value.min)
934 .and_then(|bound| match bound {
935 NumericBound::Signed(value) => Some(value),
936 _ => None,
937 })
938 .unwrap_or(min)
939 .max(min);
940 let maximum = restrictions
941 .and_then(|value| value.max)
942 .and_then(|bound| match bound {
943 NumericBound::Signed(value) => Some(value),
944 _ => None,
945 })
946 .unwrap_or(max)
947 .min(max);
948 obj([
949 ("type", Value::String("integer".to_string())),
950 ("minimum", Value::Number(Number::from(minimum))),
951 ("maximum", Value::Number(Number::from(maximum))),
952 ])
953}
954
955fn unsigned_integer_schema(
956 min: u64,
957 max: u64,
958 restrictions: Option<&NumericRestrictions>,
959) -> Value {
960 let minimum = restrictions
961 .and_then(|value| value.min)
962 .and_then(|bound| match bound {
963 NumericBound::Unsigned(value) => Some(value),
964 _ => None,
965 })
966 .unwrap_or(min)
967 .max(min);
968 let maximum = restrictions
969 .and_then(|value| value.max)
970 .and_then(|bound| match bound {
971 NumericBound::Unsigned(value) => Some(value),
972 _ => None,
973 })
974 .unwrap_or(max)
975 .min(max);
976 obj([
977 ("type", Value::String("integer".to_string())),
978 ("minimum", Value::Number(Number::from(minimum))),
979 ("maximum", Value::Number(Number::from(maximum))),
980 ])
981}
982
983fn float_schema(min: f64, max: f64, restrictions: Option<&NumericRestrictions>) -> Value {
984 let mut schema = Map::new();
985 schema.insert("type".to_string(), Value::String("number".to_string()));
986 if let Some(NumericBound::FloatBits(bits)) = restrictions.and_then(|value| value.min)
987 && let Some(value) = Number::from_f64(f64::from_bits(bits).max(min))
988 {
989 schema.insert("minimum".to_string(), Value::Number(value));
990 }
991 if let Some(NumericBound::FloatBits(bits)) = restrictions.and_then(|value| value.max)
992 && let Some(value) = Number::from_f64(f64::from_bits(bits).min(max))
993 {
994 schema.insert("maximum".to_string(), Value::Number(value));
995 }
996 Value::Object(schema)
997}
998
999fn unsigned_64_schema(restrictions: Option<&NumericRestrictions>) -> Value {
1000 let minimum = restrictions
1001 .and_then(|value| value.min)
1002 .and_then(|bound| match bound {
1003 NumericBound::Unsigned(value) => Some(value),
1004 _ => None,
1005 })
1006 .unwrap_or(0);
1007 let maximum = restrictions
1008 .and_then(|value| value.max)
1009 .and_then(|bound| match bound {
1010 NumericBound::Unsigned(value) => Some(value),
1011 _ => None,
1012 })
1013 .unwrap_or(u64::MAX);
1014 obj([
1015 ("type", Value::String("string".to_string())),
1016 ("format", Value::String("uint64".to_string())),
1017 ("pattern", Value::String("^(?:0|[1-9][0-9]*)$".to_string())),
1018 ("x-golem-minimum", Value::String(minimum.to_string())),
1019 ("x-golem-maximum", Value::String(maximum.to_string())),
1020 ])
1021}
1022
1023fn signed_64_schema(restrictions: Option<&NumericRestrictions>) -> Value {
1024 let minimum = restrictions
1025 .and_then(|value| value.min)
1026 .and_then(|bound| match bound {
1027 NumericBound::Signed(value) => Some(value),
1028 _ => None,
1029 })
1030 .unwrap_or(i64::MIN);
1031 let maximum = restrictions
1032 .and_then(|value| value.max)
1033 .and_then(|bound| match bound {
1034 NumericBound::Signed(value) => Some(value),
1035 _ => None,
1036 })
1037 .unwrap_or(i64::MAX);
1038 obj([
1039 ("type", Value::String("string".to_string())),
1040 ("format", Value::String("int64".to_string())),
1041 (
1042 "pattern",
1043 Value::String("^(?:0|-[1-9][0-9]*|[1-9][0-9]*)$".to_string()),
1044 ),
1045 ("x-golem-minimum", Value::String(minimum.to_string())),
1046 ("x-golem-maximum", Value::String(maximum.to_string())),
1047 ])
1048}
1049
1050fn variant_schema(
1051 graph: &SchemaGraph,
1052 cases: &[VariantCaseType],
1053 table: &BranchNameTable,
1054 config: JsonSchemaConfig,
1055) -> Map<String, Value> {
1056 let one_of: Vec<Value> = cases
1057 .iter()
1058 .map(|case| match &case.payload {
1059 None => obj([("const", Value::String(case.name.clone()))]),
1060 Some(payload_ty) => {
1061 let mut props = Map::new();
1062 props.insert(
1063 case.name.clone(),
1064 render_type(graph, payload_ty, false, table, config),
1065 );
1066 obj([
1067 ("type", Value::String("object".to_string())),
1068 ("properties", Value::Object(props)),
1069 (
1070 "required",
1071 Value::Array(vec![Value::String(case.name.clone())]),
1072 ),
1073 ("additionalProperties", Value::Bool(false)),
1074 ])
1075 }
1076 })
1077 .collect();
1078 let mut out = Map::new();
1079 out.insert("oneOf".to_string(), Value::Array(one_of));
1080 out
1081}
1082
1083fn result_schema(
1084 graph: &SchemaGraph,
1085 spec: &ResultSpec,
1086 table: &BranchNameTable,
1087 config: JsonSchemaConfig,
1088) -> Map<String, Value> {
1089 let ok_inner = spec
1090 .ok
1091 .as_deref()
1092 .map(|t| render_type(graph, t, false, table, config))
1093 .unwrap_or_else(|| obj([("type", Value::String("null".to_string()))]));
1094 let err_inner = spec
1095 .err
1096 .as_deref()
1097 .map(|t| render_type(graph, t, false, table, config))
1098 .unwrap_or_else(|| obj([("type", Value::String("null".to_string()))]));
1099 let one_of = vec![
1100 obj([
1101 ("type", Value::String("object".to_string())),
1102 (
1103 "properties",
1104 Value::Object({
1105 let mut m = Map::new();
1106 m.insert("ok".to_string(), ok_inner);
1107 m
1108 }),
1109 ),
1110 ("required", Value::Array(vec![Value::String("ok".into())])),
1111 ("additionalProperties", Value::Bool(false)),
1112 ]),
1113 obj([
1114 ("type", Value::String("object".to_string())),
1115 (
1116 "properties",
1117 Value::Object({
1118 let mut m = Map::new();
1119 m.insert("err".to_string(), err_inner);
1120 m
1121 }),
1122 ),
1123 ("required", Value::Array(vec![Value::String("err".into())])),
1124 ("additionalProperties", Value::Bool(false)),
1125 ]),
1126 ];
1127 let mut out = Map::new();
1128 out.insert("oneOf".to_string(), Value::Array(one_of));
1129 out
1130}
1131
1132fn text_schema(restrictions: &TextRestrictions) -> Map<String, Value> {
1133 let mut text_field = Map::new();
1136 text_field.insert("type".to_string(), Value::String("string".to_string()));
1137 if let Some(min) = restrictions.min_length {
1138 text_field.insert("minLength".to_string(), Value::Number(min.into()));
1139 }
1140 if let Some(max) = restrictions.max_length {
1141 text_field.insert("maxLength".to_string(), Value::Number(max.into()));
1142 }
1143 if let Some(regex) = &restrictions.regex {
1144 text_field.insert("pattern".to_string(), Value::String(regex.clone()));
1145 }
1146 let mut properties = Map::new();
1147 properties.insert("text".to_string(), Value::Object(text_field));
1148 let mut language_field = Map::new();
1149 language_field.insert("type".to_string(), Value::String("string".to_string()));
1150 if let Some(langs) = &restrictions.languages {
1151 language_field.insert(
1152 "enum".to_string(),
1153 Value::Array(langs.iter().cloned().map(Value::String).collect()),
1154 );
1155 }
1156 properties.insert("language".to_string(), Value::Object(language_field));
1157 let mut m = Map::new();
1158 m.insert("type".to_string(), Value::String("object".to_string()));
1159 m.insert("properties".to_string(), Value::Object(properties));
1160 m.insert(
1161 "required".to_string(),
1162 Value::Array(vec![Value::String("text".to_string())]),
1163 );
1164 m.insert("additionalProperties".to_string(), Value::Bool(false));
1165 m
1166}
1167
1168fn binary_schema(restrictions: &BinaryRestrictions) -> Map<String, Value> {
1169 let mut bytes_field = Map::new();
1174 bytes_field.insert("type".to_string(), Value::String("string".to_string()));
1175 bytes_field.insert(
1176 "contentEncoding".to_string(),
1177 Value::String("base64url".to_string()),
1178 );
1179 bytes_field.insert(
1180 "pattern".to_string(),
1181 Value::String(BASE64URL_PATTERN.to_string()),
1182 );
1183 if let Some(min) = restrictions.min_bytes {
1184 bytes_field.insert(
1185 "minLength".to_string(),
1186 Value::Number(base64url_no_pad_len(min).into()),
1187 );
1188 }
1189 if let Some(max) = restrictions.max_bytes {
1190 bytes_field.insert(
1191 "maxLength".to_string(),
1192 Value::Number(base64url_no_pad_len(max).into()),
1193 );
1194 }
1195 let mut mime_field = Map::new();
1196 mime_field.insert("type".to_string(), Value::String("string".to_string()));
1197 mime_field.insert(
1198 "pattern".to_string(),
1199 Value::String(MIME_TYPE_PATTERN.to_string()),
1200 );
1201 if let Some(mimes) = &restrictions.mime_types {
1202 mime_field.insert(
1203 "enum".to_string(),
1204 Value::Array(mimes.iter().cloned().map(Value::String).collect()),
1205 );
1206 }
1207 let mut properties = Map::new();
1208 properties.insert("bytes".to_string(), Value::Object(bytes_field));
1209 properties.insert("mimeType".to_string(), Value::Object(mime_field));
1210 let mut m = Map::new();
1211 m.insert("type".to_string(), Value::String("object".to_string()));
1212 m.insert("properties".to_string(), Value::Object(properties));
1213 m.insert(
1214 "required".to_string(),
1215 Value::Array(vec![Value::String("bytes".to_string())]),
1216 );
1217 m.insert("additionalProperties".to_string(), Value::Bool(false));
1218 m
1219}
1220
1221fn path_schema(spec: &PathSpec) -> Map<String, Value> {
1222 let mut m = Map::new();
1223 m.insert("type".to_string(), Value::String("string".to_string()));
1224 m.insert("format".to_string(), Value::String("file-path".to_string()));
1225 let kind = match spec.kind {
1226 crate::schema::schema_type::PathKind::File => "file",
1227 crate::schema::schema_type::PathKind::Directory => "directory",
1228 crate::schema::schema_type::PathKind::Any => "any",
1229 };
1230 let direction = match spec.direction {
1231 crate::schema::schema_type::PathDirection::Input => "input",
1232 crate::schema::schema_type::PathDirection::Output => "output",
1233 crate::schema::schema_type::PathDirection::InOut => "inout",
1234 };
1235 m.insert(
1236 "title".to_string(),
1237 Value::String(format!("{direction} {kind} path")),
1238 );
1239 let mut description = Vec::new();
1240 if let Some(exts) = &spec.allowed_extensions {
1241 description.push(format!("Allowed extensions: {}", exts.join(", ")));
1242 }
1243 if let Some(mimes) = &spec.allowed_mime_types {
1244 description.push(format!("Allowed MIME types: {}", mimes.join(", ")));
1245 }
1246 if !description.is_empty() {
1247 m.insert(
1248 "description".to_string(),
1249 Value::String(description.join("; ")),
1250 );
1251 }
1252 m
1253}
1254
1255fn url_schema(restrictions: &UrlRestrictions) -> Map<String, Value> {
1256 let mut m = Map::new();
1257 m.insert("type".to_string(), Value::String("string".to_string()));
1258 m.insert("format".to_string(), Value::String("uri".to_string()));
1259 m.insert("title".to_string(), Value::String("URL".to_string()));
1260 let mut description = Vec::new();
1261 if let Some(schemes) = &restrictions.allowed_schemes {
1262 description.push(format!("Allowed schemes: {}", schemes.join(", ")));
1263 }
1264 if let Some(hosts) = &restrictions.allowed_hosts {
1265 description.push(format!("Allowed hosts: {}", hosts.join(", ")));
1266 }
1267 if !description.is_empty() {
1268 m.insert(
1269 "description".to_string(),
1270 Value::String(description.join("; ")),
1271 );
1272 }
1273 m
1274}
1275
1276fn quantity_schema(spec: &QuantitySpec) -> Map<String, Value> {
1277 let mut props = Map::new();
1278 props.insert("mantissa".to_string(), signed_64_schema(None));
1279 props.insert(
1280 "scale".to_string(),
1281 obj([("type", Value::String("integer".to_string()))]),
1282 );
1283 props.insert(
1284 "unit".to_string(),
1285 obj([("type", Value::String("string".to_string()))]),
1286 );
1287 let mut m = Map::new();
1288 m.insert("type".to_string(), Value::String("object".to_string()));
1289 m.insert("properties".to_string(), Value::Object(props));
1290 m.insert(
1291 "required".to_string(),
1292 Value::Array(vec![
1293 Value::String("mantissa".to_string()),
1294 Value::String("scale".to_string()),
1295 Value::String("unit".to_string()),
1296 ]),
1297 );
1298 m.insert("additionalProperties".to_string(), Value::Bool(false));
1299 m.insert(
1300 "title".to_string(),
1301 Value::String(format!("Quantity ({})", spec.base_unit)),
1302 );
1303 let mut description = Vec::new();
1304 if let Some(min) = &spec.min {
1305 description.push(format!("min: {}", render_quantity(min)));
1306 }
1307 if let Some(max) = &spec.max {
1308 description.push(format!("max: {}", render_quantity(max)));
1309 }
1310 if !description.is_empty() {
1311 m.insert(
1312 "description".to_string(),
1313 Value::String(description.join("; ")),
1314 );
1315 }
1316 m
1317}
1318
1319fn render_quantity(q: &QuantityValue) -> String {
1320 format!("{}e-{} {}", q.mantissa, q.scale, q.unit)
1321}
1322
1323fn secret_schema(_spec: &SecretSpec) -> Map<String, Value> {
1324 let secret_id = obj_inline([
1326 ("type", Value::String("string".to_string())),
1327 ("format", Value::String("uuid".to_string())),
1328 ]);
1329 let config_key = obj_inline([
1330 ("type", Value::String("array".to_string())),
1331 (
1332 "items",
1333 obj_inline([("type", Value::String("string".to_string()))]),
1334 ),
1335 ]);
1336 let version = obj_inline([
1337 ("type", Value::String("integer".to_string())),
1338 ("minimum", Value::Number(0.into())),
1339 ]);
1340 let resolved_at = obj_inline([
1341 ("type", Value::String("string".to_string())),
1342 ("format", Value::String("date-time".to_string())),
1343 ]);
1344 let category = obj_inline([("type", Value::String("string".to_string()))]);
1345 let mut properties = Map::new();
1346 properties.insert("secretId".to_string(), secret_id);
1347 properties.insert("configKey".to_string(), config_key);
1348 properties.insert("version".to_string(), version);
1349 properties.insert("resolvedAt".to_string(), resolved_at);
1350 properties.insert("category".to_string(), category);
1351 let mut m = Map::new();
1352 m.insert("type".to_string(), Value::String("object".to_string()));
1353 m.insert("properties".to_string(), Value::Object(properties));
1354 m.insert(
1355 "required".to_string(),
1356 Value::Array(vec![
1357 Value::String("secretId".to_string()),
1358 Value::String("version".to_string()),
1359 Value::String("resolvedAt".to_string()),
1360 ]),
1361 );
1362 m.insert("additionalProperties".to_string(), Value::Bool(false));
1363 m
1364}
1365
1366fn quota_token_schema(_spec: &QuotaTokenSpec) -> Map<String, Value> {
1367 let env_id = obj_inline([
1369 ("type", Value::String("string".to_string())),
1370 ("format", Value::String("uuid".to_string())),
1371 ]);
1372 let resource_name = obj_inline([("type", Value::String("string".to_string()))]);
1373 let expected_use = obj_inline([(
1374 "oneOf",
1375 Value::Array(vec![
1376 obj_inline([
1377 ("type", Value::String("string".to_string())),
1378 ("pattern", Value::String("^[0-9]+$".to_string())),
1379 ]),
1380 obj_inline([
1381 ("type", Value::String("integer".to_string())),
1382 ("minimum", Value::Number(0u64.into())),
1383 ]),
1384 ]),
1385 )]);
1386 let last_credit = obj_inline([(
1387 "oneOf",
1388 Value::Array(vec![
1389 obj_inline([
1390 ("type", Value::String("string".to_string())),
1391 ("pattern", Value::String("^-?[0-9]+$".to_string())),
1392 ]),
1393 obj_inline([("type", Value::String("integer".to_string()))]),
1394 ]),
1395 )]);
1396 let last_credit_at = obj_inline([
1397 ("type", Value::String("string".to_string())),
1398 ("format", Value::String("date-time".to_string())),
1399 ]);
1400 let mut properties = Map::new();
1401 properties.insert("environmentId".to_string(), env_id);
1402 properties.insert("resourceName".to_string(), resource_name);
1403 properties.insert("expectedUse".to_string(), expected_use);
1404 properties.insert("lastCredit".to_string(), last_credit);
1405 properties.insert("lastCreditAt".to_string(), last_credit_at);
1406 let mut m = Map::new();
1407 m.insert("type".to_string(), Value::String("object".to_string()));
1408 m.insert("properties".to_string(), Value::Object(properties));
1409 m.insert(
1410 "required".to_string(),
1411 Value::Array(vec![
1412 Value::String("environmentId".to_string()),
1413 Value::String("resourceName".to_string()),
1414 Value::String("expectedUse".to_string()),
1415 Value::String("lastCredit".to_string()),
1416 Value::String("lastCreditAt".to_string()),
1417 ]),
1418 );
1419 m.insert("additionalProperties".to_string(), Value::Bool(false));
1420 m
1421}
1422
1423fn permission_card_schema(_spec: &PermissionCardSpec) -> Map<String, Value> {
1424 let card_id = obj_inline([
1428 ("type", Value::String("string".to_string())),
1429 ("format", Value::String("uuid".to_string())),
1430 ]);
1431 let parent_ids = obj_inline([
1432 ("type", Value::String("array".to_string())),
1433 (
1434 "items",
1435 obj_inline([
1436 ("type", Value::String("string".to_string())),
1437 ("format", Value::String("uuid".to_string())),
1438 ]),
1439 ),
1440 ]);
1441 let expires_at = obj_inline([
1442 ("type", Value::String("string".to_string())),
1443 ("format", Value::String("date-time".to_string())),
1444 ]);
1445 let polymorphic = obj_inline([("type", Value::String("boolean".to_string()))]);
1446 let mut properties = Map::new();
1447 properties.insert("cardId".to_string(), card_id);
1448 properties.insert("parentIds".to_string(), parent_ids);
1449 properties.insert("expiresAt".to_string(), expires_at);
1450 properties.insert("polymorphic".to_string(), polymorphic);
1451 let mut m = Map::new();
1452 m.insert("type".to_string(), Value::String("object".to_string()));
1453 m.insert("properties".to_string(), Value::Object(properties));
1454 m.insert(
1455 "required".to_string(),
1456 Value::Array(vec![
1457 Value::String("cardId".to_string()),
1458 Value::String("polymorphic".to_string()),
1459 ]),
1460 );
1461 m.insert("additionalProperties".to_string(), Value::Bool(false));
1462 m
1463}
1464
1465fn union_schema(
1466 graph: &SchemaGraph,
1467 spec: &UnionSpec,
1468 table: &BranchNameTable,
1469 config: JsonSchemaConfig,
1470) -> Map<String, Value> {
1471 let one_of: Vec<Value> = spec
1477 .branches
1478 .iter()
1479 .map(|b| obj([("$ref", Value::String(ref_to_def_key(table.name_for(b))))]))
1480 .collect();
1481 let mut m = Map::new();
1482 m.insert("oneOf".to_string(), Value::Array(one_of));
1483 if let Some(disc_field) = openapi_discriminator(spec) {
1484 let mut mapping = Map::new();
1485 for branch in spec.branches.iter() {
1486 let literal = match &branch.discriminator {
1487 DiscriminatorRule::FieldEquals(disc) => disc.literal.clone(),
1488 _ => None,
1489 };
1490 if let Some(lit) = literal {
1491 mapping.insert(lit, Value::String(ref_to_def_key(table.name_for(branch))));
1492 }
1493 }
1494 let mut d = Map::new();
1495 d.insert("propertyName".to_string(), Value::String(disc_field));
1496 if !mapping.is_empty() {
1497 d.insert("mapping".to_string(), Value::Object(mapping));
1498 }
1499 m.insert("discriminator".to_string(), Value::Object(d));
1500 }
1501 let _ = graph;
1502 let _ = config;
1503 m
1504}
1505
1506fn openapi_discriminator(spec: &UnionSpec) -> Option<String> {
1507 let mut field: Option<String> = None;
1508 for branch in spec.branches.iter() {
1509 match &branch.discriminator {
1510 DiscriminatorRule::FieldEquals(disc) => {
1511 let _ = disc.literal.as_ref()?;
1512 match &field {
1513 Some(prev) if prev != &disc.field_name => return None,
1514 Some(_) => {}
1515 None => field = Some(disc.field_name.clone()),
1516 }
1517 }
1518 _ => return None,
1519 }
1520 }
1521 field
1522}
1523
1524fn attach_metadata(target: &mut Value, metadata: &MetadataEnvelope) {
1525 if metadata.is_empty() {
1526 return;
1527 }
1528 let Some(obj) = target.as_object_mut() else {
1529 return;
1530 };
1531 if let Some(doc) = &metadata.doc {
1532 obj.entry("description")
1533 .or_insert(Value::String(doc.clone()));
1534 }
1535 if !metadata.examples.is_empty() {
1536 obj.entry("examples").or_insert_with(|| {
1537 Value::Array(
1538 metadata
1539 .examples
1540 .iter()
1541 .map(|e| Value::String(e.clone()))
1542 .collect(),
1543 )
1544 });
1545 }
1546 if let Some(dep) = &metadata.deprecated {
1547 obj.entry("deprecated").or_insert(Value::Bool(true));
1548 obj.entry("x-golem-deprecation-note")
1549 .or_insert(Value::String(dep.clone()));
1550 }
1551}
1552
1553fn base64url_no_pad_len(n: u32) -> u64 {
1557 let n = n as u64;
1558 4 * (n / 3)
1559 + match n % 3 {
1560 0 => 0,
1561 1 => 2,
1562 2 => 3,
1563 _ => unreachable!(),
1564 }
1565}
1566
1567fn escape_pointer_token(s: &str) -> String {
1570 let mut out = String::with_capacity(s.len());
1571 for ch in s.chars() {
1572 match ch {
1573 '~' => out.push_str("~0"),
1574 '/' => out.push_str("~1"),
1575 other => out.push(other),
1576 }
1577 }
1578 out
1579}
1580
1581fn regex_escape(s: &str) -> String {
1583 let specials: &[char] = &[
1584 '\\', '^', '$', '.', '|', '?', '*', '+', '(', ')', '[', ']', '{', '}',
1585 ];
1586 let mut out = String::with_capacity(s.len());
1587 for ch in s.chars() {
1588 if specials.contains(&ch) {
1589 out.push('\\');
1590 }
1591 out.push(ch);
1592 }
1593 out
1594}
1595
1596fn obj<I: IntoIterator<Item = (&'static str, Value)>>(entries: I) -> Value {
1597 let mut map = Map::new();
1598 for (k, v) in entries {
1599 map.insert(k.to_string(), v);
1600 }
1601 Value::Object(map)
1602}
1603
1604fn obj_inline<I: IntoIterator<Item = (&'static str, Value)>>(entries: I) -> Value {
1605 obj(entries)
1606}