1use std::collections::{BTreeMap, BTreeSet};
15use std::path::{Path, PathBuf};
16use std::sync::LazyLock;
17
18use regex::Regex;
19
20use crate::error::Result;
21use crate::utils::last_segment;
22use crate::utils::strings::lower_camel;
23
24const TRESTLE_FILES: &[&str] = &[
26 "mod.rs",
27 "labels.rs",
28 "pyo3_impls.rs",
29 crate::codegen::EXTRACTORS_SUBDIR,
30];
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub enum JsonFieldNames {
35 #[default]
37 Json,
38 Proto,
40}
41
42#[derive(Debug, Clone, Default)]
44pub struct ModelRewrites {
45 pub field_names: JsonFieldNames,
47 pub packages: BTreeMap<String, JsonFieldNames>,
50 pub int64_module: Option<String>,
53 pub json_value_module: Option<String>,
56 pub fields: BTreeMap<String, PackageFields>,
58}
59
60#[derive(Debug, Clone, Default, PartialEq, Eq)]
62pub struct PackageFields {
63 pub json_names: BTreeMap<String, String>,
65 pub emit_unpopulated: BTreeSet<String>,
67}
68
69pub fn field_options(
71 metadata: &crate::parsing::CodeGenMetadata,
72) -> BTreeMap<String, PackageFields> {
73 let mut packages: BTreeMap<String, PackageFields> = BTreeMap::new();
74 for message in metadata.messages.values() {
75 let name = message.name.trim_start_matches('.');
76 let package = name
77 .split('.')
78 .take_while(|s| s.starts_with(|c: char| c.is_ascii_lowercase()))
79 .collect::<Vec<_>>()
80 .join(".");
81 let simple = last_segment(name);
82 for field in &message.fields {
83 let key = format!("{simple}.{}", field.name);
84 if let Some(json) = &field.json_name {
85 packages
86 .entry(package.clone())
87 .or_default()
88 .json_names
89 .insert(key.clone(), json.clone());
90 }
91 if field.emit_unpopulated {
92 packages
93 .entry(package.clone())
94 .or_default()
95 .emit_unpopulated
96 .insert(key);
97 }
98 }
99 }
100 packages
101}
102
103fn package_entry<'a, T>(by_package: &'a BTreeMap<String, T>, file_name: &str) -> Option<&'a T> {
106 let stem = file_name.strip_suffix(".rs")?;
107 let parent = stem.rsplit_once('.').map(|(package, _)| package);
108 by_package
109 .get(stem)
110 .or_else(|| parent.and_then(|package| by_package.get(package)))
111}
112
113impl ModelRewrites {
114 pub fn is_empty(&self) -> bool {
116 self.field_names == JsonFieldNames::Json
117 && self.packages.values().all(|n| *n == JsonFieldNames::Json)
118 && self.int64_module.is_none()
119 && self.json_value_module.is_none()
120 && self.fields.is_empty()
121 }
122
123 fn field_names_for(&self, file_name: &str) -> JsonFieldNames {
125 package_entry(&self.packages, file_name)
126 .copied()
127 .unwrap_or(self.field_names)
128 }
129
130 fn fields_for(&self, file_name: &str) -> Option<&PackageFields> {
132 package_entry(&self.fields, file_name)
133 }
134
135 pub fn rewrite(&self, file_name: &str, src: &str) -> String {
137 let mut out = src.to_string();
138 if self.field_names_for(file_name) == JsonFieldNames::Proto {
139 out = swap_serde_renames(&out);
140 out = snakeify_serialize_entries(&out);
141 }
142 if let Some(fields) = self.fields_for(file_name) {
143 out = apply_field_options(&out, fields);
144 }
145 if let Some(module) = &self.int64_module {
146 out = int64_helpers_to(&out, module);
147 out = int64_containers_to(&out, module);
148 }
149 if let Some(module) = &self.json_value_module {
150 out = json_value_serializers(&out, module);
151 }
152 out
153 }
154
155 pub fn rewrite_dir(&self, dir: &Path) -> Result<usize> {
157 if self.is_empty() || !dir.exists() {
158 return Ok(0);
159 }
160 let mut changed = 0;
161 for path in model_files(dir)? {
162 let src = std::fs::read_to_string(&path)?;
163 let file_name = path
164 .file_name()
165 .and_then(|n| n.to_str())
166 .unwrap_or_default();
167 let rewritten = self.rewrite(file_name, &src);
168 if rewritten != src {
169 std::fs::write(&path, rewritten)?;
170 changed += 1;
171 }
172 }
173 Ok(changed)
174 }
175}
176
177fn model_files(dir: &Path) -> Result<Vec<PathBuf>> {
179 let mut out = Vec::new();
180 let mut stack = vec![dir.to_path_buf()];
181 while let Some(d) = stack.pop() {
182 for entry in std::fs::read_dir(&d)? {
183 let path = entry?.path();
184 let trestle_file = d == dir
185 && path
186 .file_name()
187 .and_then(|n| n.to_str())
188 .is_some_and(|n| TRESTLE_FILES.contains(&n));
189 if trestle_file {
190 continue;
191 }
192 if path.is_dir() {
193 stack.push(path);
194 } else if path.extension().is_some_and(|e| e == "rs") {
195 out.push(path);
196 }
197 }
198 }
199 out.sort();
200 Ok(out)
201}
202
203fn swap_serde_renames(src: &str) -> String {
206 static SERDE_ATTR: LazyLock<Regex> =
208 LazyLock::new(|| Regex::new(r"#\[serde\((?P<body>[^)]*)\)\]").expect("valid regex"));
209 static RENAME: LazyLock<Regex> =
210 LazyLock::new(|| Regex::new(r#"rename = "(?P<v>[^"]*)""#).expect("valid regex"));
211 static ALIAS: LazyLock<Regex> =
212 LazyLock::new(|| Regex::new(r#"alias = "(?P<v>[^"]*)""#).expect("valid regex"));
213
214 SERDE_ATTR
215 .replace_all(src, |caps: ®ex::Captures| {
216 let whole = &caps[0];
217 let body = &caps["body"];
218 let (Some(rn), Some(al)) = (RENAME.captures(body), ALIAS.captures(body)) else {
219 return whole.to_string();
220 };
221 let rename_val = rn["v"].to_string();
222 let alias_val = al["v"].to_string();
223 if rename_val == alias_val || rename_val != lower_camel(&alias_val) {
225 return whole.to_string();
226 }
227 let body = ALIAS.replace(body, format!(r#"alias = "{rename_val}""#));
228 let body = RENAME.replace(&body, format!(r#"rename = "{alias_val}""#));
229 format!("#[serde({body})]")
230 })
231 .into_owned()
232}
233
234fn snakeify_serialize_entries(src: &str) -> String {
237 static ENTRY: LazyLock<Regex> =
238 LazyLock::new(|| Regex::new(r#"serialize_entry\("(?P<name>[^"]*)""#).expect("valid regex"));
239 ENTRY
240 .replace_all(src, |caps: ®ex::Captures| {
241 let name = &caps["name"];
242 let snake = camel_to_snake(name);
243 let is_default_json_name =
244 name.starts_with(|c: char| c.is_ascii_lowercase()) && lower_camel(&snake) == name;
245 if is_default_json_name {
246 format!(r#"serialize_entry("{snake}""#)
247 } else {
248 caps[0].to_string()
249 }
250 })
251 .into_owned()
252}
253
254fn int64_helpers_to(src: &str, module: &str) -> String {
256 static HELPER: LazyLock<Regex> = LazyLock::new(|| {
257 Regex::new(r"::buffa::json_helpers::(?P<name>(?:opt_)?u?int64)\b").expect("valid regex")
258 });
259 HELPER
260 .replace_all(src, |caps: ®ex::Captures| {
261 format!("{module}::{}", &caps["name"])
262 })
263 .into_owned()
264}
265
266fn apply_field_options(src: &str, fields: &PackageFields) -> String {
269 static STRUCT: LazyLock<Regex> =
270 LazyLock::new(|| Regex::new(r"^\s*pub struct (\w+)").expect("valid regex"));
271 static FIELD: LazyLock<Regex> =
272 LazyLock::new(|| Regex::new(r"^\s*pub (?:r#)?(\w+):").expect("valid regex"));
273 static RENAME: LazyLock<Regex> =
274 LazyLock::new(|| Regex::new(r#"rename = "[^"]*""#).expect("valid regex"));
275 static ALIAS: LazyLock<Regex> =
276 LazyLock::new(|| Regex::new(r#"alias = "[^"]*""#).expect("valid regex"));
277 static SKIP_IF: LazyLock<Regex> =
278 LazyLock::new(|| Regex::new(r#"skip_serializing_if = "[^"]*",?\s*"#).expect("valid regex"));
279
280 let lines: Vec<&str> = src.lines().collect();
281 let mut out: Vec<String> = Vec::with_capacity(lines.len());
282 let mut message = None;
283 for (i, line) in lines.iter().enumerate() {
284 if let Some(caps) = STRUCT.captures(line) {
285 message = Some(caps[1].to_string());
286 } else if let (Some(message), Some(caps)) = (&message, FIELD.captures(line)) {
287 let name = &caps[1];
288 let key = format!("{message}.{name}");
289 let json = fields.json_names.get(&key);
290 let emit = fields.emit_unpopulated.contains(&key);
291 if json.is_some() || emit {
292 let attrs = attribute_lines(&lines, i);
293 let mut text = lines[attrs.clone()].join("\n");
294 if let Some(json) = json {
295 text = RENAME
296 .replace(&text, format!(r#"rename = "{json}""#))
297 .into_owned();
298 text = ALIAS
299 .replace(&text, format!(r#"alias = "{name}""#))
300 .into_owned();
301 }
302 if emit {
303 text = SKIP_IF.replace(&text, "").into_owned();
304 }
305 out.truncate(out.len() - attrs.len());
306 out.extend(text.lines().map(str::to_string));
307 }
308 }
309 out.push(line.to_string());
310 }
311 join_lines_like(&out, src)
312}
313
314fn join_lines_like(lines: &[String], src: &str) -> String {
316 let mut out = lines.join("\n");
317 if src.ends_with('\n') {
318 out.push('\n');
319 }
320 out
321}
322
323fn int64_containers_to(src: &str, module: &str) -> String {
326 let lines: Vec<&str> = src.lines().collect();
327 let mut out: Vec<String> = lines.iter().map(|l| l.to_string()).collect();
328 for i in 0..lines.len() {
329 let trimmed = lines[i].trim_start();
330 if !trimmed.starts_with("pub ") || !trimmed.contains(':') && !trimmed.ends_with('<') {
331 continue;
332 }
333 let (decl, _) = declaration(&lines, i);
334 let int64 = |ty: &str| ty == "i64" || ty == "u64";
335 let (from, to) = if decl
336 .split_once("Vec<")
337 .is_some_and(|(_, rest)| int64(rest.split('>').next().unwrap_or_default()))
338 {
339 ("::buffa::json_helpers::proto_seq", "repeated_int64")
340 } else if decl
341 .split_once("HashMap<")
342 .and_then(|(_, rest)| rest.rsplit_once('>'))
343 .and_then(|(args, _)| args.rsplit_once(','))
344 .is_some_and(|(_, value)| int64(value.trim()))
345 {
346 ("::buffa::json_helpers::proto_map", "map_int64")
347 } else {
348 continue;
349 };
350 for j in attribute_lines(&lines, i) {
351 out[j] = out[j].replace(from, &format!("{module}::{to}"));
352 }
353 }
354 join_lines_like(&out, src)
355}
356
357fn declaration(lines: &[&str], start: usize) -> (String, usize) {
360 let mut end = start;
361 let mut decl = String::new();
362 loop {
363 decl.push_str(lines[end].trim());
364 decl.push(' ');
365 let depth = decl.matches('<').count() as isize - decl.matches('>').count() as isize;
366 if (depth <= 0 && lines[end].trim_end().ends_with(',')) || end + 1 == lines.len() {
367 break;
368 }
369 end += 1;
370 }
371 let decl = decl
372 .replace(", >", ">")
373 .replace("< ", "<")
374 .replace(" >", ">");
375 (decl, end)
376}
377
378fn json_value_serializers(src: &str, module: &str) -> String {
381 const JSON: &str = "::buffa_types::google::protobuf::";
382 let lines: Vec<&str> = src.lines().collect();
383 let mut out = String::with_capacity(src.len() + 256);
384 let mut i = 0;
385 while i < lines.len() {
386 let line = lines[i];
387 let trimmed = line.trim_start();
388 let is_field = trimmed.starts_with("pub ");
389 let json_field_on_one_line = is_field && trimmed.contains(": ") && line.contains(JSON);
390 let field_type_continues = is_field && trimmed.ends_with('<');
392 if json_field_on_one_line || field_type_continues {
393 let (decl, end) = declaration(&lines, i);
394 let kind = if !decl.contains(JSON) {
395 None
396 } else if decl.contains(": ::buffa::MessageField<") {
397 Some("message_field")
398 } else if decl.contains(": ::buffa::alloc::vec::Vec<") {
399 Some("repeated")
400 } else if decl.contains("HashMap<") {
401 Some("map")
402 } else {
403 None
404 };
405 let attrs = field_attributes(&lines, i);
406 if let Some(kind) = kind
407 && !has_serde_key(&attrs, "serialize_with")
408 && !has_serde_key(&attrs, "with")
409 {
410 let indent = &line[..line.len() - trimmed.len()];
411 out.push_str(&format!(
412 "{indent}#[serde(serialize_with = \"{module}::{kind}\")]\n"
413 ));
414 }
415 for l in &lines[i..=end] {
416 out.push_str(l);
417 out.push('\n');
418 }
419 i = end + 1;
420 continue;
421 }
422 out.push_str(line);
423 out.push('\n');
424 i += 1;
425 }
426 if !src.ends_with('\n') {
427 out.pop();
428 }
429 out
430}
431
432fn has_serde_key(attrs: &str, key: &str) -> bool {
435 attrs.match_indices(key).any(|(at, _)| {
436 let before = attrs[..at].chars().next_back();
437 let after = attrs[at + key.len()..].trim_start();
438 !before.is_some_and(|c| c.is_ascii_alphanumeric() || c == '_') && after.starts_with('=')
439 })
440}
441
442fn field_attributes(lines: &[&str], field: usize) -> String {
444 attribute_lines(lines, field)
445 .map(|j| lines[j].trim())
446 .collect()
447}
448
449fn attribute_lines(lines: &[&str], field: usize) -> std::ops::Range<usize> {
451 let mut j = field;
452 while j > 0 {
453 let prev = lines[j - 1].trim();
454 if prev.starts_with("//") || prev.starts_with("#[") && prev.ends_with(']') {
455 j -= 1;
456 } else if prev.ends_with(")]") {
457 let mut k = j - 1;
459 while k > 0 && !lines[k].trim_start().starts_with("#[") {
460 k -= 1;
461 }
462 j = k;
463 } else {
464 break;
465 }
466 }
467 j..field
468}
469
470fn camel_to_snake(name: &str) -> String {
472 let mut out = String::with_capacity(name.len() + 4);
473 for ch in name.chars() {
474 if ch.is_ascii_uppercase() {
475 out.push('_');
476 out.push(ch.to_ascii_lowercase());
477 } else {
478 out.push(ch);
479 }
480 }
481 out
482}
483
484pub fn space_items(src: &str) -> String {
488 let lines: Vec<&str> = src.lines().collect();
489 let mut out = String::with_capacity(src.len() + src.len() / 16);
490 for (i, line) in lines.iter().enumerate() {
491 out.push_str(line);
492 out.push('\n');
493 if lines
494 .get(i + 1)
495 .is_some_and(|next| blank_between(line, next))
496 {
497 out.push('\n');
498 }
499 }
500 out
501}
502
503fn blank_between(line: &str, next: &str) -> bool {
505 let indent = |l: &str| l.len() - l.trim_start().len();
506 if line.trim().is_empty() || next.trim().is_empty() || indent(line) != indent(next) {
507 return false;
508 }
509 let (line, next) = (line.trim(), next.trim());
510 let is_use = |l: &str| {
511 ["use ", "pub use ", "pub(crate) use ", "pub(super) use "]
512 .iter()
513 .any(|p| l.starts_with(p))
514 };
515 let is_inner = |l: &str| l.starts_with("//!") || l.starts_with("#![");
516 if is_inner(line) {
517 return !is_inner(next);
518 }
519 if !starts_item(next) {
520 return false;
521 }
522 match line {
523 "}" => true,
524 "};" => !is_use(next),
526 _ => is_use(line) && line.ends_with(';') && !is_use(next),
527 }
528}
529
530fn starts_item(line: &str) -> bool {
532 const STARTS: &[&str] = &[
533 "#[",
534 "///",
535 "/**",
536 "pub ",
537 "pub(",
538 "fn ",
539 "async fn ",
540 "const fn ",
541 "unsafe ",
542 "extern ",
543 "impl ",
544 "impl<",
545 "struct ",
546 "enum ",
547 "trait ",
548 "mod ",
549 "type ",
550 "const ",
551 "static ",
552 "use ",
553 "macro_rules!",
554 "include!",
555 ];
556 STARTS.iter().any(|p| line.starts_with(p))
557}
558
559#[cfg(test)]
560mod tests {
561 use super::*;
562
563 fn proto() -> ModelRewrites {
564 ModelRewrites {
565 field_names: JsonFieldNames::Proto,
566 ..Default::default()
567 }
568 }
569
570 #[test]
571 fn camel_to_snake_conversions() {
572 assert_eq!(camel_to_snake("displayName"), "display_name");
573 assert_eq!(camel_to_snake("nextPageToken"), "next_page_token");
574 assert_eq!(camel_to_snake("name"), "name");
575 }
576
577 #[test]
578 fn snakeifies_view_serialize_entry_literal() {
579 let src = r#" __map.serialize_entry("displayName", self.display_name)?;"#;
580 let out = proto().rewrite("demo.v1.rs", src);
581 assert!(out.contains(r#"serialize_entry("display_name", self.display_name)"#));
582 }
583
584 #[test]
585 fn view_serialize_entry_explicit_names_untouched() {
586 for src in [
587 r#"__map.serialize_entry("name", self.name)?;"#,
588 r#"__map.serialize_entry("metadata-location", self.metadata_location)?;"#,
589 r#"__map.serialize_entry("Resources", self.resources)?;"#,
590 ] {
591 assert_eq!(proto().rewrite("demo.v1.rs", src), src);
592 }
593 }
594
595 #[test]
596 fn swaps_multiword_rename_alias_pair() {
597 let src = r#" #[serde(
598 rename = "storageRoot",
599 alias = "storage_root",
600 skip_serializing_if = "::core::option::Option::is_none"
601 )]
602 pub storage_root: Option<String>,"#;
603 let out = swap_serde_renames(src);
604 assert!(out.contains(r#"rename = "storage_root""#));
605 assert!(out.contains(r#"alias = "storageRoot""#));
606 assert!(out.contains(r#"skip_serializing_if = "::core::option::Option::is_none""#));
607 }
608
609 #[test]
610 fn single_word_rename_only_is_untouched() {
611 let src = r#" #[serde(rename = "name", skip_serializing_if = "Option::is_none")]"#;
612 assert_eq!(swap_serde_renames(src), src);
613 }
614
615 #[test]
616 fn explicit_json_name_is_kept() {
617 let src = r#"#[serde(rename = "metadata-location", alias = "metadata_location")]"#;
619 assert_eq!(swap_serde_renames(src), src);
620 let src = r#"#[serde(rename = "Resources", alias = "resources")]"#;
621 assert_eq!(swap_serde_renames(src), src);
622 }
623
624 #[test]
625 fn enum_and_scalar_with_helpers_preserved() {
626 let src = r#" #[serde(
627 rename = "catalogType",
628 alias = "catalog_type",
629 with = "::buffa::json_helpers::opt_enum",
630 skip_serializing_if = "::core::option::Option::is_none"
631 )]"#;
632 let out = swap_serde_renames(src);
633 assert!(out.contains(r#"rename = "catalog_type""#));
634 assert!(out.contains(r#"alias = "catalogType""#));
635 assert!(out.contains(r#"with = "::buffa::json_helpers::opt_enum""#));
636 }
637
638 #[test]
639 fn swap_is_idempotent_on_already_swapped_output() {
640 let camel = r#" #[serde(rename = "storageRoot", alias = "storage_root", skip_serializing_if = "x")]"#;
641 let once = swap_serde_renames(camel);
642 assert!(once.contains(r#"rename = "storage_root", alias = "storageRoot""#));
643 assert_eq!(swap_serde_renames(&once), once);
644 }
645
646 #[test]
647 fn non_field_serde_attr_is_untouched() {
648 assert_eq!(swap_serde_renames("#[serde(default)]"), "#[serde(default)]");
649 }
650
651 #[test]
652 fn package_override_keeps_camel_case() {
653 let mut rewrites = proto();
654 rewrites
655 .packages
656 .insert("demo.scim.v2".into(), JsonFieldNames::Json);
657 let src = r#"#[serde(rename = "userName", alias = "user_name")]"#;
658 assert_eq!(rewrites.rewrite("demo.scim.v2.rs", src), src);
659 assert_eq!(rewrites.rewrite("demo.scim.v2.users.rs", src), src);
660 assert!(
661 rewrites
662 .rewrite("demo.v1.rs", src)
663 .contains(r#"rename = "user_name""#)
664 );
665 }
666
667 #[test]
668 fn field_options_set_json_names_and_emit_unpopulated_fields() {
669 let src = r#"pub struct Spec {
670 #[serde(
671 rename = "fields",
672 skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_vec",
673 deserialize_with = "::buffa::json_helpers::null_as_default"
674 )]
675 pub fields: ::buffa::alloc::vec::Vec<Field>,
676 #[serde(rename = "spec_id", alias = "specId")]
677 pub spec_id: i32,
678}
679pub struct Other {
680 #[serde(rename = "fields", skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_vec")]
681 pub fields: ::buffa::alloc::vec::Vec<Field>,
682}
683"#;
684 let fields = PackageFields {
685 json_names: [("Spec.spec_id".to_string(), "specId".to_string())].into(),
686 emit_unpopulated: ["Spec.fields".to_string()].into(),
687 };
688 let out = apply_field_options(src, &fields);
689 assert!(
690 out.contains(r#"rename = "specId", alias = "spec_id""#),
691 "{out}"
692 );
693 assert_eq!(out.matches("skip_serializing_if").count(), 1, "{out}");
695 assert!(
696 out.contains("rename = \"fields\",\n deserialize_with"),
697 "{out}"
698 );
699 }
700
701 #[test]
702 fn int64_containers_write_numbers() {
703 let src = r#" #[serde(rename = "sizes", with = "::buffa::json_helpers::proto_seq")]
704 pub sizes: ::buffa::alloc::vec::Vec<i64>,
705 #[serde(
706 rename = "counts",
707 with = "::buffa::json_helpers::proto_map"
708 )]
709 pub counts: ::buffa::__private::HashMap<
710 ::buffa::alloc::string::String,
711 i64,
712 >,
713 #[serde(rename = "ratios", with = "::buffa::json_helpers::proto_seq")]
714 pub ratios: ::buffa::alloc::vec::Vec<f64>,
715"#;
716 let out = int64_containers_to(src, "crate::models::json_int64");
717 assert!(out.contains(r#"with = "crate::models::json_int64::repeated_int64""#));
718 assert!(out.contains(r#"with = "crate::models::json_int64::map_int64""#));
719 assert_eq!(
720 out.matches("::buffa::json_helpers::proto_seq").count(),
721 1,
722 "{out}"
723 );
724 assert!(out.ends_with(",\n"));
725 }
726
727 #[test]
728 fn int64_helpers_point_at_module() {
729 let rewrites = ModelRewrites {
730 int64_module: Some("crate::models::json_int64".into()),
731 ..Default::default()
732 };
733 let src = r#"with = "::buffa::json_helpers::opt_int64",
734 ::buffa::json_helpers::int64::serialize(&self.0, __s)
735 with = "::buffa::json_helpers::uint64",
736 with = "::buffa::json_helpers::int32","#;
737 let out = rewrites.rewrite("demo.v1.rs", src);
738 assert!(out.contains(r#"with = "crate::models::json_int64::opt_int64""#));
739 assert!(out.contains("crate::models::json_int64::int64::serialize(&self.0, __s)"));
740 assert!(out.contains(r#"with = "crate::models::json_int64::uint64""#));
741 assert!(out.contains(r#"with = "::buffa::json_helpers::int32""#));
742 assert_eq!(rewrites.rewrite("demo.v1.rs", &out), out);
744 }
745
746 #[test]
747 fn rewrite_dir_leaves_trestle_files_alone() {
748 let rewrites = ModelRewrites {
749 int64_module: Some("crate::models::json_int64".into()),
750 ..Default::default()
751 };
752 let dir = tempfile::tempdir().unwrap();
753 let src = r#"with = "::buffa::json_helpers::int64","#;
754 std::fs::create_dir(dir.path().join("extractors")).unwrap();
755 for file in ["demo.v1.rs", "mod.rs", "extractors/mod.rs"] {
756 std::fs::write(dir.path().join(file), src).unwrap();
757 }
758 assert_eq!(rewrites.rewrite_dir(dir.path()).unwrap(), 1);
759 let read = |file: &str| std::fs::read_to_string(dir.path().join(file)).unwrap();
760 assert!(read("demo.v1.rs").contains("crate::models::json_int64::int64"));
761 assert_eq!(read("mod.rs"), src);
762 assert_eq!(read("extractors/mod.rs"), src);
763 }
764
765 #[test]
766 fn space_items_separates_items_and_functions() {
767 let src = "//! Docs.
768use a::B;
769use c::D;
770/// A thing.
771pub struct S {
772 x: u8,
773}
774impl S {
775 fn a(&self) {
776 if true {
777 f();
778 }
779 g();
780 }
781 #[inline]
782 fn b(&self) -> u8 {
783 match self.x {
784 0 => {
785 1
786 }
787 _ => 2,
788 }
789 }
790}
791";
792 let spaced = space_items(src);
793 assert_eq!(
794 spaced,
795 "//! Docs.
796
797use a::B;
798use c::D;
799
800/// A thing.
801pub struct S {
802 x: u8,
803}
804
805impl S {
806 fn a(&self) {
807 if true {
808 f();
809 }
810 g();
811 }
812
813 #[inline]
814 fn b(&self) -> u8 {
815 match self.x {
816 0 => {
817 1
818 }
819 _ => 2,
820 }
821 }
822}
823"
824 );
825 assert_eq!(space_items(&spaced), spaced);
826 }
827
828 #[test]
829 fn json_value_fields_serialize_through_the_module() {
830 let rewrites = ModelRewrites {
831 json_value_module: Some("crate::models::json_value".into()),
832 ..Default::default()
833 };
834 let src = r#" #[serde(rename = "attributes")]
835 pub attributes: ::buffa::MessageField<::buffa_types::google::protobuf::Struct>,
836 #[serde(rename = "notes", deserialize_with = "::buffa::json_helpers::null_as_default")]
837 pub notes: ::buffa::alloc::vec::Vec<::buffa_types::google::protobuf::Value>,
838 #[serde(rename = "rows")]
839 pub rows: ::buffa::__private::HashMap<
840 ::buffa::alloc::string::String,
841 ::buffa_types::google::protobuf::ListValue,
842 >,
843 #[serde(rename = "name")]
844 pub name: ::buffa::alloc::string::String,
845"#;
846 let out = rewrites.rewrite("demo.v1.rs", src);
847 for kind in ["message_field", "repeated", "map"] {
848 assert!(
849 out.contains(&format!(
850 "#[serde(serialize_with = \"crate::models::json_value::{kind}\")]"
851 )),
852 "{out}"
853 );
854 }
855 assert_eq!(
856 out.matches("serialize_with = \"crate::models").count(),
857 3,
858 "{out}"
859 );
860 assert!(out.contains(" pub rows: ::buffa::__private::HashMap<\n"));
861 }
862}