1use std::{collections::BTreeMap, fmt};
8
9use enumset::EnumSet;
10use modelplease::{MediaKind, MediaSource, SourceKind};
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24#[serde(tag = "kind", content = "inner", rename_all = "snake_case")]
25pub enum FieldType {
26 String,
28 Int,
30 Float,
32 Bool,
34 List(Box<Self>),
36 Object(Vec<ObjectField>),
38 Map(Box<Self>),
40 Enum(Vec<std::string::String>),
42 Nullable(Box<Self>),
44 OneOf {
52 arms: Vec<VariantArm>,
54 discriminator: Option<OneOfDiscriminator>,
57 },
58 AnyOf {
64 arms: Vec<VariantArm>,
66 },
67 Media {
74 kind: MediaKind,
76 accepted_sources: EnumSet<SourceKind>,
80 },
81}
82
83#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
89pub struct VariantArm {
90 pub description: std::string::String,
94 pub field_type: FieldType,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105pub struct OneOfDiscriminator {
106 pub property: std::string::String,
108 pub tags: Vec<std::string::String>,
111}
112
113impl FieldType {
114 #[must_use]
116 pub fn type_label(&self) -> std::string::String {
117 match self {
118 Self::String => "str".to_owned(),
119 Self::Int => "int".to_owned(),
120 Self::Float => "float".to_owned(),
121 Self::Bool => "bool".to_owned(),
122 Self::List(inner) => format!("list[{}]", inner.type_label()),
123 Self::Object(fields) => {
124 let parts: Vec<_> = fields
125 .iter()
126 .map(|f| format!("{}: {}", f.name, f.field_type.type_label()))
127 .collect();
128 format!("{{{}}}", parts.join(", "))
129 }
130 Self::Map(value_type) => format!("map[str, {}]", value_type.type_label()),
131 Self::Enum(variants) => {
132 format!("enum[{}]", variants.join(", "))
133 }
134 Self::Nullable(inner) => format!("optional[{}]", inner.type_label()),
135 Self::OneOf {
136 arms,
137 discriminator,
138 } => discriminator.as_ref().map_or_else(
139 || {
140 let parts: Vec<_> = arms.iter().map(|a| a.field_type.type_label()).collect();
141 format!("oneof[{}]", parts.join(" | "))
142 },
143 |disc| format!("oneof[{}: {}]", disc.property, disc.tags.join(" | ")),
144 ),
145 Self::AnyOf { arms } => {
146 let parts: Vec<_> = arms.iter().map(|a| a.field_type.type_label()).collect();
147 format!("anyof[{}]", parts.join(" | "))
148 }
149 Self::Media { kind, .. } => format!("media[{}]", kind.label()),
150 }
151 }
152
153 #[must_use]
164 pub fn output_format_hint(&self) -> Option<std::string::String> {
165 match self {
166 Self::String | Self::Media { .. } => None,
167 Self::Int => Some("a single integer".to_owned()),
168 Self::Float => Some("a single number".to_owned()),
169 Self::Bool => Some("`true` or `false`".to_owned()),
170 Self::Enum(variants) => Some(format!("exactly one of: {}", variants.join(", "))),
171 Self::List(inner) => Some(format!(
172 "a JSON array of {}, e.g. [\"...\", \"...\"] — not an object, not a code fence",
173 inner.type_label()
174 )),
175 Self::Object(fields) => {
176 let example = object_payload_example(fields);
177 Some(format!(
178 "a JSON object matching {} — e.g. {example} — not a code fence",
179 self.type_label()
180 ))
181 }
182 Self::Map(value_type) => Some(format!(
183 "a JSON object with string keys and {} values — \
184 e.g. {{\"key1\": ..., \"key2\": ...}} — not a code fence",
185 value_type.type_label()
186 )),
187 Self::Nullable(inner) => {
188 Some(inner.output_format_hint().map_or_else(
197 || "a value, or null when the value is not applicable".to_owned(),
198 |hint| format!("{hint}, or null when the value is not applicable"),
199 ))
200 }
201 Self::OneOf { discriminator, .. } => Some(discriminator.as_ref().map_or_else(
202 || {
203 "a JSON value matching exactly one of the shapes listed under \"Variant \
204 shapes\" above"
205 .to_owned()
206 },
207 |d| {
208 let example_tag = d.tags.first().map_or("...", String::as_str);
215 format!(
216 "a JSON object whose `{property}` field selects the variant — \
217 e.g. {{\"{property}\": \"{example_tag}\", ...}} \
218 (see \"Variant shapes\" above for each arm's fields)",
219 property = d.property,
220 )
221 },
222 )),
223 Self::AnyOf { .. } => Some(
224 "a JSON value matching any of the shapes listed under \"Variant shapes\" \
225 above (first match wins)"
226 .to_owned(),
227 ),
228 }
229 }
230}
231
232impl fmt::Display for FieldType {
233 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234 f.write_str(&self.type_label())
235 }
236}
237
238fn object_payload_example(fields: &[ObjectField]) -> String {
246 let parts: Vec<String> = fields
247 .iter()
248 .take(3)
249 .map(|f| format!("\"{}\": {}", f.name, type_placeholder(&f.field_type)))
250 .collect();
251 let suffix = if fields.len() > 3 { ", ..." } else { "" };
252 format!("{{{}{suffix}}}", parts.join(", "))
253}
254
255const fn type_placeholder(field_type: &FieldType) -> &'static str {
260 match field_type {
261 FieldType::String | FieldType::Enum(_) | FieldType::Media { .. } => "\"...\"",
262 FieldType::Int => "123",
263 FieldType::Float => "1.5",
264 FieldType::Bool => "true",
265 FieldType::List(_) => "[...]",
266 FieldType::Object(_) | FieldType::Map(_) => "{...}",
267 FieldType::Nullable(_) => "null",
268 FieldType::OneOf { .. } | FieldType::AnyOf { .. } => "...",
269 }
270}
271
272#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
277pub struct ObjectField {
278 pub name: std::string::String,
280 pub description: std::string::String,
282 pub field_type: FieldType,
284}
285
286#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
288#[serde(rename_all = "lowercase")]
289pub enum FieldKind {
290 Input,
292 Output,
294}
295
296#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
298pub struct FieldDef {
299 pub name: std::string::String,
301 pub description: std::string::String,
303 pub field_type: FieldType,
305 pub kind: FieldKind,
307 #[serde(default)]
314 pub cacheable: bool,
315 #[serde(default, skip_serializing_if = "Vec::is_empty")]
322 pub examples: Vec<serde_json::Value>,
323}
324
325impl FieldDef {
326 pub fn input(
333 name: impl Into<std::string::String>,
334 field_type: FieldType,
335 description: impl Into<std::string::String>,
336 ) -> Self {
337 Self {
338 name: name.into(),
339 description: description.into(),
340 field_type,
341 kind: FieldKind::Input,
342 cacheable: false,
343 examples: Vec::new(),
344 }
345 }
346
347 pub fn output(
351 name: impl Into<std::string::String>,
352 field_type: FieldType,
353 description: impl Into<std::string::String>,
354 ) -> Self {
355 Self {
356 name: name.into(),
357 description: description.into(),
358 field_type,
359 kind: FieldKind::Output,
360 cacheable: false,
361 examples: Vec::new(),
362 }
363 }
364
365 #[must_use]
369 pub fn with_examples(mut self, examples: Vec<serde_json::Value>) -> Self {
370 self.examples = examples;
371 self
372 }
373}
374
375#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
401#[serde(untagged)]
402pub enum FieldValue {
403 Str(std::string::String),
405 Int(i64),
407 Float(f64),
409 Bool(bool),
411 List(Vec<Self>),
413 Variant {
418 arm_index: usize,
421 value: Box<Self>,
423 },
424 Object(BTreeMap<std::string::String, Self>),
426 Media(MediaValue),
428 Null,
430}
431
432#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
436pub struct MediaValue {
437 pub kind: MediaKind,
439 pub source: MediaSource,
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[test]
448 fn type_label_primitives() {
449 assert_eq!(FieldType::String.type_label(), "str");
450 assert_eq!(FieldType::Int.type_label(), "int");
451 assert_eq!(FieldType::Float.type_label(), "float");
452 assert_eq!(FieldType::Bool.type_label(), "bool");
453 }
454
455 #[test]
456 fn type_label_nested() {
457 let list_int = FieldType::List(Box::new(FieldType::Int));
458 assert_eq!(list_int.type_label(), "list[int]");
459
460 let nullable_str = FieldType::Nullable(Box::new(FieldType::String));
461 assert_eq!(nullable_str.type_label(), "optional[str]");
462
463 let map_float = FieldType::Map(Box::new(FieldType::Float));
464 assert_eq!(map_float.type_label(), "map[str, float]");
465 }
466
467 #[test]
468 fn type_label_enum() {
469 let e = FieldType::Enum(vec!["a".into(), "b".into(), "c".into()]);
470 assert_eq!(e.type_label(), "enum[a, b, c]");
471 }
472
473 #[test]
474 fn type_label_object() {
475 let obj = FieldType::Object(vec![
476 ObjectField {
477 name: "x".into(),
478 description: "x coord".into(),
479 field_type: FieldType::Int,
480 },
481 ObjectField {
482 name: "y".into(),
483 description: "y coord".into(),
484 field_type: FieldType::Int,
485 },
486 ]);
487 assert_eq!(obj.type_label(), "{x: int, y: int}");
488 }
489
490 #[test]
491 fn field_def_input_constructor() {
492 let f = FieldDef::input("question", FieldType::String, "The user question");
493 assert_eq!(f.name, "question");
494 assert_eq!(f.kind, FieldKind::Input);
495 }
496
497 #[test]
498 fn field_def_output_constructor() {
499 let f = FieldDef::output("answer", FieldType::String, "The answer");
500 assert_eq!(f.name, "answer");
501 assert_eq!(f.kind, FieldKind::Output);
502 }
503
504 #[test]
505 fn field_type_serde_round_trip_primitive() {
506 let ft = FieldType::String;
507 let json = serde_json::to_string(&ft).unwrap();
508 let deserialized: FieldType = serde_json::from_str(&json).unwrap();
509 assert_eq!(ft, deserialized);
510 }
511
512 #[test]
513 fn field_type_serde_round_trip_nested() {
514 let ft = FieldType::List(Box::new(FieldType::Nullable(Box::new(FieldType::Int))));
515 let json = serde_json::to_string(&ft).unwrap();
516 let deserialized: FieldType = serde_json::from_str(&json).unwrap();
517 assert_eq!(ft, deserialized);
518 }
519
520 #[test]
521 fn field_type_serde_round_trip_object() {
522 let ft = FieldType::Object(vec![ObjectField {
523 name: "name".into(),
524 description: "A name".into(),
525 field_type: FieldType::String,
526 }]);
527 let json = serde_json::to_string(&ft).unwrap();
528 let deserialized: FieldType = serde_json::from_str(&json).unwrap();
529 assert_eq!(ft, deserialized);
530 }
531
532 #[test]
533 fn field_value_serde_round_trip() {
534 let val = FieldValue::Object(BTreeMap::from([
535 ("name".into(), FieldValue::Str("Alice".into())),
536 ("age".into(), FieldValue::Int(30)),
537 ("active".into(), FieldValue::Bool(true)),
538 ]));
539 let json = serde_json::to_string(&val).unwrap();
540 let deserialized: FieldValue = serde_json::from_str(&json).unwrap();
541 assert_eq!(val, deserialized);
542 }
543
544 #[test]
545 fn field_def_serde_round_trip() {
546 let fd = FieldDef::input("text", FieldType::String, "Input text");
547 let json = serde_json::to_string(&fd).unwrap();
548 let deserialized: FieldDef = serde_json::from_str(&json).unwrap();
549 assert_eq!(fd, deserialized);
550 }
551
552 #[test]
553 fn output_format_hint_none_for_string_and_media() {
554 assert!(FieldType::String.output_format_hint().is_none());
555 let media = FieldType::Media {
556 kind: MediaKind::Image,
557 accepted_sources: EnumSet::all(),
558 };
559 assert!(media.output_format_hint().is_none());
560 }
561
562 #[test]
563 fn output_format_hint_list_says_json_array() {
564 let hint = FieldType::List(Box::new(FieldType::String))
565 .output_format_hint()
566 .expect("list has a hint");
567 assert!(hint.contains("JSON array"), "got: {hint}");
568 assert!(hint.contains('['), "should show array brackets: {hint}");
569 }
570
571 #[test]
572 fn output_format_hint_bool_says_true_false() {
573 let hint = FieldType::Bool
574 .output_format_hint()
575 .expect("bool has a hint");
576 assert!(
577 hint.contains("true") && hint.contains("false"),
578 "got: {hint}"
579 );
580 }
581
582 #[test]
583 fn output_format_hint_enum_lists_variants() {
584 let hint = FieldType::Enum(vec!["yes".into(), "no".into()])
585 .output_format_hint()
586 .expect("enum has a hint");
587 assert!(hint.contains("yes") && hint.contains("no"), "got: {hint}");
588 }
589
590 #[test]
591 fn output_format_hint_nullable_mentions_null() {
592 let hint = FieldType::Nullable(Box::new(FieldType::List(Box::new(FieldType::String))))
593 .output_format_hint()
594 .expect("nullable has a hint");
595 assert!(hint.contains("null"), "got: {hint}");
596 }
597
598 #[test]
599 fn output_format_hint_object_and_map_say_json_object() {
600 let obj = FieldType::Object(vec![ObjectField {
601 name: "x".into(),
602 description: String::new(),
603 field_type: FieldType::Int,
604 }]);
605 assert!(
606 obj.output_format_hint()
607 .expect("object hint")
608 .contains("JSON object")
609 );
610 let map = FieldType::Map(Box::new(FieldType::Int));
611 assert!(
612 map.output_format_hint()
613 .expect("map hint")
614 .contains("JSON object")
615 );
616 }
617
618 fn variant_arm_obj(name: &str, field_type: FieldType) -> VariantArm {
621 VariantArm {
622 description: format!("arm: {name}"),
623 field_type,
624 }
625 }
626
627 #[test]
628 fn type_label_oneof_tagged_names_discriminator_and_tags() {
629 let ft = FieldType::OneOf {
630 arms: vec![
631 variant_arm_obj("a", FieldType::Object(vec![])),
632 variant_arm_obj("b", FieldType::Object(vec![])),
633 ],
634 discriminator: Some(OneOfDiscriminator {
635 property: "kind".into(),
636 tags: vec!["a".into(), "b".into()],
637 }),
638 };
639 let label = ft.type_label();
640 assert!(label.contains("oneof"), "got: {label}");
641 assert!(label.contains("kind"), "got: {label}");
642 assert!(label.contains('a'), "got: {label}");
643 assert!(label.contains('b'), "got: {label}");
644 }
645
646 #[test]
647 fn type_label_oneof_untagged_lists_arm_labels() {
648 let ft = FieldType::OneOf {
649 arms: vec![
650 variant_arm_obj("int", FieldType::Int),
651 variant_arm_obj("str", FieldType::String),
652 ],
653 discriminator: None,
654 };
655 let label = ft.type_label();
656 assert!(label.starts_with("oneof["), "got: {label}");
657 assert!(label.contains("int"), "got: {label}");
658 assert!(label.contains("str"), "got: {label}");
659 }
660
661 #[test]
662 fn type_label_anyof_lists_arm_labels() {
663 let ft = FieldType::AnyOf {
664 arms: vec![
665 variant_arm_obj("int", FieldType::Int),
666 variant_arm_obj("str", FieldType::String),
667 ],
668 };
669 let label = ft.type_label();
670 assert!(label.starts_with("anyof["), "got: {label}");
671 assert!(label.contains("int"), "got: {label}");
672 assert!(label.contains("str"), "got: {label}");
673 }
674
675 #[test]
676 fn output_format_hint_oneof_tagged_points_to_variant_shapes() {
677 let ft = FieldType::OneOf {
678 arms: vec![variant_arm_obj("a", FieldType::Object(vec![]))],
679 discriminator: Some(OneOfDiscriminator {
680 property: "toolName".into(),
681 tags: vec!["a".into()],
682 }),
683 };
684 let hint = ft.output_format_hint().expect("hint");
685 assert!(hint.contains("toolName"), "got: {hint}");
686 assert!(hint.contains("Variant shapes"), "got: {hint}");
687 }
688
689 #[test]
690 fn output_format_hint_oneof_untagged_points_to_variant_shapes() {
691 let ft = FieldType::OneOf {
692 arms: vec![variant_arm_obj("int", FieldType::Int)],
693 discriminator: None,
694 };
695 let hint = ft.output_format_hint().expect("hint");
696 assert!(hint.contains("exactly one"), "got: {hint}");
697 assert!(hint.contains("Variant shapes"), "got: {hint}");
698 }
699
700 #[test]
701 fn output_format_hint_anyof_mentions_first_match() {
702 let ft = FieldType::AnyOf {
703 arms: vec![variant_arm_obj("int", FieldType::Int)],
704 };
705 let hint = ft.output_format_hint().expect("hint");
706 assert!(hint.contains("first match"), "got: {hint}");
707 }
708
709 #[test]
710 fn type_label_composes_oneof_inside_list() {
711 let ft = FieldType::List(Box::new(FieldType::OneOf {
712 arms: vec![
713 variant_arm_obj("a", FieldType::Object(vec![])),
714 variant_arm_obj("b", FieldType::Object(vec![])),
715 ],
716 discriminator: Some(OneOfDiscriminator {
717 property: "kind".into(),
718 tags: vec!["a".into(), "b".into()],
719 }),
720 }));
721 let label = ft.type_label();
722 assert!(label.starts_with("list[oneof["), "got: {label}");
723 assert!(label.contains("kind"), "got: {label}");
724 }
725
726 #[test]
727 fn field_type_oneof_serde_round_trip() {
728 let ft = FieldType::OneOf {
729 arms: vec![
730 variant_arm_obj("a", FieldType::Object(vec![])),
731 variant_arm_obj("b", FieldType::Object(vec![])),
732 ],
733 discriminator: Some(OneOfDiscriminator {
734 property: "kind".into(),
735 tags: vec!["a".into(), "b".into()],
736 }),
737 };
738 let json = serde_json::to_string(&ft).unwrap();
739 let restored: FieldType = serde_json::from_str(&json).unwrap();
740 assert_eq!(ft, restored);
741 }
742
743 #[test]
744 fn field_type_anyof_serde_round_trip() {
745 let ft = FieldType::AnyOf {
746 arms: vec![
747 variant_arm_obj("int", FieldType::Int),
748 variant_arm_obj("str", FieldType::String),
749 ],
750 };
751 let json = serde_json::to_string(&ft).unwrap();
752 let restored: FieldType = serde_json::from_str(&json).unwrap();
753 assert_eq!(ft, restored);
754 }
755
756 #[test]
757 fn field_value_variant_serde_round_trip() {
758 let value = FieldValue::Variant {
759 arm_index: 1,
760 value: Box::new(FieldValue::Object(BTreeMap::from([(
761 "toolName".into(),
762 FieldValue::Str("ranked_items".into()),
763 )]))),
764 };
765 let json = serde_json::to_string(&value).unwrap();
766 let restored: FieldValue = serde_json::from_str(&json).unwrap();
767 assert_eq!(value, restored);
768 }
769
770 #[test]
771 fn field_value_object_still_round_trips_with_variant_in_lattice() {
772 let value = FieldValue::Object(BTreeMap::from([
776 ("name".into(), FieldValue::Str("Alice".into())),
777 ("age".into(), FieldValue::Int(30)),
778 ("active".into(), FieldValue::Bool(true)),
779 ]));
780 let json = serde_json::to_string(&value).unwrap();
781 let restored: FieldValue = serde_json::from_str(&json).unwrap();
782 assert_eq!(value, restored);
783 }
784}