Skip to main content

alef_core/config/
output.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value as JsonValue;
3use std::collections::HashMap;
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Default, Serialize, Deserialize)]
7pub struct ExcludeConfig {
8    #[serde(default)]
9    pub types: Vec<String>,
10    #[serde(default)]
11    pub functions: Vec<String>,
12    /// Exclude specific methods: "TypeName.method_name"
13    #[serde(default)]
14    pub methods: Vec<String>,
15}
16
17#[derive(Debug, Clone, Default, Serialize, Deserialize)]
18pub struct IncludeConfig {
19    #[serde(default)]
20    pub types: Vec<String>,
21    #[serde(default)]
22    pub functions: Vec<String>,
23}
24
25#[derive(Debug, Clone, Default, Serialize, Deserialize)]
26pub struct OutputConfig {
27    pub python: Option<PathBuf>,
28    pub node: Option<PathBuf>,
29    pub ruby: Option<PathBuf>,
30    pub php: Option<PathBuf>,
31    pub elixir: Option<PathBuf>,
32    pub wasm: Option<PathBuf>,
33    pub ffi: Option<PathBuf>,
34    pub go: Option<PathBuf>,
35    pub java: Option<PathBuf>,
36    pub csharp: Option<PathBuf>,
37    pub r: Option<PathBuf>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ScaffoldConfig {
42    pub description: Option<String>,
43    pub license: Option<String>,
44    pub repository: Option<String>,
45    pub homepage: Option<String>,
46    #[serde(default)]
47    pub authors: Vec<String>,
48    #[serde(default)]
49    pub keywords: Vec<String>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct ReadmeConfig {
54    pub template_dir: Option<PathBuf>,
55    pub snippets_dir: Option<PathBuf>,
56    /// Deprecated: path to an external YAML config file. Prefer inline fields below.
57    pub config: Option<PathBuf>,
58    pub output_pattern: Option<String>,
59    /// Discord invite URL used in README templates.
60    pub discord_url: Option<String>,
61    /// Banner image URL used in README templates.
62    pub banner_url: Option<String>,
63    /// Per-language README configuration, keyed by language code
64    /// (e.g. "python", "typescript", "ruby"). Values are flexible JSON objects
65    /// that map directly to minijinja template context variables.
66    #[serde(default)]
67    pub languages: HashMap<String, JsonValue>,
68}
69
70/// A value that can be either a single string or a list of strings.
71///
72/// Deserializes from both `"cmd"` and `["cmd1", "cmd2"]` in TOML/JSON.
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
74#[serde(untagged)]
75pub enum StringOrVec {
76    Single(String),
77    Multiple(Vec<String>),
78}
79
80impl StringOrVec {
81    /// Return all commands as a slice-like iterator.
82    pub fn commands(&self) -> Vec<&str> {
83        match self {
84            StringOrVec::Single(s) => vec![s.as_str()],
85            StringOrVec::Multiple(v) => v.iter().map(String::as_str).collect(),
86        }
87    }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
91pub struct LintConfig {
92    /// Shell command that must exit 0 for lint to run; skip with warning on failure.
93    pub precondition: Option<String>,
94    /// Command(s) to run before the main lint commands; aborts on failure.
95    pub before: Option<StringOrVec>,
96    pub format: Option<StringOrVec>,
97    pub check: Option<StringOrVec>,
98    pub typecheck: Option<StringOrVec>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
102pub struct UpdateConfig {
103    /// Shell command that must exit 0 for update to run; skip with warning on failure.
104    pub precondition: Option<String>,
105    /// Command(s) to run before the main update commands; aborts on failure.
106    pub before: Option<StringOrVec>,
107    /// Command(s) for safe dependency updates (compatible versions only).
108    pub update: Option<StringOrVec>,
109    /// Command(s) for aggressive updates (including incompatible/major bumps).
110    pub upgrade: Option<StringOrVec>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
114pub struct TestConfig {
115    /// Shell command that must exit 0 for test to run; skip with warning on failure.
116    pub precondition: Option<String>,
117    /// Command(s) to run before the main test commands; aborts on failure.
118    pub before: Option<StringOrVec>,
119    /// Command to run unit/integration tests for this language.
120    pub command: Option<StringOrVec>,
121    /// Command to run e2e tests for this language.
122    pub e2e: Option<StringOrVec>,
123    /// Command to run tests with coverage for this language.
124    pub coverage: Option<StringOrVec>,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
128pub struct SetupConfig {
129    /// Shell command that must exit 0 for setup to run; skip with warning on failure.
130    pub precondition: Option<String>,
131    /// Command(s) to run before the main setup commands; aborts on failure.
132    pub before: Option<StringOrVec>,
133    /// Command(s) to install dependencies for this language.
134    pub install: Option<StringOrVec>,
135    /// Timeout in seconds for the complete setup (precondition + before + install).
136    #[serde(default = "default_setup_timeout")]
137    pub timeout_seconds: u64,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
141pub struct CleanConfig {
142    /// Shell command that must exit 0 for clean to run; skip with warning on failure.
143    pub precondition: Option<String>,
144    /// Command(s) to run before the main clean commands; aborts on failure.
145    pub before: Option<StringOrVec>,
146    /// Command(s) to clean build artifacts for this language.
147    pub clean: Option<StringOrVec>,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
151pub struct BuildCommandConfig {
152    /// Shell command that must exit 0 for build to run; skip with warning on failure.
153    pub precondition: Option<String>,
154    /// Command(s) to run before the main build commands; aborts on failure.
155    pub before: Option<StringOrVec>,
156    /// Command(s) to build in debug mode.
157    pub build: Option<StringOrVec>,
158    /// Command(s) to build in release mode.
159    pub build_release: Option<StringOrVec>,
160}
161
162fn default_setup_timeout() -> u64 {
163    600
164}
165
166/// A single text replacement rule for version sync.
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct TextReplacement {
169    /// Glob pattern for files to process.
170    pub path: String,
171    /// Regex pattern to search for (may contain `{version}` placeholder).
172    pub search: String,
173    /// Replacement string (may contain `{version}` placeholder).
174    pub replace: String,
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn string_or_vec_single_from_toml() {
183        let toml_str = r#"format = "ruff format""#;
184        #[derive(Deserialize)]
185        struct T {
186            format: StringOrVec,
187        }
188        let t: T = toml::from_str(toml_str).unwrap();
189        assert_eq!(t.format.commands(), vec!["ruff format"]);
190    }
191
192    #[test]
193    fn string_or_vec_multiple_from_toml() {
194        let toml_str = r#"format = ["cmd1", "cmd2", "cmd3"]"#;
195        #[derive(Deserialize)]
196        struct T {
197            format: StringOrVec,
198        }
199        let t: T = toml::from_str(toml_str).unwrap();
200        assert_eq!(t.format.commands(), vec!["cmd1", "cmd2", "cmd3"]);
201    }
202
203    #[test]
204    fn lint_config_backward_compat_string() {
205        let toml_str = r#"
206format = "ruff format ."
207check = "ruff check ."
208typecheck = "mypy ."
209"#;
210        let cfg: LintConfig = toml::from_str(toml_str).unwrap();
211        assert_eq!(cfg.format.unwrap().commands(), vec!["ruff format ."]);
212        assert_eq!(cfg.check.unwrap().commands(), vec!["ruff check ."]);
213        assert_eq!(cfg.typecheck.unwrap().commands(), vec!["mypy ."]);
214    }
215
216    #[test]
217    fn lint_config_array_commands() {
218        let toml_str = r#"
219format = ["cmd1", "cmd2"]
220check = "single-check"
221"#;
222        let cfg: LintConfig = toml::from_str(toml_str).unwrap();
223        assert_eq!(cfg.format.unwrap().commands(), vec!["cmd1", "cmd2"]);
224        assert_eq!(cfg.check.unwrap().commands(), vec!["single-check"]);
225        assert!(cfg.typecheck.is_none());
226    }
227
228    #[test]
229    fn lint_config_all_optional() {
230        let toml_str = "";
231        let cfg: LintConfig = toml::from_str(toml_str).unwrap();
232        assert!(cfg.format.is_none());
233        assert!(cfg.check.is_none());
234        assert!(cfg.typecheck.is_none());
235    }
236
237    #[test]
238    fn update_config_from_toml() {
239        let toml_str = r#"
240update = "cargo update"
241upgrade = ["cargo upgrade --incompatible", "cargo update"]
242"#;
243        let cfg: UpdateConfig = toml::from_str(toml_str).unwrap();
244        assert_eq!(cfg.update.unwrap().commands(), vec!["cargo update"]);
245        assert_eq!(
246            cfg.upgrade.unwrap().commands(),
247            vec!["cargo upgrade --incompatible", "cargo update"]
248        );
249    }
250
251    #[test]
252    fn update_config_all_optional() {
253        let toml_str = "";
254        let cfg: UpdateConfig = toml::from_str(toml_str).unwrap();
255        assert!(cfg.update.is_none());
256        assert!(cfg.upgrade.is_none());
257    }
258
259    #[test]
260    fn string_or_vec_empty_array_from_toml() {
261        let toml_str = "format = []";
262        #[derive(Deserialize)]
263        struct T {
264            format: StringOrVec,
265        }
266        let t: T = toml::from_str(toml_str).unwrap();
267        assert!(matches!(t.format, StringOrVec::Multiple(_)));
268        assert!(t.format.commands().is_empty());
269    }
270
271    #[test]
272    fn string_or_vec_single_element_array_from_toml() {
273        let toml_str = r#"format = ["cmd"]"#;
274        #[derive(Deserialize)]
275        struct T {
276            format: StringOrVec,
277        }
278        let t: T = toml::from_str(toml_str).unwrap();
279        assert_eq!(t.format.commands(), vec!["cmd"]);
280    }
281
282    #[test]
283    fn setup_config_single_string() {
284        let toml_str = r#"install = "uv sync""#;
285        let cfg: SetupConfig = toml::from_str(toml_str).unwrap();
286        assert_eq!(cfg.install.unwrap().commands(), vec!["uv sync"]);
287    }
288
289    #[test]
290    fn setup_config_array_commands() {
291        let toml_str = r#"install = ["step1", "step2"]"#;
292        let cfg: SetupConfig = toml::from_str(toml_str).unwrap();
293        assert_eq!(cfg.install.unwrap().commands(), vec!["step1", "step2"]);
294    }
295
296    #[test]
297    fn setup_config_all_optional() {
298        let toml_str = "";
299        let cfg: SetupConfig = toml::from_str(toml_str).unwrap();
300        assert!(cfg.install.is_none());
301    }
302
303    #[test]
304    fn clean_config_single_string() {
305        let toml_str = r#"clean = "rm -rf dist""#;
306        let cfg: CleanConfig = toml::from_str(toml_str).unwrap();
307        assert_eq!(cfg.clean.unwrap().commands(), vec!["rm -rf dist"]);
308    }
309
310    #[test]
311    fn clean_config_array_commands() {
312        let toml_str = r#"clean = ["step1", "step2"]"#;
313        let cfg: CleanConfig = toml::from_str(toml_str).unwrap();
314        assert_eq!(cfg.clean.unwrap().commands(), vec!["step1", "step2"]);
315    }
316
317    #[test]
318    fn clean_config_all_optional() {
319        let toml_str = "";
320        let cfg: CleanConfig = toml::from_str(toml_str).unwrap();
321        assert!(cfg.clean.is_none());
322    }
323
324    #[test]
325    fn build_command_config_single_strings() {
326        let toml_str = r#"
327build = "cargo build"
328build_release = "cargo build --release"
329"#;
330        let cfg: BuildCommandConfig = toml::from_str(toml_str).unwrap();
331        assert_eq!(cfg.build.unwrap().commands(), vec!["cargo build"]);
332        assert_eq!(cfg.build_release.unwrap().commands(), vec!["cargo build --release"]);
333    }
334
335    #[test]
336    fn build_command_config_array_commands() {
337        let toml_str = r#"
338build = ["step1", "step2"]
339build_release = ["step1 --release", "step2 --release"]
340"#;
341        let cfg: BuildCommandConfig = toml::from_str(toml_str).unwrap();
342        assert_eq!(cfg.build.unwrap().commands(), vec!["step1", "step2"]);
343        assert_eq!(
344            cfg.build_release.unwrap().commands(),
345            vec!["step1 --release", "step2 --release"]
346        );
347    }
348
349    #[test]
350    fn build_command_config_all_optional() {
351        let toml_str = "";
352        let cfg: BuildCommandConfig = toml::from_str(toml_str).unwrap();
353        assert!(cfg.build.is_none());
354        assert!(cfg.build_release.is_none());
355    }
356
357    #[test]
358    fn test_config_backward_compat_string() {
359        let toml_str = r#"command = "pytest""#;
360        let cfg: TestConfig = toml::from_str(toml_str).unwrap();
361        assert_eq!(cfg.command.unwrap().commands(), vec!["pytest"]);
362        assert!(cfg.e2e.is_none());
363        assert!(cfg.coverage.is_none());
364    }
365
366    #[test]
367    fn test_config_array_command() {
368        let toml_str = r#"command = ["cmd1", "cmd2"]"#;
369        let cfg: TestConfig = toml::from_str(toml_str).unwrap();
370        assert_eq!(cfg.command.unwrap().commands(), vec!["cmd1", "cmd2"]);
371    }
372
373    #[test]
374    fn test_config_with_coverage() {
375        let toml_str = r#"
376command = "pytest"
377coverage = "pytest --cov=. --cov-report=term-missing"
378"#;
379        let cfg: TestConfig = toml::from_str(toml_str).unwrap();
380        assert_eq!(cfg.command.unwrap().commands(), vec!["pytest"]);
381        assert_eq!(
382            cfg.coverage.unwrap().commands(),
383            vec!["pytest --cov=. --cov-report=term-missing"]
384        );
385        assert!(cfg.e2e.is_none());
386    }
387
388    #[test]
389    fn test_config_all_optional() {
390        let toml_str = "";
391        let cfg: TestConfig = toml::from_str(toml_str).unwrap();
392        assert!(cfg.command.is_none());
393        assert!(cfg.e2e.is_none());
394        assert!(cfg.coverage.is_none());
395    }
396
397    #[test]
398    fn full_alef_toml_with_lint_and_update() {
399        let toml_str = r#"
400languages = ["python", "node"]
401
402[crate]
403name = "test"
404sources = ["src/lib.rs"]
405
406[lint.python]
407format = "ruff format ."
408check = "ruff check --fix ."
409
410[lint.node]
411format = ["npx oxfmt", "npx oxlint --fix"]
412
413[update.python]
414update = "uv sync --upgrade"
415upgrade = "uv sync --all-packages --all-extras --upgrade"
416
417[update.node]
418update = "pnpm up -r"
419upgrade = ["corepack up", "pnpm up --latest -r -w"]
420"#;
421        let cfg: super::super::AlefConfig = toml::from_str(toml_str).unwrap();
422        let lint_map = cfg.lint.as_ref().unwrap();
423        assert!(lint_map.contains_key("python"));
424        assert!(lint_map.contains_key("node"));
425
426        let py_lint = lint_map.get("python").unwrap();
427        assert_eq!(py_lint.format.as_ref().unwrap().commands(), vec!["ruff format ."]);
428
429        let node_lint = lint_map.get("node").unwrap();
430        assert_eq!(
431            node_lint.format.as_ref().unwrap().commands(),
432            vec!["npx oxfmt", "npx oxlint --fix"]
433        );
434
435        let update_map = cfg.update.as_ref().unwrap();
436        assert!(update_map.contains_key("python"));
437        assert!(update_map.contains_key("node"));
438
439        let node_update = update_map.get("node").unwrap();
440        assert_eq!(node_update.update.as_ref().unwrap().commands(), vec!["pnpm up -r"]);
441        assert_eq!(
442            node_update.upgrade.as_ref().unwrap().commands(),
443            vec!["corepack up", "pnpm up --latest -r -w"]
444        );
445    }
446
447    #[test]
448    fn lint_config_with_precondition_and_before() {
449        let toml_str = r#"
450precondition = "test -f target/release/libfoo.so"
451before = "cargo build --release -p foo-ffi"
452format = "gofmt -w packages/go"
453check = "golangci-lint run ./..."
454"#;
455        let cfg: LintConfig = toml::from_str(toml_str).unwrap();
456        assert_eq!(cfg.precondition.as_deref(), Some("test -f target/release/libfoo.so"));
457        assert_eq!(cfg.before.unwrap().commands(), vec!["cargo build --release -p foo-ffi"]);
458        assert!(cfg.format.is_some());
459        assert!(cfg.check.is_some());
460    }
461
462    #[test]
463    fn test_config_with_before_list() {
464        let toml_str = r#"
465before = ["cd packages/python && maturin develop", "echo ready"]
466command = "pytest"
467"#;
468        let cfg: TestConfig = toml::from_str(toml_str).unwrap();
469        assert!(cfg.precondition.is_none());
470        assert_eq!(
471            cfg.before.unwrap().commands(),
472            vec!["cd packages/python && maturin develop", "echo ready"]
473        );
474        assert_eq!(cfg.command.unwrap().commands(), vec!["pytest"]);
475    }
476
477    #[test]
478    fn setup_config_with_precondition() {
479        let toml_str = r#"
480precondition = "which rustup"
481install = "rustup update"
482"#;
483        let cfg: SetupConfig = toml::from_str(toml_str).unwrap();
484        assert_eq!(cfg.precondition.as_deref(), Some("which rustup"));
485        assert!(cfg.before.is_none());
486        assert!(cfg.install.is_some());
487    }
488
489    #[test]
490    fn build_command_config_with_before() {
491        let toml_str = r#"
492before = "cargo build --release -p my-lib-ffi"
493build = "cd packages/go && go build ./..."
494"#;
495        let cfg: BuildCommandConfig = toml::from_str(toml_str).unwrap();
496        assert!(cfg.precondition.is_none());
497        assert_eq!(
498            cfg.before.unwrap().commands(),
499            vec!["cargo build --release -p my-lib-ffi"]
500        );
501        assert!(cfg.build.is_some());
502    }
503
504    #[test]
505    fn clean_config_precondition_and_before_optional() {
506        let toml_str = r#"clean = "cargo clean""#;
507        let cfg: CleanConfig = toml::from_str(toml_str).unwrap();
508        assert!(cfg.precondition.is_none());
509        assert!(cfg.before.is_none());
510        assert!(cfg.clean.is_some());
511    }
512
513    #[test]
514    fn update_config_with_precondition() {
515        let toml_str = r#"
516precondition = "test -f Cargo.lock"
517update = "cargo update"
518"#;
519        let cfg: UpdateConfig = toml::from_str(toml_str).unwrap();
520        assert_eq!(cfg.precondition.as_deref(), Some("test -f Cargo.lock"));
521        assert!(cfg.before.is_none());
522        assert!(cfg.update.is_some());
523    }
524
525    #[test]
526    fn full_alef_toml_with_precondition_and_before_across_sections() {
527        let toml_str = r#"
528languages = ["go", "python"]
529
530[crate]
531name = "mylib"
532sources = ["src/lib.rs"]
533
534[lint.go]
535precondition = "test -f target/release/libmylib_ffi.so"
536before = "cargo build --release -p mylib-ffi"
537format = "gofmt -w packages/go"
538check = "golangci-lint run ./..."
539
540[lint.python]
541format = "ruff format packages/python"
542check = "ruff check --fix packages/python"
543
544[test.go]
545precondition = "test -f target/release/libmylib_ffi.so"
546before = ["cargo build --release -p mylib-ffi", "cp target/release/libmylib_ffi.so packages/go/"]
547command = "cd packages/go && go test ./..."
548
549[test.python]
550command = "cd packages/python && uv run pytest"
551
552[build_commands.go]
553precondition = "which go"
554before = "cargo build --release -p mylib-ffi"
555build = "cd packages/go && go build ./..."
556build_release = "cd packages/go && go build -ldflags='-s -w' ./..."
557
558[update.go]
559precondition = "test -d packages/go"
560update = "cd packages/go && go get -u ./..."
561
562[setup.python]
563precondition = "which uv"
564install = "cd packages/python && uv sync"
565
566[clean.go]
567before = "echo cleaning go"
568clean = "cd packages/go && go clean -cache"
569"#;
570        let cfg: super::super::AlefConfig = toml::from_str(toml_str).unwrap();
571
572        // lint.go: precondition and before set
573        let lint_map = cfg.lint.as_ref().unwrap();
574        let go_lint = lint_map.get("go").unwrap();
575        assert_eq!(
576            go_lint.precondition.as_deref(),
577            Some("test -f target/release/libmylib_ffi.so"),
578            "lint.go precondition should be preserved"
579        );
580        assert_eq!(
581            go_lint.before.as_ref().unwrap().commands(),
582            vec!["cargo build --release -p mylib-ffi"],
583            "lint.go before should be preserved"
584        );
585        assert!(go_lint.format.is_some());
586        assert!(go_lint.check.is_some());
587
588        // lint.python: no precondition or before
589        let py_lint = lint_map.get("python").unwrap();
590        assert!(
591            py_lint.precondition.is_none(),
592            "lint.python should have no precondition"
593        );
594        assert!(py_lint.before.is_none(), "lint.python should have no before");
595
596        // test.go: precondition and multi-command before
597        let test_map = cfg.test.as_ref().unwrap();
598        let go_test = test_map.get("go").unwrap();
599        assert_eq!(
600            go_test.precondition.as_deref(),
601            Some("test -f target/release/libmylib_ffi.so"),
602            "test.go precondition should be preserved"
603        );
604        assert_eq!(
605            go_test.before.as_ref().unwrap().commands(),
606            vec![
607                "cargo build --release -p mylib-ffi",
608                "cp target/release/libmylib_ffi.so packages/go/"
609            ],
610            "test.go before list should be preserved"
611        );
612
613        // build_commands.go: precondition and before
614        let build_map = cfg.build_commands.as_ref().unwrap();
615        let go_build = build_map.get("go").unwrap();
616        assert_eq!(
617            go_build.precondition.as_deref(),
618            Some("which go"),
619            "build_commands.go precondition should be preserved"
620        );
621        assert_eq!(
622            go_build.before.as_ref().unwrap().commands(),
623            vec!["cargo build --release -p mylib-ffi"],
624            "build_commands.go before should be preserved"
625        );
626
627        // update.go: precondition only, no before
628        let update_map = cfg.update.as_ref().unwrap();
629        let go_update = update_map.get("go").unwrap();
630        assert_eq!(
631            go_update.precondition.as_deref(),
632            Some("test -d packages/go"),
633            "update.go precondition should be preserved"
634        );
635        assert!(go_update.before.is_none(), "update.go before should be None");
636
637        // setup.python: precondition only
638        let setup_map = cfg.setup.as_ref().unwrap();
639        let py_setup = setup_map.get("python").unwrap();
640        assert_eq!(
641            py_setup.precondition.as_deref(),
642            Some("which uv"),
643            "setup.python precondition should be preserved"
644        );
645        assert!(py_setup.before.is_none(), "setup.python before should be None");
646
647        // clean.go: before only, no precondition
648        let clean_map = cfg.clean.as_ref().unwrap();
649        let go_clean = clean_map.get("go").unwrap();
650        assert!(go_clean.precondition.is_none(), "clean.go precondition should be None");
651        assert_eq!(
652            go_clean.before.as_ref().unwrap().commands(),
653            vec!["echo cleaning go"],
654            "clean.go before should be preserved"
655        );
656    }
657}
658
659/// Configuration for the `sync-versions` command.
660#[derive(Debug, Clone, Serialize, Deserialize, Default)]
661pub struct SyncConfig {
662    /// Extra file paths to update version in (glob patterns).
663    #[serde(default)]
664    pub extra_paths: Vec<String>,
665    /// Arbitrary text replacements applied during version sync.
666    #[serde(default)]
667    pub text_replacements: Vec<TextReplacement>,
668}