krabka_protocol_codegen/emit/
default_json.rs1use proc_macro2::TokenStream;
14use quote::quote;
15
16use crate::{
17 emit::{
18 EmitError,
19 common::{format_int_literal, parse_tokens, uses_ident, validate},
20 },
21 ir::{FieldSpec, MessageSpec, VersionRange},
22};
23
24pub fn emit_default_json(spec: &MessageSpec) -> Result<String, EmitError> {
32 let field_stmts = fields_stmts_tokens(&spec.fields)?;
33 let version = if uses_ident(&field_stmts, "version") {
36 quote!(version)
37 } else {
38 quote!(_version)
39 };
40 let mut_kw = if field_stmts.is_empty() {
41 quote!()
42 } else {
43 quote!(mut)
44 };
45
46 let tokens = quote! {
47 #[doc = " Default JSON payload matching `Self::default()` for JVM oracle differential testing."]
48 #[doc = " Only includes fields valid for the given version."]
49 #[must_use]
50 #[allow(unused_comparisons)]
51 pub fn default_json(#version: i16) -> ::serde_json::Value {
52 let #mut_kw obj = ::serde_json::Map::new();
53 #field_stmts
54 ::serde_json::Value::Object(obj)
55 }
56 };
57
58 validate::<syn::Item>(&tokens, "default_json fn")?;
60
61 Ok(tokens.to_string())
62}
63
64fn fields_stmts_tokens(fields: &[FieldSpec]) -> Result<TokenStream, EmitError> {
67 let stmts = fields
68 .iter()
69 .map(|f| {
70 let key = json_field_name(&f.name);
71 let val_tokens = parse_tokens(&json_value_expr_versioned(f))?;
72 let insert = quote!(obj.insert(#key.to_owned(), #val_tokens););
73 let Some(cond) = version_cond(f.versions) else {
75 return Ok(insert);
76 };
77 let cond_tokens = parse_tokens(&cond)?;
78 Ok(quote! {
79 if #cond_tokens {
80 #insert
81 }
82 })
83 })
84 .collect::<Result<Vec<_>, EmitError>>()?;
85
86 Ok(quote! { #(#stmts)* })
87}
88
89fn version_cond(vr: VersionRange) -> Option<String> {
91 if vr.min == 0 && vr.max == i16::MAX {
92 None } else if vr.max == i16::MAX {
94 Some(format!("version >= {}", vr.min))
95 } else if vr.min == 0 {
96 Some(format!("version <= {}", vr.max))
97 } else if vr.min == vr.max {
98 Some(format!("version == {}", vr.min))
99 } else {
100 Some(format!("({}..={}).contains(&version)", vr.min, vr.max))
101 }
102}
103
104pub(crate) fn json_value_expr_versioned(f: &FieldSpec) -> String {
121 let is_array = f.field_type.starts_with("[]");
122 let default_is_null = matches!(&f.default, Some(serde_json::Value::Null))
123 || matches!(&f.default, Some(serde_json::Value::String(s)) if s == "null")
124 || (base_type(&f.field_type) == "records" && f.nullable_versions.is_some());
134
135 if default_is_null {
136 if let Some(nv) = f.nullable_versions {
137 let always_nullable =
139 nv.min <= f.versions.min && (nv.max == i16::MAX || nv.max >= f.versions.max);
140 if always_nullable {
141 return "::serde_json::Value::Null".into();
142 }
143 let zero = if is_array {
146 "::serde_json::Value::Array(vec![])".to_owned()
147 } else {
148 type_zero_expr(&f.field_type, f)
149 };
150 if let Some(cond) = version_cond(nv) {
151 return format!("if {cond} {{ ::serde_json::Value::Null }} else {{ {zero} }}");
152 }
153 return "::serde_json::Value::Null".into();
154 }
155 if is_array {
157 return "::serde_json::Value::Array(vec![])".into();
158 }
159 return type_zero_expr(&f.field_type, f);
160 }
161
162 if is_array {
163 return "::serde_json::Value::Array(vec![])".into();
164 }
165
166 f.default.as_ref().map_or_else(
167 || type_zero_expr(&f.field_type, f),
168 |v| scalar_value_expr(&f.field_type, v, f),
169 )
170}
171
172fn scalar_value_expr(field_type: &str, val: &serde_json::Value, f: &FieldSpec) -> String {
175 let base = base_type(field_type);
176
177 match val {
178 serde_json::Value::String(s) if s == "null" => "::serde_json::Value::Null".into(),
179
180 serde_json::Value::String(s) if is_numeric_type(base) => {
181 let trimmed = s.trim();
183 let decimal = trimmed
184 .strip_prefix("0x")
185 .or_else(|| trimmed.strip_prefix("0X"))
186 .and_then(|hex_str| i64::from_str_radix(hex_str, 16).ok())
187 .map_or_else(|| trimmed.to_owned(), |n| n.to_string());
188 let literal = if base == "float64" {
190 decimal
191 } else {
192 format_int_literal(&decimal, "")
193 };
194 format!("::serde_json::json!({literal})")
195 }
196
197 serde_json::Value::String(s) if base == "bool" => {
198 if s == "true" {
199 "::serde_json::Value::Bool(true)".into()
200 } else {
201 "::serde_json::Value::Bool(false)".into()
202 }
203 }
204
205 serde_json::Value::String(s) if base == "string" => {
206 if s.is_empty() {
208 return "::serde_json::Value::String(String::new())".into();
209 }
210 let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
211 format!("::serde_json::Value::String(\"{escaped}\".to_owned())")
212 }
213
214 serde_json::Value::Number(n) => {
215 let literal = if base == "float64" {
216 n.to_string()
217 } else {
218 format_int_literal(&n.to_string(), "")
219 };
220 format!("::serde_json::json!({literal})")
221 }
222
223 serde_json::Value::Bool(b) => {
224 format!("::serde_json::Value::Bool({b})")
225 }
226
227 _ => type_zero_expr(field_type, f),
228 }
229}
230
231fn type_zero_expr(field_type: &str, f: &FieldSpec) -> String {
234 let base = base_type(field_type);
235 match base {
236 "bool" => "::serde_json::Value::Bool(false)".into(),
237 "int8" | "int16" | "int32" | "int64" | "uint16" | "uint32" => {
238 "::serde_json::json!(0)".into()
239 }
240 "float64" => "::serde_json::json!(0.0)".into(),
241 "string" | "bytes" | "records" => "::serde_json::Value::String(String::new())".into(),
242 "uuid" => {
243 "::serde_json::Value::String(\"AAAAAAAAAAAAAAAAAAAAAA\".to_owned())".into()
247 }
248 _ => {
249 if f.fields.is_empty() {
250 "::serde_json::Value::Object(::serde_json::Map::new())".into()
251 } else {
252 emit_nested_struct_expr(&f.fields)
253 }
254 }
255 }
256}
257
258fn emit_nested_struct_expr(fields: &[FieldSpec]) -> String {
266 let inserts = fields
267 .iter()
268 .map(|f| {
269 let key = json_field_name(&f.name);
270 let val = json_value_expr_versioned(f);
271 format!("m.insert(\"{key}\".to_owned(), {val}); ")
272 })
273 .collect::<Vec<_>>()
274 .concat();
275 format!("{{ let mut m = ::serde_json::Map::new(); {inserts}::serde_json::Value::Object(m) }}")
276}
277
278#[must_use]
284pub fn json_field_name(name: &str) -> String {
285 let mut chars = name.chars();
286 chars.next().map_or_else(String::new, |c| {
287 c.to_lowercase().collect::<String>() + chars.as_str()
288 })
289}
290
291fn base_type(t: &str) -> &str {
292 t.strip_prefix("[]").unwrap_or(t)
293}
294
295fn is_numeric_type(t: &str) -> bool {
296 matches!(
297 t,
298 "int8" | "int16" | "int32" | "int64" | "uint16" | "uint32" | "float64"
299 )
300}
301
302#[cfg(test)]
303mod tests {
304 use assert2::assert;
305
306 use super::*;
307
308 #[test]
316 fn nullable_records_field_emits_json_null() {
317 let field: FieldSpec = serde_json::from_value(serde_json::json!({
318 "name": "Records", "type": "records", "versions": "0+",
319 "nullableVersions": "0+"
320 }))
321 .unwrap();
322
323 assert!(json_value_expr_versioned(&field) == "::serde_json::Value::Null");
324 }
325
326 #[test]
331 fn non_nullable_records_field_keeps_empty_string_placeholder() {
332 let field: FieldSpec = serde_json::from_value(serde_json::json!({
333 "name": "UnalignedRecords", "type": "records", "versions": "0+"
334 }))
335 .unwrap();
336
337 assert!(json_value_expr_versioned(&field) == "::serde_json::Value::String(String::new())");
338 }
339}