1use anyhow::{Context, Result};
4use std::collections::BTreeSet;
5use std::fs;
6use std::path::{Path, PathBuf};
7
8use crate::cli::{AddArgs, AddFeature};
9use crate::templates::{BackendTemplateContext, BackendTemplateEngine};
10use crate::{
11 TIDEWAY_VERSION, ensure_dir, error_contract, print_info, print_success, print_warning,
12 write_file,
13};
14
15const APP_BUILDER_START_MARKER: &str = "tideway:app-builder:start";
16const APP_BUILDER_END_MARKER: &str = "tideway:app-builder:end";
17
18pub fn run(args: AddArgs) -> Result<()> {
19 let project_dir = PathBuf::from(&args.path);
20 let cargo_path = project_dir.join("Cargo.toml");
21
22 if !cargo_path.exists() {
23 return Err(anyhow::anyhow!(error_contract(
24 &format!("Cargo.toml not found in {}", project_dir.display()),
25 "Run this command inside a Rust project root.",
26 "For greenfield apps, run `tideway new <app>` first."
27 )));
28 }
29
30 let cargo_contents = fs::read_to_string(&cargo_path)
31 .with_context(|| format!("Failed to read {}", cargo_path.display()))?;
32
33 let project_name = project_name_from_cargo(&cargo_contents, &project_dir);
34 let project_name_pascal = to_pascal_case(&project_name);
35
36 update_cargo_toml(&cargo_path, &cargo_contents, args.feature)?;
37 update_env_example(&project_dir, args.feature, &project_name)?;
38
39 if args.feature == AddFeature::Auth {
40 scaffold_auth(
41 &project_dir,
42 &project_name,
43 &project_name_pascal,
44 args.force,
45 )?;
46 print_info("Auth scaffold created in src/auth/");
47 if args.wire {
48 wire_auth_in_main(&project_dir, &project_name)?;
49 } else {
50 print_info("Next steps: wire AuthModule + SimpleAuthProvider in main.rs");
51 }
52 }
53
54 if args.feature == AddFeature::Database && args.wire {
55 wire_database_in_main(&project_dir)?;
56 }
57
58 if args.feature == AddFeature::Openapi {
59 ensure_openapi_docs_file(&project_dir)?;
60 if args.wire {
61 wire_openapi_in_main(&project_dir)?;
62 } else {
63 print_info("Next steps: wire OpenAPI in main.rs");
64 }
65 }
66
67 print_success(&format!("Added {}", args.feature));
68 Ok(())
69}
70
71fn update_cargo_toml(path: &Path, contents: &str, feature: AddFeature) -> Result<()> {
72 let mut doc = contents.parse::<toml_edit::DocumentMut>()?;
73
74 let deps = doc["dependencies"].or_insert(toml_edit::Item::Table(toml_edit::Table::new()));
75
76 let tideway_item = deps
77 .as_table_mut()
78 .expect("dependencies should be a table")
79 .entry("tideway");
80
81 let feature_name = feature.to_string();
82
83 match tideway_item {
84 toml_edit::Entry::Vacant(entry) => {
85 let mut table = toml_edit::InlineTable::new();
86 table.get_or_insert("version", TIDEWAY_VERSION);
87 table.get_or_insert("features", array_value(&[feature_name.as_str()]));
88 entry.insert(toml_edit::Item::Value(toml_edit::Value::InlineTable(table)));
89 }
90 toml_edit::Entry::Occupied(mut entry) => {
91 if entry.get().is_str() {
92 let version = entry.get().as_str().unwrap_or(TIDEWAY_VERSION).to_string();
93 let mut table = toml_edit::InlineTable::new();
94 table.get_or_insert("version", version);
95 table.get_or_insert("features", array_value(&[feature_name.as_str()]));
96 entry.insert(toml_edit::Item::Value(toml_edit::Value::InlineTable(table)));
97 } else {
98 let item = entry.get_mut();
99 let features = item["features"]
100 .or_insert(toml_edit::Item::Value(toml_edit::Value::Array(
101 toml_edit::Array::new(),
102 )))
103 .as_array_mut()
104 .expect("features should be an array");
105
106 if !features.iter().any(|v| v.as_str() == Some(&feature_name)) {
107 features.push(feature_name);
108 }
109 }
110 }
111 }
112
113 if feature == AddFeature::Database {
114 let deps_table = deps.as_table_mut().expect("dependencies should be a table");
115 deps_table
116 .entry("sea-orm")
117 .or_insert(toml_edit::Item::Value(toml_edit::Value::InlineTable({
118 let mut table = toml_edit::InlineTable::new();
119 table.get_or_insert("version", "1.1");
120 table.get_or_insert(
121 "features",
122 array_value(&["sqlx-postgres", "runtime-tokio-rustls"]),
123 );
124 table
125 })));
126 }
127
128 if feature == AddFeature::Auth {
129 let deps_table = deps.as_table_mut().expect("dependencies should be a table");
130 deps_table
131 .entry("async-trait")
132 .or_insert(toml_edit::value("0.1"));
133 deps_table
134 .entry("serde")
135 .or_insert(toml_edit::Item::Value(toml_edit::Value::InlineTable({
136 let mut table = toml_edit::InlineTable::new();
137 table.get_or_insert("version", "1.0");
138 table.get_or_insert("features", array_value(&["derive"]));
139 table
140 })));
141 deps_table
142 .entry("serde_json")
143 .or_insert(toml_edit::value("1.0"));
144 }
145
146 write_file(path, &doc.to_string())
147 .with_context(|| format!("Failed to write {}", path.display()))?;
148 Ok(())
149}
150
151fn update_env_example(project_dir: &Path, feature: AddFeature, project_name: &str) -> Result<()> {
152 let env_path = project_dir.join(".env.example");
153 let mut lines = if env_path.exists() {
154 fs::read_to_string(&env_path)
155 .with_context(|| format!("Failed to read {}", env_path.display()))?
156 .lines()
157 .map(|line| line.to_string())
158 .collect::<Vec<_>>()
159 } else {
160 vec![
161 "# Server".to_string(),
162 "TIDEWAY_HOST=0.0.0.0".to_string(),
163 "TIDEWAY_PORT=8000".to_string(),
164 String::new(),
165 ]
166 };
167
168 let mut existing = BTreeSet::new();
169 for line in &lines {
170 if let Some((key, _)) = line.split_once('=') {
171 existing.insert(key.trim().to_string());
172 }
173 }
174
175 match feature {
176 AddFeature::Database => {
177 if !existing.contains("DATABASE_URL") {
178 lines.push("# Database".to_string());
179 lines.push(format!(
180 "DATABASE_URL=postgres://postgres:postgres@localhost:5432/{}",
181 project_name
182 ));
183 lines.push(String::new());
184 }
185 }
186 AddFeature::Auth => {
187 if !existing.contains("JWT_SECRET") {
188 lines.push("# Auth".to_string());
189 lines.push("JWT_SECRET=your-super-secret-jwt-key-change-in-production".to_string());
190 lines.push(String::new());
191 }
192 }
193 _ => {}
194 }
195
196 write_file(&env_path, &lines.join("\n"))
197 .with_context(|| format!("Failed to write {}", env_path.display()))?;
198 Ok(())
199}
200
201fn scaffold_auth(
202 project_dir: &Path,
203 project_name: &str,
204 project_name_pascal: &str,
205 force: bool,
206) -> Result<()> {
207 let context = BackendTemplateContext {
208 project_name: project_name.to_string(),
209 project_name_pascal: project_name_pascal.to_string(),
210 has_organizations: false,
211 database: "postgres".to_string(),
212 tideway_version: TIDEWAY_VERSION.to_string(),
213 tideway_features: vec!["auth".to_string()],
214 has_tideway_features: true,
215 has_auth_feature: true,
216 has_database_feature: false,
217 has_openapi_feature: false,
218 needs_arc: true,
219 has_config: false,
220 };
221
222 let engine = BackendTemplateEngine::new(context)?;
223 let auth_dir = project_dir.join("src").join("auth");
224
225 write_file_with_force(
226 &auth_dir.join("mod.rs"),
227 &engine.render("starter/src/auth/mod.rs")?,
228 force,
229 )?;
230 write_file_with_force(
231 &auth_dir.join("provider.rs"),
232 &engine.render("starter/src/auth/provider.rs")?,
233 force,
234 )?;
235 write_file_with_force(
236 &auth_dir.join("routes.rs"),
237 &engine.render("starter/src/auth/routes.rs")?,
238 force,
239 )?;
240
241 Ok(())
242}
243
244fn wire_auth_in_main(project_dir: &Path, project_name: &str) -> Result<()> {
245 let main_path = project_dir.join("src").join("main.rs");
246 if !main_path.exists() {
247 print_warning("src/main.rs not found; skipping auto-wiring");
248 return Ok(());
249 }
250
251 let mut contents = fs::read_to_string(&main_path)
252 .with_context(|| format!("Failed to read {}", main_path.display()))?;
253
254 if !contents.contains("mod auth;") {
255 if contents.contains("mod routes;") {
256 contents = contents.replace("mod routes;\n", "mod routes;\nmod auth;\n");
257 } else {
258 contents = format!("mod auth;\n{}", contents);
259 }
260 }
261
262 contents = ensure_use_line(contents, "use axum::Extension;", "use tideway::auth");
263 contents = ensure_use_line(
264 contents,
265 "use crate::auth::{AuthModule, SimpleAuthProvider};",
266 "use tideway::auth",
267 );
268 contents = ensure_use_line(contents, "use std::sync::Arc;", "use tideway::");
269 contents = ensure_use_line(
270 contents,
271 "use tideway::auth::{JwtIssuer, JwtIssuerConfig};",
272 "use tideway::auth",
273 );
274
275 let has_jwt_secret = contents.contains("let jwt_secret");
276 let has_jwt_issuer = contents.contains("let jwt_issuer");
277 let has_auth_provider = contents.contains("auth_provider");
278 let has_auth_module = contents.contains("auth_module");
279
280 if has_jwt_secret && has_jwt_issuer {
281 if !has_auth_provider || !has_auth_module {
282 if let Some(insert_at) = contents.find("let jwt_issuer") {
283 let after = contents[insert_at..]
284 .find(";\n")
285 .map(|idx| insert_at + idx + 2)
286 .unwrap_or(insert_at);
287 let insert = " let auth_provider = SimpleAuthProvider::from_secret(&jwt_secret);\n let auth_module = AuthModule::new(jwt_issuer.clone());\n".to_string();
288 contents.insert_str(after, &insert);
289 }
290 }
291 } else {
292 let block = format!(
293 " let jwt_secret = std::env::var(\"JWT_SECRET\").expect(\"JWT_SECRET is not set\");\n let jwt_issuer = Arc::new(JwtIssuer::new(JwtIssuerConfig::with_secret(\n &jwt_secret,\n \"{}\",\n )).expect(\"Failed to create JWT issuer\"));\n let auth_provider = SimpleAuthProvider::from_secret(&jwt_secret);\n let auth_module = AuthModule::new(jwt_issuer.clone());\n\n",
294 project_name
295 );
296 contents = insert_before_app_builder(contents, &block)?;
297 }
298
299 contents = insert_auth_into_app_builder(contents)?;
300
301 write_file(&main_path, &contents)
302 .with_context(|| format!("Failed to write {}", main_path.display()))?;
303 print_success("Wired auth into src/main.rs");
304 Ok(())
305}
306
307pub fn wire_database_in_main(project_dir: &Path) -> Result<()> {
308 let main_path = project_dir.join("src").join("main.rs");
309 if !main_path.exists() {
310 print_warning("src/main.rs not found; skipping auto-wiring");
311 return Ok(());
312 }
313
314 let mut contents = fs::read_to_string(&main_path)
315 .with_context(|| format!("Failed to read {}", main_path.display()))?;
316
317 if !contents.contains("async fn main") {
318 print_warning("main.rs is not async; skipping database wiring");
319 return Ok(());
320 }
321
322 contents = ensure_use_line(
323 contents,
324 "use tideway::{AppContext, SeaOrmPool};",
325 "use tideway::",
326 );
327 contents = ensure_use_line(contents, "use std::sync::Arc;", "use tideway::");
328
329 let has_database_block = contents.contains("DATABASE_URL")
330 || contents.contains("sea_orm::Database::connect")
331 || contents.contains("with_database");
332
333 if !has_database_block {
334 let block = " let database_url = std::env::var(\"DATABASE_URL\").expect(\"DATABASE_URL is not set\");\n let db = sea_orm::Database::connect(&database_url)\n .await\n .expect(\"Failed to connect to database\");\n\n";
335 contents = insert_before_app_builder(contents, block)?;
336 }
337
338 if !contents.contains(".with_database(") {
339 contents = insert_database_into_app_builder(contents)?;
340 }
341
342 write_file(&main_path, &contents)
343 .with_context(|| format!("Failed to write {}", main_path.display()))?;
344 print_success("Wired database into src/main.rs");
345 Ok(())
346}
347
348fn ensure_use_line(mut contents: String, line: &str, anchor: &str) -> String {
349 if contents.contains(line) {
350 return contents;
351 }
352
353 if let Some(pos) = contents.find(anchor) {
354 if let Some(line_end) = contents[pos..].find('\n') {
355 let insert_at = pos + line_end + 1;
356 contents.insert_str(insert_at, &format!("{}\n", line));
357 return contents;
358 }
359 }
360
361 contents = format!("{}\n{}", line, contents);
362 contents
363}
364
365fn insert_before_app_builder(mut contents: String, block: &str) -> Result<String> {
366 if let Some(pos) = find_app_builder_start(&contents) {
367 contents.insert_str(pos, block);
368 Ok(contents)
369 } else {
370 print_warning("Could not find app builder; skipping auth wiring");
371 Ok(contents)
372 }
373}
374
375fn insert_auth_into_app_builder(mut contents: String) -> Result<String> {
376 if contents.contains("register_module(auth_module)") {
377 return Ok(contents);
378 }
379
380 if let Some(pos) = find_app_builder_start(&contents) {
381 let line_end = contents[pos..]
382 .find('\n')
383 .map(|idx| pos + idx)
384 .unwrap_or(contents.len());
385 let indent = contents[pos..]
386 .chars()
387 .take_while(|c| c.is_whitespace())
388 .collect::<String>();
389 let insert = format!(
390 "{} .with_global_layer(Extension(auth_provider))\n{} .register_module(auth_module)\n",
391 indent, indent
392 );
393 contents.insert_str(line_end + 1, &insert);
394 Ok(contents)
395 } else {
396 print_warning("Could not find app builder; skipping auth module registration");
397 Ok(contents)
398 }
399}
400
401fn insert_database_into_app_builder(mut contents: String) -> Result<String> {
402 if let Some(pos) = find_app_builder_start(&contents) {
403 let line_end = contents[pos..]
404 .find('\n')
405 .map(|idx| pos + idx)
406 .unwrap_or(contents.len());
407 let indent = contents[pos..]
408 .chars()
409 .take_while(|c| c.is_whitespace())
410 .collect::<String>();
411 let insert = format!(
412 "{} .with_context(\n{} AppContext::builder()\n{} .with_database(Arc::new(SeaOrmPool::new(db, database_url)))\n{} .build()\n{} )\n",
413 indent, indent, indent, indent, indent
414 );
415 contents.insert_str(line_end + 1, &insert);
416 Ok(contents)
417 } else {
418 print_warning("Could not find app builder; skipping database wiring");
419 Ok(contents)
420 }
421}
422
423fn wire_openapi_in_main(project_dir: &Path) -> Result<()> {
424 let main_path = project_dir.join("src").join("main.rs");
425 if !main_path.exists() {
426 print_warning("src/main.rs not found; skipping auto-wiring");
427 return Ok(());
428 }
429
430 let mut contents = fs::read_to_string(&main_path)
431 .with_context(|| format!("Failed to read {}", main_path.display()))?;
432
433 if contents.contains("openapi::create_openapi_router")
434 || contents.contains("openapi_merge_module")
435 {
436 print_info("OpenAPI already appears wired in main.rs");
437 return Ok(());
438 }
439
440 contents = ensure_use_line(contents, "use tideway::ConfigBuilder;", "use tideway::");
441 if contents.contains("mod config;") {
442 contents = ensure_use_line(contents, "use crate::config::AppConfig;", "use tideway::");
443 }
444 contents = ensure_use_line(contents, "use tideway::openapi;", "use tideway::");
445
446 if !contents.contains("mod openapi_docs;") {
447 if contents.contains("mod routes;") {
448 contents = contents.replace("mod routes;\n", "mod routes;\nmod openapi_docs;\n");
449 } else {
450 contents = format!("mod openapi_docs;\n{}", contents);
451 }
452 }
453
454 let has_config_var = contents.contains("let config = ConfigBuilder::new()")
455 || contents.contains("let config = AppConfig::from_env()");
456 let config_available =
457 contents.contains("ConfigBuilder::new()") || contents.contains("AppConfig::from_env()");
458
459 if !has_config_var && config_available {
460 let config_block = " let config = ConfigBuilder::new()\n .from_env()\n .build()\n .expect(\"Invalid TIDEWAY_* config\");\n\n";
461 contents = insert_before_app_builder(contents, config_block)?;
462 }
463
464 if contents.contains("let config = AppConfig::from_env()") {
465 contents = insert_openapi_into_app_builder(contents, "config.tideway")?;
466 } else {
467 contents = insert_openapi_into_app_builder(contents, "config")?;
468 }
469
470 write_file(&main_path, &contents)
471 .with_context(|| format!("Failed to write {}", main_path.display()))?;
472 print_success("Wired OpenAPI into src/main.rs");
473 Ok(())
474}
475
476fn insert_openapi_into_app_builder(mut contents: String, config_ref: &str) -> Result<String> {
477 if contents.contains("create_openapi_router") {
478 return Ok(contents);
479 }
480
481 if let Some(pos) = find_app_builder_start(&contents) {
482 let app_var =
483 find_app_builder_var_name(&contents, pos).unwrap_or_else(|| "app".to_string());
484 if let Some(insert_at) = find_app_builder_end_insert_at(&contents, pos) {
486 let block = format!(
487 "\n #[cfg(feature = \"openapi\")]\n if {config_ref}.openapi.enabled {{\n let openapi = tideway::openapi_merge_module!(openapi_docs, ApiDoc);\n let openapi_router = tideway::openapi::create_openapi_router(openapi, &{config_ref}.openapi);\n {app_var} = {app_var}.merge_router(openapi_router);\n }}\n"
488 );
489 contents.insert_str(insert_at, &block);
490 } else {
491 print_warning("Could not find app builder termination; skipping OpenAPI wiring");
492 }
493 Ok(contents)
494 } else {
495 print_warning("Could not find app builder; skipping OpenAPI wiring");
496 Ok(contents)
497 }
498}
499
500fn find_app_builder_start(contents: &str) -> Option<usize> {
501 if let Some(marker_pos) = contents.find(APP_BUILDER_START_MARKER) {
502 if let Some(line_end) = contents[marker_pos..].find('\n') {
503 return Some(marker_pos + line_end + 1);
504 }
505 }
506 let mut search_from = 0;
507 while let Some(rel_pos) = contents[search_from..].find(" = App::") {
508 let abs_pos = search_from + rel_pos;
509 let line_start = contents[..abs_pos]
510 .rfind('\n')
511 .map(|idx| idx + 1)
512 .unwrap_or(0);
513 if find_app_builder_var_name(contents, line_start).is_some() {
514 return Some(line_start);
515 }
516 search_from = abs_pos + 1;
517 }
518 None
519}
520
521fn find_app_builder_var_name(contents: &str, start_pos: usize) -> Option<String> {
522 let line_end = contents[start_pos..]
523 .find('\n')
524 .map(|idx| start_pos + idx)
525 .unwrap_or(contents.len());
526 let line = contents[start_pos..line_end].trim();
527
528 if !line.starts_with("let ") || !line.contains("= App::") {
529 return None;
530 }
531
532 let after_let = line.trim_start_matches("let ").trim();
533 let before_eq = after_let.split('=').next()?.trim();
534 let var = before_eq.strip_prefix("mut ").unwrap_or(before_eq).trim();
535 if var.is_empty() {
536 None
537 } else {
538 Some(var.to_string())
539 }
540}
541
542fn find_app_builder_end_insert_at(contents: &str, start_pos: usize) -> Option<usize> {
543 if let Some(marker_pos) = contents.find(APP_BUILDER_END_MARKER) {
544 if marker_pos >= start_pos {
545 let marker_line_start = contents[..marker_pos]
546 .rfind('\n')
547 .map(|idx| idx + 1)
548 .unwrap_or(0);
549 if let Some(marker_line_end_rel) = contents[marker_line_start..].find('\n') {
550 return Some(marker_line_start + marker_line_end_rel + 1);
551 }
552 return Some(contents.len());
553 }
554 }
555 find_statement_terminator(contents, start_pos).map(|idx| idx + 1)
556}
557
558fn find_statement_terminator(contents: &str, start_pos: usize) -> Option<usize> {
559 let bytes = contents.as_bytes();
560 let mut i = start_pos;
561 let mut paren_depth = 0usize;
562 let mut brace_depth = 0usize;
563 let mut bracket_depth = 0usize;
564 let mut in_single_quote = false;
565 let mut in_double_quote = false;
566 let mut escape = false;
567
568 while i < bytes.len() {
569 let b = bytes[i];
570
571 if !in_single_quote
573 && !in_double_quote
574 && i + 1 < bytes.len()
575 && bytes[i] == b'/'
576 && bytes[i + 1] == b'/'
577 {
578 while i < bytes.len() && bytes[i] != b'\n' {
579 i += 1;
580 }
581 continue;
582 }
583
584 if escape {
585 escape = false;
586 i += 1;
587 continue;
588 }
589
590 if in_single_quote {
591 if b == b'\\' {
592 escape = true;
593 } else if b == b'\'' {
594 in_single_quote = false;
595 }
596 i += 1;
597 continue;
598 }
599
600 if in_double_quote {
601 if b == b'\\' {
602 escape = true;
603 } else if b == b'"' {
604 in_double_quote = false;
605 }
606 i += 1;
607 continue;
608 }
609
610 match b {
611 b'\'' => in_single_quote = true,
612 b'"' => in_double_quote = true,
613 b'(' => paren_depth += 1,
614 b')' => paren_depth = paren_depth.saturating_sub(1),
615 b'{' => brace_depth += 1,
616 b'}' => brace_depth = brace_depth.saturating_sub(1),
617 b'[' => bracket_depth += 1,
618 b']' => bracket_depth = bracket_depth.saturating_sub(1),
619 b';' if paren_depth == 0 && brace_depth == 0 && bracket_depth == 0 => return Some(i),
620 _ => {}
621 }
622
623 i += 1;
624 }
625 None
626}
627
628fn ensure_openapi_docs_file(project_dir: &Path) -> Result<()> {
629 let docs_path = project_dir.join("src").join("openapi_docs.rs");
630 if docs_path.exists() {
631 return Ok(());
632 }
633
634 let contents = r#"#[cfg(feature = "openapi")]
635tideway::openapi_doc!(pub(crate) ApiDoc, paths());
636"#;
637
638 if let Some(parent) = docs_path.parent() {
639 ensure_dir(parent).with_context(|| format!("Failed to create {}", parent.display()))?;
640 }
641
642 write_file(&docs_path, contents)
643 .with_context(|| format!("Failed to write {}", docs_path.display()))?;
644 print_success("Created src/openapi_docs.rs");
645 Ok(())
646}
647
648fn write_file_with_force(path: &Path, contents: &str, force: bool) -> Result<()> {
649 if path.exists() && !force {
650 print_warning(&format!(
651 "Skipping {} (use --force to overwrite)",
652 path.display()
653 ));
654 return Ok(());
655 }
656
657 if let Some(parent) = path.parent() {
658 ensure_dir(parent).with_context(|| format!("Failed to create {}", parent.display()))?;
659 }
660
661 write_file(path, contents).with_context(|| format!("Failed to write {}", path.display()))?;
662 Ok(())
663}
664
665fn project_name_from_cargo(contents: &str, project_dir: &Path) -> String {
666 let doc = contents
667 .parse::<toml_edit::DocumentMut>()
668 .ok()
669 .and_then(|doc| doc["package"]["name"].as_str().map(|s| s.to_string()));
670
671 doc.unwrap_or_else(|| {
672 project_dir
673 .file_name()
674 .and_then(|n| n.to_str())
675 .unwrap_or("my_app")
676 .to_string()
677 })
678 .replace('-', "_")
679}
680
681fn to_pascal_case(s: &str) -> String {
682 s.split('_')
683 .filter(|part| !part.is_empty())
684 .map(|word| {
685 let mut chars = word.chars();
686 match chars.next() {
687 None => String::new(),
688 Some(first) => first.to_uppercase().chain(chars).collect(),
689 }
690 })
691 .collect()
692}
693
694pub fn array_value(values: &[&str]) -> toml_edit::Value {
695 let mut array = toml_edit::Array::new();
696 for value in values {
697 array.push(*value);
698 }
699 toml_edit::Value::Array(array)
700}