Skip to content

Commit e0cae78

Browse files
Merge pull request #130 from dotindustries/feat/create-async-handler-template-projects-for-rust-go-and-typ-e97krwrforijfo6q050ftykq
feat: add warp init --template scaffolding with async handler templates
2 parents 96ffdcd + d45f8eb commit e0cae78

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+5533
-0
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/warp-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ tracing-subscriber.workspace = true
2121

2222
[dev-dependencies]
2323
tempfile = "3"
24+
walkdir = "2"
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
use std::path::Path;
2+
3+
use anyhow::{bail, Result};
4+
use tracing::info;
5+
6+
use crate::templates;
7+
8+
pub fn init(template: &str, path: Option<&str>) -> Result<()> {
9+
let target_dir = match path {
10+
Some(p) => p.to_string(),
11+
None => format!("./{template}"),
12+
};
13+
14+
let target = Path::new(&target_dir);
15+
if target.exists() {
16+
bail!(
17+
"Target directory '{}' already exists. Remove it or choose a different path with --path.",
18+
target_dir
19+
);
20+
}
21+
22+
info!("Scaffolding template '{template}' into {target_dir}");
23+
templates::scaffold(template, target)?;
24+
println!("✓ Project scaffolded at {target_dir}");
25+
println!(" cd {target_dir} && warp pack");
26+
Ok(())
27+
}
28+
29+
#[cfg(test)]
30+
mod tests {
31+
use super::*;
32+
use std::collections::BTreeSet;
33+
34+
#[test]
35+
fn test_unknown_template() {
36+
let dir = tempfile::tempdir().unwrap();
37+
let target = dir.path().join("out");
38+
let result = crate::templates::scaffold("no-such-template", &target);
39+
assert!(result.is_err());
40+
assert!(
41+
result
42+
.unwrap_err()
43+
.to_string()
44+
.contains("Unknown template")
45+
);
46+
}
47+
48+
#[test]
49+
fn test_target_exists() {
50+
let dir = tempfile::tempdir().unwrap();
51+
// dir already exists, so init should fail
52+
let result = init("async-rust", Some(dir.path().to_str().unwrap()));
53+
assert!(result.is_err());
54+
assert!(
55+
result
56+
.unwrap_err()
57+
.to_string()
58+
.contains("already exists")
59+
);
60+
}
61+
62+
#[test]
63+
fn test_scaffold_async_rust() {
64+
let dir = tempfile::tempdir().unwrap();
65+
let target = dir.path().join("my-project");
66+
crate::templates::scaffold("async-rust", &target).unwrap();
67+
assert!(target.join("Cargo.toml").exists());
68+
assert!(target.join("src/lib.rs").exists());
69+
assert!(target.join("warp.toml").exists());
70+
assert!(target.join("README.md").exists());
71+
assert!(target.join("wit/world.wit").exists());
72+
assert!(target.join("wit/async-handler.wit").exists());
73+
assert!(target.join("wit/http-types.wit").exists());
74+
}
75+
76+
#[test]
77+
fn test_scaffold_async_go() {
78+
let dir = tempfile::tempdir().unwrap();
79+
let target = dir.path().join("my-project");
80+
crate::templates::scaffold("async-go", &target).unwrap();
81+
assert!(target.join("go.mod").exists());
82+
assert!(target.join("main.go").exists());
83+
assert!(target.join("main_test.go").exists());
84+
assert!(target.join("warp.toml").exists());
85+
assert!(target.join("README.md").exists());
86+
}
87+
88+
#[test]
89+
fn test_scaffold_async_ts() {
90+
let dir = tempfile::tempdir().unwrap();
91+
let target = dir.path().join("my-project");
92+
crate::templates::scaffold("async-ts", &target).unwrap();
93+
assert!(target.join("package.json").exists());
94+
assert!(target.join("src/handler.ts").exists());
95+
assert!(target.join("warp.toml").exists());
96+
assert!(target.join("README.md").exists());
97+
assert!(target.join("wit/handler.wit").exists());
98+
assert!(target.join("wit/deps/http/types.wit").exists());
99+
assert!(target.join("wit/deps/shim/dns.wit").exists());
100+
}
101+
102+
// ── Template ↔ Fixture consistency tests ─────────────────────
103+
//
104+
// These tests ensure the embedded template content stays in sync with
105+
// the fixture directories that integration tests build. A drift between
106+
// template and fixture means integration tests validate stale code.
107+
108+
/// Collect all relative file paths under `dir`, excluding build artifacts.
109+
fn collect_files(dir: &std::path::Path) -> BTreeSet<String> {
110+
let mut files = BTreeSet::new();
111+
for entry in walkdir::WalkDir::new(dir)
112+
.into_iter()
113+
.filter_map(|e| e.ok())
114+
.filter(|e| e.file_type().is_file())
115+
{
116+
let rel = entry
117+
.path()
118+
.strip_prefix(dir)
119+
.unwrap()
120+
.to_string_lossy()
121+
.to_string();
122+
// Skip build artifacts that aren't part of the template
123+
if rel.starts_with("target/") || rel == "Cargo.lock" {
124+
continue;
125+
}
126+
files.insert(rel);
127+
}
128+
files
129+
}
130+
131+
/// Normalize fixture content so it can be compared to template output.
132+
///
133+
/// Fixtures use their directory name as the project name (e.g.
134+
/// "async-rust-template") whereas templates use the placeholder
135+
/// "my-async-handler". The Go fixture also has a local `replace`
136+
/// directive needed for workspace builds that the template omits.
137+
fn normalize_fixture_content(
138+
content: &str,
139+
fixture_name: &str,
140+
) -> String {
141+
content
142+
.replace(fixture_name, "my-async-handler")
143+
// Go fixture has a local replace directive for workspace builds
144+
.replace(
145+
"\nreplace github.com/anthropics/warpgrid/packages/warpgrid-go => ../../../packages/warpgrid-go\n",
146+
"",
147+
)
148+
}
149+
150+
/// Scaffold a template and compare every generated file against the
151+
/// corresponding fixture file. Fails if content differs or if the
152+
/// fixture has files the template doesn't produce (or vice versa).
153+
///
154+
/// Known differences (project name, local replace directives) are
155+
/// normalized before comparison.
156+
fn assert_template_matches_fixture(template_name: &str, fixture_subdir: &str) {
157+
let dir = tempfile::tempdir().unwrap();
158+
let scaffolded = dir.path().join("project");
159+
crate::templates::scaffold(template_name, &scaffolded).unwrap();
160+
161+
let fixture_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
162+
.parent()
163+
.unwrap()
164+
.parent()
165+
.unwrap()
166+
.join("tests/fixtures")
167+
.join(fixture_subdir);
168+
169+
let scaffolded_files = collect_files(&scaffolded);
170+
let fixture_files = collect_files(&fixture_dir);
171+
172+
// Check for files in fixture but missing from template
173+
let missing_from_template: BTreeSet<_> =
174+
fixture_files.difference(&scaffolded_files).collect();
175+
assert!(
176+
missing_from_template.is_empty(),
177+
"Fixture '{fixture_subdir}' has files not produced by template '{template_name}': {missing_from_template:?}"
178+
);
179+
180+
// Check for files in template but missing from fixture
181+
let missing_from_fixture: BTreeSet<_> =
182+
scaffolded_files.difference(&fixture_files).collect();
183+
assert!(
184+
missing_from_fixture.is_empty(),
185+
"Template '{template_name}' produces files not in fixture '{fixture_subdir}': {missing_from_fixture:?}"
186+
);
187+
188+
// Compare content of every file (normalizing known differences)
189+
for file in &scaffolded_files {
190+
let scaffolded_content =
191+
std::fs::read_to_string(scaffolded.join(file)).unwrap();
192+
let fixture_content =
193+
std::fs::read_to_string(fixture_dir.join(file)).unwrap();
194+
let normalized_fixture =
195+
normalize_fixture_content(&fixture_content, fixture_subdir);
196+
assert_eq!(
197+
scaffolded_content, normalized_fixture,
198+
"Content mismatch in '{file}' between template '{template_name}' and fixture '{fixture_subdir}'"
199+
);
200+
}
201+
}
202+
203+
#[test]
204+
fn test_async_rust_template_matches_fixture() {
205+
assert_template_matches_fixture("async-rust", "async-rust-template");
206+
}
207+
208+
#[test]
209+
fn test_async_go_template_matches_fixture() {
210+
assert_template_matches_fixture("async-go", "async-go-template");
211+
}
212+
213+
#[test]
214+
fn test_async_ts_template_matches_fixture() {
215+
assert_template_matches_fixture("async-ts", "async-ts-template");
216+
}
217+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod convert;
22
pub mod dev;
3+
pub mod init;
34
pub mod pack;

crates/warp-cli/src/main.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use clap::{Parser, Subcommand};
22

33
mod commands;
4+
mod templates;
45

56
#[derive(Parser)]
67
#[command(
@@ -37,6 +38,17 @@ enum Commands {
3738
#[arg(short, long)]
3839
lang: Option<String>,
3940
},
41+
/// Scaffold a new WarpGrid project from a template.
42+
///
43+
/// Available templates: async-rust, async-go, async-ts
44+
Init {
45+
/// Template name (async-rust, async-go, async-ts)
46+
#[arg(short, long)]
47+
template: String,
48+
/// Target directory (default: ./<template-name>)
49+
#[arg(short, long)]
50+
path: Option<String>,
51+
},
4052
// Phase 3+:
4153
// Deploy { ... },
4254
// Status { ... },
@@ -85,5 +97,8 @@ fn main() -> anyhow::Result<()> {
8597
Commands::Pack { path, lang } => {
8698
commands::pack::pack(&path, lang.as_deref())
8799
}
100+
Commands::Init { template, path } => {
101+
commands::init::init(&template, path.as_deref())
102+
}
88103
}
89104
}

0 commit comments

Comments
 (0)