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