Skip to content

Commit 0a3ced7

Browse files
committed
feat: server to/fro implementation with composable models, routes, mocks/fakes
This commit introduces a massive upgrade to the code generator: - Bidirectional Server implementation for Actix and Axum. - Full DAO layer scaffolding and database configurations. - Middlewares for authentication (security), validation, and webhooks. - Included mock-server spec for test validation. - Fixes Docker I/O errors in local pre-commit hooks by using local native mock HTTP server.
1 parent b160d07 commit 0a3ced7

39 files changed

Lines changed: 2532 additions & 176 deletions

.github/workflows/ci.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
runs-on: ubuntu-latest
2222

2323
steps:
24-
- uses: actions/checkout@v6
24+
- uses: actions/checkout@v7
2525

2626
- name: Set up Rust
2727
run: |
@@ -63,6 +63,12 @@ jobs:
6363
- name: Test OpenAPI 3.2.0 Petstore SDK Generation
6464
run: cargo run -p cdd-cli -- from_openapi to_sdk -i ./petstore_oas3.yaml -o ./target/petstore3
6565

66+
- name: Test Swagger 2.0 Petstore Server & Client
67+
run: bash scripts/test_server_and_client.sh ./petstore.json ./target/server_petstore2 ./target/client_petstore2
68+
69+
- name: Test OpenAPI 3.2.0 Petstore Server & Client
70+
run: bash scripts/test_server_and_client.sh ./petstore_oas3.yaml ./target/server_petstore3 ./target/client_petstore3
71+
6672
- name: Check Documentation Coverage
6773
run: |
6874
rustup toolchain install nightly
@@ -89,7 +95,7 @@ jobs:
8995
artifact_name: cdd-rust-aarch64-apple-darwin
9096
runs-on: ${{ matrix.os }}
9197
steps:
92-
- uses: actions/checkout@v6
98+
- uses: actions/checkout@v7
9399
- name: Install Rust
94100
uses: dtolnay/rust-toolchain@stable
95101
with:
@@ -109,7 +115,7 @@ jobs:
109115
if: github.event_name == 'push'
110116
runs-on: ubuntu-latest
111117
steps:
112-
- uses: actions/checkout@v6
118+
- uses: actions/checkout@v7
113119
- name: Install Rust
114120
uses: dtolnay/rust-toolchain@stable
115121
with:
@@ -137,7 +143,7 @@ jobs:
137143
if: startsWith(github.ref, 'refs/tags/v')
138144
runs-on: ubuntu-latest
139145
steps:
140-
- uses: actions/checkout@v6
146+
- uses: actions/checkout@v7
141147

142148
- name: Download Artifacts
143149
uses: actions/download-artifact@v8

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,6 @@ swagger2_*.rs
6464
*.profraw
6565

6666
tarpaulin-report.json
67+
coverage*
68+
Lcov
69+
Html

.pre-commit-config.yaml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,33 @@ repos:
4444
types: [rust]
4545

4646
- id: test-swagger-2
47-
name: Test Swagger 2.0 Petstore
47+
name: Test Swagger 2.0 Petstore SDK
4848
entry: bash scripts/test_sdk.sh ../petstore.json ./target/petstore2
4949
language: system
5050
pass_filenames: false
5151
types: [rust]
5252

5353
- id: test-openapi-3
54-
name: Test OpenAPI 3.2.0 Petstore
54+
name: Test OpenAPI 3.2.0 Petstore SDK
5555
entry: bash scripts/test_sdk.sh ../petstore_oas3.json ./target/petstore3
5656
language: system
5757
pass_filenames: false
5858
types: [rust]
5959

60+
- id: test-server-swagger-2
61+
name: Test Swagger 2.0 Petstore Server & Client
62+
entry: bash scripts/test_server_and_client.sh ../petstore.json ./target/server_petstore2 ./target/client_petstore2
63+
language: system
64+
pass_filenames: false
65+
types: [rust]
66+
67+
- id: test-server-openapi-3
68+
name: Test OpenAPI 3.2.0 Petstore Server & Client
69+
entry: bash scripts/test_server_and_client.sh ../petstore_oas3.json ./target/server_petstore3 ./target/client_petstore3
70+
language: system
71+
pass_filenames: false
72+
types: [rust]
73+
6074
- id: wasm-build
6175
name: WASM Build Test
6276
entry: python scripts/build_wasm.py

README.md

Lines changed: 325 additions & 84 deletions
Large diffs are not rendered by default.

cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cdd-cli"
3-
version = "0.0.2"
3+
version = "0.0.3"
44
edition = "2021"
55

66
[[bin]]

cli/src/from_openapi.rs

Lines changed: 118 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -168,42 +168,54 @@ fn run_generation(
168168
}
169169

170170
let out_dir = args.get_output_dir();
171-
fs::create_dir_all(&out_dir).unwrap_or_default();
171+
fs::create_dir_all(&out_dir).map_err(|e| AppError::General(e.to_string()))?;
172172

173173
if !args.no_installable_package {
174174
let cargo_toml_path = out_dir.join("Cargo.toml");
175175
if !cargo_toml_path.exists() {
176+
let mut dependencies = String::from(
177+
r#"tokio = { version = "1", features = ["full"] }
178+
serde = { version = "1.0", features = ["derive"] }
179+
serde_json = "1.0"
180+
serde_qs = "1.0"
181+
reqwest = { version = "0.11", features = ["json"] }
182+
actix-web = "4.0"
183+
clap = { version = "4.0", features = ["derive"] }
184+
chrono = { version = "0.4", features = ["serde"] }
185+
utoipa = "4.2"
186+
uuid = { version = "1.8", features = ["serde", "v4"] }
187+
"#,
188+
);
189+
190+
if strategy.is_server() {
191+
dependencies.push_str("actix-multipart = \"0.7.2\"\n");
192+
dependencies.push_str("diesel = { version = \"2.1\", features = [\"postgres\", \"sqlite\", \"r2d2\", \"chrono\", \"uuid\"] }\n");
193+
dependencies.push_str("fake = \"2.9\"\n");
194+
dependencies.push_str("actix-cors = \"0.7.1\"\n");
195+
}
196+
176197
let cargo_toml = format!(
177198
r#"[package]
178199
name = "{}"
179200
version = "0.0.1"
180201
edition = "2021"
181202
182203
[dependencies]
183-
tokio = {{ version = "1", features = ["full"] }}
184-
serde = {{ version = "1.0", features = ["derive"] }}
185-
serde_json = "1.0"
186-
serde_qs = "1.0"
187-
reqwest = {{ version = "0.11", features = ["json"] }}
188-
actix-web = "4.0"
189-
diesel = {{ version = "2.0", features = ["postgres"] }}
190-
clap = {{ version = "4.0", features = ["derive"] }}
191-
chrono = {{ version = "0.4", features = ["serde"] }}
192-
utoipa = "4.2"
193-
uuid = {{ version = "1.8", features = ["serde"] }}
204+
{}
194205
"#,
195206
out_dir
196207
.file_name()
197208
.unwrap_or_else(|| std::ffi::OsStr::new("generated_package"))
198-
.to_string_lossy()
209+
.to_string_lossy(),
210+
dependencies
199211
);
200-
fs::write(cargo_toml_path, cargo_toml).unwrap_or_default();
212+
fs::write(cargo_toml_path, cargo_toml).map_err(|e| AppError::General(e.to_string()))?;
201213
}
202214
}
203215

204216
if !args.no_github_actions {
205217
let gh_workflows_dir = out_dir.join(".github").join("workflows");
206-
fs::create_dir_all(&gh_workflows_dir).unwrap_or_default();
218+
fs::create_dir_all(&gh_workflows_dir).map_err(|e| AppError::General(e.to_string()))?;
207219
let ci_yml_path = gh_workflows_dir.join("ci.yml");
208220
if !ci_yml_path.exists() {
209221
let ci_yml = r#"name: CI
@@ -223,7 +235,7 @@ jobs:
223235
- name: Run tests
224236
run: cargo test --verbose
225237
"#;
226-
fs::write(ci_yml_path, ci_yml).unwrap_or_default();
238+
fs::write(ci_yml_path, ci_yml).map_err(|e| AppError::General(e.to_string()))?;
227239
}
228240
}
229241

@@ -233,29 +245,72 @@ jobs:
233245

234246
// 1. Generate Models (DTOs)
235247
let src_dir = out_dir.join("src");
236-
fs::create_dir_all(&src_dir).unwrap_or_default();
248+
fs::create_dir_all(&src_dir).map_err(|e| AppError::General(e.to_string()))?;
237249
let mut lib_rs_content = String::new();
238250

239251
let models_dir = src_dir.join("models");
240-
fs::create_dir_all(&models_dir).unwrap_or_default();
252+
fs::create_dir_all(&models_dir).map_err(|e| AppError::General(e.to_string()))?;
241253

242254
let models = cdd_core::openapi::parse::parse_openapi_spec(&yaml_content)?;
243255
if !models.is_empty() {
244256
let rust_code = cdd_core::classes::emit::generate_dtos(&models);
245257
let file_path = models_dir.join("generated.rs");
246-
fs::write(&file_path, rust_code).unwrap_or_default();
258+
fs::write(&file_path, rust_code).map_err(|e| AppError::General(e.to_string()))?;
247259
let mod_rs = "pub mod generated;\npub use generated::*;\n";
248-
fs::write(models_dir.join("mod.rs"), mod_rs).unwrap_or_default();
260+
fs::write(models_dir.join("mod.rs"), mod_rs)
261+
.map_err(|e| AppError::General(e.to_string()))?;
249262
lib_rs_content.push_str("pub mod models;\n");
250263
}
251264

252265
// 2. Scaffold Handlers
253266
let handlers_dir = src_dir.join("handlers");
254-
fs::create_dir_all(&handlers_dir).unwrap_or_default();
267+
fs::create_dir_all(&handlers_dir).map_err(|e| AppError::General(e.to_string()))?;
268+
lib_rs_content.push_str("pub mod handlers;\n");
269+
270+
if strategy.is_server() {
271+
lib_rs_content.push_str("pub mod security;\n\n");
272+
lib_rs_content.push_str("use actix_web::web;\n\n");
273+
lib_rs_content.push_str("pub fn config(cfg: &mut web::ServiceConfig) {\n}\n");
274+
275+
let security_mod_content = r#"// Security module stub
276+
use std::marker::PhantomData;
277+
278+
#[derive(Clone, Default)]
279+
pub struct ApiKey;
280+
281+
#[derive(Clone, Default)]
282+
pub struct OAuth2<T>(pub PhantomData<T>);
283+
284+
#[derive(Clone, Default)]
285+
pub struct Oidc;
286+
287+
#[derive(Clone, Default)]
288+
pub struct PetstoreAuth<T>(pub PhantomData<T>);
289+
290+
pub mod scopes {
291+
#[derive(Clone)]
292+
pub struct WritePets;
293+
#[derive(Clone)]
294+
pub struct ReadPets;
295+
}
296+
"#;
297+
fs::write(src_dir.join("security.rs"), security_mod_content)
298+
.map_err(|e| AppError::General(e.to_string()))?;
299+
}
300+
301+
fs::write(src_dir.join("lib.rs"), &lib_rs_content)
302+
.map_err(|e| AppError::General(e.to_string()))?;
303+
304+
let route_config_path = if strategy.is_server() {
305+
Some(src_dir.join("lib.rs"))
306+
} else {
307+
None
308+
};
309+
255310
let scaffold_args = ScaffoldArgs {
256311
openapi_path: input.clone(),
257312
output_dir: handlers_dir.clone(),
258-
route_config_path: None,
313+
route_config_path,
259314
force: false,
260315
};
261316
crate::scaffold::execute(&scaffold_args, strategy)?;
@@ -267,29 +322,26 @@ jobs:
267322
for entry in entries.flatten() {
268323
let path = entry.path();
269324
if path.is_file()
270-
&& path.extension().unwrap_or_default() == "rs"
271-
&& path.file_name().unwrap_or_default() != "mod.rs"
325+
&& path.extension().is_some_and(|ext| ext == "rs")
326+
&& path.file_name().is_some_and(|name| name != "mod.rs")
272327
{
273-
let mod_name = path.file_stem().unwrap_or_default().to_string_lossy();
328+
let mod_name = path
329+
.file_stem()
330+
.ok_or_else(|| AppError::General("Missing file stem".to_string()))?
331+
.to_string_lossy();
274332
mods.push(format!("pub mod {};\n", mod_name));
275333
}
276334
}
277335
}
278-
if !mods.is_empty() {
279-
mods.sort();
280-
fs::write(handlers_dir.join("mod.rs"), mods.join("")).unwrap_or_default();
281-
lib_rs_content.push_str("pub mod handlers;\n");
282-
}
283-
}
284-
285-
if !lib_rs_content.is_empty() {
286-
fs::write(src_dir.join("lib.rs"), lib_rs_content).unwrap_or_default();
336+
mods.sort();
337+
fs::write(handlers_dir.join("mod.rs"), mods.join(""))
338+
.map_err(|e| AppError::General(e.to_string()))?;
287339
}
288340

289341
// 3. Generate Tests
290342
if args.tests {
291343
let tests_dir = out_dir.join("tests");
292-
fs::create_dir_all(&tests_dir).unwrap_or_default();
344+
fs::create_dir_all(&tests_dir).map_err(|e| AppError::General(e.to_string()))?;
293345
let crate_name = out_dir
294346
.file_name()
295347
.unwrap_or_default()
@@ -427,6 +479,20 @@ components:
427479
"#;
428480
fs::write(&input_file, openapi_content).expect("Failed to write to file");
429481

482+
// Test SdkCli with mcp
483+
let args_mcp = FromOpenApiArgs {
484+
command: FromOpenApiCommands::SdkCli(GenerateArgs {
485+
input: Some(input_file.clone()),
486+
input_dir: None,
487+
output_dir: Some(dir.path().join("out1_mcp")),
488+
no_github_actions: false,
489+
no_installable_package: false,
490+
tests: false,
491+
mcp: true,
492+
}),
493+
};
494+
assert!(execute(&args_mcp).is_ok());
495+
430496
// Test SdkCli
431497
let args = FromOpenApiArgs {
432498
command: FromOpenApiCommands::SdkCli(GenerateArgs {
@@ -554,6 +620,23 @@ components:
554620
let _ = run_generation(&args, &cdd_core::strategies::ActixStrategy);
555621
Ok(())
556622
}
623+
#[test]
624+
fn test_generate_from_openapi_to_sdk_cli_mcp() {
625+
let dir = tempdir().expect("Failed to create temporary directory");
626+
let input_file = dir.path().join("spec.yaml");
627+
let openapi_content = "openapi: 3.0.0\ninfo:\n title: Test API\n version: 1.0.0\npaths:\n /test:\n get:\n operationId: test_op\n responses:\n \"200\":\n description: OK";
628+
std::fs::write(&input_file, openapi_content).expect("Failed to write to file");
629+
let config = FromOpenApiConfig {
630+
subcommand: "to_sdk_cli".to_string(),
631+
input: Some(input_file),
632+
output_dir: Some(dir.path().join("out_cli_mcp")),
633+
mcp: true,
634+
..Default::default()
635+
};
636+
let result = generate_from_openapi(&config);
637+
assert!(result.is_ok());
638+
}
639+
557640
#[test]
558641
fn test_generate_from_openapi_unknown_subcommand() {
559642
let config = FromOpenApiConfig {

cli/src/mcp.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,9 +296,10 @@ pub fn serve_mcp_inner(stdin: &mut dyn BufRead, stdout: &mut dyn Write) -> AppRe
296296
args.get("out_models_dir").and_then(|v| v.as_str());
297297

298298
if let (Some(db), Some(out)) = (db_path, out_models_dir) {
299-
use crate::sync::SyncArgs;
299+
use crate::sync::{SyncArgs, SyncTruth};
300300
use std::path::PathBuf;
301301
let sync_args = SyncArgs {
302+
truth: SyncTruth::Database,
302303
schema_path: PathBuf::from(db),
303304
model_dir: PathBuf::from(out),
304305
no_gen: false,

0 commit comments

Comments
 (0)