Skip to main content

foundation_models/
schema.rs

1//! JSON-schema and dynamic schema builders for structured generation.
2
3use core::ffi::{c_char, c_void};
4use std::collections::BTreeMap;
5use std::ffi::CString;
6use std::sync::mpsc;
7
8use serde_json::{json, Map, Value};
9
10use crate::content::{FromGeneratedContent, ToGeneratedContent};
11use crate::error::FMError;
12use crate::ffi;
13
14/// A validated FoundationModels generation schema encoded as JSON Schema.
15#[derive(Debug, Clone)]
16pub struct GenerationSchema {
17    json_schema: String,
18    bridge_request_json: Option<String>,
19}
20
21impl PartialEq for GenerationSchema {
22    fn eq(&self, other: &Self) -> bool {
23        self.json_schema == other.json_schema
24    }
25}
26
27impl Eq for GenerationSchema {}
28
29impl GenerationSchema {
30    /// Validate and store a JSON schema definition.
31    ///
32    /// # Errors
33    ///
34    /// Returns an [`FMError`] if Apple's `GenerationSchema` rejects the schema.
35    pub fn from_json_schema(json_schema: impl Into<String>) -> Result<Self, FMError> {
36        let json_schema = json_schema.into();
37        let schema_c = CString::new(json_schema.as_str()).map_err(|error| {
38            FMError::InvalidArgument(format!(
39                "schema JSON contains an interior NUL byte: {error}"
40            ))
41        })?;
42        let mut error_ptr: *mut c_char = core::ptr::null_mut();
43        let status =
44            unsafe { ffi::fm_generation_schema_validate_json(schema_c.as_ptr(), &mut error_ptr) };
45        if status != ffi::status::OK {
46            return Err(crate::error::from_swift(status, error_ptr));
47        }
48        Ok(Self {
49            json_schema,
50            bridge_request_json: None,
51        })
52    }
53
54    /// Create a schema from a dynamic root schema plus optional dependencies.
55    ///
56    /// # Errors
57    ///
58    /// Returns an [`FMError`] if the dynamic schema is invalid.
59    pub fn from_dynamic(
60        root: DynamicGenerationSchema,
61        dependencies: impl IntoIterator<Item = DynamicGenerationSchema>,
62    ) -> Result<Self, FMError> {
63        let request = json!({
64            "root": root.to_json_value(),
65            "dependencies": dependencies
66                .into_iter()
67                .map(|schema| schema.to_json_value())
68                .collect::<Vec<_>>(),
69        });
70        let request_json = serde_json::to_string(&request).map_err(|error| {
71            FMError::InvalidArgument(format!(
72                "dynamic schema request is not JSON-serializable: {error}"
73            ))
74        })?;
75        let request_c = CString::new(request_json.as_str()).map_err(|error| {
76            FMError::InvalidArgument(format!("dynamic schema JSON contains NUL byte: {error}"))
77        })?;
78        let (tx, rx) = mpsc::channel();
79        let tx_box: Box<mpsc::Sender<Result<String, FMError>>> = Box::new(tx);
80        let context = Box::into_raw(tx_box).cast::<c_void>();
81        unsafe {
82            ffi::fm_generation_schema_compile_json(
83                request_c.as_ptr(),
84                context,
85                schema_callback_trampoline,
86            );
87        }
88        let json_schema = rx.recv().map_err(|_| FMError::Unknown {
89            code: ffi::status::UNKNOWN,
90            message: "Swift bridge dropped the schema callback channel".into(),
91        })??;
92        Ok(Self {
93            json_schema,
94            bridge_request_json: Some(request_json),
95        })
96    }
97
98    /// Build an object schema with FoundationModels' typed `GenerationSchema` initializer.
99    ///
100    /// This Rust wrapper uses `GeneratedContent` as the root Swift type and
101    /// mirrors the SDK's property-based schema builder.
102    ///
103    /// # Errors
104    ///
105    /// Returns an [`FMError`] if the typed schema is invalid.
106    pub fn new(
107        description: Option<String>,
108        properties: impl IntoIterator<Item = (impl Into<String>, DynamicGenerationProperty)>,
109    ) -> Result<Self, FMError> {
110        Self::new_with_nil_repr(description, false, properties)
111    }
112
113    /// Build an object schema with FoundationModels' typed `GenerationSchema` initializer,
114    /// optionally requiring explicit `null` values for optional properties.
115    ///
116    /// # Errors
117    ///
118    /// Returns an [`FMError`] if the typed schema is invalid or the current
119    /// runtime does not support explicit null representation.
120    pub fn new_with_nil_repr(
121        description: Option<String>,
122        represent_nil_explicitly_in_generated_content: bool,
123        properties: impl IntoIterator<Item = (impl Into<String>, DynamicGenerationProperty)>,
124    ) -> Result<Self, FMError> {
125        let request = json!({
126            "description": description,
127            "representNilExplicitlyInGeneratedContent": represent_nil_explicitly_in_generated_content,
128            "properties": properties
129                .into_iter()
130                .map(|(name, property)| {
131                    let DynamicGenerationProperty {
132                        schema,
133                        description,
134                        optional,
135                    } = property;
136                    json!({
137                        "name": name.into(),
138                        "description": description,
139                        "optional": optional,
140                        "schema": schema.to_json_value(),
141                    })
142                })
143                .collect::<Vec<_>>(),
144        });
145        let request_json = serde_json::to_string(&request).map_err(|error| {
146            FMError::InvalidArgument(format!(
147                "typed schema request is not JSON-serializable: {error}"
148            ))
149        })?;
150        let request_c = CString::new(request_json.as_str()).map_err(|error| {
151            FMError::InvalidArgument(format!("typed schema JSON contains NUL byte: {error}"))
152        })?;
153        let (tx, rx) = mpsc::channel();
154        let tx_box: Box<mpsc::Sender<Result<String, FMError>>> = Box::new(tx);
155        let context = Box::into_raw(tx_box).cast::<c_void>();
156        unsafe {
157            ffi::fm_generation_schema_create_typed_json(
158                request_c.as_ptr(),
159                context,
160                schema_callback_trampoline,
161            );
162        }
163        let json_schema = rx.recv().map_err(|_| FMError::Unknown {
164            code: ffi::status::UNKNOWN,
165            message: "Swift bridge dropped the typed schema callback channel".into(),
166        })??;
167        Ok(Self {
168            json_schema,
169            bridge_request_json: Some(request_json),
170        })
171    }
172
173    /// The JSON Schema payload accepted by Apple's `GenerationSchema`.
174    #[must_use]
175    pub fn json_schema(&self) -> &str {
176        &self.json_schema
177    }
178
179    /// Best-effort name (the schema's `title`).
180    #[must_use]
181    pub fn name(&self) -> Option<String> {
182        let value: Value = serde_json::from_str(&self.json_schema).ok()?;
183        value.get("title")?.as_str().map(ToOwned::to_owned)
184    }
185
186    pub(crate) fn bridge_request_json(&self) -> &str {
187        self.bridge_request_json
188            .as_deref()
189            .unwrap_or_else(|| self.json_schema())
190    }
191
192    pub(crate) fn effective_include_schema_in_prompt(&self, requested: bool) -> bool {
193        requested && !self.uses_explicit_null_representation()
194    }
195
196    fn uses_explicit_null_representation(&self) -> bool {
197        self.bridge_request_json
198            .as_ref()
199            .is_some_and(|json| json.contains("\"representNilExplicitlyInGeneratedContent\":true"))
200            || self.json_schema.contains("\"type\":\"null\"")
201            || self.json_schema.contains("\"type\": \"null\"")
202    }
203
204    /// A JSON string schema.
205    #[must_use]
206    pub fn string() -> Self {
207        Self::from_json_schema_unchecked(r#"{"type":"string"}"#.into())
208    }
209
210    /// A JSON integer schema.
211    #[must_use]
212    pub fn integer() -> Self {
213        Self::from_json_schema_unchecked(r#"{"type":"integer"}"#.into())
214    }
215
216    /// A JSON number schema.
217    #[must_use]
218    pub fn number() -> Self {
219        Self::from_json_schema_unchecked(r#"{"type":"number"}"#.into())
220    }
221
222    /// A JSON boolean schema.
223    #[must_use]
224    pub fn boolean() -> Self {
225        Self::from_json_schema_unchecked(r#"{"type":"boolean"}"#.into())
226    }
227
228    /// A schema for arbitrary JSON (`GeneratedContent`).
229    #[must_use]
230    pub fn generated_content() -> Self {
231        Self::from_json_schema_unchecked(
232            r##"{"title":"GeneratedContent","description":"Any legal JSON","anyOf":[{"type":"object","additionalProperties":{"$ref":"#"}},{"type":"array","items":{"$ref":"#"}},{"type":"boolean"},{"type":"number"},{"type":"string"}]}"##.into(),
233        )
234    }
235
236    pub(crate) fn from_json_schema_unchecked(json_schema: String) -> Self {
237        Self {
238            json_schema,
239            bridge_request_json: None,
240        }
241    }
242}
243
244/// A dynamic FoundationModels schema description.
245#[derive(Debug, Clone, PartialEq)]
246pub enum DynamicGenerationSchema {
247    Object {
248        name: String,
249        description: Option<String>,
250        represent_nil_explicitly_in_generated_content: bool,
251        properties: BTreeMap<String, DynamicGenerationProperty>,
252    },
253    Array {
254        item: Box<DynamicGenerationSchema>,
255        minimum_elements: Option<usize>,
256        maximum_elements: Option<usize>,
257        guides: Vec<GenerationGuide>,
258    },
259    AnyOf {
260        name: String,
261        description: Option<String>,
262        choices: Vec<DynamicGenerationSchema>,
263    },
264    AnyOfStrings {
265        name: String,
266        description: Option<String>,
267        choices: Vec<String>,
268    },
269    String {
270        description: Option<String>,
271        guides: Vec<GenerationGuide>,
272    },
273    Integer {
274        description: Option<String>,
275        guides: Vec<GenerationGuide>,
276    },
277    Float {
278        description: Option<String>,
279        guides: Vec<GenerationGuide>,
280    },
281    Number {
282        description: Option<String>,
283        guides: Vec<GenerationGuide>,
284    },
285    Decimal {
286        description: Option<String>,
287        guides: Vec<GenerationGuide>,
288    },
289    Boolean {
290        description: Option<String>,
291    },
292    GeneratedContent {
293        description: Option<String>,
294    },
295    Reference {
296        name: String,
297    },
298    Null,
299}
300
301impl DynamicGenerationSchema {
302    /// Create an object schema.
303    #[must_use]
304    pub fn object(name: impl Into<String>) -> Self {
305        Self::Object {
306            name: name.into(),
307            description: None,
308            represent_nil_explicitly_in_generated_content: false,
309            properties: BTreeMap::new(),
310        }
311    }
312
313    /// Create an object schema that keeps optional properties as explicit `null`s.
314    #[must_use]
315    pub fn new_with_nil_repr(
316        name: impl Into<String>,
317        description: Option<String>,
318        represent_nil_explicitly_in_generated_content: bool,
319        properties: impl IntoIterator<Item = (impl Into<String>, DynamicGenerationProperty)>,
320    ) -> Self {
321        Self::Object {
322            name: name.into(),
323            description,
324            represent_nil_explicitly_in_generated_content,
325            properties: properties
326                .into_iter()
327                .map(|(name, property)| (name.into(), property))
328                .collect(),
329        }
330    }
331
332    /// The SDK's typed `DynamicGenerationSchema.null` marker.
333    pub const NULL: Self = Self::Null;
334
335    /// Create the SDK's typed `DynamicGenerationSchema.null` marker.
336    #[must_use]
337    pub const fn null() -> Self {
338        Self::Null
339    }
340
341    /// Create a string schema.
342    #[must_use]
343    pub fn string() -> Self {
344        Self::String {
345            description: None,
346            guides: Vec::new(),
347        }
348    }
349
350    /// Create an integer schema.
351    #[must_use]
352    pub fn integer() -> Self {
353        Self::Integer {
354            description: None,
355            guides: Vec::new(),
356        }
357    }
358
359    /// Create a floating-point schema.
360    #[must_use]
361    pub fn float() -> Self {
362        Self::Float {
363            description: None,
364            guides: Vec::new(),
365        }
366    }
367
368    /// Create a number schema.
369    #[must_use]
370    pub fn number() -> Self {
371        Self::Number {
372            description: None,
373            guides: Vec::new(),
374        }
375    }
376
377    /// Create a decimal schema.
378    #[must_use]
379    pub fn decimal() -> Self {
380        Self::Decimal {
381            description: None,
382            guides: Vec::new(),
383        }
384    }
385
386    /// Create a boolean schema.
387    #[must_use]
388    pub fn boolean() -> Self {
389        Self::Boolean { description: None }
390    }
391
392    /// Create an arbitrary-JSON schema.
393    #[must_use]
394    pub fn generated_content() -> Self {
395        Self::GeneratedContent { description: None }
396    }
397
398    /// Create an array schema.
399    #[must_use]
400    pub fn array_of(item: Self) -> Self {
401        Self::Array {
402            item: Box::new(item),
403            minimum_elements: None,
404            maximum_elements: None,
405            guides: Vec::new(),
406        }
407    }
408
409    /// Create a reference to a named dependency.
410    #[must_use]
411    pub fn reference(name: impl Into<String>) -> Self {
412        Self::Reference { name: name.into() }
413    }
414
415    /// Create a named union of schemas.
416    #[must_use]
417    pub fn any_of(name: impl Into<String>, choices: Vec<Self>) -> Self {
418        Self::AnyOf {
419            name: name.into(),
420            description: None,
421            choices,
422        }
423    }
424
425    /// Create a named union of constant string choices.
426    #[must_use]
427    pub fn any_of_strings(
428        name: impl Into<String>,
429        choices: impl IntoIterator<Item = impl Into<String>>,
430    ) -> Self {
431        Self::AnyOfStrings {
432            name: name.into(),
433            description: None,
434            choices: choices.into_iter().map(Into::into).collect(),
435        }
436    }
437
438    /// Attach a description.
439    #[must_use]
440    pub fn with_description(mut self, description: impl Into<String>) -> Self {
441        match &mut self {
442            Self::Object {
443                description: slot, ..
444            }
445            | Self::AnyOf {
446                description: slot, ..
447            }
448            | Self::AnyOfStrings {
449                description: slot, ..
450            }
451            | Self::String {
452                description: slot, ..
453            }
454            | Self::Integer {
455                description: slot, ..
456            }
457            | Self::Float {
458                description: slot, ..
459            }
460            | Self::Number {
461                description: slot, ..
462            }
463            | Self::Decimal {
464                description: slot, ..
465            }
466            | Self::Boolean { description: slot }
467            | Self::GeneratedContent { description: slot } => *slot = Some(description.into()),
468            Self::Array { .. } | Self::Reference { .. } | Self::Null => {}
469        }
470        self
471    }
472
473    /// Add a property to an object schema.
474    #[must_use]
475    pub fn with_property(
476        mut self,
477        name: impl Into<String>,
478        property: DynamicGenerationProperty,
479    ) -> Self {
480        if let Self::Object { properties, .. } = &mut self {
481            properties.insert(name.into(), property);
482        }
483        self
484    }
485
486    /// Set the array bounds.
487    #[must_use]
488    pub fn with_element_bounds(mut self, minimum: Option<usize>, maximum: Option<usize>) -> Self {
489        if let Self::Array {
490            minimum_elements,
491            maximum_elements,
492            ..
493        } = &mut self
494        {
495            *minimum_elements = minimum;
496            *maximum_elements = maximum;
497        }
498        self
499    }
500
501    /// Attach FoundationModels generation guides.
502    #[must_use]
503    pub fn with_guides(mut self, guides: impl IntoIterator<Item = GenerationGuide>) -> Self {
504        let guides: Vec<_> = guides.into_iter().collect();
505        match &mut self {
506            Self::String { guides: slot, .. }
507            | Self::Integer { guides: slot, .. }
508            | Self::Float { guides: slot, .. }
509            | Self::Number { guides: slot, .. }
510            | Self::Decimal { guides: slot, .. }
511            | Self::Array { guides: slot, .. } => *slot = guides,
512            Self::Object { .. }
513            | Self::AnyOf { .. }
514            | Self::AnyOfStrings { .. }
515            | Self::Boolean { .. }
516            | Self::GeneratedContent { .. }
517            | Self::Reference { .. }
518            | Self::Null => {}
519        }
520        self
521    }
522
523    fn to_json_value(&self) -> Value {
524        match self {
525            Self::Object {
526                name,
527                description,
528                represent_nil_explicitly_in_generated_content,
529                properties,
530            } => object_schema_json(
531                name,
532                description,
533                *represent_nil_explicitly_in_generated_content,
534                properties,
535            ),
536            Self::Array {
537                item,
538                minimum_elements,
539                maximum_elements,
540                guides,
541            } => array_schema_json(item, *minimum_elements, *maximum_elements, guides),
542            Self::AnyOf {
543                name,
544                description,
545                choices,
546            } => named_schema_json(
547                "any_of",
548                name,
549                description,
550                Value::Array(choices.iter().map(Self::to_json_value).collect()),
551            ),
552            Self::AnyOfStrings {
553                name,
554                description,
555                choices,
556            } => named_schema_json(
557                "any_of",
558                name,
559                description,
560                Value::Array(choices.iter().cloned().map(Value::String).collect()),
561            ),
562            Self::String {
563                description,
564                guides,
565            } => primitive_schema_json("string", description, guides),
566            Self::Integer {
567                description,
568                guides,
569            } => primitive_schema_json("integer", description, guides),
570            Self::Float {
571                description,
572                guides,
573            } => primitive_schema_json("float", description, guides),
574            Self::Number {
575                description,
576                guides,
577            } => primitive_schema_json("number", description, guides),
578            Self::Decimal {
579                description,
580                guides,
581            } => primitive_schema_json("decimal", description, guides),
582            Self::Boolean { description } => primitive_schema_json("boolean", description, &[]),
583            Self::GeneratedContent { description } => {
584                primitive_schema_json("generated_content", description, &[])
585            }
586            Self::Reference { name } => json!({ "$ref": name }),
587            Self::Null => json!({ "type": "null" }),
588        }
589    }
590}
591
592fn named_schema_json(
593    kind: &str,
594    name: &str,
595    description: &Option<String>,
596    choices: Value,
597) -> Value {
598    let mut map = Map::new();
599    map.insert("type".into(), Value::String(kind.into()));
600    map.insert("name".into(), Value::String(name.to_string()));
601    if let Some(description) = description {
602        map.insert("description".into(), Value::String(description.clone()));
603    }
604    map.insert("choices".into(), choices);
605    Value::Object(map)
606}
607
608fn object_schema_json(
609    name: &str,
610    description: &Option<String>,
611    represent_nil_explicitly_in_generated_content: bool,
612    properties: &BTreeMap<String, DynamicGenerationProperty>,
613) -> Value {
614    let property_map = properties
615        .iter()
616        .map(|(property_name, property)| (property_name.clone(), property.to_json_value()))
617        .collect::<Map<String, Value>>();
618    let mut map = Map::new();
619    map.insert("type".into(), Value::String("object".into()));
620    map.insert("name".into(), Value::String(name.to_string()));
621    if let Some(description) = description {
622        map.insert("description".into(), Value::String(description.clone()));
623    }
624    if represent_nil_explicitly_in_generated_content {
625        map.insert(
626            "representNilExplicitlyInGeneratedContent".into(),
627            Value::Bool(true),
628        );
629    }
630    map.insert("properties".into(), Value::Object(property_map));
631    Value::Object(map)
632}
633
634fn array_schema_json(
635    item: &DynamicGenerationSchema,
636    minimum_elements: Option<usize>,
637    maximum_elements: Option<usize>,
638    guides: &[GenerationGuide],
639) -> Value {
640    let mut map = Map::new();
641    map.insert("type".into(), Value::String("array".into()));
642    map.insert("items".into(), item.to_json_value());
643    if let Some(minimum_elements) = minimum_elements {
644        map.insert("min".into(), Value::from(minimum_elements));
645    }
646    if let Some(maximum_elements) = maximum_elements {
647        map.insert("max".into(), Value::from(maximum_elements));
648    }
649    if !guides.is_empty() {
650        map.insert(
651            "guides".into(),
652            Value::Array(guides.iter().map(GenerationGuide::to_json_value).collect()),
653        );
654    }
655    Value::Object(map)
656}
657
658/// A property in a dynamic object schema.
659#[derive(Debug, Clone, PartialEq)]
660pub struct DynamicGenerationProperty {
661    pub schema: DynamicGenerationSchema,
662    pub description: Option<String>,
663    pub optional: bool,
664}
665
666impl DynamicGenerationProperty {
667    /// Create a property from a nested schema.
668    #[must_use]
669    pub fn new(schema: DynamicGenerationSchema) -> Self {
670        Self {
671            schema,
672            description: None,
673            optional: false,
674        }
675    }
676
677    /// Mark the property as optional.
678    #[must_use]
679    pub const fn optional(mut self, optional: bool) -> Self {
680        self.optional = optional;
681        self
682    }
683
684    /// Attach a property description.
685    #[must_use]
686    pub fn with_description(mut self, description: impl Into<String>) -> Self {
687        self.description = Some(description.into());
688        self
689    }
690
691    fn to_json_value(&self) -> Value {
692        let mut value = self.schema.to_json_value();
693        if let Value::Object(map) = &mut value {
694            if let Some(description) = &self.description {
695                map.insert("description".into(), Value::String(description.clone()));
696            }
697            if self.optional {
698                map.insert("optional".into(), Value::Bool(true));
699            }
700        }
701        value
702    }
703}
704
705/// One of Apple's public `GenerationGuide` builders.
706#[derive(Debug, Clone, PartialEq)]
707pub enum GenerationGuide {
708    StringConstant(String),
709    StringAnyOf(Vec<String>),
710    StringPattern(String),
711    MinimumI64(i64),
712    MaximumI64(i64),
713    RangeI64(i64, i64),
714    MinimumF32(f32),
715    MaximumF32(f32),
716    RangeF32(f32, f32),
717    MinimumF64(f64),
718    MaximumF64(f64),
719    RangeF64(f64, f64),
720    MinimumDecimal(String),
721    MaximumDecimal(String),
722    RangeDecimal(String, String),
723    MinimumCount(usize),
724    MaximumCount(usize),
725    CountRange(usize, usize),
726    CountExact(usize),
727    Element(Box<GenerationGuide>),
728}
729
730impl GenerationGuide {
731    #[must_use]
732    pub fn string_constant(value: impl Into<String>) -> Self {
733        Self::StringConstant(value.into())
734    }
735
736    #[must_use]
737    pub fn string_any_of(values: impl IntoIterator<Item = impl Into<String>>) -> Self {
738        Self::StringAnyOf(values.into_iter().map(Into::into).collect())
739    }
740
741    #[must_use]
742    pub fn string_pattern(pattern: impl Into<String>) -> Self {
743        Self::StringPattern(pattern.into())
744    }
745
746    #[must_use]
747    pub const fn minimum_i64(value: i64) -> Self {
748        Self::MinimumI64(value)
749    }
750
751    #[must_use]
752    pub const fn maximum_i64(value: i64) -> Self {
753        Self::MaximumI64(value)
754    }
755
756    #[must_use]
757    pub const fn range_i64(minimum: i64, maximum: i64) -> Self {
758        Self::RangeI64(minimum, maximum)
759    }
760
761    #[must_use]
762    pub const fn minimum_f32(value: f32) -> Self {
763        Self::MinimumF32(value)
764    }
765
766    #[must_use]
767    pub const fn maximum_f32(value: f32) -> Self {
768        Self::MaximumF32(value)
769    }
770
771    #[must_use]
772    pub const fn range_f32(minimum: f32, maximum: f32) -> Self {
773        Self::RangeF32(minimum, maximum)
774    }
775
776    #[must_use]
777    pub const fn minimum_f64(value: f64) -> Self {
778        Self::MinimumF64(value)
779    }
780
781    #[must_use]
782    pub const fn maximum_f64(value: f64) -> Self {
783        Self::MaximumF64(value)
784    }
785
786    #[must_use]
787    pub const fn range_f64(minimum: f64, maximum: f64) -> Self {
788        Self::RangeF64(minimum, maximum)
789    }
790
791    #[must_use]
792    pub fn minimum_decimal(value: impl Into<String>) -> Self {
793        Self::MinimumDecimal(value.into())
794    }
795
796    #[must_use]
797    pub fn maximum_decimal(value: impl Into<String>) -> Self {
798        Self::MaximumDecimal(value.into())
799    }
800
801    #[must_use]
802    pub fn range_decimal(minimum: impl Into<String>, maximum: impl Into<String>) -> Self {
803        Self::RangeDecimal(minimum.into(), maximum.into())
804    }
805
806    #[must_use]
807    pub const fn minimum_count(count: usize) -> Self {
808        Self::MinimumCount(count)
809    }
810
811    #[must_use]
812    pub const fn maximum_count(count: usize) -> Self {
813        Self::MaximumCount(count)
814    }
815
816    #[must_use]
817    pub const fn count_range(minimum: usize, maximum: usize) -> Self {
818        Self::CountRange(minimum, maximum)
819    }
820
821    #[must_use]
822    pub const fn count(count: usize) -> Self {
823        Self::CountExact(count)
824    }
825
826    #[must_use]
827    pub fn element(guide: GenerationGuide) -> Self {
828        Self::Element(Box::new(guide))
829    }
830
831    fn to_json_value(&self) -> Value {
832        match self {
833            Self::StringConstant(value) => json!({ "kind": "constant", "value": value }),
834            Self::StringAnyOf(values) => json!({ "kind": "any_of", "values": values }),
835            Self::StringPattern(pattern) => json!({ "kind": "pattern", "pattern": pattern }),
836            Self::MinimumI64(value) => json!({ "kind": "minimum", "value": value }),
837            Self::MaximumI64(value) => json!({ "kind": "maximum", "value": value }),
838            Self::RangeI64(minimum, maximum) => {
839                json!({ "kind": "range", "min": minimum, "max": maximum })
840            }
841            Self::MinimumF32(value) => json!({ "kind": "minimum", "value": value }),
842            Self::MaximumF32(value) => json!({ "kind": "maximum", "value": value }),
843            Self::RangeF32(minimum, maximum) => {
844                json!({ "kind": "range", "min": minimum, "max": maximum })
845            }
846            Self::MinimumF64(value) => json!({ "kind": "minimum", "value": value }),
847            Self::MaximumF64(value) => json!({ "kind": "maximum", "value": value }),
848            Self::RangeF64(minimum, maximum) => {
849                json!({ "kind": "range", "min": minimum, "max": maximum })
850            }
851            Self::MinimumDecimal(value) => json!({ "kind": "minimum", "value": value }),
852            Self::MaximumDecimal(value) => json!({ "kind": "maximum", "value": value }),
853            Self::RangeDecimal(minimum, maximum) => {
854                json!({ "kind": "range", "min": minimum, "max": maximum })
855            }
856            Self::MinimumCount(count) => json!({ "kind": "minimum_count", "value": count }),
857            Self::MaximumCount(count) => json!({ "kind": "maximum_count", "value": count }),
858            Self::CountRange(minimum, maximum) => {
859                json!({ "kind": "count", "min": minimum, "max": maximum })
860            }
861            Self::CountExact(count) => json!({ "kind": "count", "value": count }),
862            Self::Element(guide) => json!({ "kind": "element", "guide": guide.to_json_value() }),
863        }
864    }
865}
866
867fn primitive_schema_json(
868    kind: &str,
869    description: &Option<String>,
870    guides: &[GenerationGuide],
871) -> Value {
872    let mut map = Map::new();
873    map.insert("type".into(), Value::String(kind.into()));
874    if let Some(description) = description {
875        map.insert("description".into(), Value::String(description.clone()));
876    }
877    if !guides.is_empty() {
878        map.insert(
879            "guides".into(),
880            Value::Array(guides.iter().map(GenerationGuide::to_json_value).collect()),
881        );
882    }
883    Value::Object(map)
884}
885
886/// Rust analogue of FoundationModels' `Generable` protocol.
887pub trait Generable: Sized + FromGeneratedContent + ToGeneratedContent {
888    /// Return the generation schema that describes `Self`.
889    fn generation_schema() -> Result<GenerationSchema, FMError>;
890}
891
892// SAFETY: `context` is a `Box<mpsc::Sender<...>>` raw pointer created by
893// `GenerationSchema::compile`. Swift calls this callback exactly once, so
894// there is no double-free risk. `response` and `error` are C strings owned
895// by the Swift bridge and only valid for this call.
896unsafe extern "C" fn schema_callback_trampoline(
897    context: *mut c_void,
898    response: *mut c_char,
899    error: *mut c_char,
900    status: i32,
901) {
902    let tx = Box::from_raw(context.cast::<mpsc::Sender<Result<String, FMError>>>());
903    let result = if status == ffi::status::OK && !response.is_null() {
904        let value = core::ffi::CStr::from_ptr(response)
905            .to_string_lossy()
906            .into_owned();
907        ffi::fm_string_free(response);
908        Ok(value)
909    } else {
910        Err(crate::error::from_swift(status, error))
911    };
912    let _ = tx.send(result);
913}