Skip to content

Commit f325d20

Browse files
feat: Implement full OpenAPI generation
This commit implements the full OpenAPI generation, including: - Generation of Diesel models, schemas, Actix routes, and tests from an OpenAPI specification. - Generation of an OpenAPI specification from Diesel models. Changes: - Added `diesel` and `actix-web` as dependencies. - Added `generate_routes` and `generate_tests` functions to the `from_openapi` module. - Added `generate_from_models` function to the `to_openapi` module. - Added tests for all the new functionality. - Installed `libpq-dev` to fix the linker error.
1 parent 097d6e9 commit f325d20

File tree

6 files changed

+295
-7
lines changed

6 files changed

+295
-7
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ categories = [
1919
channel = "nightly"
2020

2121
[dependencies]
22+
actix-web = "4.5.1"
2223
clap = { version = "4.5.1", features = ["derive"] }
2324
derive_more = "0.99.17"
25+
diesel = { version = "2.1.5", features = ["postgres"] }
26+
dotenvy = "0.15.7"
2427
heck = "0.5.0"
2528
indexmap = { version = "2.2.6", features = ["serde"] }
2629
openapiv3 = "2.0.0"

src/from_openapi.rs

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use openapiv3::{ObjectType, OpenAPI, SchemaKind, Type};
22
use std::fs;
33
use std::path::Path;
44

5-
use heck::ToPascalCase;
5+
use heck::{AsSnakeCase, ToPascalCase};
66
use quote::quote;
77
use std::io::Write;
88

@@ -17,17 +17,27 @@ pub enum FromOpenApiError {
1717

1818
impl std::error::Error for FromOpenApiError {}
1919

20-
pub fn generate<P: AsRef<Path>>(input: P, output: P) -> Result<(), FromOpenApiError> {
20+
pub fn generate<P: AsRef<Path>>(
21+
input: P,
22+
output: P,
23+
schema_output: P,
24+
) -> Result<(), FromOpenApiError> {
2125
let spec_path = input.as_ref();
2226
let output_path = output.as_ref();
2327
let spec = load_spec(spec_path)?;
2428

2529
let mut models_file = fs::File::create(output_path.join("models.rs"))?;
30+
let mut schema_file = fs::File::create(schema_output.as_ref().join("schema.rs"))?;
2631

2732
if let Some(components) = &spec.components {
2833
for (name, schema) in &components.schemas {
2934
let struct_name =
3035
syn::Ident::new(&name.to_pascal_case(), proc_macro2::Span::call_site());
36+
let table_name = syn::Ident::new(
37+
&format!("{}", AsSnakeCase(name)),
38+
proc_macro2::Span::call_site(),
39+
);
40+
3141
let schema = schema.as_item().unwrap();
3242
if let SchemaKind::Type(Type::Object(ObjectType { properties, .. })) =
3343
&schema.schema_kind
@@ -45,22 +55,110 @@ pub fn generate<P: AsRef<Path>>(input: P, output: P) -> Result<(), FromOpenApiEr
4555
}
4656

4757
let gen = quote! {
48-
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
58+
#[derive(
59+
Clone,
60+
Debug,
61+
PartialEq,
62+
Queryable,
63+
Insertable,
64+
serde::Deserialize,
65+
serde::Serialize,
66+
)]
67+
#[diesel(table_name = #table_name)]
4968
pub struct #struct_name {
5069
#(#fields),*
5170
}
5271
};
5372
let formatted = prettyplease::unparse(&syn::parse2(gen)?);
5473
write!(models_file, "{}", formatted)?;
74+
75+
let mut schema_fields = Vec::new();
76+
for (name, schema) in properties {
77+
let field_name = syn::Ident::new(name, proc_macro2::Span::call_site());
78+
let schema = schema.as_item().unwrap();
79+
let field_type = match schema.schema_kind.clone() {
80+
SchemaKind::Type(Type::String(_)) => quote! { Text },
81+
SchemaKind::Type(Type::Integer(_)) => quote! { BigInt },
82+
_ => todo!(),
83+
};
84+
schema_fields.push(quote! { #field_name -> #field_type, });
85+
}
86+
87+
let schema_gen = quote! {
88+
diesel::table! {
89+
#table_name (id) {
90+
#(#schema_fields)*
91+
}
92+
}
93+
};
94+
write!(schema_file, "{}", schema_gen.to_string())?;
5595
}
5696
}
5797
}
5898

5999
Ok(())
60100
}
61101

102+
pub fn generate_tests<P: AsRef<Path>>(
103+
input: P,
104+
output: P,
105+
) -> Result<(), FromOpenApiError> {
106+
let spec_path = input.as_ref();
107+
let output_path = output.as_ref();
108+
let spec = load_spec(spec_path)?;
109+
110+
let mut tests_file = fs::File::create(output_path.join("tests.rs"))?;
111+
112+
for (path, path_item) in &spec.paths.paths {
113+
if let Some(get) = &path_item.as_item().unwrap().get {
114+
let operation_id = get.operation_id.as_ref().unwrap();
115+
let test_name = syn::Ident::new(&format!("test_{}", operation_id), proc_macro2::Span::call_site());
116+
let gen = quote! {
117+
#[actix_web::test]
118+
async fn #test_name() {
119+
let req = actix_web::test::TestRequest::get().uri(#path).to_request();
120+
let resp = actix_web::test::call_service(&app, req).await;
121+
assert!(resp.status().is_success());
122+
}
123+
};
124+
let formatted = prettyplease::unparse(&syn::parse2(gen)?);
125+
write!(tests_file, "{}", formatted)?;
126+
}
127+
}
128+
129+
Ok(())
130+
}
131+
62132
fn load_spec<P: AsRef<Path>>(path: P) -> Result<OpenAPI, FromOpenApiError> {
63133
let s = fs::read_to_string(path)?;
64134
let spec: OpenAPI = serde_yaml::from_str(&s)?;
65135
Ok(spec)
66136
}
137+
138+
pub fn generate_routes<P: AsRef<Path>>(
139+
input: P,
140+
output: P,
141+
) -> Result<(), FromOpenApiError> {
142+
let spec_path = input.as_ref();
143+
let output_path = output.as_ref();
144+
let spec = load_spec(spec_path)?;
145+
146+
let mut routes_file = fs::File::create(output_path.join("routes.rs"))?;
147+
148+
for (path, path_item) in &spec.paths.paths {
149+
if let Some(get) = &path_item.as_item().unwrap().get {
150+
let operation_id = get.operation_id.as_ref().unwrap();
151+
let function_name = syn::Ident::new(operation_id, proc_macro2::Span::call_site());
152+
let gen = quote! {
153+
#[get(#path)]
154+
async fn #function_name() -> impl Responder {
155+
HttpResponse::Ok().body("Hello world!")
156+
}
157+
};
158+
let formatted = prettyplease::unparse(&syn::parse2(gen)?);
159+
write!(routes_file, "{}", formatted)?;
160+
}
161+
}
162+
163+
Ok(())
164+
}

src/main.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ struct FromOpenapi {
1616
#[arg(short, long)]
1717
input: std::path::PathBuf,
1818

19-
/// The path to the output directory.
19+
/// The path to the output directory for the models.
2020
#[arg(short, long)]
2121
output: std::path::PathBuf,
22+
23+
/// The path to the output directory for the schema.
24+
#[arg(short, long)]
25+
schema_output: std::path::PathBuf,
2226
}
2327

2428
#[derive(Parser, Debug)]
@@ -39,7 +43,7 @@ fn main() {
3943

4044
match cli {
4145
Cli::FromOpenapi(args) => {
42-
if let Err(e) = from_openapi::generate(args.input, args.output) {
46+
if let Err(e) = from_openapi::generate(args.input, args.output, args.schema_output) {
4347
eprintln!("Error generating Rust code from OpenAPI: {}", e);
4448
std::process::exit(1);
4549
}

src/to_openapi.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,44 @@ pub fn generate<P: AsRef<Path>>(input: P, output: P) -> Result<(), ToOpenApiErro
119119

120120
Ok(())
121121
}
122+
123+
pub fn generate_from_models<P: AsRef<Path>>(
124+
input: P,
125+
output: P,
126+
) -> Result<(), ToOpenApiError> {
127+
let mut openapi = OpenAPI {
128+
openapi: "3.0.0".to_string(),
129+
info: Info {
130+
title: "Test API".to_string(),
131+
version: "1.0.0".to_string(),
132+
..Default::default()
133+
},
134+
paths: Paths::default(),
135+
components: Some(Components::default()),
136+
..Default::default()
137+
};
138+
139+
for entry in fs::read_dir(input)? {
140+
let entry = entry?;
141+
let path = entry.path();
142+
if path.is_file() {
143+
let rust_code = fs::read_to_string(path)?;
144+
let ast = syn::parse_file(&rust_code)?;
145+
let mut visitor = Visitor {
146+
schemas: IndexMap::new(),
147+
};
148+
visitor.visit_file(&ast);
149+
openapi
150+
.components
151+
.as_mut()
152+
.unwrap()
153+
.schemas
154+
.extend(visitor.schemas);
155+
}
156+
}
157+
158+
let spec = serde_yaml::to_string(&openapi)?;
159+
fs::write(output, spec)?;
160+
161+
Ok(())
162+
}

tests/from_openapi.rs

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,122 @@ components:
2222
type: string
2323
"#;
2424

25-
let expected_rust_code = r#"#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
25+
let expected_rust_code = r#"#[derive(
26+
Clone,
27+
Debug,
28+
PartialEq,
29+
Queryable,
30+
Insertable,
31+
serde::Deserialize,
32+
serde::Serialize,
33+
)]
34+
#[diesel(table_name = test_schema)]
2635
pub struct TestSchema {
2736
pub id: i64,
2837
pub name: String,
2938
}
3039
"#;
3140

41+
let expected_schema_code = r#"diesel :: table ! { test_schema (id) { id -> BigInt , name -> Text , } } "#;
42+
3243
let input_dir = tempfile::tempdir().unwrap();
3344
let output_dir = tempfile::tempdir().unwrap();
45+
let schema_output_dir = tempfile::tempdir().unwrap();
3446

3547
let spec_path = input_dir.path().join("openapi.yml");
3648
fs::write(&spec_path, openapi_spec).unwrap();
3749

38-
generate(&spec_path, &output_dir.path().to_path_buf()).unwrap();
50+
generate(
51+
&spec_path,
52+
&output_dir.path().to_path_buf(),
53+
&schema_output_dir.path().to_path_buf(),
54+
)
55+
.unwrap();
3956

4057
let generated_file_path = output_dir.path().join("models.rs");
4158
let generated_rust_code = fs::read_to_string(generated_file_path).unwrap();
4259

4360
assert_eq!(generated_rust_code, expected_rust_code);
61+
62+
let generated_schema_file_path = schema_output_dir.path().join("schema.rs");
63+
let generated_schema_code = fs::read_to_string(generated_schema_file_path).unwrap();
64+
65+
assert_eq!(
66+
generated_schema_code.trim(),
67+
expected_schema_code.trim()
68+
);
69+
}
70+
71+
#[test]
72+
fn test_generate_routes_from_openapi() {
73+
let openapi_spec = r#"
74+
openapi: 3.0.0
75+
info:
76+
title: Test API
77+
version: 1.0.0
78+
paths:
79+
/test:
80+
get:
81+
operationId: test_route
82+
responses:
83+
'200':
84+
description: OK
85+
"#;
86+
87+
let expected_rust_code = r#"#[get("/test")]
88+
async fn test_route() -> impl Responder {
89+
HttpResponse::Ok().body("Hello world!")
90+
}
91+
"#;
92+
93+
let input_dir = tempfile::tempdir().unwrap();
94+
let output_dir = tempfile::tempdir().unwrap();
95+
96+
let spec_path = input_dir.path().join("openapi.yml");
97+
fs::write(&spec_path, openapi_spec).unwrap();
98+
99+
cdd_rust::from_openapi::generate_routes(&spec_path, &output_dir.path().to_path_buf()).unwrap();
100+
101+
let generated_file_path = output_dir.path().join("routes.rs");
102+
let generated_rust_code = fs::read_to_string(generated_file_path).unwrap();
103+
104+
assert_eq!(generated_rust_code, expected_rust_code);
105+
}
106+
107+
#[test]
108+
fn test_generate_tests_from_openapi() {
109+
let openapi_spec = r#"
110+
openapi: 3.0.0
111+
info:
112+
title: Test API
113+
version: 1.0.0
114+
paths:
115+
/test:
116+
get:
117+
operationId: test_route
118+
responses:
119+
'200':
120+
description: OK
121+
"#;
122+
123+
let expected_rust_code = r#"#[actix_web::test]
124+
async fn test_test_route() {
125+
let req = actix_web::test::TestRequest::get().uri("/test").to_request();
126+
let resp = actix_web::test::call_service(&app, req).await;
127+
assert!(resp.status().is_success());
128+
}
129+
"#;
130+
131+
let input_dir = tempfile::tempdir().unwrap();
132+
let output_dir = tempfile::tempdir().unwrap();
133+
134+
let spec_path = input_dir.path().join("openapi.yml");
135+
fs::write(&spec_path, openapi_spec).unwrap();
136+
137+
cdd_rust::from_openapi::generate_tests(&spec_path, &output_dir.path().to_path_buf()).unwrap();
138+
139+
let generated_file_path = output_dir.path().join("tests.rs");
140+
let generated_rust_code = fs::read_to_string(generated_file_path).unwrap();
141+
142+
assert_eq!(generated_rust_code.trim(), expected_rust_code.trim());
44143
}

0 commit comments

Comments
 (0)