1use serde::{Deserialize, Serialize};
12
13use crate::{
14 error::{PredictError, Result},
15 field::{FieldDef, FieldKind, FieldType, OneOfDiscriminator, VariantArm},
16};
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct Signature {
23 instructions: String,
24 fields: Vec<FieldDef>,
25}
26
27impl Signature {
28 pub fn builder(instructions: impl Into<String>) -> SignatureBuilder {
30 SignatureBuilder {
31 instructions: instructions.into(),
32 fields: Vec::new(),
33 }
34 }
35
36 #[must_use]
38 pub fn instructions(&self) -> &str {
39 &self.instructions
40 }
41
42 pub fn input_fields(&self) -> impl Iterator<Item = &FieldDef> {
44 self.fields.iter().filter(|f| f.kind == FieldKind::Input)
45 }
46
47 pub fn output_fields(&self) -> impl Iterator<Item = &FieldDef> {
49 self.fields.iter().filter(|f| f.kind == FieldKind::Output)
50 }
51
52 #[must_use]
54 pub fn fields(&self) -> &[FieldDef] {
55 &self.fields
56 }
57
58 pub fn dump_state(&self) -> Result<String> {
65 serde_json::to_string(self).map_err(Into::into)
66 }
67
68 pub fn set_instructions(&mut self, instructions: impl Into<String>) {
70 self.instructions = instructions.into();
71 }
72
73 #[must_use]
78 pub fn with_prepended_output(&self, field: FieldDef) -> Self {
79 let mut new_fields = Vec::with_capacity(self.fields.len() + 1);
80 let mut inserted = false;
81 for f in &self.fields {
82 if f.kind == FieldKind::Output && !inserted {
83 new_fields.push(FieldDef {
84 kind: FieldKind::Output,
85 ..field.clone()
86 });
87 inserted = true;
88 }
89 new_fields.push(f.clone());
90 }
91 if !inserted {
92 new_fields.push(FieldDef {
93 kind: FieldKind::Output,
94 ..field
95 });
96 }
97 Self {
98 instructions: self.instructions.clone(),
99 fields: new_fields,
100 }
101 }
102
103 pub fn load_state(json: &str) -> Result<Self> {
110 let sig: Self = serde_json::from_str(json)?;
111 if sig.input_fields().next().is_none() {
113 return Err(PredictError::invalid_signature(
114 "signature must have at least one input field",
115 ));
116 }
117 if sig.output_fields().next().is_none() {
118 return Err(PredictError::invalid_signature(
119 "signature must have at least one output field",
120 ));
121 }
122 Ok(sig)
123 }
124}
125
126pub struct SignatureBuilder {
131 instructions: String,
132 fields: Vec<FieldDef>,
133}
134
135impl SignatureBuilder {
136 #[must_use]
139 pub fn input(mut self, mut def: FieldDef) -> Self {
140 def.kind = FieldKind::Input;
141 self.fields.push(def);
142 self
143 }
144
145 #[must_use]
148 pub fn output(mut self, mut def: FieldDef) -> Self {
149 def.kind = FieldKind::Output;
150 self.fields.push(def);
151 self
152 }
153
154 pub fn build(self) -> Result<Signature> {
175 let has_input = self.fields.iter().any(|f| f.kind == FieldKind::Input);
176 let has_output = self.fields.iter().any(|f| f.kind == FieldKind::Output);
177
178 if !has_input {
179 return Err(PredictError::invalid_signature(
180 "signature must have at least one input field",
181 ));
182 }
183 if !has_output {
184 return Err(PredictError::invalid_signature(
185 "signature must have at least one output field",
186 ));
187 }
188
189 for field in &self.fields {
190 validate_variant_shapes(&field.name, &field.field_type)?;
191 }
192
193 Ok(Signature {
194 instructions: self.instructions,
195 fields: self.fields,
196 })
197 }
198}
199
200fn validate_variant_shapes(path: &str, ft: &FieldType) -> Result<()> {
204 match ft {
205 FieldType::OneOf {
206 arms,
207 discriminator,
208 } => {
209 validate_variant_arms(path, arms, "OneOf")?;
210 if let Some(disc) = discriminator {
211 validate_one_of_discriminator(path, arms, disc)?;
212 }
213 for (i, arm) in arms.iter().enumerate() {
214 let arm_path = format!("{path}#arm{i}");
215 validate_variant_shapes(&arm_path, &arm.field_type)?;
216 }
217 Ok(())
218 }
219 FieldType::AnyOf { arms } => {
220 validate_variant_arms(path, arms, "AnyOf")?;
221 for (i, arm) in arms.iter().enumerate() {
222 let arm_path = format!("{path}#arm{i}");
223 validate_variant_shapes(&arm_path, &arm.field_type)?;
224 }
225 Ok(())
226 }
227 FieldType::List(inner) | FieldType::Nullable(inner) | FieldType::Map(inner) => {
228 validate_variant_shapes(path, inner)
229 }
230 FieldType::Object(fields) => {
231 for f in fields {
232 let nested = format!("{path}.{}", f.name);
233 validate_variant_shapes(&nested, &f.field_type)?;
234 }
235 Ok(())
236 }
237 FieldType::String
238 | FieldType::Int
239 | FieldType::Float
240 | FieldType::Bool
241 | FieldType::Enum(_)
242 | FieldType::Media { .. } => Ok(()),
243 }
244}
245
246fn validate_variant_arms(path: &str, arms: &[VariantArm], kind: &str) -> Result<()> {
247 if arms.is_empty() {
248 return Err(PredictError::invalid_signature(format!(
249 "{kind} field at `{path}` must declare at least one arm"
250 )));
251 }
252 Ok(())
253}
254
255fn validate_one_of_discriminator(
260 path: &str,
261 arms: &[VariantArm],
262 discriminator: &OneOfDiscriminator,
263) -> Result<()> {
264 if discriminator.tags.len() != arms.len() {
265 return Err(PredictError::invalid_signature(format!(
266 "OneOf at `{path}` discriminator has {} tags but {} arms; the \
267 vectors must be parallel",
268 discriminator.tags.len(),
269 arms.len()
270 )));
271 }
272
273 let mut seen = std::collections::HashSet::with_capacity(discriminator.tags.len());
274 for tag in &discriminator.tags {
275 if !seen.insert(tag.as_str()) {
276 return Err(PredictError::invalid_signature(format!(
277 "OneOf at `{path}` discriminator tag `{tag}` appears more than \
278 once; tags must be unique"
279 )));
280 }
281 }
282
283 for (i, arm) in arms.iter().enumerate() {
284 let arm_path = format!("{path}#{tag}", tag = discriminator.tags[i]);
285 validate_arm_carries_discriminator(
286 &arm_path,
287 &arm.field_type,
288 &discriminator.property,
289 &discriminator.tags[i],
290 )?;
291 }
292 Ok(())
293}
294
295fn validate_arm_carries_discriminator(
299 arm_path: &str,
300 arm_type: &FieldType,
301 property: &str,
302 tag: &str,
303) -> Result<()> {
304 let inner = match arm_type {
305 FieldType::Nullable(inner) => inner.as_ref(),
306 other => other,
307 };
308 let FieldType::Object(fields) = inner else {
309 return Err(PredictError::invalid_signature(format!(
310 "tagged OneOf arm at `{arm_path}` must be an Object (or \
311 Nullable<Object>); got `{}`",
312 arm_type.type_label()
313 )));
314 };
315 let prop = fields.iter().find(|f| f.name == property).ok_or_else(|| {
316 PredictError::invalid_signature(format!(
317 "tagged OneOf arm at `{arm_path}` is missing the discriminator \
318 property `{property}`"
319 ))
320 })?;
321 match &prop.field_type {
322 FieldType::Enum(variants) if variants.iter().any(|v| v == tag) => Ok(()),
323 other => Err(PredictError::invalid_signature(format!(
324 "tagged OneOf arm at `{arm_path}` declares discriminator \
325 property `{property}` as `{}`, but the discriminator's tag \
326 `{tag}` requires an Enum variant containing it (typical: \
327 single-element Enum from a const-restricted schema)",
328 other.type_label()
329 ))),
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 use crate::field::FieldType;
337
338 fn simple_signature() -> Signature {
339 Signature::builder("Answer the question.")
340 .input(FieldDef::input(
341 "question",
342 FieldType::String,
343 "The user question",
344 ))
345 .output(FieldDef::output("answer", FieldType::String, "The answer"))
346 .build()
347 .unwrap()
348 }
349
350 #[test]
351 fn builder_produces_valid_signature() {
352 let sig = simple_signature();
353 assert_eq!(sig.instructions(), "Answer the question.");
354 assert_eq!(sig.input_fields().count(), 1);
355 assert_eq!(sig.output_fields().count(), 1);
356 }
357
358 #[test]
359 fn builder_no_inputs_fails() {
360 let result = Signature::builder("test")
361 .output(FieldDef::output("answer", FieldType::String, "answer"))
362 .build();
363 assert!(result.is_err());
364 let err = result.unwrap_err().to_string();
365 assert!(err.contains("input"));
366 }
367
368 #[test]
369 fn builder_no_outputs_fails() {
370 let result = Signature::builder("test")
371 .input(FieldDef::input("question", FieldType::String, "question"))
372 .build();
373 assert!(result.is_err());
374 let err = result.unwrap_err().to_string();
375 assert!(err.contains("output"));
376 }
377
378 #[test]
379 fn field_ordering_preserved() {
380 let sig = Signature::builder("test")
381 .input(FieldDef::input("a", FieldType::String, "first"))
382 .input(FieldDef::input("b", FieldType::Int, "second"))
383 .output(FieldDef::output("x", FieldType::String, "third"))
384 .output(FieldDef::output("y", FieldType::Bool, "fourth"))
385 .build()
386 .unwrap();
387
388 let input_names: Vec<_> = sig.input_fields().map(|f| f.name.as_str()).collect();
389 assert_eq!(input_names, vec!["a", "b"]);
390
391 let output_names: Vec<_> = sig.output_fields().map(|f| f.name.as_str()).collect();
392 assert_eq!(output_names, vec!["x", "y"]);
393 }
394
395 #[test]
396 fn dump_load_round_trip() {
397 let sig = Signature::builder("Classify sentiment.")
398 .input(FieldDef::input("text", FieldType::String, "Input text"))
399 .output(FieldDef::output(
400 "sentiment",
401 FieldType::Enum(vec!["positive".into(), "negative".into(), "neutral".into()]),
402 "The sentiment",
403 ))
404 .build()
405 .unwrap();
406
407 let json = sig.dump_state().unwrap();
408 let restored = Signature::load_state(&json).unwrap();
409
410 assert_eq!(sig.instructions(), restored.instructions());
411 assert_eq!(sig.fields().len(), restored.fields().len());
412 for (a, b) in sig.fields().iter().zip(restored.fields().iter()) {
413 assert_eq!(a, b);
414 }
415 }
416
417 #[test]
418 fn dump_load_with_nested_types() {
419 let sig = Signature::builder("Extract info.")
420 .input(FieldDef::input("doc", FieldType::String, "The document"))
421 .output(FieldDef::output(
422 "entities",
423 FieldType::List(Box::new(FieldType::Object(vec![
424 crate::field::ObjectField {
425 name: "name".into(),
426 description: "Entity name".into(),
427 field_type: FieldType::String,
428 },
429 crate::field::ObjectField {
430 name: "type".into(),
431 description: "Entity type".into(),
432 field_type: FieldType::String,
433 },
434 ]))),
435 "Extracted entities",
436 ))
437 .build()
438 .unwrap();
439
440 let json = sig.dump_state().unwrap();
441 let restored = Signature::load_state(&json).unwrap();
442 assert_eq!(sig.fields(), restored.fields());
443 }
444
445 use crate::field::{ObjectField, OneOfDiscriminator, VariantArm};
457
458 fn tagged_oneof_object_arm(tag: &str, extra_field: (&str, FieldType)) -> VariantArm {
459 VariantArm {
460 description: tag.into(),
461 field_type: FieldType::Object(vec![
462 ObjectField {
463 name: "kind".into(),
464 description: String::new(),
465 field_type: FieldType::Enum(vec![tag.into()]),
466 },
467 ObjectField {
468 name: extra_field.0.into(),
469 description: String::new(),
470 field_type: extra_field.1,
471 },
472 ]),
473 }
474 }
475
476 #[test]
477 fn build_rejects_oneof_with_empty_arms() {
478 let result = Signature::builder("inst")
479 .input(FieldDef::input("q", FieldType::String, ""))
480 .output(FieldDef::output(
481 "out",
482 FieldType::OneOf {
483 arms: vec![],
484 discriminator: None,
485 },
486 "",
487 ))
488 .build();
489 let err = result.unwrap_err().to_string();
490 assert!(err.contains("at least one arm"), "got: {err}");
491 assert!(err.contains("OneOf"), "got: {err}");
492 }
493
494 #[test]
495 fn build_rejects_anyof_with_empty_arms() {
496 let result = Signature::builder("inst")
497 .input(FieldDef::input("q", FieldType::String, ""))
498 .output(FieldDef::output(
499 "out",
500 FieldType::AnyOf { arms: vec![] },
501 "",
502 ))
503 .build();
504 let err = result.unwrap_err().to_string();
505 assert!(err.contains("at least one arm"), "got: {err}");
506 assert!(err.contains("AnyOf"), "got: {err}");
507 }
508
509 #[test]
510 fn build_rejects_discriminator_with_mismatched_tag_count() {
511 let result = Signature::builder("inst")
512 .input(FieldDef::input("q", FieldType::String, ""))
513 .output(FieldDef::output(
514 "out",
515 FieldType::OneOf {
516 arms: vec![
517 tagged_oneof_object_arm("a", ("x", FieldType::Int)),
518 tagged_oneof_object_arm("b", ("y", FieldType::String)),
519 ],
520 discriminator: Some(OneOfDiscriminator {
521 property: "kind".into(),
522 tags: vec!["a".into()],
523 }),
524 },
525 "",
526 ))
527 .build();
528 let err = result.unwrap_err().to_string();
529 assert!(err.contains("1 tags but 2 arms"), "got: {err}");
530 assert!(err.contains("parallel"), "got: {err}");
531 }
532
533 #[test]
534 fn build_rejects_duplicate_discriminator_tags() {
535 let result = Signature::builder("inst")
536 .input(FieldDef::input("q", FieldType::String, ""))
537 .output(FieldDef::output(
538 "out",
539 FieldType::OneOf {
540 arms: vec![
541 tagged_oneof_object_arm("dup", ("x", FieldType::Int)),
542 tagged_oneof_object_arm("dup", ("y", FieldType::String)),
543 ],
544 discriminator: Some(OneOfDiscriminator {
545 property: "kind".into(),
546 tags: vec!["dup".into(), "dup".into()],
547 }),
548 },
549 "",
550 ))
551 .build();
552 let err = result.unwrap_err().to_string();
553 assert!(err.contains("dup"), "got: {err}");
554 assert!(err.contains("unique"), "got: {err}");
555 }
556
557 #[test]
558 fn build_rejects_tagged_arm_that_is_not_an_object() {
559 let result = Signature::builder("inst")
560 .input(FieldDef::input("q", FieldType::String, ""))
561 .output(FieldDef::output(
562 "out",
563 FieldType::OneOf {
564 arms: vec![VariantArm {
565 description: "scalar arm".into(),
566 field_type: FieldType::Int,
567 }],
568 discriminator: Some(OneOfDiscriminator {
569 property: "kind".into(),
570 tags: vec!["a".into()],
571 }),
572 },
573 "",
574 ))
575 .build();
576 let err = result.unwrap_err().to_string();
577 assert!(err.contains("must be an Object"), "got: {err}");
578 }
579
580 #[test]
581 fn build_rejects_tagged_arm_missing_discriminator_property() {
582 let result = Signature::builder("inst")
583 .input(FieldDef::input("q", FieldType::String, ""))
584 .output(FieldDef::output(
585 "out",
586 FieldType::OneOf {
587 arms: vec![VariantArm {
588 description: "no discriminator field".into(),
589 field_type: FieldType::Object(vec![ObjectField {
590 name: "x".into(),
591 description: String::new(),
592 field_type: FieldType::Int,
593 }]),
594 }],
595 discriminator: Some(OneOfDiscriminator {
596 property: "kind".into(),
597 tags: vec!["a".into()],
598 }),
599 },
600 "",
601 ))
602 .build();
603 let err = result.unwrap_err().to_string();
604 assert!(
605 err.contains("missing the discriminator property"),
606 "got: {err}"
607 );
608 assert!(err.contains("kind"), "got: {err}");
609 }
610
611 #[test]
612 fn build_rejects_tagged_arm_with_wrong_enum_for_discriminator() {
613 let result = Signature::builder("inst")
614 .input(FieldDef::input("q", FieldType::String, ""))
615 .output(FieldDef::output(
616 "out",
617 FieldType::OneOf {
618 arms: vec![VariantArm {
619 description: "wrong tag enum".into(),
620 field_type: FieldType::Object(vec![
621 ObjectField {
622 name: "kind".into(),
623 description: String::new(),
624 field_type: FieldType::Enum(vec!["other".into()]),
625 },
626 ObjectField {
627 name: "x".into(),
628 description: String::new(),
629 field_type: FieldType::Int,
630 },
631 ]),
632 }],
633 discriminator: Some(OneOfDiscriminator {
634 property: "kind".into(),
635 tags: vec!["a".into()],
636 }),
637 },
638 "",
639 ))
640 .build();
641 let err = result.unwrap_err().to_string();
642 assert!(err.contains("Enum variant containing"), "got: {err}");
643 }
644
645 #[test]
646 fn build_accepts_well_formed_tagged_oneof() {
647 Signature::builder("inst")
648 .input(FieldDef::input("q", FieldType::String, ""))
649 .output(FieldDef::output(
650 "out",
651 FieldType::OneOf {
652 arms: vec![
653 tagged_oneof_object_arm("a", ("x", FieldType::Int)),
654 tagged_oneof_object_arm("b", ("y", FieldType::String)),
655 ],
656 discriminator: Some(OneOfDiscriminator {
657 property: "kind".into(),
658 tags: vec!["a".into(), "b".into()],
659 }),
660 },
661 "",
662 ))
663 .build()
664 .expect("well-formed tagged OneOf must build");
665 }
666
667 #[test]
668 fn build_validates_variant_nested_inside_list_and_nullable() {
669 let result = Signature::builder("inst")
672 .input(FieldDef::input("q", FieldType::String, ""))
673 .output(FieldDef::output(
674 "out",
675 FieldType::List(Box::new(FieldType::Nullable(Box::new(FieldType::OneOf {
676 arms: vec![],
677 discriminator: None,
678 })))),
679 "",
680 ))
681 .build();
682 let err = result.unwrap_err().to_string();
683 assert!(err.contains("at least one arm"), "got: {err}");
684 }
685
686 #[test]
687 fn load_state_validates_fields() {
688 let json = serde_json::json!({
690 "instructions": "test",
691 "fields": [{
692 "name": "answer",
693 "description": "answer",
694 "field_type": {"kind": "string"},
695 "kind": "output"
696 }]
697 });
698 let result = Signature::load_state(&json.to_string());
699 assert!(result.is_err());
700 }
701
702 #[test]
703 fn builder_forces_kind() {
704 let wrong_kind = FieldDef::output("question", FieldType::String, "A question");
706 let sig = Signature::builder("test")
707 .input(wrong_kind) .output(FieldDef::output("answer", FieldType::String, "answer"))
709 .build()
710 .unwrap();
711
712 let input = sig.input_fields().next().unwrap();
713 assert_eq!(input.kind, FieldKind::Input);
714 }
715}