1use serde_json::{Map, Value, json};
15use zenkey::schema::TypeSchema;
16
17use crate::model::jsonschema::resolve_ref;
18
19const DEPTH_CAP: usize = 16;
28
29#[derive(Debug, Clone, Copy)]
31pub struct Synth {
32 pub seed: u64,
33}
34
35fn fnv(s: &str) -> u64 {
38 let mut h: u64 = 0xcbf29ce484222325;
39 for b in s.bytes() {
40 h ^= u64::from(b);
41 h = h.wrapping_mul(0x100000001b3);
42 }
43 h
44}
45
46impl Synth {
47 pub fn new(seed: u64) -> Synth {
48 Synth { seed }
49 }
50
51 fn wander(&self, field: &str, tick: u64, min: f64, max: f64) -> f64 {
54 let phase = (fnv(field) ^ self.seed) % 628 ;
55 let x = (tick as f64) / 10.0 + (phase as f64) / 100.0;
56 let mid = f64::midpoint(min, max);
57 let amp = (max - min) / 2.0;
58 mid + amp * x.sin()
59 }
60
61 pub fn instance(&self, schema: &TypeSchema, tick: u64) -> Option<Value> {
65 match schema.kind_str() {
66 zenkey::schema::SchemaKind::JSON_SCHEMA => schema
67 .json_document()
68 .map(|doc| self.json_schema_value(doc, doc, "", tick, 0)),
69 zenkey::schema::SchemaKind::CDR => {
70 let fields = schema.cdr_fields()?;
71 let types = schema.cdr_types();
72 Some(self.cdr_fields_value(fields, types, tick, 0))
73 }
74 #[cfg(feature = "decode-protobuf")]
75 zenkey::schema::SchemaKind::PROTOBUF => self.protobuf_value(schema, tick),
76 _ => None,
77 }
78 }
79
80 fn json_schema_value(
88 &self,
89 root: &Value,
90 doc: &Value,
91 field: &str,
92 tick: u64,
93 depth: usize,
94 ) -> Value {
95 if depth > DEPTH_CAP {
96 return Value::Null;
97 }
98 if let Some(c) = doc.get("const") {
99 return c.clone();
100 }
101 if let Some(e) = doc.get("enum").and_then(Value::as_array)
102 && let Some(first) = e.first()
103 {
104 return first.clone();
105 }
106 if let Some(pointer) = doc.get("$ref").and_then(Value::as_str)
111 && let Some(target) = resolve_ref(root, pointer)
112 {
113 return self.json_schema_value(root, target, field, tick, depth + 1);
114 }
115 if let Some(members) = doc.get("allOf").and_then(Value::as_array) {
118 let mut merged = Map::new();
119 for member in members {
120 if let Value::Object(o) =
121 self.json_schema_value(root, member, field, tick, depth + 1)
122 {
123 merged.extend(o);
124 }
125 }
126 return Value::Object(merged);
127 }
128 for branch in ["oneOf", "anyOf"] {
133 if let Some(b) = doc.get(branch).and_then(Value::as_array)
134 && let Some(first) = b.first()
135 {
136 return self.json_schema_value(root, first, field, tick, depth + 1);
137 }
138 }
139 let ty = doc.get("type").and_then(Value::as_str).unwrap_or("number");
140 match ty {
141 "object" => {
142 let mut out = Map::new();
143 if let Some(props) = doc.get("properties").and_then(Value::as_object) {
144 for (name, sub) in props {
145 out.insert(
146 name.clone(),
147 self.json_schema_value(root, sub, name, tick, depth + 1),
148 );
149 }
150 }
151 Value::Object(out)
152 }
153 "array" => {
154 let n = doc
155 .get("minItems")
156 .and_then(Value::as_u64)
157 .unwrap_or(1)
158 .max(1);
159 let item = doc.get("items").cloned().unwrap_or(json!({}));
160 Value::Array(
161 (0..n)
162 .map(|i| self.json_schema_value(root, &item, field, tick + i, depth + 1))
163 .collect(),
164 )
165 }
166 "string" => Value::String(format!(
167 "{}-{}",
168 if field.is_empty() { "s" } else { field },
169 tick % 10
170 )),
171 "boolean" => Value::Bool(tick.is_multiple_of(2)),
172 "integer" => {
173 let (min, max) = bounds(doc, 0.0, 100.0);
174 json!(self.wander(field, tick, min, max).round() as i64)
175 }
176 "null" => Value::Null,
177 _ => {
179 let (min, max) = bounds(doc, 0.0, 100.0);
180 json!(self.wander(field, tick, min, max))
181 }
182 }
183 }
184
185 fn cdr_fields_value(
188 &self,
189 fields: &Value,
190 types: Option<&Map<String, Value>>,
191 tick: u64,
192 depth: usize,
193 ) -> Value {
194 if depth > DEPTH_CAP {
195 return Value::Null;
196 }
197 let Some(list) = fields.as_array() else {
198 return Value::Null;
199 };
200 let mut out = Map::new();
201 for f in list {
202 let Some(name) = f.get("name").and_then(Value::as_str) else {
203 continue;
204 };
205 let ty = f.get("type").cloned().unwrap_or(Value::Null);
206 out.insert(
207 name.to_string(),
208 self.cdr_value(&ty, types, name, tick, depth),
209 );
210 }
211 Value::Object(out)
212 }
213
214 fn cdr_value(
215 &self,
216 ty: &Value,
217 types: Option<&Map<String, Value>>,
218 field: &str,
219 tick: u64,
220 depth: usize,
221 ) -> Value {
222 if depth > DEPTH_CAP {
223 return Value::Null;
224 }
225 match ty {
226 Value::String(name) => match name.as_str() {
227 "bool" => Value::Bool(tick.is_multiple_of(2)),
228 "string" => Value::String(format!("{field}-{}", tick % 10)),
229 "float32" | "float" | "float64" | "double" => {
230 json!(self.wander(field, tick, 0.0, 100.0))
231 }
232 "int8" | "char" | "int16" | "short" | "int32" | "long" | "int64" | "long long" => {
235 json!(self.wander(field, tick, 0.0, 100.0).round() as i64)
236 }
237 "uint8" | "byte" | "octet" | "uint16" | "unsigned short" | "uint32"
238 | "unsigned long" | "uint64" | "unsigned long long" => {
239 json!(self.wander(field, tick, 0.0, 100.0).round().abs() as u64)
240 }
241 other => match types.and_then(|t| t.get(other)) {
243 Some(composite) => {
244 let fields = composite.get("fields").unwrap_or(composite);
245 self.cdr_fields_value(fields, types, tick, depth + 1)
246 }
247 None => Value::Null,
248 },
249 },
250 Value::Object(o) => {
252 if let Some(arr) = o.get("array") {
253 let n = arr.get("len").and_then(Value::as_u64).unwrap_or(1).max(1);
254 let of = arr.get("of").cloned().unwrap_or(Value::Null);
255 Value::Array(
256 (0..n)
257 .map(|i| self.cdr_value(&of, types, field, tick + i, depth + 1))
258 .collect(),
259 )
260 } else if let Some(seq) = o.get("sequence") {
261 let of = seq.get("of").cloned().unwrap_or(Value::Null);
262 Value::Array(vec![self.cdr_value(&of, types, field, tick, depth + 1)])
263 } else {
264 Value::Null
265 }
266 }
267 _ => Value::Null,
268 }
269 }
270
271 #[cfg(feature = "decode-protobuf")]
275 fn protobuf_value(&self, schema: &TypeSchema, tick: u64) -> Option<Value> {
276 use prost_reflect::{DescriptorPool, Kind};
277 let fds = schema.protobuf_descriptor_set()?;
278 let message = schema.protobuf_message()?;
279 let pool = DescriptorPool::decode(fds.as_slice()).ok()?;
280 let desc = pool.get_message_by_name(message)?;
281 fn message_value(
282 synth: &Synth,
283 desc: &prost_reflect::MessageDescriptor,
284 tick: u64,
285 depth: usize,
286 ) -> Value {
287 if depth > DEPTH_CAP {
288 return Value::Object(Map::new());
289 }
290 let mut out = Map::new();
291 for field in desc.fields() {
292 let name = field.json_name().to_string();
293 let v = match field.kind() {
294 Kind::Double | Kind::Float => json!(synth.wander(&name, tick, 0.0, 100.0)),
295 Kind::Int32
296 | Kind::Int64
297 | Kind::Sint32
298 | Kind::Sint64
299 | Kind::Sfixed32
300 | Kind::Sfixed64 => {
301 json!(synth.wander(&name, tick, 0.0, 100.0).round() as i64)
302 }
303 Kind::Uint32 | Kind::Uint64 | Kind::Fixed32 | Kind::Fixed64 => {
304 json!(synth.wander(&name, tick, 0.0, 100.0).round().abs() as u64)
305 }
306 Kind::Bool => Value::Bool(tick.is_multiple_of(2)),
307 Kind::String => Value::String(format!("{name}-{}", tick % 10)),
308 Kind::Bytes => Value::String(String::new()),
309 Kind::Enum(e) => e
310 .values()
311 .next()
312 .map(|v| Value::String(v.name().to_string()))
313 .unwrap_or(Value::Null),
314 Kind::Message(m) => message_value(synth, &m, tick, depth + 1),
315 };
316 let v = if field.is_list() {
317 Value::Array(vec![v])
318 } else {
319 v
320 };
321 out.insert(name, v);
322 }
323 Value::Object(out)
324 }
325 Some(message_value(self, &desc, tick, 0))
326 }
327}
328
329fn bounds(doc: &Value, dmin: f64, dmax: f64) -> (f64, f64) {
330 let min = doc.get("minimum").and_then(Value::as_f64).unwrap_or(dmin);
331 let max = doc
332 .get("maximum")
333 .and_then(Value::as_f64)
334 .unwrap_or_else(|| dmax.max(min + 1.0));
335 (min, max.max(min))
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use zenkey::schema::WireEncoding;
342 use zenkey::schema::decode::DecoderRegistry;
343
344 #[test]
348 fn a_synthesized_json_instance_encodes_and_validates() {
349 let schema = TypeSchema::json_schema(json!({
350 "type": "object",
351 "required": ["status", "load", "cores"],
352 "properties": {
353 "status": { "type": "string", "enum": ["ok", "degraded"] },
354 "load": { "type": "number", "minimum": 0.0, "maximum": 1.0 },
355 "cores": { "type": "integer", "minimum": 1, "maximum": 128 },
356 "tags": { "type": "array", "items": { "type": "string" } },
357 "nested": {
358 "type": "object",
359 "properties": { "up": { "type": "boolean" } },
360 },
361 },
362 }));
363 let registry = DecoderRegistry::new();
364 let synth = Synth::new(42);
365 for tick in 0..20 {
366 let v = synth
367 .instance(&schema, tick)
368 .expect("json-schema synthesizes");
369 let bytes = registry
370 .encode(&schema, &v, &WireEncoding::Json)
371 .unwrap_or_else(|e| panic!("tick {tick}: {v} refused: {e}"));
372 let back = registry
373 .decode(&schema, &WireEncoding::Json, &bytes)
374 .unwrap();
375 assert_eq!(
376 back.verdict,
377 zenkey::schema::validate::Verdict::Valid,
378 "tick {tick}"
379 );
380 }
381 }
382
383 #[test]
389 fn a_ref_into_defs_synthesizes_the_referenced_shape() {
390 let schema = TypeSchema::json_schema(json!({
391 "type": "object",
392 "required": ["name", "value"],
393 "properties": {
394 "name": { "type": "string" },
395 "value": { "$ref": "#/$defs/TelemetryValue" },
396 },
397 "$defs": {
398 "TelemetryValue": {
399 "oneOf": [
400 { "type": "object",
401 "required": ["type", "value"],
402 "properties": {
403 "type": { "const": "counter", "type": "string" },
404 "value": { "type": "integer", "minimum": 0 } } },
405 { "type": "object",
406 "required": ["type", "value"],
407 "properties": {
408 "type": { "const": "gauge", "type": "string" },
409 "value": { "type": "number" } } },
410 ],
411 },
412 },
413 }));
414 let registry = DecoderRegistry::new();
415 let synth = Synth::new(7);
416 let v = synth.instance(&schema, 3).expect("json-schema synthesizes");
417 assert!(
418 v["value"].is_object(),
419 "the reference resolved to the enum, not to a placeholder number: {v}"
420 );
421 assert_eq!(v["value"]["type"], "counter", "the first branch, satisfied");
422
423 let bytes = registry
424 .encode(&schema, &v, &WireEncoding::Json)
425 .unwrap_or_else(|e| panic!("{v} refused: {e}"));
426 let back = registry
427 .decode(&schema, &WireEncoding::Json, &bytes)
428 .unwrap();
429 assert_eq!(back.verdict, zenkey::schema::validate::Verdict::Valid);
430 }
431
432 #[test]
434 fn synthesis_is_deterministic_and_wanders() {
435 let schema = TypeSchema::json_schema(json!({
436 "type": "object",
437 "properties": { "v": { "type": "number" } },
438 }));
439 let synth = Synth::new(7);
440 assert_eq!(
441 synth.instance(&schema, 3),
442 synth.instance(&schema, 3),
443 "reproducible runs are the point"
444 );
445 assert_ne!(synth.instance(&schema, 3), synth.instance(&schema, 4));
446 }
447
448 #[cfg(feature = "decode-cdr")]
450 #[test]
451 fn a_synthesized_cdr_instance_encodes() {
452 let schema = TypeSchema::cdr(json!({
453 "fields": [
454 { "name": "x", "type": "float64" },
455 { "name": "n", "type": "uint32" },
456 { "name": "label", "type": "string" },
457 ],
458 }));
459 let registry = DecoderRegistry::new();
460 let v = Synth::new(1).instance(&schema, 0).expect("cdr synthesizes");
461 let bytes = registry
462 .encode(&schema, &v, &WireEncoding::Cdr)
463 .expect("the instance encodes");
464 assert!(!bytes.is_empty());
465 }
466
467 #[test]
469 fn an_unknown_kind_declines_instead_of_guessing() {
470 let set = zenkey::schema::SchemaSet::parse(
471 r#"{"schema_version":1,"app":"t",
472 "types":{"W":{"kind":"cddl","hash":"sha256:00","spec":"x = int"}}}"#,
473 )
474 .unwrap();
475 assert_eq!(Synth::new(0).instance(set.get("W").unwrap(), 0), None);
476 }
477}