1use serde_json::{Map, Value, json};
15use zenkey::schema::TypeSchema;
16
17const DEPTH_CAP: usize = 6;
20
21#[derive(Debug, Clone, Copy)]
23pub struct Synth {
24 pub seed: u64,
25}
26
27fn fnv(s: &str) -> u64 {
30 let mut h: u64 = 0xcbf29ce484222325;
31 for b in s.bytes() {
32 h ^= u64::from(b);
33 h = h.wrapping_mul(0x100000001b3);
34 }
35 h
36}
37
38impl Synth {
39 pub fn new(seed: u64) -> Synth {
40 Synth { seed }
41 }
42
43 fn wander(&self, field: &str, tick: u64, min: f64, max: f64) -> f64 {
46 let phase = (fnv(field) ^ self.seed) % 628 ;
47 let x = (tick as f64) / 10.0 + (phase as f64) / 100.0;
48 let mid = f64::midpoint(min, max);
49 let amp = (max - min) / 2.0;
50 mid + amp * x.sin()
51 }
52
53 pub fn instance(&self, schema: &TypeSchema, tick: u64) -> Option<Value> {
57 match schema.kind_str() {
58 zenkey::schema::SchemaKind::JSON_SCHEMA => schema
59 .json_document()
60 .map(|doc| self.json_schema_value(doc, "", tick, 0)),
61 zenkey::schema::SchemaKind::CDR => {
62 let fields = schema.cdr_fields()?;
63 let types = schema.cdr_types();
64 Some(self.cdr_fields_value(fields, types, tick, 0))
65 }
66 #[cfg(feature = "decode-protobuf")]
67 zenkey::schema::SchemaKind::PROTOBUF => self.protobuf_value(schema, tick),
68 _ => None,
69 }
70 }
71
72 fn json_schema_value(&self, doc: &Value, field: &str, tick: u64, depth: usize) -> Value {
77 if depth > DEPTH_CAP {
78 return Value::Null;
79 }
80 if let Some(c) = doc.get("const") {
81 return c.clone();
82 }
83 if let Some(e) = doc.get("enum").and_then(Value::as_array)
84 && let Some(first) = e.first()
85 {
86 return first.clone();
87 }
88 for branch in ["oneOf", "anyOf"] {
89 if let Some(b) = doc.get(branch).and_then(Value::as_array)
90 && let Some(first) = b.first()
91 {
92 return self.json_schema_value(first, field, tick, depth + 1);
93 }
94 }
95 let ty = doc.get("type").and_then(Value::as_str).unwrap_or("number");
96 match ty {
97 "object" => {
98 let mut out = Map::new();
99 if let Some(props) = doc.get("properties").and_then(Value::as_object) {
100 for (name, sub) in props {
101 out.insert(
102 name.clone(),
103 self.json_schema_value(sub, name, tick, depth + 1),
104 );
105 }
106 }
107 Value::Object(out)
108 }
109 "array" => {
110 let n = doc
111 .get("minItems")
112 .and_then(Value::as_u64)
113 .unwrap_or(1)
114 .max(1);
115 let item = doc.get("items").cloned().unwrap_or(json!({}));
116 Value::Array(
117 (0..n)
118 .map(|i| self.json_schema_value(&item, field, tick + i, depth + 1))
119 .collect(),
120 )
121 }
122 "string" => Value::String(format!(
123 "{}-{}",
124 if field.is_empty() { "s" } else { field },
125 tick % 10
126 )),
127 "boolean" => Value::Bool(tick.is_multiple_of(2)),
128 "integer" => {
129 let (min, max) = bounds(doc, 0.0, 100.0);
130 json!(self.wander(field, tick, min, max).round() as i64)
131 }
132 "null" => Value::Null,
133 _ => {
135 let (min, max) = bounds(doc, 0.0, 100.0);
136 json!(self.wander(field, tick, min, max))
137 }
138 }
139 }
140
141 fn cdr_fields_value(
144 &self,
145 fields: &Value,
146 types: Option<&Map<String, Value>>,
147 tick: u64,
148 depth: usize,
149 ) -> Value {
150 if depth > DEPTH_CAP {
151 return Value::Null;
152 }
153 let Some(list) = fields.as_array() else {
154 return Value::Null;
155 };
156 let mut out = Map::new();
157 for f in list {
158 let Some(name) = f.get("name").and_then(Value::as_str) else {
159 continue;
160 };
161 let ty = f.get("type").cloned().unwrap_or(Value::Null);
162 out.insert(
163 name.to_string(),
164 self.cdr_value(&ty, types, name, tick, depth),
165 );
166 }
167 Value::Object(out)
168 }
169
170 fn cdr_value(
171 &self,
172 ty: &Value,
173 types: Option<&Map<String, Value>>,
174 field: &str,
175 tick: u64,
176 depth: usize,
177 ) -> Value {
178 if depth > DEPTH_CAP {
179 return Value::Null;
180 }
181 match ty {
182 Value::String(name) => match name.as_str() {
183 "bool" => Value::Bool(tick.is_multiple_of(2)),
184 "string" => Value::String(format!("{field}-{}", tick % 10)),
185 "float32" | "float" | "float64" | "double" => {
186 json!(self.wander(field, tick, 0.0, 100.0))
187 }
188 "int8" | "char" | "int16" | "short" | "int32" | "long" | "int64" | "long long" => {
191 json!(self.wander(field, tick, 0.0, 100.0).round() as i64)
192 }
193 "uint8" | "byte" | "octet" | "uint16" | "unsigned short" | "uint32"
194 | "unsigned long" | "uint64" | "unsigned long long" => {
195 json!(self.wander(field, tick, 0.0, 100.0).round().abs() as u64)
196 }
197 other => match types.and_then(|t| t.get(other)) {
199 Some(composite) => {
200 let fields = composite.get("fields").unwrap_or(composite);
201 self.cdr_fields_value(fields, types, tick, depth + 1)
202 }
203 None => Value::Null,
204 },
205 },
206 Value::Object(o) => {
208 if let Some(arr) = o.get("array") {
209 let n = arr.get("len").and_then(Value::as_u64).unwrap_or(1).max(1);
210 let of = arr.get("of").cloned().unwrap_or(Value::Null);
211 Value::Array(
212 (0..n)
213 .map(|i| self.cdr_value(&of, types, field, tick + i, depth + 1))
214 .collect(),
215 )
216 } else if let Some(seq) = o.get("sequence") {
217 let of = seq.get("of").cloned().unwrap_or(Value::Null);
218 Value::Array(vec![self.cdr_value(&of, types, field, tick, depth + 1)])
219 } else {
220 Value::Null
221 }
222 }
223 _ => Value::Null,
224 }
225 }
226
227 #[cfg(feature = "decode-protobuf")]
231 fn protobuf_value(&self, schema: &TypeSchema, tick: u64) -> Option<Value> {
232 use prost_reflect::{DescriptorPool, Kind};
233 let fds = schema.protobuf_descriptor_set()?;
234 let message = schema.protobuf_message()?;
235 let pool = DescriptorPool::decode(fds.as_slice()).ok()?;
236 let desc = pool.get_message_by_name(message)?;
237 fn message_value(
238 synth: &Synth,
239 desc: &prost_reflect::MessageDescriptor,
240 tick: u64,
241 depth: usize,
242 ) -> Value {
243 if depth > DEPTH_CAP {
244 return Value::Object(Map::new());
245 }
246 let mut out = Map::new();
247 for field in desc.fields() {
248 let name = field.json_name().to_string();
249 let v = match field.kind() {
250 Kind::Double | Kind::Float => json!(synth.wander(&name, tick, 0.0, 100.0)),
251 Kind::Int32
252 | Kind::Int64
253 | Kind::Sint32
254 | Kind::Sint64
255 | Kind::Sfixed32
256 | Kind::Sfixed64 => {
257 json!(synth.wander(&name, tick, 0.0, 100.0).round() as i64)
258 }
259 Kind::Uint32 | Kind::Uint64 | Kind::Fixed32 | Kind::Fixed64 => {
260 json!(synth.wander(&name, tick, 0.0, 100.0).round().abs() as u64)
261 }
262 Kind::Bool => Value::Bool(tick.is_multiple_of(2)),
263 Kind::String => Value::String(format!("{name}-{}", tick % 10)),
264 Kind::Bytes => Value::String(String::new()),
265 Kind::Enum(e) => e
266 .values()
267 .next()
268 .map(|v| Value::String(v.name().to_string()))
269 .unwrap_or(Value::Null),
270 Kind::Message(m) => message_value(synth, &m, tick, depth + 1),
271 };
272 let v = if field.is_list() {
273 Value::Array(vec![v])
274 } else {
275 v
276 };
277 out.insert(name, v);
278 }
279 Value::Object(out)
280 }
281 Some(message_value(self, &desc, tick, 0))
282 }
283}
284
285fn bounds(doc: &Value, dmin: f64, dmax: f64) -> (f64, f64) {
286 let min = doc.get("minimum").and_then(Value::as_f64).unwrap_or(dmin);
287 let max = doc
288 .get("maximum")
289 .and_then(Value::as_f64)
290 .unwrap_or_else(|| dmax.max(min + 1.0));
291 (min, max.max(min))
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use zenkey::schema::WireEncoding;
298 use zenkey::schema::decode::DecoderRegistry;
299
300 #[test]
304 fn a_synthesized_json_instance_encodes_and_validates() {
305 let schema = TypeSchema::json_schema(json!({
306 "type": "object",
307 "required": ["status", "load", "cores"],
308 "properties": {
309 "status": { "type": "string", "enum": ["ok", "degraded"] },
310 "load": { "type": "number", "minimum": 0.0, "maximum": 1.0 },
311 "cores": { "type": "integer", "minimum": 1, "maximum": 128 },
312 "tags": { "type": "array", "items": { "type": "string" } },
313 "nested": {
314 "type": "object",
315 "properties": { "up": { "type": "boolean" } },
316 },
317 },
318 }));
319 let registry = DecoderRegistry::new();
320 let synth = Synth::new(42);
321 for tick in 0..20 {
322 let v = synth
323 .instance(&schema, tick)
324 .expect("json-schema synthesizes");
325 let bytes = registry
326 .encode(&schema, &v, &WireEncoding::Json)
327 .unwrap_or_else(|e| panic!("tick {tick}: {v} refused: {e}"));
328 let back = registry
329 .decode(&schema, &WireEncoding::Json, &bytes)
330 .unwrap();
331 assert_eq!(
332 back.verdict,
333 zenkey::schema::validate::Verdict::Valid,
334 "tick {tick}"
335 );
336 }
337 }
338
339 #[test]
341 fn synthesis_is_deterministic_and_wanders() {
342 let schema = TypeSchema::json_schema(json!({
343 "type": "object",
344 "properties": { "v": { "type": "number" } },
345 }));
346 let synth = Synth::new(7);
347 assert_eq!(
348 synth.instance(&schema, 3),
349 synth.instance(&schema, 3),
350 "reproducible runs are the point"
351 );
352 assert_ne!(synth.instance(&schema, 3), synth.instance(&schema, 4));
353 }
354
355 #[cfg(feature = "decode-cdr")]
357 #[test]
358 fn a_synthesized_cdr_instance_encodes() {
359 let schema = TypeSchema::cdr(json!({
360 "fields": [
361 { "name": "x", "type": "float64" },
362 { "name": "n", "type": "uint32" },
363 { "name": "label", "type": "string" },
364 ],
365 }));
366 let registry = DecoderRegistry::new();
367 let v = Synth::new(1).instance(&schema, 0).expect("cdr synthesizes");
368 let bytes = registry
369 .encode(&schema, &v, &WireEncoding::Cdr)
370 .expect("the instance encodes");
371 assert!(!bytes.is_empty());
372 }
373
374 #[test]
376 fn an_unknown_kind_declines_instead_of_guessing() {
377 let set = zenkey::schema::SchemaSet::parse(
378 r#"{"schema_version":1,"app":"t",
379 "types":{"W":{"kind":"cddl","hash":"sha256:00","spec":"x = int"}}}"#,
380 )
381 .unwrap();
382 assert_eq!(Synth::new(0).instance(set.get("W").unwrap(), 0), None);
383 }
384}