Skip to main content

gproxy_protocol/transform/generate/gemini_schema/
to_json.rs

1use crate::{
2    transform::{Converted, Report, TransformError},
3    wire::gemini::content::{Schema, SchemaType},
4};
5use serde_json::{Map, Value};
6
7/// Read only declared schema fields. Formal `default` and `example` JSON values
8/// retain all their members, including members named `rest`.
9pub fn to_json(input: &Schema) -> Result<Converted<Map<String, Value>>, TransformError> {
10    let mut report = Report::default();
11    let value = convert(input, "schema", &mut report)?;
12    Ok(Converted { value, report })
13}
14
15fn convert(
16    input: &Schema,
17    path: &str,
18    report: &mut Report,
19) -> Result<Map<String, Value>, TransformError> {
20    let mut out = Map::new();
21    let type_name = match input.type_ {
22        SchemaType::Unspecified => None,
23        SchemaType::String => Some("string"),
24        SchemaType::Number => Some("number"),
25        SchemaType::Integer => Some("integer"),
26        SchemaType::Boolean => Some("boolean"),
27        SchemaType::Array => Some("array"),
28        SchemaType::Object => Some("object"),
29        SchemaType::Null => Some("null"),
30    };
31    if let Some(name) = type_name {
32        out.insert("type".into(), name.into());
33    } else if input.any_of.as_ref().is_none_or(Vec::is_empty) {
34        return Err(TransformError::shape(
35            format!("{path}.type"),
36            "unspecified type requires an anyOf schema",
37        ));
38    }
39    for (key, value) in [
40        ("format", &input.format),
41        ("title", &input.title),
42        ("description", &input.description),
43        ("pattern", &input.pattern),
44    ] {
45        if let Some(value) = value {
46            out.insert(key.into(), value.clone().into());
47        }
48    }
49    for (key, value) in [
50        ("minItems", &input.min_items),
51        ("maxItems", &input.max_items),
52        ("minLength", &input.min_length),
53        ("maxLength", &input.max_length),
54        ("minProperties", &input.min_properties),
55        ("maxProperties", &input.max_properties),
56    ] {
57        if let Some(value) = value {
58            let number = value
59                .parse::<i64>()
60                .ok()
61                .filter(|n| *n >= 0)
62                .ok_or_else(|| {
63                    TransformError::shape(
64                        format!("{path}.{key}"),
65                        "expected a nonnegative int64 string",
66                    )
67                })?;
68            out.insert(key.into(), number.into());
69        }
70    }
71    for (key, value) in [("minimum", input.minimum), ("maximum", input.maximum)] {
72        if let Some(value) = value {
73            let number = serde_json::Number::from_f64(value).ok_or_else(|| {
74                TransformError::shape(format!("{path}.{key}"), "bound must be finite")
75            })?;
76            out.insert(key.into(), Value::Number(number));
77        }
78    }
79    if let Some(values) = &input.r#enum {
80        if values.is_empty() {
81            return Err(TransformError::shape(
82                format!("{path}.enum"),
83                "enum must be nonempty",
84            ));
85        }
86        if input.type_ != SchemaType::String {
87            return Err(TransformError::shape(
88                format!("{path}.enum"),
89                "Gemini enum is declared only for STRING schemas",
90            ));
91        }
92        out.insert("enum".into(), serde_json::to_value(values)?);
93    }
94    if let Some(values) = &input.required {
95        out.insert("required".into(), serde_json::to_value(values)?);
96    }
97    if let Some(values) = &input.properties {
98        let mut properties = Map::new();
99        for (name, schema) in values {
100            properties.insert(
101                name.clone(),
102                Value::Object(convert(
103                    schema,
104                    &format!("{path}.properties.{name}"),
105                    report,
106                )?),
107            );
108        }
109        out.insert("properties".into(), Value::Object(properties));
110    }
111    if let Some(items) = &input.items {
112        out.insert(
113            "items".into(),
114            Value::Object(convert(items, &format!("{path}.items"), report)?),
115        );
116    }
117    if let Some(values) = &input.any_of {
118        if values.is_empty() {
119            return Err(TransformError::shape(
120                format!("{path}.anyOf"),
121                "anyOf must be nonempty",
122            ));
123        }
124        let mut variants = Vec::with_capacity(values.len());
125        for (index, value) in values.iter().enumerate() {
126            variants.push(Value::Object(convert(
127                value,
128                &format!("{path}.anyOf[{index}]"),
129                report,
130            )?));
131        }
132        out.insert("anyOf".into(), Value::Array(variants));
133    }
134    // In OpenAPI 3.0 nullable expands the type, but other constraints (including
135    // enum and anyOf) still apply. Do not wrap the whole schema in a null union.
136    if input.nullable == Some(true)
137        && let Some(name) = type_name.filter(|name| *name != "null")
138    {
139        out.insert("type".into(), serde_json::json!([name, "null"]));
140    }
141    if let Some(value) = &input.default {
142        out.insert("default".into(), value.clone());
143    }
144    if let Some(value) = &input.example {
145        out.insert("examples".into(), Value::Array(vec![value.clone()]));
146    }
147    if input.property_ordering.is_some() {
148        report.omitted(
149            format!("{path}.propertyOrdering"),
150            "JSON Schema has no property emission ordering constraint",
151        );
152    }
153    Ok(out)
154}