oag_node_client/emitters/
scaffold.rs1use minijinja::{Environment, context};
2use oag_core::GeneratedFile;
3use oag_core::config::ToolSetting;
4use serde::Deserialize;
5
6#[derive(Debug, Clone, Default, Deserialize)]
8#[serde(default)]
9pub struct NodeScaffoldConfig {
10 pub package_name: Option<String>,
11 pub repository: Option<String>,
12 pub index: Option<bool>,
13 pub formatter: Option<ToolSetting>,
14 pub test_runner: Option<ToolSetting>,
15 pub bundler: Option<ToolSetting>,
16}
17
18#[derive(Debug, Clone)]
20pub struct ScaffoldOptions {
21 pub name: String,
23 pub package_name: Option<String>,
25 pub repository: Option<String>,
27 pub formatter: Option<String>,
29 pub test_runner: Option<String>,
31 pub bundler: Option<String>,
33 pub react: bool,
35}
36
37pub fn emit_scaffold(options: &ScaffoldOptions) -> Vec<GeneratedFile> {
39 let mut files = Vec::new();
40
41 files.push(GeneratedFile {
43 path: "package.json".to_string(),
44 content: emit_package_json(options),
45 });
46
47 files.push(GeneratedFile {
49 path: "tsconfig.json".to_string(),
50 content: emit_tsconfig(options),
51 });
52
53 if options.formatter.as_deref() == Some("biome") {
55 files.push(GeneratedFile {
56 path: "biome.json".to_string(),
57 content: emit_biome(),
58 });
59 }
60
61 if options.bundler.as_deref() == Some("tsdown") {
63 files.push(GeneratedFile {
64 path: "tsdown.config.ts".to_string(),
65 content: emit_tsdown(),
66 });
67 }
68
69 files
70}
71
72fn emit_package_json(options: &ScaffoldOptions) -> String {
73 let mut env = Environment::new();
74 env.add_template(
75 "package.json.j2",
76 include_str!("../../templates/package.json.j2"),
77 )
78 .expect("template should be valid");
79 let tmpl = env.get_template("package.json.j2").unwrap();
80
81 let pkg_name = options
82 .package_name
83 .clone()
84 .unwrap_or_else(|| slugify(&options.name));
85
86 let biome = options.formatter.as_deref() == Some("biome");
87 let vitest = options.test_runner.as_deref() == Some("vitest");
88 let tsdown = options.bundler.as_deref() == Some("tsdown");
89
90 tmpl.render(context! {
91 name => pkg_name,
92 repository => options.repository,
93 react => options.react,
94 biome => biome,
95 vitest => vitest,
96 tsdown => tsdown,
97 })
98 .expect("render should succeed")
99}
100
101fn emit_tsconfig(options: &ScaffoldOptions) -> String {
102 let mut env = Environment::new();
103 env.add_template(
104 "tsconfig.json.j2",
105 include_str!("../../templates/tsconfig.json.j2"),
106 )
107 .expect("template should be valid");
108 let tmpl = env.get_template("tsconfig.json.j2").unwrap();
109
110 tmpl.render(context! {
111 react => options.react,
112 })
113 .expect("render should succeed")
114}
115
116fn emit_biome() -> String {
117 include_str!("../../templates/biome.json.j2").to_string()
118}
119
120fn emit_tsdown() -> String {
121 include_str!("../../templates/tsdown.config.ts.j2").to_string()
122}
123
124fn slugify(title: &str) -> String {
126 let slug: String = title
127 .chars()
128 .map(|c| {
129 if c.is_alphanumeric() {
130 c.to_ascii_lowercase()
131 } else {
132 '-'
133 }
134 })
135 .collect();
136
137 let mut result = String::new();
139 let mut prev_dash = false;
140 for c in slug.chars() {
141 if c == '-' {
142 if !prev_dash && !result.is_empty() {
143 result.push('-');
144 }
145 prev_dash = true;
146 } else {
147 result.push(c);
148 prev_dash = false;
149 }
150 }
151
152 result.trim_end_matches('-').to_string()
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_slugify() {
161 assert_eq!(slugify("My API Service"), "my-api-service");
162 assert_eq!(slugify("SSE Chat API"), "sse-chat-api");
163 assert_eq!(slugify("Petstore - OpenAPI 3.2"), "petstore-openapi-3-2");
164 }
165
166 #[test]
167 fn test_emit_scaffold_with_all_options() {
168 let options = ScaffoldOptions {
169 name: "Test API".to_string(),
170 package_name: None,
171 repository: Some("https://github.com/test/repo".to_string()),
172 formatter: Some("biome".to_string()),
173 bundler: Some("tsdown".to_string()),
174 test_runner: Some("vitest".to_string()),
175 react: true,
176 };
177 let files = emit_scaffold(&options);
178 assert_eq!(files.len(), 4);
179 assert!(files.iter().any(|f| f.path == "package.json"));
180 assert!(files.iter().any(|f| f.path == "tsconfig.json"));
181 assert!(files.iter().any(|f| f.path == "biome.json"));
182 assert!(files.iter().any(|f| f.path == "tsdown.config.ts"));
183 }
184
185 #[test]
186 fn test_emit_scaffold_minimal() {
187 let options = ScaffoldOptions {
188 name: "Test".to_string(),
189 package_name: None,
190 repository: None,
191 formatter: None,
192 bundler: None,
193 test_runner: None,
194 react: false,
195 };
196 let files = emit_scaffold(&options);
197 assert_eq!(files.len(), 2); }
199
200 #[test]
201 fn test_custom_package_name() {
202 let options = ScaffoldOptions {
203 name: "Some API".to_string(),
204 package_name: Some("@myorg/api-client".to_string()),
205 repository: None,
206 formatter: None,
207 bundler: None,
208 test_runner: None,
209 react: false,
210 };
211 let files = emit_scaffold(&options);
212 let pkg = files.iter().find(|f| f.path == "package.json").unwrap();
213 assert!(pkg.content.contains("@myorg/api-client"));
214 }
215}