gproxy_protocol/transform/generate/gemini_schema/
from_json.rs1use crate::{
2 transform::{Converted, Report, TransformError},
3 wire::gemini::content::{Schema, SchemaType},
4};
5use serde_json::{Map, Value};
6
7pub fn from_json(input: &Map<String, Value>) -> Result<Converted<Schema>, 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: &Map<String, Value>,
17 path: &str,
18 report: &mut Report,
19) -> Result<Schema, TransformError> {
20 let (kind, nullable) = match input.get("type") {
21 Some(Value::String(name)) => (schema_type(name, path)?, false),
22 Some(Value::Array(names)) => {
23 if names.len() != 2 || !names.iter().any(|name| name == "null") {
24 return Err(TransformError::unsupported(
25 format!("{path}.type"),
26 "typed Gemini supports only a single type with optional null",
27 ));
28 }
29 let name = names
30 .iter()
31 .find(|name| *name != "null")
32 .and_then(Value::as_str)
33 .ok_or_else(|| TransformError::shape(path, "invalid type union"))?;
34 (schema_type(name, path)?, true)
35 }
36 None if input.contains_key("anyOf") => (SchemaType::Unspecified, false),
37 None => {
38 return Err(TransformError::unsupported(
39 format!("{path}.type"),
40 "Gemini typed schema requires a type or anyOf",
41 ));
42 }
43 Some(_) => {
44 return Err(TransformError::shape(
45 format!("{path}.type"),
46 "invalid JSON Schema type",
47 ));
48 }
49 };
50 let mut out = Schema::builder(kind).build();
51 if nullable {
52 out.nullable = Some(true);
53 }
54 for (key, value) in input {
55 let field = format!("{path}.{key}");
56 match key.as_str() {
57 "type" => {}
58 "format" => out.format = Some(decode(value, &field)?),
59 "title" => out.title = Some(decode(value, &field)?),
60 "description" => out.description = Some(decode(value, &field)?),
61 "pattern" => out.pattern = Some(decode(value, &field)?),
62 "required" => out.required = Some(decode(value, &field)?),
63 "enum" => {
64 if out.type_ != SchemaType::String {
65 continue;
66 }
67 let values: Vec<String> = decode(value, &field)?;
68 if values.is_empty() {
69 return Err(TransformError::shape(&field, "enum must be nonempty"));
70 }
71 out.r#enum = Some(values);
72 }
73 "minItems" => out.min_items = Some(bound(value, &field)?),
74 "maxItems" => out.max_items = Some(bound(value, &field)?),
75 "minLength" => out.min_length = Some(bound(value, &field)?),
76 "maxLength" => out.max_length = Some(bound(value, &field)?),
77 "minProperties" => out.min_properties = Some(bound(value, &field)?),
78 "maxProperties" => out.max_properties = Some(bound(value, &field)?),
79 "minimum" => out.minimum = Some(number(value, &field)?),
80 "maximum" => out.maximum = Some(number(value, &field)?),
81 "default" => out.default = Some(value.clone()),
82 "examples" => {
83 let examples = value
84 .as_array()
85 .ok_or_else(|| TransformError::shape(&field, "examples must be an array"))?;
86 out.example = examples.first().cloned();
87 if examples.len() > 1 {
88 report.omitted(field, "Gemini carries only the first example");
89 }
90 }
91 "properties" => {
92 let values = object(value, &field)?;
93 let mut properties = std::collections::BTreeMap::new();
94 for (name, value) in values {
95 let child = format!("{field}.{name}");
96 properties.insert(
97 name.clone(),
98 Box::new(convert(object(value, &child)?, &child, report)?),
99 );
100 }
101 out.properties = Some(properties);
102 }
103 "items" => out.items = Some(Box::new(convert(object(value, &field)?, &field, report)?)),
104 "anyOf" => {
105 let values = value.as_array().filter(|v| !v.is_empty()).ok_or_else(|| {
106 TransformError::shape(&field, "anyOf must be a nonempty array")
107 })?;
108 let mut variants = Vec::with_capacity(values.len());
109 for (index, value) in values.iter().enumerate() {
110 let child = format!("{field}[{index}]");
111 variants.push(Box::new(convert(object(value, &child)?, &child, report)?));
112 }
113 out.any_of = Some(variants);
114 }
115 _ => {
116 continue;
117 }
118 }
119 }
120 Ok(out)
121}
122
123fn schema_type(name: &str, path: &str) -> Result<SchemaType, TransformError> {
124 Ok(match name {
125 "string" => SchemaType::String,
126 "number" => SchemaType::Number,
127 "integer" => SchemaType::Integer,
128 "boolean" => SchemaType::Boolean,
129 "array" => SchemaType::Array,
130 "object" => SchemaType::Object,
131 "null" => SchemaType::Null,
132 _ => {
133 return Err(TransformError::shape(
134 format!("{path}.type"),
135 "unknown JSON Schema type",
136 ));
137 }
138 })
139}
140
141fn object<'a>(value: &'a Value, path: &str) -> Result<&'a Map<String, Value>, TransformError> {
142 value
143 .as_object()
144 .ok_or_else(|| TransformError::unsupported(path, "typed Gemini requires an object schema"))
145}
146
147fn decode<T: serde::de::DeserializeOwned>(value: &Value, path: &str) -> Result<T, TransformError> {
148 serde_json::from_value(value.clone())
149 .map_err(|error| TransformError::shape(path, error.to_string()))
150}
151
152fn bound(value: &Value, path: &str) -> Result<String, TransformError> {
153 let integer = value.as_i64().filter(|n| *n >= 0).or_else(|| {
154 let n = value.as_f64()?;
155 (n.is_finite() && (0.0..9_223_372_036_854_775_808.0).contains(&n) && n.fract() == 0.0)
156 .then_some(n as i64)
157 });
158 integer
159 .map(|n| n.to_string())
160 .ok_or_else(|| TransformError::shape(path, "expected a nonnegative int64 bound"))
161}
162
163fn number(value: &Value, path: &str) -> Result<f64, TransformError> {
164 let n = value
165 .as_f64()
166 .filter(|value| value.is_finite())
167 .ok_or_else(|| TransformError::shape(path, "expected a number"))?;
168 if value.as_i64().is_some_and(|v| n as i128 != i128::from(v))
170 || value.as_u64().is_some_and(|v| n as i128 != i128::from(v))
171 {
172 return Err(TransformError::unsupported(
173 path,
174 "numeric bound cannot be represented exactly by Gemini double",
175 ));
176 }
177 Ok(n)
178}