├── .gitignore ├── docs ├── generators │ ├── ts.md │ ├── ts_nestjs.md │ ├── openapi.md │ ├── rust_axum.md │ └── rust.md ├── references │ ├── yaml.md │ └── api.md └── index.md ├── rust-toolchain.toml ├── .DS_Store ├── lib ├── spec │ ├── testdata │ │ └── parse_yaml_happy.yaml │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── parser │ ├── src │ │ ├── api_parser.rs │ │ ├── api.pest │ │ └── lib.rs │ └── Cargo.toml ├── generator │ ├── testdata │ │ └── hello │ │ │ ├── rust │ │ │ ├── Cargo.toml │ │ │ ├── main.api │ │ │ ├── src │ │ │ │ ├── generated.rs │ │ │ │ └── main.rs │ │ │ └── Cargo.lock │ │ │ └── rust_axum │ │ │ ├── Cargo.toml │ │ │ ├── main.api │ │ │ └── src │ │ │ ├── main.rs │ │ │ └── generated.rs │ ├── Cargo.toml │ └── src │ │ ├── rust_utils.rs │ │ ├── ts_nestjs.rs │ │ ├── ts.rs │ │ ├── python_redis.rs │ │ ├── openapi_utils.rs │ │ ├── lib.rs │ │ ├── golang.rs │ │ ├── python.rs │ │ ├── utils.rs │ │ ├── java.rs │ │ ├── java_springweb.rs │ │ ├── rust.rs │ │ └── rust_axum.rs └── generator-wasm │ ├── Cargo.toml │ └── src │ └── lib.rs ├── dev ├── publish.sh └── publish.ps1 ├── .vscode └── settings.json ├── examples ├── todo-py │ └── main.api └── todo-rs │ ├── main.api │ └── src │ └── generated.rs ├── Cargo.toml ├── bin └── cli │ ├── Cargo.toml │ └── src │ └── main.rs ├── mkdocs.yaml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | site -------------------------------------------------------------------------------- /docs/generators/ts.md: -------------------------------------------------------------------------------- 1 | TODO -------------------------------------------------------------------------------- /docs/generators/ts_nestjs.md: -------------------------------------------------------------------------------- 1 | TODO -------------------------------------------------------------------------------- /docs/generators/openapi.md: -------------------------------------------------------------------------------- 1 | # OpenAPI 2 | 3 | TODO -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | -------------------------------------------------------------------------------- /docs/generators/rust_axum.md: -------------------------------------------------------------------------------- 1 | # Rust Axum Generator 2 | 3 | TODO -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theogonic/cronus/HEAD/.DS_Store -------------------------------------------------------------------------------- /lib/spec/testdata/parse_yaml_happy.yaml: -------------------------------------------------------------------------------- 1 | types: 2 | schemas: 3 | usecases: -------------------------------------------------------------------------------- /lib/parser/src/api_parser.rs: -------------------------------------------------------------------------------- 1 | 2 | use pest::{Parser, iterators::Pair}; 3 | 4 | #[derive(pest_derive::Parser)] 5 | #[grammar = "api.pest"] 6 | pub struct APIParser; 7 | -------------------------------------------------------------------------------- /lib/generator/testdata/hello/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "testdata-hello" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | async-trait = "0.1.77" 8 | serde = { version = "1.0", features = ["derive"] } 9 | serde_json = "1.0" 10 | 11 | [workspace] -------------------------------------------------------------------------------- /lib/generator/testdata/hello/rust/main.api: -------------------------------------------------------------------------------- 1 | global [generator.rust.file = "src/generated.rs"] 2 | 3 | usecase Hello { 4 | createHello { 5 | in { 6 | hi: string 7 | } 8 | out { 9 | answer: string 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /lib/generator/testdata/hello/rust_axum/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "testdata-hello" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | async-trait = "0.1.77" 8 | serde = { version = "1.0", features = ["derive"] } 9 | serde_json = "1.0" 10 | axum = "0.7" 11 | [workspace] -------------------------------------------------------------------------------- /dev/publish.sh: -------------------------------------------------------------------------------- 1 | crates=("cronus_spec" "cronus_parser" "cronus_generator" "cronus_cli") 2 | 3 | for crate in "${crates[@]}"; do 4 | echo "Publishing $crate..." 5 | cargo publish -p "$crate" 6 | sleep 5 # Wait for a few seconds to avoid rate limiting 7 | done 8 | 9 | echo "All crates published successfully." -------------------------------------------------------------------------------- /dev/publish.ps1: -------------------------------------------------------------------------------- 1 | $crates = @("cronus_spec", "cronus_parser", "cronus_generator", "cronus_cli") 2 | 3 | foreach ($crate in $crates) { 4 | Write-Host "Publishing $crate..." 5 | cargo publish -p "$crate" 6 | Start-Sleep -Seconds 5 # Wait for a few seconds to avoid rate limiting 7 | } 8 | 9 | Write-Host "All crates published successfully." -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.linkedProjects": [ 3 | "./lib/generator/testdata/hello/rust/Cargo.toml", 4 | "lib/generator/Cargo.toml", 5 | "lib/parser/Cargo.toml", 6 | "lib/spec/Cargo.toml", 7 | "lib/generator-wasm/Cargo.toml", 8 | "./lib/generator/testdata/hello/rust_axum/Cargo.toml" 9 | ] 10 | } -------------------------------------------------------------------------------- /examples/todo-py/main.api: -------------------------------------------------------------------------------- 1 | #[@python.file = "src/generated.py"] 2 | 3 | 4 | struct Todo { 5 | id: string 6 | content: string 7 | } 8 | 9 | usecase Todo { 10 | 11 | [rest.method = "post"] 12 | createTodo { 13 | in { 14 | content: string 15 | } 16 | 17 | out { 18 | todo: Todo 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /docs/generators/rust.md: -------------------------------------------------------------------------------- 1 | # Rust Generator 2 | 3 | ## Configuration 4 | 5 | ### Global 6 | Used with #[rust.] 7 | 8 | ### Config: 9 | | Config | Type | Description | 10 | |--|--|--| 11 | | file | string? | output .rs file | 12 | | no_default_derive | bool? | Do not place default derive for struct | 13 | | default_derive | string[]? | Default derive(s) for every struct | 14 | | uses | string[]? |Custom extra uses | -------------------------------------------------------------------------------- /lib/spec/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cronus_spec" 3 | version.workspace = true 4 | edition = "2021" 5 | description = "The definitions for cronus API spec." 6 | license = "MIT" 7 | repository = "https://github.com/theogonic/cronus" 8 | documentation = "https://theogonic.github.io/cronus/" 9 | 10 | [dependencies] 11 | serde = { version = "1.0", features = ["derive"] } 12 | serde_yaml = {workspace = true} 13 | anyhow = "1.0" -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "lib/parser", 4 | "lib/generator", 5 | "bin/cli", 6 | "lib/spec", 7 | "lib/generator-wasm" 8 | ] 9 | resolver = "2" 10 | 11 | [workspace.package] 12 | version = "0.7.0" 13 | 14 | [workspace.dependencies] 15 | cronus_spec = { path = "lib/spec", version = "0.7.0" } 16 | cronus_parser = { path = "lib/parser", version = "0.7.0" } 17 | cronus_generator = { path = "lib/generator", version = "0.7.0" } 18 | serde_yaml = "0.9.33" -------------------------------------------------------------------------------- /lib/generator-wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cronus_generator_wasm" 3 | version.workspace = true 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | wasm-bindgen = "0.2" 12 | serde-wasm-bindgen = "0.6.5" 13 | console_error_panic_hook = "0.1.7" 14 | cronus_spec = { workspace = true } 15 | cronus_parser = { workspace = true } 16 | cronus_generator = { workspace = true } 17 | -------------------------------------------------------------------------------- /lib/parser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cronus_parser" 3 | version.workspace = true 4 | edition = "2021" 5 | description = "The DSL parser for cronus API spec." 6 | license = "MIT" 7 | repository = "https://github.com/theogonic/cronus" 8 | documentation = "https://theogonic.github.io/cronus/" 9 | 10 | [dependencies] 11 | pest="2.7.5" 12 | pest_derive = "2.7.5" 13 | cronus_spec = { workspace = true } 14 | serde = { version = "1.0", features = ["derive"] } 15 | serde_yaml = "0.8" 16 | tracing = "0.1" 17 | anyhow = "1.0" -------------------------------------------------------------------------------- /examples/todo-rs/main.api: -------------------------------------------------------------------------------- 1 | #[@rust.file = "src/generated.rs"] 2 | #[@rust_axum.file = "src/generated.rs"] 3 | #[@rust.async] 4 | #[@rust.async_trait] 5 | 6 | 7 | 8 | [@rust = "tcp::TcpStream"] 9 | type A 10 | 11 | 12 | 13 | 14 | struct Todo { 15 | id: string 16 | content: string 17 | } 18 | 19 | usecase Todo { 20 | 21 | [rest.method = "post"] 22 | createTodo { 23 | in { 24 | content: string 25 | } 26 | 27 | out { 28 | todo: Todo 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /lib/generator/testdata/hello/rust/src/generated.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use async_trait::async_trait; 3 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 4 | pub struct CreateHelloRequest { 5 | pub hi: String, 6 | } 7 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 8 | pub struct CreateHelloResponse { 9 | pub answer: String, 10 | } 11 | pub trait HelloUsecase { 12 | fn create_hello(&self, request: CreateHelloRequest) -> Result>; 13 | } 14 | -------------------------------------------------------------------------------- /lib/generator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cronus_generator" 3 | version.workspace = true 4 | edition = "2021" 5 | description = "The generators for cronus API spec." 6 | license = "MIT" 7 | repository = "https://github.com/theogonic/cronus" 8 | documentation = "https://theogonic.github.io/cronus/" 9 | 10 | [dependencies] 11 | cronus_spec = { workspace = true } 12 | cronus_parser = { workspace = true } 13 | convert_case = "0.6.0" 14 | tracing = "0.1" 15 | anyhow = "1.0" 16 | serde = { version = "1.0", features = ["derive"] } 17 | serde_yaml = "0.8" 18 | serde_json = "1.0" -------------------------------------------------------------------------------- /bin/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cronus_cli" 3 | version.workspace = true 4 | edition = "2021" 5 | description = "The CLI for cronus" 6 | license = "MIT" 7 | repository = "https://github.com/theogonic/cronus" 8 | documentation = "https://theogonic.github.io/cronus/" 9 | readme = "../../README.md" 10 | 11 | [dependencies] 12 | clap = { version = "4.4.7", features = ["derive"] } 13 | cronus_generator = { workspace = true } 14 | cronus_parser = { workspace = true } 15 | cronus_spec = { workspace = true } 16 | serde_yaml = {workspace = true} 17 | tracing = "0.1" 18 | tracing-subscriber = "0.3" 19 | anyhow = "1.0" -------------------------------------------------------------------------------- /lib/generator/testdata/hello/rust/src/main.rs: -------------------------------------------------------------------------------- 1 | mod generated; 2 | use generated::{CreateHelloRequest, CreateHelloResponse, HelloUsecase}; 3 | 4 | struct HelloUsecaseImpl { 5 | 6 | } 7 | 8 | impl generated::HelloUsecase for HelloUsecaseImpl { 9 | fn create_hello(&self, request: CreateHelloRequest) -> Result> { 10 | Ok(generated::CreateHelloResponse { 11 | answer: request.hi 12 | }) 13 | } 14 | } 15 | 16 | 17 | fn main() { 18 | let usecase = HelloUsecaseImpl {}; 19 | usecase.create_hello(CreateHelloRequest{ hi: "world".to_string()}); 20 | } 21 | -------------------------------------------------------------------------------- /lib/generator/testdata/hello/rust_axum/main.api: -------------------------------------------------------------------------------- 1 | global [generator.rust.file = "src/generated.rs"] 2 | global [generator.rust_axum.file = "src/generated.rs"] 3 | 4 | global [generator.rust.async] 5 | global [generator.rust.async_trait] 6 | 7 | [rest.path = "hello"] 8 | usecase Hello { 9 | [rest.method = "post"] 10 | createHello { 11 | in { 12 | hi: string 13 | } 14 | out { 15 | answer: string 16 | } 17 | } 18 | 19 | [rest.method = "get"] 20 | [rest.path = "item"] 21 | getHello { 22 | in { 23 | [rest.query] 24 | hi: string 25 | } 26 | out { 27 | answer: string 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /lib/generator/testdata/hello/rust_axum/src/main.rs: -------------------------------------------------------------------------------- 1 | mod generated; 2 | use generated::{CreateHelloRequest, CreateHelloResponse, HelloUsecase}; 3 | use async_trait::async_trait; 4 | struct HelloUsecaseImpl { 5 | 6 | } 7 | 8 | #[async_trait] 9 | impl generated::HelloUsecase for HelloUsecaseImpl { 10 | async fn create_hello(&self, request: CreateHelloRequest) -> Result> { 11 | Ok(generated::CreateHelloResponse { 12 | answer: request.hi 13 | }) 14 | } 15 | 16 | async fn get_hello(&self, request: generated::GetHelloRequest) -> Result> { 17 | todo!() 18 | } 19 | } 20 | 21 | 22 | fn main() { 23 | let usecase = HelloUsecaseImpl {}; 24 | usecase.create_hello(CreateHelloRequest{ hi: "world".to_string()}); 25 | } 26 | -------------------------------------------------------------------------------- /mkdocs.yaml: -------------------------------------------------------------------------------- 1 | theme: 2 | name: material 3 | features: 4 | - navigation.tabs 5 | - content.code.copy 6 | - attr_list 7 | - md_in_html 8 | - navigation.top 9 | - header.autohide 10 | palette: 11 | primary: black 12 | accent: indigo 13 | logo: 14 | repo_url: https://github.com/theogonic/cronus 15 | site_name: Cronus 16 | nav: 17 | - Home: index.md 18 | - Generators: 19 | - rust: generators/rust.md 20 | - rust-axum: generators/rust_axum.md 21 | - typescript: generators/ts.md 22 | - ts-nestjs: generators/ts_nestjs.md 23 | - openapi: generators/openapi.md 24 | - References: 25 | - YAML Format: references/yaml.md 26 | - API Format: references/api.md 27 | 28 | markdown_extensions: 29 | - pymdownx.highlight: 30 | anchor_linenums: true 31 | line_spans: __span 32 | pygments_lang_class: true 33 | - pymdownx.tabbed: 34 | alternate_style: true 35 | - pymdownx.inlinehilite 36 | - pymdownx.snippets 37 | - pymdownx.superfences -------------------------------------------------------------------------------- /examples/todo-rs/src/generated.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use async_trait::async_trait; 3 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 4 | pub struct Todo { 5 | pub id: String, 6 | pub content: String, 7 | } 8 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 9 | pub struct CreateTodoRequest { 10 | pub content: String, 11 | } 12 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 13 | pub struct CreateTodoResponse { 14 | pub todo: Todo, 15 | } 16 | #[async_trait] 17 | pub trait TodoUsecase { 18 | async fn create_todo(&self, request: CreateTodoRequest) -> Result>; 19 | } 20 | 21 | use axum::{ 22 | extract::State, 23 | http::{header, Response, StatusCode}, 24 | response::IntoResponse, 25 | Extension, Json, 26 | Router 27 | }; 28 | pub async fn create_todo(State(state): State>, Json(request): Json) -> Result)> { 29 | 30 | match state.todo.create_todo(request).await { 31 | Ok(res) => { 32 | Ok(Json(res)) 33 | }, 34 | Err(err) => { 35 | let mut err_obj = serde_json::Map::new(); 36 | err_obj.insert("message".to_owned(), serde_json::Value::from(err.to_string())); 37 | Err((StatusCode::BAD_REQUEST, Json(serde_json::Value::Object(err_obj)))) 38 | }, 39 | } 40 | } 41 | #[derive(Clone)] 42 | pub struct Usecases { 43 | pub todo: std::sync::Arc, 44 | } 45 | pub fn router_init(usecases: std::sync::Arc) -> Router { 46 | Router::new() 47 | .route("", axum::routing::post(create_todo)) 48 | .with_state(usecases) 49 | } 50 | -------------------------------------------------------------------------------- /lib/parser/src/api.pest: -------------------------------------------------------------------------------- 1 | 2 | // Basic rules for whitespace and comments 3 | WHITESPACE = _{ " " | "\t" | "\r" | "\n" } 4 | COMMENT = _{ "//" ~ (!NEWLINE ~ ANY)* } 5 | 6 | // Identifiers and basic types 7 | identifier = @{ ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_")* } 8 | option_identifier = @{ ("@" | ASCII_ALPHA) ~ (ASCII_ALPHANUMERIC | "_" | ".")* } 9 | type_identifier = @{ ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_" | "[" | "]" | "<" | ">" | ",")* } 10 | path = @{ (!"\n" ~ ANY)+ } 11 | 12 | 13 | // Import statements 14 | import = { "import" ~ path } 15 | 16 | // Options 17 | option_value = { integer | string | bool | array } 18 | integer = { ASCII_DIGIT+ } 19 | string = { "\"" ~ (("\\\"" | !"\"" ~ ANY))* ~ "\"" } 20 | bool = { "true" | "false" } 21 | array = { "(" ~ (option_value ~ ("," ~ option_value)*)? ~ ")" } 22 | option = { "[" ~ option_identifier ~ ("=" ~ option_value)? ~ "]" } 23 | 24 | 25 | // Property definitions 26 | property = { option* ~ identifier ~ optional_property? ~ ":" ~ type_identifier } 27 | optional_property = { "?" } 28 | 29 | 30 | // Sections for 'in' and 'out' blocks 31 | in_block = { struct_body } 32 | out_block = { struct_body } 33 | 34 | // Usecase definitions 35 | usecase = { 36 | option* ~ 37 | "usecase" ~ identifier ~ "{" ~ 38 | method_def* ~ 39 | "}" 40 | } 41 | 42 | method_def = { 43 | option* ~ 44 | identifier ~ 45 | in_block? ~ 46 | ("->" ~ out_block)? 47 | } 48 | 49 | // Struct definitions 50 | struct_def = { 51 | option* ~ 52 | "struct" ~ identifier ~ struct_body 53 | } 54 | 55 | struct_body = { 56 | "{" ~ 57 | 58 | property* ~ 59 | "}" 60 | } 61 | 62 | // Enum definitions 63 | enum_def = { 64 | option* ~ 65 | "enum" ~ identifier ~ enum_body 66 | } 67 | 68 | enum_property = { option* ~ identifier } 69 | 70 | 71 | enum_body = { 72 | "{" ~ 73 | enum_property* ~ 74 | "}" 75 | } 76 | 77 | // repository definitions 78 | repository_def = { 79 | option* ~ 80 | "repository" ~ identifier ~ repository_body 81 | } 82 | 83 | repository_body = { 84 | "{" ~ 85 | method_def* ~ 86 | "}" 87 | } 88 | 89 | 90 | global_option = { 91 | "#" ~ 92 | option 93 | } 94 | 95 | // Root rule 96 | file = { 97 | SOI ~ 98 | (usecase | struct_def | enum_def | import | global_option | repository_def)* ~ 99 | EOI 100 | } -------------------------------------------------------------------------------- /lib/generator-wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, error::Error, path::PathBuf}; 2 | 3 | use cronus_generator::Ctxt; 4 | use cronus_spec::RawSpec; 5 | use wasm_bindgen::prelude::*; 6 | 7 | 8 | #[wasm_bindgen] 9 | extern "C" { 10 | // Use `js_namespace` here to bind `console.log(..)` instead of just 11 | // `log(..)` 12 | #[wasm_bindgen(js_namespace = console)] 13 | fn log(s: &JsValue); 14 | 15 | // Multiple arguments too! 16 | #[wasm_bindgen(js_namespace = console, js_name = log)] 17 | fn log_many(a: &JsValue, b: &JsValue); 18 | } 19 | 20 | 21 | #[wasm_bindgen] 22 | pub fn generate_from_yaml(content: &str) -> Result { 23 | match cronus_parser::from_yaml_str(content) { 24 | Ok(spec) => { 25 | run_raw_spec(spec) 26 | }, 27 | Err(err) => { 28 | Err(err.to_string()) 29 | }, 30 | } 31 | } 32 | 33 | #[wasm_bindgen] 34 | pub fn api_to_yaml(content: &str) -> Result { 35 | match cronus_parser::api_parse::parse(PathBuf::new(), content) { 36 | Ok(spec) => { 37 | let yaml = cronus_parser::to_yaml_str(&spec).map_err(|e| e.to_string())?; 38 | Ok(serde_wasm_bindgen::to_value(&yaml).unwrap()) 39 | }, 40 | Err(err) => { 41 | Err(err.to_string()) 42 | }, 43 | } 44 | } 45 | 46 | fn run_raw_spec(spec: RawSpec) -> Result { 47 | log(&serde_wasm_bindgen::to_value(&spec).unwrap()); 48 | 49 | let ctx = Ctxt::new(spec, None); 50 | match cronus_generator::generate(&ctx) { 51 | Ok(_) => { 52 | 53 | let gfs = &*ctx.generator_fs.borrow(); 54 | let result: HashMap> = gfs 55 | .iter() 56 | .map(|(key, value)| { 57 | let inner_map = value.borrow().clone(); 58 | (key.to_string(), inner_map) 59 | }) 60 | .collect(); 61 | Ok(serde_wasm_bindgen::to_value(&result).unwrap()) 62 | }, 63 | Err(err) => { 64 | Err(err.to_string()) 65 | }, 66 | } 67 | } 68 | 69 | #[wasm_bindgen] 70 | pub fn generate_from_api(content: &str) -> Result { 71 | console_error_panic_hook::set_once(); 72 | 73 | match cronus_parser::api_parse::parse(PathBuf::new(), content) { 74 | Ok(spec) => { 75 | run_raw_spec(spec) 76 | }, 77 | Err(err) => { 78 | Err(err.to_string()) 79 | }, 80 | } 81 | 82 | } 83 | 84 | -------------------------------------------------------------------------------- /lib/generator/testdata/hello/rust_axum/src/generated.rs: -------------------------------------------------------------------------------- 1 | 2 | use axum::{ 3 | extract::State, 4 | http::{header, Response, StatusCode}, 5 | response::IntoResponse, 6 | Extension, Json, 7 | Router 8 | }; 9 | pub async fn create_hello(State(state): State>, Json(request): Json) -> Result)> { 10 | 11 | match state.hello.create_hello(request).await { 12 | Ok(res) => { 13 | Ok(Json(res)) 14 | }, 15 | Err(err) => { 16 | let mut err_obj = serde_json::Map::new(); 17 | err_obj.insert("message".to_owned(), serde_json::Value::from(err.to_string())); 18 | Err((StatusCode::BAD_REQUEST, Json(serde_json::Value::Object(err_obj)))) 19 | }, 20 | } 21 | } 22 | pub async fn get_hello(State(state): State>, Json(request): Json) -> Result)> { 23 | 24 | match state.hello.get_hello(request).await { 25 | Ok(res) => { 26 | Ok(Json(res)) 27 | }, 28 | Err(err) => { 29 | let mut err_obj = serde_json::Map::new(); 30 | err_obj.insert("message".to_owned(), serde_json::Value::from(err.to_string())); 31 | Err((StatusCode::BAD_REQUEST, Json(serde_json::Value::Object(err_obj)))) 32 | }, 33 | } 34 | } 35 | #[derive(Clone)] 36 | pub struct Usecases { 37 | pub hello: std::sync::Arc, 38 | } 39 | pub fn router_init(usecases: std::sync::Arc) -> Router { 40 | Router::new() 41 | .route("", axum::routing::post(create_hello)) 42 | .route("item", axum::routing::get(get_hello)) 43 | .with_state(usecases) 44 | } 45 | use serde::{Deserialize, Serialize}; 46 | use async_trait::async_trait; 47 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 48 | pub struct CreateHelloRequest { 49 | pub hi: String, 50 | } 51 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 52 | pub struct CreateHelloResponse { 53 | pub answer: String, 54 | } 55 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 56 | pub struct GetHelloRequest { 57 | pub hi: String, 58 | } 59 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 60 | pub struct GetHelloResponse { 61 | pub answer: String, 62 | } 63 | #[async_trait] 64 | pub trait HelloUsecase { 65 | async fn create_hello(&self, request: CreateHelloRequest) -> Result>; 66 | async fn get_hello(&self, request: GetHelloRequest) -> Result>; 67 | } 68 | -------------------------------------------------------------------------------- /lib/generator/testdata/hello/rust/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "async-trait" 7 | version = "0.1.77" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" 10 | dependencies = [ 11 | "proc-macro2", 12 | "quote", 13 | "syn", 14 | ] 15 | 16 | [[package]] 17 | name = "itoa" 18 | version = "1.0.10" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 21 | 22 | [[package]] 23 | name = "proc-macro2" 24 | version = "1.0.78" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 27 | dependencies = [ 28 | "unicode-ident", 29 | ] 30 | 31 | [[package]] 32 | name = "quote" 33 | version = "1.0.35" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 36 | dependencies = [ 37 | "proc-macro2", 38 | ] 39 | 40 | [[package]] 41 | name = "ryu" 42 | version = "1.0.17" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" 45 | 46 | [[package]] 47 | name = "serde" 48 | version = "1.0.197" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" 51 | dependencies = [ 52 | "serde_derive", 53 | ] 54 | 55 | [[package]] 56 | name = "serde_derive" 57 | version = "1.0.197" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" 60 | dependencies = [ 61 | "proc-macro2", 62 | "quote", 63 | "syn", 64 | ] 65 | 66 | [[package]] 67 | name = "serde_json" 68 | version = "1.0.114" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" 71 | dependencies = [ 72 | "itoa", 73 | "ryu", 74 | "serde", 75 | ] 76 | 77 | [[package]] 78 | name = "syn" 79 | version = "2.0.50" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb" 82 | dependencies = [ 83 | "proc-macro2", 84 | "quote", 85 | "unicode-ident", 86 | ] 87 | 88 | [[package]] 89 | name = "testdata-hello" 90 | version = "0.1.0" 91 | dependencies = [ 92 | "async-trait", 93 | "serde", 94 | "serde_json", 95 | ] 96 | 97 | [[package]] 98 | name = "unicode-ident" 99 | version = "1.0.12" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 102 | -------------------------------------------------------------------------------- /docs/references/yaml.md: -------------------------------------------------------------------------------- 1 | **For the users, it is recommended to use [API style](api.md) instead of YAML style to write API spec due to the consideration of readability and convenience.** 2 | 3 | To create a valid YAML file that represents an API specification (`RawSpec`), you need to follow the structure defined by the Rust structs. Here's a documentation guide on how to define keys and values in the YAML file: 4 | 5 | ### RawSpec 6 | 7 | The root structure for the API Spec. It contains the following optional fields: 8 | 9 | - `types`: A map of type names to `RawSchema` objects. 10 | - `usecases`: A map of use case names to `RawUsecase` objects. 11 | - `option`: A `GlobalOption` object with global configuration options. 12 | - `imports`: A list of strings representing import paths. 13 | 14 | Example: 15 | 16 | ```yaml 17 | types: 18 | MyType: 19 | type: object 20 | properties: 21 | id: 22 | type: string 23 | required: true 24 | usecases: 25 | MyUsecase: 26 | methods: 27 | myMethod: 28 | req: 29 | type: object 30 | properties: 31 | id: 32 | type: string 33 | required: true 34 | res: 35 | type: object 36 | properties: 37 | message: 38 | type: string 39 | required: true 40 | option: 41 | generator: 42 | rust: 43 | file: "output.rs" 44 | imports: 45 | - "path/to/another/spec.yaml" 46 | ``` 47 | 48 | ### RawSchema 49 | 50 | Defines the schema for a type. It has the following optional fields: 51 | 52 | - `type`: The type of the schema (e.g., "string", "object"). 53 | - `items`: A `RawSchema` object for array item types. 54 | - `properties`: A map of property names to `RawSchema` objects for object types. 55 | - `required`: A boolean indicating if the schema is required. 56 | - `namespace`: A string specifying the namespace for the type. 57 | - `enum_items`: A list of `RawSchemaEnumItem` objects for enum types. 58 | - `option`: A `RawSchemaPropertyOption` object with additional options. 59 | - `extends`: A map of strings for extending other schemas. 60 | - `flat_extends`: A list of strings for flat extending other schemas. 61 | 62 | ### RawUsecase 63 | 64 | Represents a use case in the API. It contains: 65 | 66 | - `methods`: A map of method names to `RawMethod` objects. 67 | - `option`: An optional `RawUsecaseOption` object with additional options. 68 | 69 | ### RawMethod 70 | 71 | Defines a method in a use case. It has the following optional fields: 72 | 73 | - `req`: A `RawSchema` object for the request schema. 74 | - `res`: A `RawSchema` object for the response schema. 75 | - `option`: A `RawMethodOption` object with additional options. 76 | 77 | ### GlobalOption, GeneratorOption, RustGeneratorOption, etc. 78 | 79 | These structs define various configuration options for the generator. They contain fields that specify file paths, suffixes, and other generator-specific options. 80 | 81 | When creating your YAML file, you should match the structure and field names defined in these Rust structs. Each key in the YAML file corresponds to a field in the Rust struct, and the value should match the expected type (e.g., string, boolean, object, list). -------------------------------------------------------------------------------- /lib/parser/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::{path::{Path, PathBuf}, error::Error, collections::{VecDeque, HashSet}, fs, fmt::format}; 3 | use anyhow::{bail, Result}; 4 | use cronus_spec::RawSpec; 5 | 6 | pub mod api_parse; 7 | pub mod api_parser; 8 | 9 | pub fn from_yaml(file: &Path) -> Result { 10 | let contents = fs::read_to_string(file)?; 11 | from_yaml_str(&contents) 12 | } 13 | 14 | pub fn from_api(file: &Path) -> Result { 15 | let contents = fs::read_to_string(file)?; 16 | api_parse::parse(PathBuf::from(file), &contents) 17 | } 18 | 19 | #[tracing::instrument] 20 | pub fn from_file(file: &Path, resolve_import: bool, search_paths: Option<&Vec>, explored: &mut HashSet) -> Result { 21 | match file.extension() { 22 | Some(ext) => { 23 | match ext.to_str() { 24 | Some(ext_str) => { 25 | let mut spec: RawSpec; 26 | if ext_str.ends_with("yaml") || ext_str.ends_with("yml") { 27 | spec = from_yaml(file)?; 28 | } 29 | else if ext_str.ends_with("api") { 30 | spec = from_api(file)?; 31 | } else { 32 | bail!("unsupported file extension '{:?}', expect .yaml, .yml or .api", ext) 33 | } 34 | 35 | if resolve_import { 36 | resolve_imports(&mut spec, explored, file.parent().unwrap(), search_paths)?; 37 | } 38 | 39 | Ok(spec) 40 | }, 41 | None => bail!("unsupported file extension '{:?}', expect .yaml, .yml or .api", ext), 42 | } 43 | 44 | }, 45 | None => bail!("no file extension, expect .yaml, .yml or .api"), 46 | } 47 | 48 | } 49 | 50 | pub fn from_yaml_str(str: &str) -> Result { 51 | let spec: RawSpec = serde_yaml::from_str(&str)?; 52 | Ok(spec) 53 | } 54 | 55 | pub fn to_yaml_str(spec: &RawSpec) -> Result { 56 | let yaml = serde_yaml::to_string(spec)?; 57 | Ok(yaml) 58 | } 59 | 60 | pub fn resolve_imports(spec: &mut RawSpec, explored: &mut HashSet, spec_parent:&Path, search_paths: Option<&Vec>) -> Result<()> { 61 | 62 | for import in spec.imports.clone().into_iter().flatten() { 63 | 64 | let import_path = get_import_path(&import, spec_parent,search_paths)?; 65 | if explored.contains(&import_path) { 66 | continue 67 | } 68 | explored.insert(import_path.clone()); 69 | let imported_spec = from_file(&import_path, true, search_paths, explored)?; 70 | 71 | spec.merge(imported_spec)? 72 | } 73 | 74 | Ok(()) 75 | } 76 | 77 | #[tracing::instrument] 78 | fn get_import_path(import: &str, default_path:&Path, available_paths: Option<&Vec>) -> Result { 79 | let cleaned = import.replace("\r", ""); 80 | let defualt_relative = std::path::absolute(default_path.join(&cleaned))?; 81 | if defualt_relative.exists() { 82 | return Ok(defualt_relative) 83 | } 84 | 85 | if let Some(paths) = available_paths { 86 | for p in paths { 87 | let candidate = std::path::absolute(p.join(&cleaned))?; 88 | if candidate.exists() { 89 | return Ok(candidate) 90 | } 91 | } 92 | } 93 | 94 | bail!("no available file found for import '{}', search paths: {:?}", cleaned, available_paths) 95 | } -------------------------------------------------------------------------------- /lib/generator/src/rust_utils.rs: -------------------------------------------------------------------------------- 1 | pub struct RustFile { 2 | pub use_root: RustFileUsesNode, 3 | pub traits: Vec 4 | } 5 | 6 | pub struct RustTrait { 7 | pub attributes: Vec, 8 | pub name: String 9 | } 10 | 11 | pub struct RustStruct { 12 | pub attributes: Vec, 13 | pub name: String 14 | } 15 | 16 | pub struct RustAttribute { 17 | 18 | } 19 | 20 | pub struct RustFileUsesNode { 21 | pub path: String, 22 | pub next: Vec> 23 | } 24 | 25 | impl RustFileUsesNode { 26 | pub fn add(&mut self, use_path: &str) { 27 | let parts = use_path.split("::"); 28 | let mut current_node = self as *mut RustFileUsesNode; 29 | 30 | for part in parts { 31 | if let Some(existing_node) = unsafe {&mut (&mut *current_node).next}.iter_mut().find(|node| node.path == part) { 32 | current_node = existing_node.as_mut() as *mut RustFileUsesNode; 33 | } else { 34 | let new_node = Box::new(RustFileUsesNode { 35 | path: part.to_string(), 36 | next: Vec::new(), 37 | }); 38 | unsafe {&mut (&mut *current_node).next}.push(new_node); 39 | current_node = unsafe {&mut (&mut *current_node).next}.last_mut().unwrap().as_mut() as *mut RustFileUsesNode; 40 | } 41 | } 42 | 43 | } 44 | 45 | pub fn to_rust_uses(&self) -> String { 46 | fn build_use_statement(node: &RustFileUsesNode, prefix: &str) -> String { 47 | if node.next.is_empty() { 48 | return format!("{}{}", prefix, node.path); 49 | } 50 | 51 | let mut children = node.next.iter().map(|child| build_use_statement(child, "")).collect::>(); 52 | children.sort(); // Ensure consistent ordering 53 | 54 | if children.len() == 1 { 55 | format!("{}{}::{}", prefix, node.path, children[0]) 56 | } else { 57 | format!("{}{}::{{{}}}", prefix, node.path, children.join(", ")) 58 | } 59 | } 60 | 61 | let mut use_statements = Vec::new(); 62 | for child in &self.next { 63 | use_statements.push(build_use_statement(child, "use ") + ";"); 64 | } 65 | use_statements.join("\n") 66 | } 67 | } 68 | 69 | #[cfg(test)] 70 | mod test { 71 | use crate::rust_utils::RustFileUsesNode; 72 | 73 | #[test] 74 | fn test_add_single_path() { 75 | let mut root = RustFileUsesNode { 76 | path: String::new(), 77 | next: Vec::new(), 78 | }; 79 | 80 | root.add("a::b::c"); 81 | 82 | assert_eq!(root.next.len(), 1); 83 | assert_eq!(root.next[0].path, "a"); 84 | assert_eq!(root.next[0].next.len(), 1); 85 | assert_eq!(root.next[0].next[0].path, "b"); 86 | assert_eq!(root.next[0].next[0].next.len(), 1); 87 | assert_eq!(root.next[0].next[0].next[0].path, "c"); 88 | } 89 | 90 | #[test] 91 | fn test_add_two_paths() { 92 | let mut root = RustFileUsesNode { 93 | path: String::new(), 94 | next: Vec::new(), 95 | }; 96 | 97 | root.add("a::b::c"); 98 | root.add("a::b::d"); 99 | 100 | assert_eq!(root.next.len(), 1); 101 | assert_eq!(root.next[0].path, "a"); 102 | assert_eq!(root.next[0].next.len(), 1); 103 | assert_eq!(root.next[0].next[0].path, "b"); 104 | assert_eq!(root.next[0].next[0].next.len(), 2); 105 | assert_eq!(root.next[0].next[0].next[0].path, "c"); 106 | assert_eq!(root.next[0].next[0].next[1].path, "d"); 107 | } 108 | 109 | #[test] 110 | fn test_to_rust_uses_single() { 111 | let mut root = RustFileUsesNode { 112 | path: String::new(), 113 | next: Vec::new(), 114 | }; 115 | 116 | root.add("a::b::c"); 117 | 118 | assert_eq!(root.to_rust_uses(), "use a::b::c;"); 119 | } 120 | 121 | #[test] 122 | fn test_to_rust_uses_combined() { 123 | let mut root = RustFileUsesNode { 124 | path: String::new(), 125 | next: Vec::new(), 126 | }; 127 | 128 | root.add("a::b::d"); 129 | root.add("a::b::e"); 130 | 131 | assert_eq!(root.to_rust_uses(), "use a::b::{d, e};"); 132 | } 133 | 134 | 135 | } -------------------------------------------------------------------------------- /lib/generator/src/ts_nestjs.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | use convert_case::{Casing, Case}; 4 | use cronus_spec::{RawUsecase, RawMethod, RawMethodRestOption, RawSchema}; 5 | 6 | use crate::{Generator, Ctxt}; 7 | use anyhow::{Ok, Result}; 8 | 9 | 10 | pub struct TypescriptNestjsGenerator { 11 | } 12 | 13 | impl TypescriptNestjsGenerator { 14 | pub fn new() -> Self { 15 | Self { 16 | 17 | } 18 | } 19 | } 20 | 21 | impl Generator for TypescriptNestjsGenerator { 22 | fn name(&self) -> &'static str { 23 | return "typescript_nestjs" 24 | } 25 | 26 | fn generate_usecase(&self, ctx: &Ctxt, name: &str, usecase: &RawUsecase) -> Result<()> { 27 | let mut nestjs_code = String::new(); 28 | 29 | // Start of the controller class 30 | nestjs_code.push_str(&format!("@Controller('/{}')\n", name.to_lowercase())); 31 | nestjs_code.push_str(&format!("export class {}Controller {{\n", name.to_case(Case::UpperCamel))); 32 | 33 | for (usecase_name, usecase_method) in &usecase.methods { 34 | // Generate methods within the controller 35 | if let Some(options) = &usecase_method.option { 36 | if let Some(rest_option) = &options.rest { 37 | nestjs_code.push_str(&self.generate_method(usecase_name, usecase_method, rest_option)); 38 | } 39 | } 40 | 41 | } 42 | 43 | // End of the controller class 44 | nestjs_code.push_str("}\n\n"); 45 | 46 | ctx.append_file(self.name(), &self.dst(ctx), &nestjs_code); 47 | Ok(()) 48 | } 49 | 50 | 51 | } 52 | 53 | impl TypescriptNestjsGenerator { 54 | 55 | 56 | 57 | fn generate_method(&self, usecase_name: &str, method: &RawMethod, rest_option: &RawMethodRestOption) -> String { 58 | let mut method_code = String::new(); 59 | 60 | // Generate NestJS method code 61 | method_code.push_str(&format!(" @{}('{}')\n", &rest_option.method.to_case(Case::UpperCamel), rest_option.path.clone().unwrap_or("".to_string()))); 62 | method_code.push_str(&format!(" async {}() {{\n", usecase_name.to_case(Case::Camel))); 63 | method_code.push_str(" // Handler logic here\n"); 64 | method_code.push_str(" }\n"); 65 | method_code 66 | } 67 | 68 | fn dst(&self, ctx: &Ctxt) -> String { 69 | if let Some(gen_config) = &ctx.spec.option.as_ref().unwrap().generator { 70 | if let Some(tsnestjs_gen_config) = &gen_config.typescript_nestjs { 71 | if let Some(file) = &tsnestjs_gen_config.file { 72 | return file.clone() 73 | } 74 | 75 | } 76 | } 77 | 78 | return "controller.ts".to_string(); 79 | } 80 | 81 | 82 | pub fn generate_dto(&self, schema: &RawSchema, dto_name: &str) -> String { 83 | let mut dto_code = format!("export class {} {{\n", dto_name); 84 | 85 | if let Some(ty) = &schema.ty { 86 | dto_code.push_str(&map_field("type", schema, false)); 87 | } 88 | 89 | if let Some(items) = &schema.items { 90 | let nested_dto_name = format!("{}Item", dto_name); 91 | dto_code.push_str(&self.generate_dto(items, &nested_dto_name)); 92 | dto_code.push_str(&format!(" items: {}[];\n", nested_dto_name)); 93 | } 94 | 95 | if let Some(properties) = &schema.properties { 96 | for (key, prop_schema) in properties { 97 | let is_optional = prop_schema.required.unwrap_or(false); 98 | dto_code.push_str(&map_field(key, prop_schema, is_optional)); 99 | } 100 | } 101 | 102 | if let Some(enum_items) = &schema.enum_items { 103 | // Handle enum items 104 | } 105 | 106 | // Handle other fields like extends, flat_extends, etc. 107 | 108 | dto_code.push_str("}\n\n"); 109 | dto_code 110 | } 111 | 112 | 113 | 114 | } 115 | 116 | fn map_field(field_name: &str, schema: &RawSchema, is_optional: bool) -> String { 117 | let ts_type = match schema.ty.as_deref() { 118 | Some("string") => "string", 119 | Some("number") => "number", 120 | Some("boolean") => "boolean", 121 | Some("object") => "any", // Or map to a specific object type if possible 122 | Some("array") => "Array", // Or map to a specific array type if possible 123 | _ => "any" 124 | }; 125 | 126 | format!(" {}: {}{};\n", field_name, ts_type, if is_optional { "?" } else {""}) 127 | } -------------------------------------------------------------------------------- /lib/generator/src/ts.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | use anyhow::{Ok, Result}; 4 | use convert_case::{Casing, Case}; 5 | use cronus_spec::{RawUsecase, RawSchema}; 6 | use tracing::{span, Level}; 7 | 8 | use crate::{Generator, Ctxt, utils::{get_request_name, get_usecase_name, get_response_name}}; 9 | 10 | 11 | 12 | pub struct TypescriptGenerator { 13 | } 14 | 15 | impl TypescriptGenerator { 16 | pub fn new() -> Self { 17 | Self { 18 | 19 | } 20 | } 21 | } 22 | 23 | impl Generator for TypescriptGenerator { 24 | fn name(&self) -> &'static str { 25 | return "typescript" 26 | } 27 | 28 | fn generate_schema(&self, ctx: &Ctxt, schema_name:&str, schema: &RawSchema)-> Result<()> { 29 | self.generate_schema(ctx, Some(schema_name.to_owned()), schema); 30 | Ok(()) 31 | } 32 | 33 | fn generate_usecase(&self, ctx: &Ctxt, name: &str, usecase: &RawUsecase) -> Result<()> { 34 | let span = span!(Level::TRACE, "generate_usecase", "usecase" = name); 35 | // Enter the span, returning a guard object. 36 | let _enter = span.enter(); 37 | 38 | let usecase_name = get_usecase_name(ctx, name); 39 | 40 | let mut result = format!("export interface {} {{\n", usecase_name); 41 | 42 | for (method_name, method) in &usecase.methods { 43 | let method_name_camel = method_name.to_case(Case::Camel); 44 | 45 | let request_type = match &method.req { 46 | Some(req) => { 47 | let request_type = get_request_name(ctx, &method_name_camel); 48 | self.generate_schema(ctx, Some(request_type.clone()), req, ); 49 | request_type 50 | }, 51 | None => String::new(), 52 | }; 53 | 54 | let response_type = match &method.res { 55 | Some(res) => { 56 | let response_type = get_response_name(ctx, &method_name_camel); 57 | self.generate_schema(ctx, Some(response_type.clone()), res, ); 58 | response_type 59 | }, 60 | None => "Promise".to_string(), 61 | }; 62 | 63 | let method_signature = if request_type.is_empty() { 64 | format!(" {}(): {};\n", method_name_camel, response_type) 65 | } else { 66 | format!(" {}(request: {}): {};\n", method_name_camel, request_type, response_type) 67 | }; 68 | 69 | result += &method_signature; 70 | } 71 | 72 | result += "}\n"; 73 | ctx.append_file(self.name(), &self.dst(ctx), &result); 74 | 75 | Ok(()) 76 | } 77 | 78 | 79 | } 80 | 81 | 82 | impl TypescriptGenerator { 83 | 84 | 85 | fn dst(&self, ctx: &Ctxt) -> String { 86 | if let Some(gen_config) = &ctx.spec.option.as_ref().unwrap().generator { 87 | if let Some(ts_gen_config) = &gen_config.typescript { 88 | if let Some(file) = &ts_gen_config.file { 89 | return file.clone() 90 | } 91 | 92 | } 93 | } 94 | 95 | return "types.ts".to_string(); 96 | } 97 | 98 | pub fn generate_schema(&self, 99 | ctx: &Ctxt, 100 | override_name: Option, 101 | schema: &RawSchema, 102 | ) { 103 | let interface_name: String; 104 | if let Some(ty) = override_name { 105 | interface_name = ty.to_case(Case::UpperCamel); 106 | } else { 107 | interface_name = schema.ty.as_ref().unwrap().clone(); 108 | } 109 | 110 | let span = span!(Level::TRACE, "generate_inteface", "interface" = interface_name); 111 | // Enter the span, returning a guard object. 112 | let _enter = span.enter(); 113 | 114 | let ts_type = schema_to_ts_type(&schema); 115 | 116 | let result = format!("export interface {} {}\n", interface_name, ts_type); 117 | 118 | ctx.append_file(self.name(), &self.dst(ctx), &result); 119 | 120 | } 121 | } 122 | 123 | 124 | // Helper function to generate TypeScript type from RawSchema 125 | fn schema_to_ts_type(schema: &RawSchema) -> String { 126 | if let Some(ref ty) = schema.ty { 127 | ty.clone() 128 | } else if let Some(ref items) = schema.items { 129 | format!("Array<{}>", schema_to_ts_type(items)) 130 | } else if let Some(ref properties) = schema.properties { 131 | let mut props = String::new(); 132 | for (key, value) in properties { 133 | props += &format!(" {}: {};\n", key, schema_to_ts_type(value)); 134 | } 135 | format!("{{\n{}}}", props) 136 | } else { 137 | "any".to_string() // Fallback to 'any' type if no other information is available 138 | } 139 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cronus 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/cronus_cli)](https://crates.io/crates/cronus_cli) 4 | 5 | Cronus aims to help you focusing on **business logic code only** instead of the other **glue code**. 6 | 7 | 8 | Online playground is [here](https://theogonic.github.io/cronus-playground/). 9 | Documentation is [here](https://theogonic.github.io/cronus). 10 | 11 | ## Usage 12 | ```bash 13 | $ cargo install cronus_cli 14 | ``` 15 | 16 | And it can be used like: 17 | ```bash 18 | $ cronus_cli 19 | ``` 20 | 21 | And it can be further integrated into the building process: 22 | ```rust 23 | // build.rs 24 | fn main() { 25 | let dir: String = env::var("CARGO_MANIFEST_DIR").unwrap(); 26 | 27 | // Suppose your api file named "main.api" is located at 28 | // same directory with the Cargo.toml. 29 | // 30 | // If your api file does not have the name "main.api", 31 | // the path should point to the that file instead of 32 | // a simple directory. 33 | std::process::Command::new("cronus_cli") 34 | .arg(&dir) 35 | .output() 36 | .expect("failed to generate API"); 37 | } 38 | 39 | ``` 40 | 41 | ## Introduction 42 | Cronus contains a list of code generators, which insipred by the **Clean Architecture**, for **Rust**, **Typescript**, **OpenAPI**, and more. 43 | 44 | According to one or more configuration files( can be either in YAML (.yml or .yaml) or our DSL(.api) ), **Cronus** can generate nice and clean business logic related code and glue code for a bunch of different controller layers(HTTP, GraphQL, etc.) powered by different libraries or frameworks. 45 | 46 | 47 | Cronus 48 | ``` 49 | # More fine-grained configuration can be found at documentation 50 | 51 | # For 'rust' generator 52 | global [generator.rust.file = "src/generated.rs"] 53 | global [generator.rust.async] 54 | global [generator.rust.async_trait] 55 | 56 | # For 'rust_axum' generator 57 | global [generator.rust_axum.file = "src/generated.rs"] 58 | 59 | 60 | struct Todo { 61 | id: string 62 | content: string 63 | } 64 | 65 | usecase Todo { 66 | createTodo { 67 | in { 68 | content: string 69 | } 70 | 71 | out { 72 | todo: Todo 73 | } 74 | } 75 | } 76 | ``` 77 | 78 | **Cronus** can be used to generate the following **Business Logic** interface code: 79 | 80 | 81 | Generated Rust 82 | ```rust 83 | use serde::{Deserialize, Serialize}; 84 | use async_trait::async_trait; 85 | 86 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 87 | pub struct Todo { 88 | pub id: String, 89 | pub content: String, 90 | } 91 | 92 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 93 | pub struct CreateTodoRequest { 94 | pub content: String, 95 | } 96 | 97 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 98 | pub struct CreateTodoResponse { 99 | pub todo: Todo, 100 | } 101 | 102 | #[async_trait] 103 | pub trait TodoUsecase { 104 | async fn create_todo(&self, request: CreateTodoRequest) -> Result>; 105 | } 106 | ``` 107 | 108 | **Cronus** can even step further to generate the following **Controller** glue code: 109 | 110 | Generated Rust (Axum) 111 | ```rust 112 | use axum::{ 113 | extract::State, 114 | http::{header, Response, StatusCode}, 115 | response::IntoResponse, 116 | Extension, Json, 117 | Router 118 | }; 119 | 120 | pub async fn create_todo(State(state): State>, Json(request): Json) -> Result)> { 121 | 122 | match state.todo.create_todo(request).await { 123 | Ok(res) => { 124 | Ok(Json(res)) 125 | }, 126 | Err(err) => { 127 | let mut err_obj = serde_json::Map::new(); 128 | err_obj.insert("message".to_owned(), serde_json::Value::from(err.to_string())); 129 | Err((StatusCode::BAD_REQUEST, Json(serde_json::Value::Object(err_obj)))) 130 | }, 131 | } 132 | } 133 | 134 | #[derive(Clone)] 135 | pub struct Usecases { 136 | pub todo: std::sync::Arc, 137 | } 138 | 139 | pub fn router_init(usecases: std::sync::Arc) -> Router { 140 | Router::new() 141 | .route("", axum::routing::post(create_todo)) 142 | .with_state(usecases) 143 | } 144 | ``` 145 | 146 | ## Usecase Layer Generators 147 | - Rust 148 | - Typescript 149 | - Python 150 | 151 | ## Transportation Layer Generator 152 | - Axum (HTTP, Rust) 153 | - FastAPI (HTTP, Python) 154 | - Tauri (work in progress) 155 | 156 | ## Schema Generator 157 | - OpenAPI v3 158 | - Protobuf 159 | 160 | ## Dev 161 | 162 | ### Common 163 | 164 | ```bash 165 | # Install the cli binary to the default folder so that you can call it 166 | # everywhere as long as the folder is included in environment variable. 167 | $ cargo install --path bin/cli 168 | # Run the generators by the given API spec 169 | $ cargo run -- examples/todo-rs/main.api 170 | $ cargo run -- examples/todo-py/main.api 171 | 172 | ``` 173 | 174 | ## Docs 175 | 176 | ### Dev 177 | ```bash 178 | $ pip install mkdocs-material 179 | $ mkdocs serve -f mkdocs.yaml 180 | ``` 181 | 182 | ### Publish 183 | ```bash 184 | $ mkdocs gh-deploy 185 | ``` -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Cronus 2 | Cronus aims to help you focusing on **business logic code only** instead of the other **glue code**. 3 | 4 | ## Introduction 5 | Cronus contains a list of code generators, which insipred by the **Clean Architecture**, for **Rust**, **Typescript**, **OpenAPI**, and more. 6 | 7 | According to one or more configuration files( can be either in YAML (.yml or .yaml) or our DSL(.api) ), **Cronus** can generate nice and clean business logic related code and glue code for a bunch of different controller layers(HTTP, GraphQL, etc.) powered by different libraries or frameworks. 8 | 9 | 10 | Cronus 11 | ``` 12 | # More fine-grained configuration can be found at documentation 13 | 14 | # For 'rust' generator 15 | global [generator.rust.file = "src/generated.rs"] 16 | global [generator.rust.async] 17 | global [generator.rust.async_trait] 18 | 19 | # For 'rust_axum' generator 20 | global [generator.rust_axum.file = "src/generated.rs"] 21 | 22 | 23 | struct Todo { 24 | id: string 25 | content: string 26 | } 27 | 28 | usecase Todo { 29 | createTodo { 30 | in { 31 | content: string 32 | } 33 | 34 | out { 35 | todo: Todo 36 | } 37 | } 38 | } 39 | ``` 40 | 41 | **Cronus** can be used to generate the following **Business Logic** interface code: 42 | 43 | 44 | Generated Rust 45 | ```rust 46 | use serde::{Deserialize, Serialize}; 47 | use async_trait::async_trait; 48 | 49 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 50 | pub struct Todo { 51 | pub id: String, 52 | pub content: String, 53 | } 54 | 55 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 56 | pub struct CreateTodoRequest { 57 | pub content: String, 58 | } 59 | 60 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 61 | pub struct CreateTodoResponse { 62 | pub todo: Todo, 63 | } 64 | 65 | #[async_trait] 66 | pub trait TodoUsecase { 67 | async fn create_todo(&self, request: CreateTodoRequest) -> Result>; 68 | } 69 | ``` 70 | 71 | **Cronus** can even step further to generate the following **Controller** glue code: 72 | 73 | Generated Rust (Axum) 74 | ```rust 75 | use axum::{ 76 | extract::State, 77 | http::{header, Response, StatusCode}, 78 | response::IntoResponse, 79 | Extension, Json, 80 | Router 81 | }; 82 | 83 | pub async fn create_todo(State(state): State>, Json(request): Json) -> Result)> { 84 | 85 | match state.todo.create_todo(request).await { 86 | Ok(res) => { 87 | Ok(Json(res)) 88 | }, 89 | Err(err) => { 90 | let mut err_obj = serde_json::Map::new(); 91 | err_obj.insert("message".to_owned(), serde_json::Value::from(err.to_string())); 92 | Err((StatusCode::BAD_REQUEST, Json(serde_json::Value::Object(err_obj)))) 93 | }, 94 | } 95 | } 96 | 97 | #[derive(Clone)] 98 | pub struct Usecases { 99 | pub todo: std::sync::Arc, 100 | } 101 | 102 | pub fn router_init(usecases: std::sync::Arc) -> Router { 103 | Router::new() 104 | .route("", axum::routing::post(create_todo)) 105 | .with_state(usecases) 106 | } 107 | ``` 108 | 109 | ## CLI Usage 110 | ```bash 111 | $ Cronus 112 | # Ex. Cronus main.api 113 | ``` 114 | 115 | 116 | ## What is the **Clean Architecture** and Why we need it? 117 | [The Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html), tl;dr, proposed by Robert C. Martin, offers several benefits during software development. 118 | 119 | - **Independent of Frameworks:** 120 | The architecture does not depend on specific libraries or frameworks, enhancing robustness and flexibility. 121 | 122 | - **Testability:** 123 | Business rules can be tested independently of UI, database, web server, or other external elements. 124 | 125 | - **Independence of UI:** 126 | UI can change easily without affecting the business rules, allowing for flexibility in user interface design. 127 | 128 | - **Independence of Database:** 129 | Business rules are not tied to a specific database, facilitating easy changes in database technologies. 130 | 131 | - **Independence from External Agencies:** 132 | Business rules remain unaffected by external changes, maintaining their integrity and effectiveness. 133 | 134 | - **Manageable Complexity:** 135 | Separation of concerns makes the code more manageable and different aspects of the application more understandable. 136 | 137 | - **Adaptability to Change:** 138 | The architecture is adaptable to changing requirements and technology shifts, thanks to its decoupled nature. 139 | 140 | - **Reusability:** 141 | Components and business logic can be reused across different parts of the application or in various projects. 142 | 143 | - **Scalability:** 144 | Decoupled layers allow for independent scalability and parallel development across multiple teams. 145 | 146 | - **Maintainability:** 147 | The separation of concerns enhances maintainability, simplifying issue resolution and system updates. 148 | 149 | - **Clear Business Rules:** 150 | Business logic is clear and distinct, making it easier to understand, maintain, and develop further. 151 | -------------------------------------------------------------------------------- /docs/references/api.md: -------------------------------------------------------------------------------- 1 | ## Example 2 | 3 | ``` 4 | global [generator.rust.file = "src/generated.rs"] 5 | global [generator.rust_axum.file = "src/generated.rs"] 6 | 7 | global [generator.rust.async] 8 | global [generator.rust.async_trait] 9 | 10 | [rest.path = "hello"] 11 | usecase Hello { 12 | 13 | [rest.method = "post"] 14 | createHello { 15 | in { 16 | hi: string 17 | } 18 | out { 19 | answer: string 20 | } 21 | } 22 | 23 | [rest.method = "get"] 24 | [rest.path = "item"] 25 | getHello { 26 | in { 27 | [rest.query] 28 | hi: string 29 | } 30 | out { 31 | answer: string 32 | } 33 | } 34 | } 35 | ``` 36 | 37 | ## Pest Grammer 38 | ```pest 39 | 40 | // Basic rules for whitespace and comments 41 | WHITESPACE = _{ " " | "\t" | "\r" | "\n" } 42 | COMMENT = _{ "//" ~ (!NEWLINE ~ ANY)* } 43 | 44 | // Identifiers and basic types 45 | identifier = @{ ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_")* } 46 | type_identifier = @{ ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_" | "[" | "]")* } 47 | path = @{ (!"\n" ~ ANY)+ } 48 | 49 | 50 | // Import statements 51 | import = { "import" ~ path } 52 | 53 | // Options 54 | option_value = { integer | string | bool | array } 55 | integer = { ASCII_DIGIT+ } 56 | string = { "\"" ~ (!"\"" ~ ANY)* ~ "\"" } 57 | bool = { "true" | "false" } 58 | array = { "(" ~ (option_value ~ ("," ~ option_value)*)? ~ ")" } 59 | option = { "[" ~ identifier ~ ("." ~ identifier)* ~ ("=" ~ option_value)? ~ "]" } 60 | 61 | 62 | // Property definitions 63 | property = { option* ~ identifier ~ optional_property? ~ ":" ~ type_identifier } 64 | optional_property = { "?" } 65 | 66 | 67 | // Sections for 'in' and 'out' blocks 68 | in_block = { "in" ~ struct_body } 69 | out_block = { "out" ~ struct_body } 70 | 71 | // Usecase definitions 72 | usecase = { 73 | option* ~ 74 | "usecase" ~ identifier ~ "{" ~ 75 | method_def* ~ 76 | "}" 77 | } 78 | 79 | method_def = { 80 | option* ~ 81 | identifier ~ "{" ~ 82 | in_block? ~ 83 | out_block? ~ 84 | "}" 85 | } 86 | 87 | // Struct definitions 88 | struct_def = { 89 | option* ~ 90 | "struct" ~ identifier ~ struct_body 91 | } 92 | 93 | struct_body = { 94 | "{" ~ 95 | 96 | property* ~ 97 | "}" 98 | } 99 | 100 | global_option = { 101 | "global" ~ 102 | option 103 | } 104 | 105 | // Root rule 106 | file = { 107 | SOI ~ 108 | (usecase | struct_def | import | global_option)* ~ 109 | EOI 110 | } 111 | ``` 112 | 113 | ### Basic Rules 114 | 115 | - **WHITESPACE**: Matches any whitespace character including space, tab, carriage return, and newline. 116 | - **COMMENT**: Matches comments that start with `//` and continue until the end of the line. 117 | 118 | ### Identifiers and Basic Types 119 | 120 | - **identifier**: Matches an identifier that starts with an ASCII alphabetic character followed by zero or more alphanumeric characters or underscores. 121 | - Example: `createHello` 122 | - **type_identifier**: Similar to `identifier`, but can also include square brackets `[]` to denote array types. 123 | - Example: `string` 124 | - **path**: Matches a path, which is a sequence of any characters except newline. 125 | - Example: `"src/generated.rs"` 126 | 127 | ### Import Statements 128 | 129 | - **import**: Matches an import statement, which starts with the keyword `import` followed by a path. 130 | - Example: `import "another_file.pest"` 131 | 132 | ### Options 133 | 134 | - **option_value**: Matches an option value, which can be an integer, string, boolean, or array. 135 | - Example: `"src/generated.rs"` 136 | - **option**: Matches an option, which is an identifier followed by an optional value assignment. 137 | - Example: `[generator.rust.file = "src/generated.rs"]` 138 | 139 | ### Property Definitions 140 | 141 | - **property**: Matches a property definition, which consists of optional options, an identifier, an optional question mark for optional properties, and a type identifier separated by a colon. 142 | - Example: `hi: string` 143 | 144 | ### Sections for 'in' and 'out' Blocks 145 | 146 | - **in_block**: Matches an 'in' block, which starts with the keyword `in` followed by a struct body. 147 | - Example: 148 | ``` 149 | in { 150 | hi: string 151 | } 152 | ``` 153 | - **out_block**: Matches an 'out' block, which starts with the keyword `out' followed by a struct body. 154 | - Example: 155 | ``` 156 | out { 157 | answer: string 158 | } 159 | ``` 160 | 161 | ### Usecase Definitions 162 | 163 | - **usecase**: Matches a usecase definition, which consists of optional options, the keyword `usecase`, an identifier, and a block containing method definitions. 164 | - Example: 165 | ``` 166 | usecase Hello { 167 | createHello { 168 | in { 169 | hi: string 170 | } 171 | out { 172 | answer: string 173 | } 174 | } 175 | } 176 | ``` 177 | 178 | ### Method Definitions 179 | 180 | - **method_def**: Matches a method definition, which consists of optional options, an identifier, and optional 'in' and 'out' blocks enclosed in curly braces. 181 | - Example: 182 | ``` 183 | [rest.method = "post"] 184 | createHello { 185 | in { 186 | hi: string 187 | } 188 | out { 189 | answer: string 190 | } 191 | } 192 | ``` 193 | 194 | ### Struct Definitions 195 | 196 | - **struct_def**: Matches a struct definition, which consists of optional options, the keyword `struct`, an identifier, and a struct body. 197 | - Example: 198 | ``` 199 | struct Person { 200 | name: string 201 | age: integer 202 | } 203 | ``` 204 | 205 | ### Global Options 206 | 207 | - **global_option**: Matches a global option, which starts with the keyword `global` followed by an option. 208 | - Example: `global [generator.rust.file = "src/generated.rs"]` 209 | 210 | ### Root Rule 211 | 212 | - **file**: The root rule that matches the entire file, which can contain usecase definitions, struct definitions, import statements, and global options. 213 | - Example: 214 | ``` 215 | global [generator.rust.file = "src/generated.rs"] 216 | usecase Hello { 217 | createHello { 218 | in { 219 | hi: string 220 | } 221 | out { 222 | answer: string 223 | } 224 | } 225 | } 226 | ``` 227 | 228 | This updated documentation provides concrete examples for each section of the DSL, illustrating how the grammar can be used to define usecases, structs, and options in a domain-specific language. -------------------------------------------------------------------------------- /bin/cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use cronus_generator::{Ctxt, generate}; 3 | use tracing::{Level, span, debug}; 4 | use anyhow::Result; 5 | use tracing_subscriber::{util::SubscriberInitExt, fmt::format::FmtSpan}; 6 | use std::{collections::{HashMap, HashSet}, error::Error, fs::metadata, io::Read, path::{Path, PathBuf}}; 7 | 8 | 9 | #[derive(Parser, Debug)] 10 | #[command(author, version, about, long_about = None)] 11 | struct Args { 12 | #[command(subcommand)] 13 | command: Option, 14 | } 15 | 16 | #[derive(Parser, Debug)] 17 | enum Commands { 18 | /// Generate from a yaml file 19 | Gen { 20 | /// Input file path 21 | #[arg(short, long, value_parser)] 22 | input: Option, 23 | 24 | /// Output to stdout 25 | #[arg(short, long, default_value_t = false)] 26 | stdout: bool, 27 | 28 | // Output directory 29 | #[arg(short, long, value_parser)] 30 | output: Option, 31 | 32 | /// Search paths 33 | #[arg(short, long, value_parser)] 34 | search_paths: Option>, 35 | }, 36 | /// Convert api to yaml 37 | Yaml { 38 | /// Input file path 39 | #[arg(short, long, value_parser)] 40 | input: Option, 41 | 42 | /// Output to stdout 43 | #[arg(short, long, default_value_t = false)] 44 | stdout: bool, 45 | }, 46 | /// Convert yaml to api 47 | Api { 48 | /// Input file path 49 | #[arg(short, long, value_parser)] 50 | input: Option, 51 | 52 | /// Output to stdout 53 | #[arg(short, long, default_value_t = false)] 54 | stdout: bool, 55 | }, 56 | } 57 | 58 | 59 | 60 | fn main() -> Result<(), Box> { 61 | // tracing_subscriber::FmtSubscriber::builder() 62 | // .with_level(true) 63 | // .with_max_level(Level::TRACE) 64 | // .with_span_events(FmtSpan::CLOSE) 65 | // .init(); 66 | 67 | let args = Args::parse(); 68 | match args.command { 69 | Some(Commands::Gen { input, stdout, output, search_paths }) => { 70 | match input { 71 | Some(i) => { 72 | let target_path = PathBuf::from(i); 73 | match metadata(&target_path) { 74 | Ok(md) => { 75 | if md.is_dir() { 76 | let default_files = vec![PathBuf::from("main.yaml"), PathBuf::from("main.api")]; 77 | for default_file in &default_files { 78 | let try_file = Path::join(&target_path, default_file); 79 | if try_file.exists() { 80 | run(&try_file, search_paths.as_ref(), output)?; 81 | break; 82 | } 83 | } 84 | 85 | } else if md.is_file() { 86 | run(&target_path, search_paths.as_ref(), output)?; 87 | } else { 88 | return Err(format!("Unsupported path: {:?}", md).into()) 89 | } 90 | }, 91 | Err(err) => { 92 | return Err(format!("Error to open the path '{:?}': {:?}", target_path, err).into()) 93 | }, 94 | }; 95 | }, 96 | None => { 97 | let stdin_content = read_from_stdin(); 98 | let output = generate_from_api(&stdin_content)?; 99 | if stdout { 100 | print!("{}", output); 101 | } 102 | } 103 | } 104 | 105 | }, 106 | Some(Commands::Yaml { input, stdout }) => { 107 | match input { 108 | Some(i) => { 109 | let target_path = PathBuf::from(i); 110 | match metadata(&target_path) { 111 | Ok(md) => { 112 | if md.is_file() { 113 | let content = std::fs::read_to_string(&target_path)?; 114 | let output = api_to_yaml(&content)?; 115 | if stdout { 116 | print!("{}", output); 117 | } 118 | } else { 119 | return Err(format!("Unsupported path: {:?}", md).into()) 120 | } 121 | }, 122 | Err(err) => { 123 | return Err(format!("Error to open the path '{:?}': {:?}", target_path, err).into()) 124 | }, 125 | }; 126 | }, 127 | None => { 128 | let stdin_content = read_from_stdin(); 129 | let output = api_to_yaml(&stdin_content)?; 130 | if stdout { 131 | print!("{}", output); 132 | } 133 | } 134 | } 135 | 136 | }, 137 | Some(Commands::Api { input, stdout }) => { 138 | // TODO 139 | 140 | }, 141 | None => { 142 | return Err("No command provided".into()); 143 | } 144 | } 145 | 146 | 147 | Ok(()) 148 | 149 | } 150 | 151 | fn read_from_stdin() -> String { 152 | let mut buffer = String::new(); 153 | let stdin = std::io::stdin(); 154 | let mut handle = stdin.lock(); 155 | 156 | handle.read_to_string(&mut buffer).expect("Failed to read from stdin"); 157 | buffer 158 | } 159 | 160 | #[tracing::instrument] 161 | pub fn run(entry_file: &Path, search_paths: Option<&Vec>, output: Option) -> Result<()> { 162 | let abs_file = std::path::absolute(entry_file)?; 163 | let mut explored = HashSet::new(); 164 | let spec = cronus_parser::from_file(&abs_file, true, search_paths, &mut explored)?; 165 | let ctx = Ctxt::new(spec, output); 166 | generate(&ctx)?; 167 | ctx.dump() 168 | } 169 | 170 | 171 | pub fn generate_from_yaml(content: &str) -> Result { 172 | match cronus_parser::from_yaml_str(content) { 173 | Ok(spec) => { 174 | run_raw_spec(spec) 175 | }, 176 | Err(err) => { 177 | Err(err) 178 | }, 179 | } 180 | } 181 | 182 | pub fn api_to_yaml(content: &str) -> Result { 183 | match cronus_parser::api_parse::parse(PathBuf::new(), content) { 184 | Ok(spec) => { 185 | let yaml = cronus_parser::to_yaml_str(&spec)?; 186 | Ok(yaml) 187 | }, 188 | Err(err) => { 189 | Err(err) 190 | }, 191 | } 192 | } 193 | 194 | fn run_raw_spec(spec: cronus_spec::RawSpec) -> Result { 195 | 196 | let ctx = Ctxt::new(spec, None); 197 | match cronus_generator::generate(&ctx) { 198 | Ok(_) => { 199 | 200 | let gfs = &*ctx.generator_fs.borrow(); 201 | let result: HashMap> = gfs 202 | .iter() 203 | .map(|(key, value)| { 204 | let inner_map = value.borrow().clone(); 205 | (key.to_string(), inner_map) 206 | }) 207 | .collect(); 208 | serde_yaml::to_string(&result).map_err(|e| e.into()) 209 | }, 210 | Err(err) => { 211 | Err(err) 212 | }, 213 | } 214 | } 215 | 216 | pub fn generate_from_api(content: &str) -> Result { 217 | 218 | match cronus_parser::api_parse::parse(PathBuf::new(), content) { 219 | Ok(spec) => { 220 | run_raw_spec(spec) 221 | }, 222 | Err(err) => { 223 | Err(err) 224 | }, 225 | } 226 | 227 | } 228 | 229 | -------------------------------------------------------------------------------- /lib/generator/src/python_redis.rs: -------------------------------------------------------------------------------- 1 | use crate::{utils::{get_path_from_optional_parent, get_request_name, get_usecase_name}, Ctxt, Generator}; 2 | use anyhow::{ bail, Result}; 3 | use convert_case::Casing; 4 | use cronus_spec::{PythonFastApiGeneratorOption, PythonRedisGeneratorOption}; 5 | use serde::ser; 6 | 7 | 8 | 9 | 10 | pub struct PythonRedisGenerator { 11 | 12 | } 13 | 14 | impl PythonRedisGenerator { 15 | pub fn new() -> Self { 16 | Self {} 17 | } 18 | } 19 | 20 | impl PythonRedisGenerator { 21 | fn get_gen_option<'a>(&self, ctx: &'a Ctxt) -> Option<&'a PythonRedisGeneratorOption> { 22 | ctx.spec.option.as_ref().and_then(|go| { 23 | go.generator 24 | .as_ref() 25 | .and_then(|gen| gen.python_redis.as_ref()) 26 | }) 27 | } 28 | 29 | fn dst(&self, ctx: &Ctxt) -> String { 30 | let default_file = "generated.py"; 31 | 32 | self.get_gen_option(ctx) 33 | .and_then(|gen| { 34 | Some(get_path_from_optional_parent( 35 | gen.def_loc.file.parent(), 36 | gen.file.as_ref(), 37 | default_file, 38 | )) 39 | }) 40 | .unwrap_or_else(|| default_file.into()) 41 | } 42 | 43 | 44 | } 45 | 46 | 47 | fn async_sender_str(service_name:&str, methods: &Vec<(&String, &cronus_spec::RawMethod, &cronus_spec::RawMethodRedisOption)>) -> String { 48 | let mut method_strs = String::new(); 49 | 50 | for (method_name, method, option) in methods { 51 | 52 | let queue_name = option.queue_name.clone().unwrap_or_else(|| { 53 | let snaked_name = service_name.to_case(convert_case::Case::Snake); 54 | let method_name = method_name.to_case(convert_case::Case::Snake); 55 | format!("{snaked_name}_{method_name}") 56 | }); 57 | let method_str = format!( 58 | r#" 59 | async def {method_name}(self, request): 60 | task_data = json.dumps(asdict(request)) 61 | await self._redis.rpush("{queue_name}", task_data) 62 | "#); 63 | method_strs.push_str(&method_str); 64 | } 65 | 66 | format!( 67 | r#" 68 | class {service_name}RedisSender({service_name}): 69 | def __init__(self, redis: Redis): 70 | self._redis = redis 71 | 72 | {method_strs} 73 | "# 74 | ) 75 | } 76 | 77 | fn async_receiver_str(ctx: &crate::Ctxt, service_name:&str, methods: &Vec<(&String, &cronus_spec::RawMethod, &cronus_spec::RawMethodRedisOption)>) -> String { 78 | let mut method_strs = String::new(); 79 | let mut listen_methods = vec![]; 80 | for (method_name, method, option) in methods { 81 | let method_name = method_name.to_case(convert_case::Case::Snake); 82 | let queue_name = option.queue_name.clone().unwrap_or_else(|| { 83 | let snaked_name = service_name.to_case(convert_case::Case::Snake); 84 | format!("{snaked_name}_{method_name}") 85 | }); 86 | let ack_queue_name = option.ack_queue_name.clone().unwrap_or_else(|| { 87 | format!("{queue_name}_ack") 88 | }); 89 | let listen_method_name = format!("_listen_{method_name}"); 90 | 91 | let request_ty = get_request_name(ctx, &method_name); 92 | 93 | listen_methods.push(listen_method_name.clone()); 94 | let method_str = format!( 95 | r#" 96 | async def _listen_{method_name}(self, request): 97 | while True: 98 | try: 99 | task_item = await self._redis.brpoplpush( 100 | "{queue_name}", 101 | "{ack_queue_name}", 102 | ) 103 | if task_item: 104 | task_data = json.loads(task_item) 105 | request = {request_ty}(**task_data) 106 | await self._service.{method_name}(request) 107 | await self._redis.lrem( 108 | "{ack_queue_name}", 1, task_item 109 | ) 110 | except Exception as e: 111 | logger.error(f"Error processing send_reset_password task: {{e}}") 112 | await asyncio.sleep(5) 113 | "#); 114 | method_strs.push_str(&method_str); 115 | } 116 | 117 | let create_task_stmts = listen_methods.iter().map(|method| { 118 | format!("self.{method}()") 119 | }).collect::>().join(",\n"); 120 | let start_method_str = format!( 121 | r#" 122 | def start(self): 123 | tasks = [{create_task_stmts}] 124 | for task in tasks: 125 | self._tasks.append(asyncio.create_task(task)) 126 | "#); 127 | 128 | 129 | format!( 130 | r#" 131 | class {service_name}RedisReceiver({service_name}): 132 | def __init__(self, redis: Redis, service: {service_name}): 133 | self._redis = redis 134 | self._service = service 135 | self._tasks = [] 136 | 137 | {start_method_str} 138 | 139 | {method_strs} 140 | "# 141 | ) 142 | } 143 | 144 | impl Generator for PythonRedisGenerator { 145 | fn name(&self) -> &'static str { 146 | return "python_redis" 147 | } 148 | 149 | 150 | 151 | fn before_all(&self, ctx: &crate::Ctxt) -> Result<()> { 152 | let gen_opt = self.get_gen_option(ctx); 153 | 154 | 155 | let mut common_imports = vec![ 156 | "import json", 157 | "from dataclasses import asdict", 158 | "import logging", 159 | ]; 160 | 161 | let async_flag = gen_opt 162 | .and_then(|gen_opt| gen_opt.async_flag) 163 | .unwrap_or(false); 164 | let redis_import = if async_flag { 165 | "from redis.asyncio import Redis" 166 | } else { 167 | "from redis import Redis" 168 | }; 169 | if async_flag { 170 | common_imports.push("import asyncio"); 171 | } 172 | common_imports.push(redis_import); 173 | common_imports.push("logger = logging.getLogger(__name__)"); 174 | 175 | let common_imports_str = common_imports.join("\n") + "\n"; 176 | ctx.append_file(self.name(), &self.dst(ctx), &common_imports_str); 177 | Ok(()) 178 | 179 | } 180 | 181 | 182 | fn generate_usecase(&self, ctx: &crate::Ctxt, usecase_name: &str, usecase: &cronus_spec::RawUsecase) -> Result<()> { 183 | let usecase_from = self.get_gen_option(ctx) 184 | .and_then(|gen_opt| gen_opt.usecase_from.as_ref()) 185 | .ok_or(anyhow::anyhow!("usecase_from option is not set"))?; 186 | 187 | let redis_methods: Vec<(&String, &cronus_spec::RawMethod, &cronus_spec::RawMethodRedisOption)> = usecase.methods.iter().filter_map(|(method_name, method)| { 188 | let redis_option = method 189 | .option 190 | .as_ref() 191 | .and_then(|opt| opt.redis.as_ref()); 192 | if redis_option.is_some() { 193 | Some((method_name, method, redis_option.unwrap())) 194 | } else { 195 | None 196 | } 197 | 198 | }).collect::>(); 199 | 200 | if redis_methods.is_empty() { 201 | return Ok(()); 202 | } 203 | 204 | let service_name = get_usecase_name(ctx, usecase_name); 205 | 206 | let sender_str = async_sender_str(&service_name, &redis_methods); 207 | let receiver_str = async_receiver_str(&ctx, &service_name, &redis_methods); 208 | let mut types_import_from_interfaces = vec![service_name.clone()]; 209 | for (method_name, method, option) in &redis_methods { 210 | if method.req.is_some() { 211 | let request_ty = get_request_name(ctx, method_name); 212 | types_import_from_interfaces.push(request_ty); 213 | } 214 | } 215 | let import_items = types_import_from_interfaces.join(", "); 216 | let import_str = format!("from {usecase_from} import {import_items}"); 217 | 218 | ctx.append_file( 219 | self.name(), 220 | &self.dst(ctx), 221 | &import_str, 222 | ); 223 | 224 | ctx.append_file( 225 | self.name(), 226 | &self.dst(ctx), 227 | &receiver_str, 228 | ); 229 | ctx.append_file( 230 | self.name(), 231 | &self.dst(ctx), 232 | &sender_str, 233 | ); 234 | 235 | Ok(()) 236 | } 237 | } -------------------------------------------------------------------------------- /lib/generator/src/openapi_utils.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::collections::HashMap; 3 | 4 | #[derive(Debug, Serialize, Deserialize, Clone)] 5 | pub struct OpenApiDocument { 6 | pub openapi: String, 7 | pub info: InfoObject, 8 | pub paths: HashMap, 9 | 10 | #[serde(skip_serializing_if = "Option::is_none")] 11 | pub components: Option 12 | // Other fields like servers, components, security, tags, etc. can be added here 13 | } 14 | 15 | #[derive(Debug, Serialize, Deserialize, Clone)] 16 | pub struct OpenApiComponentsObject { 17 | #[serde(skip_serializing_if = "Option::is_none")] 18 | pub schemas: Option>, 19 | } 20 | 21 | 22 | impl OpenApiComponentsObject { 23 | pub fn new() -> Self { 24 | Self { 25 | schemas: Default::default() 26 | } 27 | } 28 | } 29 | 30 | 31 | impl OpenApiDocument { 32 | pub fn new(openapi: &str, info: InfoObject) -> Self { 33 | return Self { 34 | openapi: openapi.to_owned(), 35 | info, 36 | paths: Default::default(), 37 | components: Default::default(), 38 | }; 39 | } 40 | } 41 | #[derive(Debug, Serialize, Deserialize, Clone)] 42 | pub struct InfoObject { 43 | pub title: String, 44 | pub version: String, 45 | #[serde(skip_serializing_if = "Option::is_none")] 46 | pub description: Option 47 | // Other fields like description, termsOfService, contact, license, etc. can be added here 48 | } 49 | 50 | #[derive(Debug, Serialize, Deserialize, Clone)] 51 | pub struct PathItemObject { 52 | #[serde(skip_serializing_if = "Option::is_none")] 53 | pub get: Option, 54 | 55 | #[serde(skip_serializing_if = "Option::is_none")] 56 | pub put: Option, 57 | 58 | #[serde(skip_serializing_if = "Option::is_none")] 59 | pub post: Option, 60 | 61 | #[serde(skip_serializing_if = "Option::is_none")] 62 | pub delete: Option, 63 | 64 | #[serde(skip_serializing_if = "Option::is_none")] 65 | pub options: Option, 66 | 67 | #[serde(skip_serializing_if = "Option::is_none")] 68 | pub head: Option, 69 | 70 | #[serde(skip_serializing_if = "Option::is_none")] 71 | pub patch: Option, 72 | 73 | #[serde(skip_serializing_if = "Option::is_none")] 74 | pub trace: Option, 75 | // Additional fields like parameters can be added here 76 | } 77 | 78 | impl Default for PathItemObject { 79 | fn default() -> Self { 80 | PathItemObject { 81 | get: None, 82 | put: None, 83 | post: None, 84 | delete: None, 85 | options: None, 86 | head: None, 87 | patch: None, 88 | trace: None, 89 | } 90 | } 91 | } 92 | 93 | #[derive(Debug, Serialize, Deserialize, Clone)] 94 | pub struct OperationObject { 95 | 96 | #[serde(skip_serializing_if = "Option::is_none")] 97 | pub summary: Option, 98 | 99 | #[serde(skip_serializing_if = "Option::is_none")] 100 | pub description: Option, 101 | 102 | #[serde(rename = "operationId", skip_serializing_if = "Option::is_none")] 103 | pub operation_id: Option, 104 | 105 | #[serde(skip_serializing_if = "Option::is_none")] 106 | pub parameters: Option>, 107 | 108 | #[serde(rename = "requestBody", skip_serializing_if = "Option::is_none")] 109 | pub request_body: Option, 110 | 111 | pub responses: ResponsesObject, 112 | 113 | #[serde(skip_serializing_if = "Option::is_none")] 114 | pub tags: Option>, 115 | } 116 | 117 | #[derive(Debug, Serialize, Deserialize, Clone)] 118 | pub struct ParameterObject { 119 | pub name: String, 120 | #[serde(rename = "in")] 121 | pub in_: String, // 'in' is a reserved keyword in Rust 122 | 123 | #[serde(skip_serializing_if = "Option::is_none")] 124 | pub description: Option, 125 | pub required: bool, 126 | pub schema: SchemaObject 127 | // Other fields like schema, allowEmptyValue, deprecated, etc. can be added here 128 | } 129 | 130 | #[derive(Debug, Serialize, Deserialize, Clone)] 131 | pub struct RequestBodyObject { 132 | #[serde(skip_serializing_if = "Option::is_none")] 133 | pub description: Option, 134 | pub content: HashMap, 135 | 136 | #[serde(skip_serializing_if = "Option::is_none")] 137 | pub required: Option, 138 | } 139 | 140 | #[derive(Debug, Serialize, Deserialize, Clone)] 141 | pub struct MediaTypeObject { 142 | #[serde(skip_serializing_if = "Option::is_none")] 143 | pub schema: Option, 144 | // Additional fields like examples, encoding can be added here 145 | } 146 | 147 | #[derive(Debug, Serialize, Deserialize, Clone)] 148 | pub struct ResponsesObject { 149 | #[serde(flatten)] 150 | pub responses: HashMap, 151 | // Default responses can be added as additional fields 152 | } 153 | 154 | #[derive(Debug, Serialize, Deserialize, Clone)] 155 | pub struct ResponseObject { 156 | pub description: String, 157 | 158 | #[serde(skip_serializing_if = "Option::is_none")] 159 | pub content: Option>, 160 | // Additional fields like headers, links can be added here 161 | } 162 | 163 | #[derive(Debug, Serialize, Deserialize, Clone)] 164 | pub struct SchemaObject { 165 | #[serde(rename = "type", skip_serializing_if = "Option::is_none")] 166 | pub type_: Option, // 'type' is a reserved keyword in Rust 167 | 168 | #[serde(skip_serializing_if = "Option::is_none")] 169 | pub items: Option>, 170 | 171 | #[serde(skip_serializing_if = "Option::is_none")] 172 | pub format: Option, 173 | 174 | #[serde(skip_serializing_if = "Option::is_none")] 175 | pub properties: Option>, 176 | 177 | #[serde(rename = "additionalProperties", skip_serializing_if = "Option::is_none")] 178 | pub additional_properties: Option>, 179 | 180 | #[serde(skip_serializing_if = "Option::is_none")] 181 | pub required: Option>, 182 | #[serde(rename = "enum", skip_serializing_if = "Option::is_none")] 183 | pub enum_: Option>, 184 | 185 | #[serde(skip_serializing_if = "Option::is_none")] 186 | pub all_of: Option>, 187 | 188 | #[serde(skip_serializing_if = "Option::is_none")] 189 | pub one_of: Option>, 190 | 191 | #[serde(skip_serializing_if = "Option::is_none")] 192 | pub any_of: Option>, 193 | 194 | #[serde(skip_serializing_if = "Option::is_none")] 195 | pub not: Option>, 196 | 197 | #[serde(skip_serializing_if = "Option::is_none")] 198 | pub description: Option, 199 | 200 | #[serde(skip_serializing_if = "Option::is_none")] 201 | pub default: Option, 202 | 203 | #[serde(rename = "$ref", skip_serializing_if = "Option::is_none")] 204 | pub ref_: Option, 205 | 206 | #[serde(skip_serializing_if = "Option::is_none")] 207 | pub nullable: Option 208 | } 209 | 210 | impl Default for SchemaObject { 211 | fn default() -> Self { 212 | Self { 213 | type_: Default::default(), 214 | items: Default::default(), 215 | format: Default::default(), 216 | properties: Default::default(), 217 | required: Default::default(), 218 | enum_: Default::default(), 219 | all_of: Default::default(), 220 | one_of: Default::default(), 221 | any_of: Default::default(), 222 | not: Default::default(), 223 | description: Default::default(), 224 | default: Default::default(), 225 | ref_: Default::default(), 226 | nullable: Default::default(), 227 | additional_properties: Default::default() 228 | } 229 | } 230 | } 231 | 232 | impl SchemaObject { 233 | pub fn new_with_ref(r: String) -> Self { 234 | Self { 235 | ref_: Some(r), 236 | ..Default::default() 237 | } 238 | } 239 | 240 | pub fn new_with_type(ty: String) -> Self { 241 | Self { 242 | type_: Some(ty), 243 | ..Default::default() 244 | } 245 | } 246 | 247 | pub fn new_with_items(items: Box) -> Self { 248 | Self { 249 | items: Some(items), 250 | ..Default::default() 251 | 252 | } 253 | } 254 | 255 | pub fn new_dict(additional_properties: Option>) -> Self { 256 | Self { 257 | type_: Some("object".to_string()), 258 | additional_properties, 259 | ..Default::default() 260 | } 261 | 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /lib/generator/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod rust; 2 | mod rust_axum; 3 | mod openapi; 4 | mod openapi_utils; 5 | mod utils; 6 | mod ts; 7 | mod ts_nestjs; 8 | mod python; 9 | mod rust_utils; 10 | mod python_fastapi; 11 | mod python_redis; 12 | mod golang; 13 | mod golang_gin; 14 | mod java; 15 | mod java_springweb; 16 | 17 | use std::{rc::Rc, cell::{RefCell}, collections::{HashMap, HashSet}, path::{Path, PathBuf}, error::Error, fs::{self, OpenOptions, File}, io::Write}; 18 | 19 | use openapi::OpenAPIGenerator; 20 | use rust::RustGenerator; 21 | use rust_axum::RustAxumGenerator; 22 | use cronus_spec::{RawSchema, RawSpec, RawUsecase, RawMethod}; 23 | use tracing::info; 24 | use ts::TypescriptGenerator; 25 | use ts_nestjs::TypescriptNestjsGenerator; 26 | use anyhow::{bail, Context as _, Ok, Result}; 27 | 28 | /// relative path => file content 29 | type GeneratorFileSystem = Rc>>; 30 | 31 | pub struct Context { 32 | pub generator_fs: RefCell>, 33 | pub spec: RawSpec, 34 | pub output: Option, 35 | } 36 | 37 | impl Context { 38 | pub fn new(spec: RawSpec, output: Option) -> Self { 39 | Self { 40 | generator_fs: RefCell::new(HashMap::new()), 41 | spec, 42 | output, 43 | } 44 | } 45 | 46 | pub fn get_gfs(&self, name: &'static str) -> GeneratorFileSystem { 47 | if self.generator_fs.borrow().contains_key(name) { 48 | self.generator_fs.borrow().get(name).unwrap().clone() 49 | } else { 50 | self.init_gfs(name) 51 | } 52 | } 53 | 54 | fn init_gfs(&self, name: &'static str) -> GeneratorFileSystem { 55 | let fs = Rc::new(RefCell::new(HashMap::new())); 56 | self.generator_fs.borrow_mut().insert(name, fs.clone()); 57 | fs 58 | } 59 | 60 | pub fn append_file(&self, name:&'static str, path:&str, content: &str) { 61 | let fs = self.get_gfs(name); 62 | let mut mutated_fs = fs.borrow_mut(); 63 | match mutated_fs.get_mut(path) { 64 | Some(f) => { 65 | f.push_str(&content); 66 | }, 67 | None => { 68 | mutated_fs.insert(path.to_string(), content.to_string()); 69 | }, 70 | }; 71 | 72 | } 73 | 74 | 75 | 76 | /// Write the results/files of the generator to the disk 77 | /// 78 | /// 79 | pub fn dump(&self) -> Result<()> { 80 | let mut touched_files: HashSet = Default::default(); 81 | 82 | for (g, fs) in self.generator_fs.borrow().iter() { 83 | for (path, contents) in fs.borrow().iter() { 84 | let pb = PathBuf::from(path); 85 | println!("Dumping {}: {}", g, pb.display()); 86 | let par = pb.parent().unwrap(); 87 | if !par.exists() { 88 | std::fs::create_dir_all(par)?; 89 | } 90 | let mut file: File; 91 | if touched_files.contains(path) { 92 | file = OpenOptions::new() 93 | .write(true) 94 | .append(true) 95 | .create(true) 96 | .open(path).context(format!("failed to open {}", path))?; 97 | } else { 98 | file = OpenOptions::new() 99 | .write(true) 100 | .create(true) 101 | .truncate(true) 102 | .open(path).context(format!("failed to open {}", path))?; 103 | touched_files.insert(path.to_string()); 104 | } 105 | 106 | file.write_all(contents.as_bytes())?; 107 | info!("[+] {}", path); 108 | 109 | } 110 | } 111 | Ok(()) 112 | } 113 | 114 | } 115 | 116 | 117 | #[derive(Clone)] 118 | pub struct Ctxt(std::sync::Arc); 119 | 120 | impl std::ops::Deref for Ctxt { 121 | type Target = Context; 122 | 123 | fn deref(&self) -> &Self::Target { 124 | self.0.as_ref() 125 | } 126 | } 127 | 128 | impl Ctxt { 129 | pub fn new(spec: RawSpec, output: Option) -> Self { 130 | Self(std::sync::Arc::new( Context::new(spec, output))) 131 | } 132 | } 133 | 134 | pub trait Generator { 135 | fn name(&self) -> &'static str; 136 | fn before_all(&self, _ctx: &Ctxt) -> Result<()> { 137 | Ok(()) 138 | } 139 | fn after_all(&self, _ctx: &Ctxt) -> Result<()> { 140 | Ok(()) 141 | } 142 | fn generate_schema(&self, _ctx: &Ctxt, _schema_name:&str, _schema: &RawSchema)-> Result<()> { 143 | Ok(()) 144 | } 145 | fn generate_usecase(&self, _ctx: &Ctxt, _usecase_name: &str, _usecase: &RawUsecase) -> Result<()> { 146 | Ok(()) 147 | } 148 | } 149 | 150 | pub fn generate(ctx: &Ctxt) -> Result<()> { 151 | let generators:Vec> = vec![ 152 | Rc::new(RustGenerator::new()), 153 | Rc::new(RustAxumGenerator::new()), 154 | Rc::new(OpenAPIGenerator::new()), 155 | Rc::new(TypescriptGenerator::new()), 156 | Rc::new(TypescriptNestjsGenerator::new()), 157 | Rc::new(python::PythonGenerator::new()), 158 | Rc::new(python_fastapi::PythonFastApiGenerator::new()), 159 | Rc::new(python_redis::PythonRedisGenerator::new()), 160 | Rc::new(golang::GolangGenerator::new()), 161 | Rc::new(golang_gin::GolangGinGenerator::new()), 162 | Rc::new(java::JavaGenerator::new()), 163 | Rc::new(java_springweb::JavaSpringWebGenerator::new()), 164 | ]; 165 | let mut generator_map: HashMap<&str, Rc> = HashMap::new(); 166 | generators 167 | .iter() 168 | .for_each(|g| { 169 | generator_map.insert(g.name(), g.clone()); 170 | }); 171 | 172 | 173 | if ctx.spec.option.is_none() { 174 | info!("No generator(s) is configured."); 175 | } else { 176 | if let Some(generator) = &ctx.spec.option.as_ref().unwrap().generator { 177 | 178 | let json_value = serde_yaml::to_value(generator).expect("Failed to serialize"); 179 | 180 | if let serde_yaml::Value::Mapping(map) = &json_value { 181 | for (generator_name, config) in map { 182 | if config.is_null(){ 183 | continue; 184 | } 185 | match generator_map.get(generator_name.as_str().unwrap()) { 186 | Some(g) => { 187 | run_generator(g.as_ref(), ctx)?; 188 | }, 189 | None => { 190 | bail!("Cannot find generator '{}'", generator_name.as_str().unwrap()) 191 | }, 192 | } 193 | 194 | } 195 | } 196 | 197 | } else { 198 | info!("No generator(s) is configured."); 199 | } 200 | } 201 | Ok(()) 202 | 203 | } 204 | 205 | pub fn run_generator(g: &dyn Generator, ctx: &Ctxt) -> Result<()> { 206 | g.before_all(ctx)?; 207 | let schema_items = ctx.spec 208 | .ty 209 | .iter() 210 | .flat_map(|t| t.iter()); 211 | 212 | for (name, schema) in schema_items { 213 | g.generate_schema(ctx, name,schema)? 214 | } 215 | 216 | 217 | let usecase_items = ctx.spec 218 | .usecases 219 | .iter() 220 | .flat_map(|m| m.iter()); 221 | 222 | for (name, usecase) in usecase_items { 223 | g.generate_usecase(ctx, name, usecase)? 224 | } 225 | 226 | 227 | g.after_all(ctx) 228 | 229 | } 230 | 231 | 232 | #[cfg(test)] 233 | mod test { 234 | use std::{collections::HashSet, path::{Path, PathBuf}, process::Command}; 235 | 236 | use cronus_spec::RawSpec; 237 | use anyhow::{bail, Result}; 238 | use crate::{generate, Context, Ctxt}; 239 | 240 | 241 | #[test] 242 | fn context_get_files_by_generator(){ 243 | let ctx = Context::new(RawSpec::new(), None); 244 | ctx.init_gfs("abcde"); 245 | ctx.get_gfs("abcde"); 246 | } 247 | 248 | #[test] 249 | fn context_append_file(){ 250 | let ctx = Context::new( RawSpec::new(), None); 251 | ctx.init_gfs("agenerator"); 252 | 253 | ctx.append_file("agenerator", "src/lib.rs", "hello"); 254 | } 255 | 256 | fn get_cargo_manifest_dir() -> Option { 257 | std::env::var("CARGO_MANIFEST_DIR").ok().map(PathBuf::from) 258 | } 259 | 260 | #[test] 261 | fn e2e_hello_rust() -> Result<()> { 262 | let proj_dir = get_cargo_manifest_dir().unwrap().join("testdata").join("hello").join("rust"); 263 | let spec_file = proj_dir.join("main.api"); 264 | let mut explored = HashSet::new(); 265 | let spec = cronus_parser::from_file(&spec_file, true, None, &mut explored)?; 266 | let ctx = Ctxt::new(spec, None); 267 | generate(&ctx)?; 268 | run_cargo_check(&proj_dir) 269 | } 270 | 271 | #[test] 272 | fn e2e_hello_rust_axum() -> Result<()> { 273 | let proj_dir = get_cargo_manifest_dir().unwrap().join("testdata").join("hello").join("rust_axum"); 274 | let spec_file = proj_dir.join("main.api"); 275 | let mut explored = HashSet::new(); 276 | let spec = cronus_parser::from_file(&spec_file, true, None, &mut explored)?; 277 | let ctx = Ctxt::new(spec, None); 278 | generate(&ctx)?; 279 | run_cargo_check(&proj_dir) 280 | } 281 | 282 | fn run_cargo_check(dir: &Path) -> Result<()> { 283 | let output = Command::new("cargo") 284 | .arg("check") 285 | .current_dir(dir) 286 | .output()?; 287 | 288 | if !output.status.success() { 289 | bail!("Stdout: {}\nStderr: {}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)) 290 | } 291 | 292 | Ok(()) 293 | } 294 | } -------------------------------------------------------------------------------- /lib/generator/src/golang.rs: -------------------------------------------------------------------------------- 1 | use std::{any::type_name, cell::RefCell, collections::HashSet, fmt::format, path::PathBuf}; 2 | 3 | use convert_case::{Case, Casing}; 4 | use cronus_spec::{RawSchema, GolangGeneratorOption}; 5 | 6 | use crate::{ 7 | utils::{self, get_path_from_optional_parent, get_request_name, get_response_name, get_schema_by_name, get_usecase_name, spec_ty_to_golang_builtin_ty, spec_ty_to_rust_builtin_ty}, Ctxt, Generator 8 | }; 9 | use tracing::{self, debug, span, Level}; 10 | use anyhow::{Ok, Result}; 11 | 12 | pub struct GolangGenerator { 13 | generated_tys: RefCell> 14 | } 15 | 16 | 17 | impl GolangGenerator { 18 | pub fn new() -> Self { 19 | Self { 20 | generated_tys: RefCell::new(HashSet::new()) 21 | } 22 | } 23 | } 24 | 25 | impl Generator for GolangGenerator { 26 | fn name(&self) -> &'static str { 27 | "golang" 28 | } 29 | 30 | fn before_all(&self, ctx: &Ctxt) -> Result<()> { 31 | 32 | let mut imports = vec![ 33 | "context" 34 | ]; 35 | 36 | 37 | let pkg = self.get_gen_option(ctx) 38 | .and_then(|gen_opt| gen_opt.package.clone()) 39 | .unwrap_or_else(|| "domain".to_string()); 40 | ctx.append_file(self.name(), &self.dst(ctx), 41 | &format!("package {}\n\n", pkg)); 42 | 43 | let import_str = imports.iter().map(|imp| format!("\"{}\"", imp)).collect::>().join("\n"); 44 | ctx.append_file(self.name(), &self.dst(ctx), 45 | &format!("import (\n{}\n)\n\n", import_str)); 46 | 47 | Ok(()) 48 | 49 | } 50 | 51 | fn generate_schema(&self, ctx: &Ctxt, schema_name:&str, schema: &RawSchema) -> Result<()> { 52 | self.generate_struct(ctx, schema, Some(schema_name.to_owned()), None); 53 | Ok(()) 54 | } 55 | 56 | 57 | /// Generate the Rust trait for the usecase 58 | /// 59 | /// trait { 60 | /// fn (&self, request) -> response; 61 | /// } 62 | /// 63 | fn generate_usecase(&self, ctx: &Ctxt, name: &str, usecase: &cronus_spec::RawUsecase) -> Result<()> { 64 | let span = span!(Level::TRACE, "generate_usecase", "usecase" = name); 65 | // Enter the span, returning a guard object. 66 | let _enter = span.enter(); 67 | let trait_name = get_usecase_name(ctx, name); 68 | 69 | let mut result = String::new(); 70 | 71 | 72 | result += &format!("type {} interface {{\n", trait_name); 73 | for (method_name, method) in &usecase.methods { 74 | 75 | result += " "; 76 | result += &method_name.to_case(Case::UpperCamel); 77 | let mut method_params: Vec = vec![]; 78 | method_params.push("ctx context.Context".to_string()); 79 | 80 | if let Some(req) = &method.req { 81 | let request_ty = get_request_name(ctx, method_name); 82 | self.generate_struct(ctx, &req, Some(request_ty.clone()), None)?; 83 | method_params.push(format!("request *{}", request_ty)); 84 | } 85 | let params_str = method_params.join(", "); 86 | result += &format!("({})", params_str); 87 | 88 | 89 | if let Some(res) = &method.res { 90 | let response_ty = get_response_name(ctx, method_name); 91 | self.generate_struct(ctx, &res, Some(response_ty.clone()), None)?; 92 | result += format!("(*{}, error)", response_ty).as_str(); 93 | } else { 94 | result += "error"; 95 | } 96 | 97 | 98 | 99 | 100 | 101 | result += "\n"; 102 | } 103 | result += "}\n"; 104 | 105 | ctx.append_file(self.name(), &self.dst(ctx), &result); 106 | 107 | Ok(()) 108 | } 109 | 110 | 111 | 112 | 113 | 114 | } 115 | 116 | impl GolangGenerator { 117 | 118 | 119 | 120 | 121 | /// Generate the Rust struct definition 122 | /// 123 | fn generate_struct( 124 | &self, 125 | ctx: &Ctxt, 126 | schema: &RawSchema, 127 | override_ty: Option, 128 | root_schema_ty: Option 129 | ) -> Result { 130 | let type_name: String; 131 | 132 | // find out the correct type name 133 | if let Some(ty) = &override_ty { 134 | type_name = ty.to_case(Case::UpperCamel); 135 | } 136 | else if schema.items.is_some() { 137 | 138 | type_name = self.generate_struct(ctx, schema.items.as_ref().unwrap(), None, root_schema_ty.clone())?; 139 | 140 | return Ok(format!("[]{}", type_name).to_owned()); 141 | } 142 | else { 143 | type_name = schema.ty.as_ref().unwrap().clone(); 144 | } 145 | 146 | 147 | 148 | 149 | let span = span!(Level::TRACE, "generate_struct", "type" = type_name); 150 | // Enter the span, returning a guard object. 151 | let _enter = span.enter(); 152 | 153 | // if type name belongs to built-in type, return directly 154 | if let Some(ty) = spec_ty_to_golang_builtin_ty(&type_name) { 155 | return Ok(ty); 156 | } 157 | 158 | if self.generated_tys.borrow().contains(&type_name) { 159 | if let Some(root_schema_ty) = root_schema_ty { 160 | if root_schema_ty == type_name { 161 | return Ok(format!("{type_name}")) 162 | } 163 | } 164 | return Ok(type_name); 165 | } 166 | 167 | 168 | 169 | // if it is referenced to a custom type, find and return 170 | if let Some(ref_schema) = get_schema_by_name(&ctx, &type_name) { 171 | // check whether schema is a type referencing another user type 172 | if schema.properties.is_none() && schema.enum_items.is_none() && schema.items.is_none() { 173 | return self.generate_struct(ctx, ref_schema, Some(type_name.to_string()), Some(type_name.to_string())); 174 | } 175 | } 176 | 177 | self.generated_tys.borrow_mut().insert(type_name.clone()); 178 | 179 | // if it is a enum type, generate the enum definition 180 | if let Some(enum_items) = &schema.enum_items { 181 | let enum_int = enum_items.iter().any(|item| item.value.is_some()); 182 | let enum_actual_ty = if enum_int { 183 | "int" 184 | } else { 185 | "string" 186 | }; 187 | let mut enum_def = format!("type {} {}\n", type_name, enum_actual_ty); 188 | for item in enum_items { 189 | let enum_value = if enum_int { 190 | if item.value.is_none() { 191 | return Err(anyhow::anyhow!("Enum item {} has no value when other enum has set", item.name)); 192 | } 193 | format!("{}", item.value.unwrap()) 194 | } else { 195 | format!("\"{}\"", item.name.to_uppercase()) 196 | }; 197 | enum_def += &format!("const {} {} = {}\n", item.name.to_case(Case::UpperSnake), type_name, enum_value); 198 | } 199 | ctx.append_file(self.name(), &self.dst(ctx), &enum_def); 200 | return Ok(type_name); 201 | } 202 | 203 | 204 | 205 | let mut result = format!("type {} struct {{\n", type_name).to_string(); 206 | 207 | 208 | if let Some(properties) = &schema.properties { 209 | for (prop_name, prop_schema) in properties { 210 | 211 | // let mut attrs: Vec = vec![]; 212 | // match &prop_schema.option { 213 | // Some(option) => { 214 | // match &option.rust { 215 | // Some(rust_opt) => { 216 | // match &rust_opt.attrs { 217 | // Some(custom_attrs) => { 218 | // attrs.extend(custom_attrs.iter().map(|attr| format!("#[{}]", attr).to_string()) ); 219 | // }, 220 | // None => {}, 221 | // } 222 | // }, 223 | // None => {} 224 | // } 225 | // }, 226 | // None => {}, 227 | // } 228 | 229 | // if !attrs.is_empty() { 230 | // result += &format!(" {}\n", attrs.join("\n")); 231 | // } 232 | 233 | result += " "; 234 | result += prop_name.to_case(Case::UpperCamel).as_str(); 235 | result += " "; 236 | 237 | let optional = match prop_schema.required { 238 | Some(req) => !req, 239 | None => false 240 | }; 241 | 242 | let prop_ty = self.generate_struct(ctx, &prop_schema, None, Some(type_name.clone()))?; 243 | 244 | if optional { 245 | result += &format!("*{}", prop_ty); 246 | 247 | } else { 248 | result += &prop_ty; 249 | } 250 | 251 | let add_json_tag = true; // TODO: make it configurable 252 | if add_json_tag { 253 | result += &format!(" `json:\"{}\"`", prop_name.to_case(Case::Camel)); 254 | } 255 | 256 | result += "\n"; 257 | } 258 | } 259 | 260 | result += "}\n"; 261 | ctx.append_file(self.name(), &self.dst(ctx), &result); 262 | 263 | 264 | 265 | Ok(type_name) 266 | } 267 | 268 | fn get_gen_option<'a>(&self, ctx: &'a Ctxt) -> Option<&'a GolangGeneratorOption> { 269 | ctx.spec.option.as_ref().and_then(|go| go.generator.as_ref().and_then(|gen| gen.golang.as_ref())) 270 | } 271 | 272 | fn dst(&self, ctx: &Ctxt) -> String { 273 | let default_file = "domain.go"; 274 | 275 | 276 | 277 | if let Some(go_opt) = self.get_gen_option(ctx) { 278 | if let Some(file) = ctx.output.as_ref() { 279 | let file_str = file.to_string_lossy().to_string(); 280 | return get_path_from_optional_parent(go_opt.def_loc.file.parent(), Some(&file_str), default_file); 281 | } 282 | let dest_path = get_path_from_optional_parent(go_opt.def_loc.file.parent(), go_opt.file.as_ref(), default_file); 283 | return dest_path; 284 | } 285 | default_file.into() 286 | 287 | } 288 | } -------------------------------------------------------------------------------- /lib/generator/src/python.rs: -------------------------------------------------------------------------------- 1 | use std::{any::type_name, cell::RefCell, collections::HashSet, fmt::format, path::PathBuf}; 2 | 3 | use convert_case::{Case, Casing}; 4 | use cronus_spec::{RawSchema, PythonGeneratorOption}; 5 | 6 | use crate::{ 7 | utils::{self, get_path_from_optional_parent, get_request_name, get_response_name, get_schema_by_name, get_usecase_name, spec_ty_to_py_builtin_ty, spec_ty_to_rust_builtin_ty}, Ctxt, Generator 8 | }; 9 | use tracing::{self, debug, span, Level}; 10 | use anyhow::{Ok, Result}; 11 | 12 | pub struct PythonGenerator { 13 | generated_tys: RefCell> 14 | } 15 | 16 | 17 | impl PythonGenerator { 18 | pub fn new() -> Self { 19 | Self { 20 | generated_tys: Default::default() 21 | } 22 | } 23 | } 24 | 25 | impl Generator for PythonGenerator { 26 | fn name(&self) -> &'static str { 27 | "python" 28 | } 29 | 30 | fn before_all(&self, ctx: &Ctxt) -> Result<()> { 31 | 32 | let common_imports = vec![ 33 | "from abc import ABC, abstractmethod", 34 | "from dataclasses import dataclass", 35 | "from typing import Optional", 36 | "from enum import Enum" 37 | ]; 38 | let common_imports_str = common_imports.join("\n") + "\n"; 39 | ctx.append_file(self.name(), &self.dst(ctx), &common_imports_str); 40 | 41 | // custom uses 42 | match self.get_gen_option(ctx) { 43 | Some(rust_gen) => { 44 | // match &rust_gen.uses { 45 | // Some(uses) => { 46 | // let use_stmts:Vec = uses.iter().map(|u| format!("use {};", u).to_string()).collect(); 47 | 48 | // let str = use_stmts.join("\n") + "\n"; 49 | // ctx.append_file(self.name(), &self.dst(ctx), &str); 50 | 51 | // }, 52 | // None => {}, 53 | // } 54 | }, 55 | None => {}, 56 | } 57 | 58 | Ok(()) 59 | 60 | } 61 | 62 | fn generate_schema(&self, ctx: &Ctxt, schema_name:&str, schema: &RawSchema) -> Result<()> { 63 | self.generate_struct(ctx, schema, Some(schema_name.to_owned()), None); 64 | Ok(()) 65 | } 66 | 67 | 68 | /// Generate the Python trait for the usecase 69 | /// 70 | /// trait { 71 | /// fn (&self, request) -> response; 72 | /// } 73 | /// 74 | fn generate_usecase(&self, ctx: &Ctxt, name: &str, usecase: &cronus_spec::RawUsecase) -> Result<()> { 75 | let span = span!(Level::TRACE, "generate_usecase", "usecase" = name); 76 | // Enter the span, returning a guard object. 77 | let _enter = span.enter(); 78 | let trait_name = get_usecase_name(ctx, name); 79 | // TODO: customized error type 80 | let default_error_ty: &str = "Box"; 81 | let mut result = String::new(); 82 | 83 | // handle async trait 84 | match self.get_gen_option(ctx) { 85 | Some(rust_gen) => { 86 | // match rust_gen.async_trait { 87 | // Some(flag) => { 88 | // if flag { 89 | // result += "#[async_trait]\n"; 90 | // } 91 | // }, 92 | // _ => {} 93 | // } 94 | }, 95 | _ => {} 96 | } 97 | result += &format!("class {}(ABC):\n", trait_name); 98 | for (method_name, method) in &usecase.methods { 99 | result += " @abstractmethod\n"; 100 | // handle async fn 101 | let mut has_async = false; 102 | match self.get_gen_option(ctx) { 103 | Some(gen_opt) => { 104 | match gen_opt.async_flag { 105 | Some(flag) => { 106 | if flag { 107 | result += " async "; 108 | has_async = true; 109 | } 110 | }, 111 | _ => {} 112 | } 113 | }, 114 | _ => {} 115 | } 116 | if !has_async { 117 | result += " "; 118 | } 119 | result += "def "; 120 | result += &method_name.to_case(Case::Snake); 121 | result += "(self"; 122 | 123 | if let Some(req) = &method.req { 124 | let request_ty = get_request_name(ctx, method_name); 125 | self.generate_struct(ctx, &req, Some(request_ty.clone()), None); 126 | result += ", request: "; 127 | result += &request_ty; 128 | } 129 | result += ")"; 130 | 131 | let mut result_type: String = "None".to_string(); 132 | 133 | if let Some(res) = &method.res { 134 | let response_ty = get_response_name(ctx, method_name); 135 | self.generate_struct(ctx, &res, Some(response_ty.clone()), None); 136 | result_type = response_ty; 137 | } 138 | 139 | result += &format!(" -> {}", result_type); 140 | result += ":\n"; 141 | result += " pass\n"; 142 | } 143 | 144 | 145 | ctx.append_file(self.name(), &self.dst(ctx), &result); 146 | 147 | Ok(()) 148 | } 149 | 150 | 151 | 152 | 153 | 154 | } 155 | 156 | impl PythonGenerator { 157 | 158 | 159 | 160 | 161 | /// Generate the Python struct definition 162 | /// 163 | fn generate_struct( 164 | &self, 165 | ctx: &Ctxt, 166 | schema: &RawSchema, 167 | override_ty: Option, 168 | root_schema_ty: Option 169 | ) -> String { 170 | let type_name: String; 171 | 172 | // find out the correct type name 173 | if let Some(ty) = &override_ty { 174 | type_name = ty.to_case(Case::UpperCamel); 175 | } 176 | else if schema.items.is_some() { 177 | 178 | type_name = self.generate_struct(ctx, schema.items.as_ref().unwrap(), None, root_schema_ty.clone()); 179 | 180 | return format!("list[{}]", type_name).to_owned() 181 | } 182 | else { 183 | type_name = schema.ty.as_ref().unwrap().clone(); 184 | } 185 | 186 | 187 | 188 | 189 | let span = span!(Level::TRACE, "generate_struct", "type" = type_name); 190 | // Enter the span, returning a guard object. 191 | let _enter = span.enter(); 192 | 193 | // if type name belongs to built-in type, return directly 194 | if let Some(ty) = spec_ty_to_py_builtin_ty(&type_name) { 195 | return ty; 196 | } 197 | 198 | if self.generated_tys.borrow().contains(&type_name) { 199 | if let Some(root_schema_ty) = root_schema_ty { 200 | if root_schema_ty == type_name { 201 | return format!("'{type_name}'") 202 | } 203 | } 204 | return type_name; 205 | } 206 | 207 | 208 | 209 | // if it is referenced to a custom type, find and return 210 | if let Some(ref_schema) = get_schema_by_name(&ctx, &type_name) { 211 | // check whether schema is a type referencing another user type 212 | if schema.properties.is_none() && schema.enum_items.is_none() && schema.items.is_none() { 213 | return self.generate_struct(ctx, ref_schema, Some(type_name.to_string()), Some(type_name.to_string())); 214 | } 215 | } 216 | 217 | 218 | self.generated_tys.borrow_mut().insert(type_name.clone()); 219 | // if it is a enum type, generate the enum definition 220 | if let Some(enum_items) = &schema.enum_items { 221 | let mut enum_def = format!("class {}(str, Enum):\n", type_name); 222 | for item in enum_items { 223 | enum_def += &format!(" {} = '{}'\n", item.name.to_case(Case::UpperSnake), item.name.to_case(Case::UpperSnake)); 224 | } 225 | ctx.append_file(self.name(), &self.dst(ctx), &enum_def); 226 | return type_name; 227 | } 228 | 229 | let mut result = format!("@dataclass\nclass {}:\n", type_name).to_string(); 230 | 231 | let mut required_fields = Vec::new(); 232 | let mut optional_fields = Vec::new(); 233 | if let Some(properties) = &schema.properties { 234 | for (prop_name, prop_schema) in properties { 235 | let snaked_prop_name = prop_name.to_case(Case::Snake); 236 | let mut field = String::new(); 237 | field += " "; 238 | field += &snaked_prop_name; 239 | field += ": "; 240 | 241 | let optional = match prop_schema.required { 242 | Some(req) => !req, 243 | None => false 244 | }; 245 | 246 | let prop_ty = self.generate_struct(ctx, &prop_schema, None, Some(type_name.clone())); 247 | 248 | if optional { 249 | field += &format!("Optional[{}] = None", prop_ty); 250 | 251 | } else { 252 | field += &prop_ty; 253 | } 254 | field += "\n"; 255 | 256 | if optional { 257 | optional_fields.push(field); 258 | } else { 259 | required_fields.push(field); 260 | } 261 | } 262 | } 263 | result += required_fields.join("").as_str(); 264 | result += optional_fields.join("").as_str(); 265 | 266 | 267 | ctx.append_file(self.name(), &self.dst(ctx), &result); 268 | 269 | 270 | 271 | type_name 272 | } 273 | 274 | fn get_gen_option<'a>(&self, ctx: &'a Ctxt) -> Option<&'a PythonGeneratorOption> { 275 | ctx.spec.option.as_ref().and_then(|go| go.generator.as_ref().and_then(|gen| gen.python.as_ref())) 276 | } 277 | 278 | fn dst(&self, ctx: &Ctxt) -> String { 279 | let default_file = "generated.py"; 280 | 281 | match &ctx.spec.option { 282 | Some(go) => { 283 | match &go.generator { 284 | Some(gen) => { 285 | match &gen.python { 286 | Some(gen) => { 287 | let dest_path = get_path_from_optional_parent(gen.def_loc.file.parent(), gen.file.as_ref(), default_file); 288 | return dest_path; 289 | }, 290 | None => default_file.into(), 291 | } 292 | }, 293 | None => { 294 | default_file.into() 295 | }, 296 | } 297 | }, 298 | None => { 299 | default_file.into() 300 | }, 301 | } 302 | 303 | } 304 | } 305 | 306 | 307 | #[cfg(test)] 308 | mod test { 309 | use std::path::PathBuf; 310 | 311 | use cronus_parser::api_parse; 312 | 313 | use crate::{run_generator, Ctxt, Generator}; 314 | use anyhow::{Ok, Result}; 315 | use super::PythonGenerator; 316 | 317 | #[test] 318 | fn py_struct() -> Result<()>{ 319 | let api_file: &'static str = r#" 320 | struct hello { 321 | a: string 322 | } 323 | "#; 324 | 325 | let spec = api_parse::parse(PathBuf::from(""), api_file)?; 326 | let ctx = Ctxt::new(spec, None); 327 | let g = PythonGenerator::new(); 328 | run_generator(&g, &ctx)?; 329 | let gfs = ctx.get_gfs("python"); 330 | let gfs_borrow = gfs.borrow(); 331 | let file_content = gfs_borrow.get("generated.py").unwrap(); 332 | 333 | assert!(file_content.find("a: str").is_some()); 334 | 335 | Ok(()) 336 | } 337 | 338 | #[test] 339 | fn py_async_def() -> Result<()>{ 340 | let api_file: &'static str = r#" 341 | #[@python.async] 342 | usecase User { 343 | createUser {} 344 | } 345 | "#; 346 | 347 | let spec = api_parse::parse(PathBuf::from(""), api_file)?; 348 | let ctx = Ctxt::new(spec, None); 349 | let g = PythonGenerator::new(); 350 | run_generator(&g, &ctx)?; 351 | let gfs = ctx.get_gfs("python"); 352 | let gfs_borrow = gfs.borrow(); 353 | let file_content = gfs_borrow.get("generated.py").unwrap(); 354 | assert!(file_content.find("async def create_user").is_some()); 355 | 356 | Ok(()) 357 | } 358 | 359 | 360 | } -------------------------------------------------------------------------------- /lib/generator/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashSet, path::{Path, PathBuf}}; 2 | 3 | use convert_case::{Casing, Case}; 4 | use cronus_spec::{RawSchema, RawUsecase, RawMethod}; 5 | 6 | use crate::Ctxt; 7 | 8 | 9 | pub fn spec_ty_to_rust_builtin_ty(spec_ty: &str) -> Option { 10 | if spec_ty == "string" { 11 | return Some("String".to_string()); 12 | } 13 | else if spec_ty == "integer" || spec_ty == "int" || spec_ty == "i32" { 14 | return Some("i32".to_string()); 15 | } 16 | else if spec_ty == "u32" { 17 | return Some("u32".to_string()); 18 | } 19 | else if spec_ty == "bool" || spec_ty == "boolean" { 20 | return Some("bool".to_string()); 21 | } 22 | else if spec_ty.starts_with("map<") { 23 | let (left_ty, right_ty) = parse_map_type(spec_ty); 24 | let left = spec_ty_to_rust_builtin_ty(left_ty).unwrap_or(left_ty.to_case(Case::UpperCamel)); 25 | let right = spec_ty_to_rust_builtin_ty(right_ty).unwrap_or(right_ty.to_case(Case::UpperCamel)); 26 | return Some(format!("HashMap<{left},{right}>").to_string()) 27 | } 28 | return None; 29 | } 30 | 31 | pub fn spec_ty_to_java_builtin_ty(spec_ty: &str) -> Option { 32 | if spec_ty == "string" { 33 | return Some("String".to_string()); 34 | } 35 | else if spec_ty == "integer" || spec_ty == "int" || spec_ty == "i32" { 36 | return Some("Integer".to_string()); 37 | } 38 | else if spec_ty == "u32" { 39 | return Some("Long".to_string()); 40 | } 41 | else if spec_ty == "bool" || spec_ty == "boolean" { 42 | return Some("Boolean".to_string()); 43 | } 44 | else if spec_ty.starts_with("map<") { 45 | let (left_ty, right_ty) = parse_map_type(spec_ty); 46 | let left = spec_ty_to_java_builtin_ty(left_ty).unwrap_or(left_ty.to_case(Case::UpperCamel)); 47 | let right = spec_ty_to_java_builtin_ty(right_ty).unwrap_or(right_ty.to_case(Case::UpperCamel)); 48 | return Some(format!("Map<{left},{right}>").to_string()) 49 | } 50 | return None; 51 | } 52 | 53 | pub fn spec_ty_to_golang_builtin_ty(spec_ty: &str) -> Option { 54 | if spec_ty == "string" { 55 | return Some("string".to_string()); 56 | } 57 | else if spec_ty == "integer" || spec_ty == "int" || spec_ty == "i32" { 58 | return Some("int32".to_string()); 59 | } 60 | else if spec_ty == "u32" { 61 | return Some("uint32".to_string()); 62 | } 63 | else if spec_ty == "bool" || spec_ty == "boolean" { 64 | return Some("bool".to_string()); 65 | } 66 | else if spec_ty.starts_with("map<") { 67 | let (left_ty, right_ty) = parse_map_type(spec_ty); 68 | let left = spec_ty_to_golang_builtin_ty(left_ty).unwrap_or(left_ty.to_case(Case::UpperCamel)); 69 | let right = spec_ty_to_golang_builtin_ty(right_ty).unwrap_or(right_ty.to_case(Case::UpperCamel)); 70 | return Some(format!("map[{left}]{right}").to_string()) 71 | } 72 | return None; 73 | } 74 | 75 | pub fn spec_ty_to_py_builtin_ty(spec_ty: &str) -> Option { 76 | if spec_ty == "string" { 77 | return Some("str".to_string()); 78 | } 79 | else if spec_ty == "integer" || spec_ty == "int" || spec_ty == "i32"|| spec_ty == "number" { 80 | return Some("int".to_string()); 81 | } 82 | else if spec_ty == "u32" { 83 | return Some("int".to_string()); 84 | } 85 | else if spec_ty == "bool" || spec_ty == "boolean" { 86 | return Some("bool".to_string()); 87 | } 88 | else if spec_ty == "float" { 89 | return Some("float".to_string()); 90 | } 91 | else if spec_ty.starts_with("map<") { 92 | let (left_ty, right_ty) = parse_map_type(spec_ty); 93 | let left = spec_ty_to_rust_builtin_ty(left_ty).unwrap_or(left_ty.to_case(Case::UpperCamel)); 94 | let right = spec_ty_to_rust_builtin_ty(right_ty).unwrap_or(right_ty.to_case(Case::UpperCamel)); 95 | return Some(format!("dict[{left},{right}]").to_string()) 96 | } 97 | return None; 98 | } 99 | 100 | pub fn parse_map_type(map_ty: &str) -> (&str,&str) { 101 | let child_types:Vec<&str> = map_ty[4..map_ty.len()-1].split(",").collect(); 102 | let left_ty = child_types.get(0).unwrap(); 103 | let right_ty = child_types.get(1).unwrap(); 104 | return (left_ty, right_ty) 105 | } 106 | 107 | 108 | /// valid openapi builtin types: "array", "boolean", "integer", "number", "object", "string" 109 | pub fn spec_ty_to_openapi_builtin_ty(spec_ty: &str) -> Option { 110 | let result = if spec_ty == "string" { 111 | Some("string".to_string()) 112 | } 113 | else if spec_ty == "integer" || spec_ty == "int" || spec_ty == "i32" || spec_ty == "u32" { 114 | Some("integer".to_string()) 115 | } 116 | else if spec_ty == "bool" || spec_ty == "boolean" { 117 | Some("boolean".to_string()) 118 | } else { 119 | None 120 | }; 121 | 122 | result 123 | } 124 | 125 | /// Extract variables from url path like /abcde/:var1/jdf/:var2 => [var1, var2] 126 | pub fn extract_url_variables(url: &str) -> Vec { 127 | let mut variables = Vec::new(); 128 | for segment in url.split('/') { 129 | if segment.starts_with(':') { 130 | variables.push(segment[1..].to_string()); 131 | } 132 | } 133 | variables 134 | } 135 | 136 | pub fn get_path_from_optional_parent(par: Option<&Path>, file:Option<&String>, default_file:&str) -> String { 137 | if par.is_none() { 138 | if let Some(file) = file { 139 | return file.into(); 140 | } 141 | return default_file.into(); 142 | } 143 | let rel_root = par.unwrap(); 144 | if let Some(file) = file { 145 | if PathBuf::from(file).is_absolute() { 146 | return file.clone(); 147 | } 148 | 149 | return rel_root.join(file).to_str().unwrap().to_string(); 150 | 151 | } 152 | 153 | rel_root.join(default_file).to_str().unwrap().to_string() 154 | } 155 | 156 | pub fn get_schema_by_name<'ctx>(ctx: &'ctx Ctxt, ty_name: &str) -> Option<&'ctx RawSchema> { 157 | ctx.spec.ty.as_ref().and_then(|tys| tys.get(ty_name)) 158 | } 159 | 160 | pub fn get_usecase_suffix(ctx: &Ctxt) -> String { 161 | let mut suffix = "Usecase".to_owned(); 162 | if let Some(global_option) = &ctx.spec.option { 163 | if let Some(override_suffix) = &global_option.usecase_suffix { 164 | suffix = override_suffix.to_owned(); 165 | } 166 | 167 | } 168 | 169 | return suffix; 170 | } 171 | 172 | pub fn get_usecase_name(ctx: &Ctxt, usecase_name:&str) -> String { 173 | return ( usecase_name.to_owned() + &get_usecase_suffix(ctx) ).to_case(Case::UpperCamel) 174 | } 175 | 176 | 177 | fn get_request_suffix(ctx: &Ctxt) -> String { 178 | let mut suffix = "Request".to_owned(); 179 | if let Some(global_option) = &ctx.spec.option { 180 | if let Some(override_suffix) = &global_option.usecase_request_suffix { 181 | suffix = override_suffix.to_owned(); 182 | } 183 | 184 | } 185 | 186 | return suffix; 187 | } 188 | 189 | fn get_response_suffix(ctx: &Ctxt) -> String { 190 | let mut suffix = "Response".to_owned(); 191 | if let Some(global_option) = &ctx.spec.option { 192 | if let Some(override_suffix) = &global_option.usecase_response_suffix { 193 | suffix = override_suffix.to_owned(); 194 | } 195 | 196 | } 197 | 198 | return suffix; 199 | } 200 | 201 | pub fn get_response_name(ctx: &Ctxt, method_name:&str) ->String { 202 | return ( method_name.to_owned() + &get_response_suffix(ctx)).to_case(Case::UpperCamel) 203 | } 204 | 205 | pub fn get_request_name(ctx: &Ctxt, method_name:&str) ->String { 206 | return (method_name.to_owned() + &get_request_suffix(ctx)).to_case(Case::UpperCamel) 207 | } 208 | 209 | pub fn get_path_params(method: &RawMethod) -> Option> { 210 | method.option.as_ref().and_then(|option| { 211 | option.rest.as_ref().and_then(|rest|{ 212 | rest.path.as_ref().and_then(|path| { 213 | let vars = extract_url_variables(path); 214 | if vars.is_empty() { 215 | None 216 | } else { 217 | Some(vars.into_iter().collect()) 218 | } 219 | 220 | }) 221 | }) 222 | }) 223 | } 224 | 225 | pub fn get_usecase_rest_path_prefix(usecase: &RawUsecase) -> String { 226 | usecase.option.as_ref() 227 | .and_then(|opt| opt.rest.as_ref()) 228 | .and_then(|rest_opt| rest_opt.path.as_ref()) 229 | .map(|p| if p.starts_with('/') { p.clone() } else { format!("/{}", p) }) 230 | .unwrap_or_else(|| "/".to_string()) 231 | } 232 | 233 | pub fn get_pqb(method: &RawMethod, should_prop_exclude: impl Fn(&RawSchema) -> bool) -> (Option>, Option>, Option>) { 234 | 235 | if method.req.is_none() { 236 | return (None, None, None); 237 | } 238 | 239 | let path_props: Option> = get_path_params(method); 240 | 241 | 242 | let mut query_props: HashSet = HashSet::new(); 243 | let mut body_props: HashSet = HashSet::new(); 244 | 245 | 246 | method.req.as_ref().and_then(|req_schema| { 247 | req_schema.properties.as_ref().and_then(|props| { 248 | for (name, schema) in props.iter() { 249 | // if the property is excluded, skip it 250 | if should_prop_exclude(schema) { 251 | continue; 252 | } 253 | 254 | // if the property is a path parameter, skip it 255 | if path_props.as_ref().map_or(false, |pp| pp.contains(name)) { 256 | continue; 257 | } 258 | let rest_option = method.option.as_ref().and_then(|option| option.rest.as_ref()); 259 | let rest_method: Option<&String> = rest_option.and_then(|rest| Some( &rest.method )); 260 | let mut is_query = rest_method.and_then(|m| Some(m == "get")).unwrap_or(false); 261 | 262 | if !is_query { 263 | is_query = schema.option.as_ref() 264 | .and_then(|opt| opt.rest.as_ref()) 265 | .and_then(|rest| rest.query) 266 | .unwrap_or(false); 267 | } 268 | 269 | if is_query { 270 | query_props.insert(name.clone()); 271 | } else { 272 | body_props.insert(name.clone()); 273 | } 274 | 275 | 276 | } 277 | Some(()) 278 | }) 279 | }); 280 | 281 | return (path_props, Some(query_props), Some(body_props)); 282 | 283 | 284 | 285 | } 286 | 287 | pub fn get_query_params(method: &RawMethod) -> Option> { 288 | method.req.as_ref().and_then(|req_schema| { 289 | req_schema.properties.as_ref() 290 | .and_then(|props| { 291 | let result: HashSet = props.iter() 292 | .filter_map(|(name, schema)| { 293 | // if method is get, then all parameters, except path parameters, are query parameters 294 | match schema.option.as_ref().and_then(|option| { 295 | option.rest.as_ref().and_then(|rest| rest.query) 296 | }) { 297 | Some(query) => Some(name.clone()), 298 | None => None, 299 | } 300 | }) 301 | .collect(); 302 | if result.is_empty() { 303 | None 304 | } else { 305 | Some(result) 306 | } 307 | }) 308 | 309 | }) 310 | } 311 | 312 | #[cfg(test)] 313 | mod tests { 314 | use super::*; 315 | 316 | #[test] 317 | fn test_extract_variables() { 318 | let path = "/people/:name/id/:id"; 319 | let expected = vec!["name".to_string(), "id".to_string()]; 320 | assert_eq!(extract_url_variables(path), expected); 321 | } 322 | 323 | #[test] 324 | fn test_no_variables() { 325 | let path = "/people/name/id"; 326 | let expected: Vec = vec![]; 327 | assert_eq!(extract_url_variables(path), expected); 328 | } 329 | 330 | #[test] 331 | fn test_multiple_variables() { 332 | let path = "/people/:name/:surname/id/:id"; 333 | let expected = vec!["name".to_string(), "surname".to_string(), "id".to_string()]; 334 | assert_eq!(extract_url_variables(path), expected); 335 | } 336 | 337 | #[test] 338 | fn test_empty_path() { 339 | let path = ""; 340 | let expected: Vec = vec![]; 341 | assert_eq!(extract_url_variables(path), expected); 342 | } 343 | 344 | #[test] 345 | fn test_single_variable() { 346 | let path = "/:variable"; 347 | let expected = vec!["variable".to_string()]; 348 | assert_eq!(extract_url_variables(path), expected); 349 | } 350 | } -------------------------------------------------------------------------------- /lib/generator/src/java.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, collections::HashSet, path::{Path, PathBuf}, vec}; 2 | 3 | use convert_case::{Case, Casing}; 4 | use cronus_spec::{RawSchema, JavaGeneratorOption}; 5 | use serde::de; 6 | 7 | use crate::{ 8 | utils::{get_path_from_optional_parent, get_request_name, get_response_name, get_schema_by_name, get_usecase_name, spec_ty_to_java_builtin_ty}, 9 | Ctxt, Generator 10 | }; 11 | use tracing::{debug, span, Level}; 12 | use anyhow::{Ok, Result}; 13 | 14 | pub struct JavaGenerator { 15 | generated_tys: RefCell> 16 | } 17 | 18 | impl JavaGenerator { 19 | pub fn new() -> Self { 20 | Self { 21 | generated_tys: Default::default() 22 | } 23 | } 24 | } 25 | 26 | impl Generator for JavaGenerator { 27 | fn name(&self) -> &'static str { 28 | "java" 29 | } 30 | 31 | fn before_all(&self, ctx: &Ctxt) -> Result<()> { 32 | // TODO: make lombok configurable 33 | 34 | 35 | Ok(()) 36 | } 37 | 38 | fn generate_schema(&self, ctx: &Ctxt, schema_name: &str, schema: &RawSchema) -> Result<()> { 39 | self.generate_class(ctx, schema, Some(schema_name.to_owned()), None); 40 | Ok(()) 41 | } 42 | 43 | fn generate_usecase(&self, ctx: &Ctxt, name: &str, usecase: &cronus_spec::RawUsecase) -> Result<()> { 44 | let span = span!(Level::TRACE, "generate_usecase", "usecase" = name); 45 | let _enter = span.enter(); 46 | 47 | let interface_name = get_usecase_name(ctx, name); 48 | let mut result = String::new(); 49 | 50 | // Add package declaration if specified 51 | if let Some(package) = self.get_package(ctx) { 52 | let package_str = format!("package {};\n\n", package); 53 | result += &package_str; 54 | } 55 | 56 | 57 | // Custom imports 58 | if let Some(java_gen) = self.get_gen_option(ctx) { 59 | if let Some(imports) = &java_gen.imports { 60 | let import_stmts: Vec = imports.iter() 61 | .map(|i| format!("import {};", i)) 62 | .collect(); 63 | let str = import_stmts.join("\n") + "\n\n"; 64 | result += &str; 65 | } 66 | } 67 | 68 | result += &format!("public interface {} {{\n", interface_name); 69 | 70 | for (method_name, method) in &usecase.methods { 71 | result += " "; 72 | 73 | let method_name_java = method_name.to_case(Case::Camel); 74 | 75 | let mut return_type = "void".to_string(); 76 | if let Some(res) = &method.res { 77 | let response_ty = get_response_name(ctx, method_name); 78 | self.generate_class(ctx, res, Some(response_ty.clone()), None); 79 | return_type = response_ty; 80 | } 81 | 82 | result += &format!("{} {}", return_type, method_name_java); 83 | 84 | let mut method_params: Vec = Vec::new(); 85 | 86 | // check generator options for extra parameters 87 | if let Some(java_gen) = self.get_gen_option(ctx) { 88 | if let Some(extra_params) = &java_gen.extra_method_parameters { 89 | for param in extra_params { 90 | method_params.push(param.to_string()); 91 | } 92 | } 93 | } 94 | 95 | 96 | if let Some(req) = &method.req { 97 | let request_ty = get_request_name(ctx, method_name); 98 | self.generate_class(ctx, req, Some(request_ty.clone()), None); 99 | method_params.push(format!("{} request", request_ty)); 100 | } 101 | 102 | result += &format!("({})", method_params.join(", ")); 103 | // Add throws clause if error handling is enabled 104 | if let Some(java_gen) = self.get_gen_option(ctx) { 105 | if let Some(exception_type) = &java_gen.exception_type { 106 | result += &format!(" throws {}", exception_type); 107 | } else if !java_gen.no_exceptions.unwrap_or(false) { 108 | result += " throws Exception"; 109 | } 110 | } else { 111 | result += " throws Exception"; 112 | } 113 | 114 | result += ";\n"; 115 | } 116 | 117 | result += "}\n\n"; 118 | let dst_dir = self.dst_dir(ctx); 119 | let dst_file = PathBuf::from(dst_dir).join(format!("{}.java", interface_name)); 120 | ctx.append_file(self.name(), &dst_file.to_str().unwrap(), &result); 121 | 122 | Ok(()) 123 | } 124 | } 125 | 126 | impl JavaGenerator { 127 | fn generate_class( 128 | &self, 129 | ctx: &Ctxt, 130 | schema: &RawSchema, 131 | override_ty: Option, 132 | root_schema_ty: Option 133 | ) -> String { 134 | let type_name: String; 135 | 136 | // Find out the correct type name 137 | if let Some(ty) = &override_ty { 138 | type_name = ty.to_case(Case::UpperCamel); 139 | } else if schema.items.is_some() { 140 | let item_type = self.generate_class(ctx, schema.items.as_ref().unwrap(), None, root_schema_ty.clone()); 141 | return format!("List<{}>", item_type); 142 | } else { 143 | type_name = schema.ty.as_ref().unwrap().clone(); 144 | } 145 | 146 | let span = span!(Level::TRACE, "generate_class", "type" = type_name); 147 | let _enter = span.enter(); 148 | 149 | // If type name belongs to built-in type, return directly 150 | if let Some(ty) = spec_ty_to_java_builtin_ty(&type_name) { 151 | return ty; 152 | } 153 | 154 | // If the type is excluded from generation, return itself 155 | if let Some(java_gen) = self.get_gen_option(ctx) { 156 | if java_gen.exclude_types.as_ref().map(|e| e.contains(&type_name)).unwrap_or(false) { 157 | return type_name; 158 | } 159 | } 160 | 161 | if self.generated_tys.borrow().contains(&type_name) { 162 | return type_name; 163 | } 164 | 165 | // If it is referenced to a custom type, find and return 166 | if let Some(ref_schema) = get_schema_by_name(ctx, &type_name) { 167 | if schema.properties.is_none() && schema.enum_items.is_none() && schema.items.is_none() { 168 | return self.generate_class(ctx, ref_schema, Some(type_name.to_string()), Some(type_name.to_string())); 169 | } 170 | } 171 | 172 | self.generated_tys.borrow_mut().insert(type_name.clone()); 173 | 174 | let mut result = String::new(); 175 | 176 | let common_imports = vec![ 177 | "import java.util.List;", 178 | "import java.util.Map;", 179 | "import java.util.HashMap;", 180 | "import lombok.Data;", 181 | "import lombok.NoArgsConstructor;", 182 | "import lombok.AllArgsConstructor;", 183 | "import lombok.Builder;", 184 | ]; 185 | let common_imports_str = common_imports.join("\n") + "\n\n"; 186 | 187 | // Add package declaration if specified 188 | if let Some(package) = self.get_package(ctx) { 189 | let package_str = format!("package {};\n\n", package); 190 | result += &package_str; 191 | } 192 | 193 | result += &common_imports_str; 194 | 195 | // Custom imports 196 | if let Some(java_gen) = self.get_gen_option(ctx) { 197 | if let Some(imports) = &java_gen.imports { 198 | let import_stmts: Vec = imports.iter() 199 | .map(|i| format!("import {};", i)) 200 | .collect(); 201 | let str = import_stmts.join("\n") + "\n\n"; 202 | result += &str; 203 | } 204 | } 205 | 206 | 207 | 208 | // Handle enum 209 | if let Some(enum_items) = &schema.enum_items { 210 | result += &format!("public enum {} {{\n", type_name); 211 | for (i, item) in enum_items.iter().enumerate() { 212 | let enum_name = item.name.to_case(Case::ScreamingSnake); 213 | result += &format!(" {}", enum_name); 214 | if let Some(value) = item.value { 215 | result += &format!("({})", value); 216 | } 217 | if i < enum_items.len() - 1 { 218 | result += ","; 219 | } 220 | result += "\n"; 221 | } 222 | 223 | 224 | 225 | result += "}\n\n"; 226 | } else { 227 | // Regular class 228 | 229 | // Add class annotations 230 | let annotations = vec![ 231 | "@Data", 232 | "@NoArgsConstructor", 233 | "@AllArgsConstructor", 234 | "@Builder", 235 | ]; 236 | result += annotations.iter() 237 | .map(|a| format!("{}\n", a)) 238 | .collect::().as_str(); 239 | 240 | result += &format!("public class {} {{\n", type_name); 241 | 242 | if let Some(properties) = &schema.properties { 243 | for (prop_name, prop_schema) in properties { 244 | let java_prop_name = prop_name.to_case(Case::Camel); 245 | 246 | 247 | result += " private "; 248 | 249 | 250 | 251 | let prop_ty = self.generate_class(ctx, prop_schema, None, Some(type_name.clone())); 252 | 253 | result += &format!("{} {};\n", prop_ty, java_prop_name); 254 | } 255 | 256 | result += "\n"; 257 | 258 | 259 | } 260 | 261 | result += "}\n\n"; 262 | } 263 | let dest_dir = self.dst_dir(ctx); 264 | let dst_file = PathBuf::from(dest_dir).join(format!("{}.java", type_name)); 265 | ctx.append_file(self.name(), &dst_file.to_str().unwrap(), &result); 266 | type_name 267 | } 268 | 269 | fn get_gen_option<'a>(&self, ctx: &'a Ctxt) -> Option<&'a JavaGeneratorOption> { 270 | ctx.spec.option.as_ref() 271 | .and_then(|go| go.generator.as_ref()) 272 | .and_then(|gen| gen.java.as_ref()) 273 | } 274 | 275 | fn get_package(&self, ctx: &Ctxt) -> Option { 276 | self.get_gen_option(ctx) 277 | .and_then(|java_gen| java_gen.package.clone()) 278 | } 279 | 280 | fn dst_dir(&self, ctx: &Ctxt) -> String { 281 | let default_dir = "."; 282 | 283 | match &ctx.spec.option { 284 | Some(go) => { 285 | match &go.generator { 286 | Some(gen) => { 287 | match &gen.java { 288 | Some(java_gen) => { 289 | get_path_from_optional_parent( 290 | java_gen.def_loc.file.parent(), 291 | java_gen.dir.as_ref(), 292 | default_dir 293 | ) 294 | }, 295 | None => default_dir.into(), 296 | } 297 | }, 298 | None => default_dir.into(), 299 | } 300 | }, 301 | None => default_dir.into(), 302 | } 303 | } 304 | } 305 | 306 | #[cfg(test)] 307 | mod test { 308 | use std::path::PathBuf; 309 | use cronus_parser::api_parse; 310 | use crate::{run_generator, Ctxt, Generator}; 311 | use anyhow::{Ok, Result}; 312 | use super::JavaGenerator; 313 | 314 | #[test] 315 | fn custom_class() -> Result<()> { 316 | let api_file: &'static str = r#" 317 | struct Hello { 318 | a: string 319 | } 320 | "#; 321 | 322 | let spec = api_parse::parse(PathBuf::from(""), api_file)?; 323 | let ctx = Ctxt::new(spec, None); 324 | let g = JavaGenerator::new(); 325 | run_generator(&g, &ctx)?; 326 | let gfs = ctx.get_gfs("java"); 327 | let gfs_borrow = gfs.borrow(); 328 | let file_content = gfs_borrow.get("Types.java").unwrap(); 329 | 330 | assert!(file_content.contains("public class Hello")); 331 | assert!(file_content.contains("String a;")); 332 | assert!(file_content.contains("public String getA()")); 333 | assert!(file_content.contains("public void setA(String a)")); 334 | 335 | Ok(()) 336 | } 337 | 338 | #[test] 339 | fn array_type() -> Result<()> { 340 | let api_file: &'static str = r#" 341 | struct Hello { 342 | items: string[] 343 | } 344 | "#; 345 | 346 | let spec = api_parse::parse(PathBuf::from(""), api_file)?; 347 | let ctx = Ctxt::new(spec, None); 348 | let g = JavaGenerator::new(); 349 | run_generator(&g, &ctx)?; 350 | let gfs = ctx.get_gfs("java"); 351 | let gfs_borrow = gfs.borrow(); 352 | let file_content = gfs_borrow.get("Types.java").unwrap(); 353 | 354 | assert!(file_content.contains("List items;")); 355 | 356 | Ok(()) 357 | } 358 | } -------------------------------------------------------------------------------- /lib/generator/src/java_springweb.rs: -------------------------------------------------------------------------------- 1 | use anyhow::bail; 2 | use convert_case::{Case, Casing}; 3 | use cronus_spec::{ 4 | JavaSpringWebGeneratorOption, RawSchema, RawMethod, 5 | RawMethodRestOption, 6 | }; 7 | use std::{any::type_name, cell::RefCell, collections::HashSet, fmt::format, path::PathBuf}; 8 | 9 | use crate::{ 10 | utils::{ 11 | self, get_path_from_optional_parent, get_request_name, get_response_name, 12 | get_schema_by_name, get_usecase_name, spec_ty_to_java_builtin_ty, 13 | }, 14 | Ctxt, Generator, 15 | }; 16 | use anyhow::{Ok, Result}; 17 | use tracing::{self, debug, span, Level}; 18 | 19 | pub struct JavaSpringWebGenerator {} 20 | 21 | impl JavaSpringWebGenerator { 22 | pub fn new() -> Self { 23 | Self {} 24 | } 25 | } 26 | 27 | impl Generator for JavaSpringWebGenerator { 28 | fn name(&self) -> &'static str { 29 | "java_springweb" 30 | } 31 | 32 | 33 | /// Generate the Spring MVC controller for the given usecase. 34 | fn generate_usecase( 35 | &self, 36 | ctx: &Ctxt, 37 | name: &str, 38 | usecase: &cronus_spec::RawUsecase, 39 | ) -> Result<()> { 40 | let span = span!(Level::TRACE, "generate_usecase", "usecase" = name); 41 | let _enter = span.enter(); 42 | 43 | let has_rest_methods = usecase.methods.iter().any(|(_, method)| { 44 | method 45 | .option 46 | .as_ref() 47 | .and_then(|option| option.rest.as_ref()) 48 | .is_some() 49 | }); 50 | 51 | if !has_rest_methods { 52 | return Ok(()); 53 | } 54 | 55 | let full_usecase_name = get_usecase_name(ctx, name); 56 | 57 | 58 | let mut result = String::new(); 59 | 60 | // Add package declaration 61 | let gen_opt = self.get_gen_option(ctx); 62 | let pkg_name = gen_opt 63 | .and_then(|opt| opt.package.as_ref()) 64 | .ok_or_else(|| anyhow::anyhow!("java_springweb package option is not set"))?; 65 | 66 | result += &format!("package {};\n\n", pkg_name); 67 | 68 | // Add imports 69 | result += &common_imports(); 70 | result += "\n"; 71 | 72 | 73 | // Add business logic imports 74 | let domain_import = gen_opt 75 | .and_then(|opt| opt.domain_import.as_ref()) 76 | .ok_or_else(|| anyhow::anyhow!("java_springweb domain_import option is not set"))?; 77 | 78 | if !domain_import.is_empty() { 79 | result += &format!("import {}.*;\n\n", domain_import); 80 | } 81 | 82 | // Add extra imports 83 | let mut extra_imports = Vec::new(); 84 | if let Some(extra_imports_opt) = gen_opt 85 | .and_then(|opt| opt.extra_imports.as_ref()) 86 | { 87 | extra_imports.extend(extra_imports_opt.iter().cloned()); 88 | } 89 | 90 | for import in extra_imports { 91 | result += &format!("import {};\n", import); 92 | } 93 | 94 | 95 | 96 | 97 | let path_prefix = usecase 98 | .option 99 | .as_ref() 100 | .and_then(|usecase_opt| usecase_opt.rest.as_ref()) 101 | .and_then(|rest| rest.path.as_ref()) 102 | .cloned() 103 | .unwrap_or_default(); 104 | 105 | let controller_name = format!("{}Controller", full_usecase_name); 106 | let service_field = format!("{}Service", name.to_case(Case::Camel)); 107 | 108 | result += "@RestController\n"; 109 | if !path_prefix.is_empty() { 110 | result += &format!("@RequestMapping(\"{}\")\n", path_prefix); 111 | } 112 | result += &format!("public class {} {{\n\n", controller_name); 113 | 114 | // Add service field 115 | result += " @Autowired\n"; 116 | result += &format!(" private {} {};\n\n", full_usecase_name, service_field); 117 | 118 | for (method_name, method) in &usecase.methods { 119 | let rest = match method.option { 120 | Some(ref option) => { 121 | if let Some(rest) = &option.rest { 122 | rest 123 | } else { 124 | continue; 125 | } 126 | } 127 | None => continue, 128 | }; 129 | 130 | result += &self.gen_controller_method(ctx, &service_field, &method_name, method)?; 131 | result += "\n"; 132 | } 133 | 134 | result += "}\n"; 135 | let dest_dir = self.dst_dir(ctx); 136 | let dst_file = PathBuf::from(dest_dir).join(format!("{}.java", controller_name)); 137 | ctx.append_file(self.name(), &dst_file.to_str().unwrap(), &result); 138 | 139 | Ok(()) 140 | } 141 | } 142 | 143 | impl JavaSpringWebGenerator { 144 | fn gen_controller_method( 145 | &self, 146 | ctx: &Ctxt, 147 | service_field: &str, 148 | method_name: &str, 149 | method: &RawMethod, 150 | ) -> Result { 151 | let mut dto_result = String::new(); 152 | let mut result = String::new(); 153 | let rest = method 154 | .option 155 | .as_ref() 156 | .and_then(|option| option.rest.as_ref()) 157 | .ok_or_else(|| anyhow::anyhow!("No rest option for method {}", method_name))?; 158 | 159 | let binding = String::new(); 160 | let rest_path = rest.path.as_ref().unwrap_or(&binding); 161 | let http_method = rest.method.to_uppercase(); 162 | let java_method_name = method_name.to_case(Case::Camel); 163 | 164 | let (path_params, query_params, body_params) = utils::get_pqb(method, |prop| { 165 | prop.option.as_ref() 166 | .and_then(|o| o.java_springweb.as_ref()) 167 | .and_then(|j| j.exclude) 168 | .unwrap_or(false) 169 | }); 170 | let is_multipart = method 171 | .option 172 | .as_ref() 173 | .and_then(|opt| opt.rest.as_ref()) 174 | .and_then(|rest_opt| rest_opt.content_type.as_ref()) 175 | .and_then(|ct| Some(ct == "multipart/form-data")) 176 | .unwrap_or(false); 177 | 178 | // Add Spring mapping annotation 179 | match http_method.as_str() { 180 | "GET" => result += &format!(" @GetMapping(\"{}\")\n", rest_path), 181 | "POST" => result += &format!(" @PostMapping(\"{}\")\n", rest_path), 182 | "PUT" => result += &format!(" @PutMapping(\"{}\")\n", rest_path), 183 | "DELETE" => result += &format!(" @DeleteMapping(\"{}\")\n", rest_path), 184 | "PATCH" => result += &format!(" @PatchMapping(\"{}\")\n", rest_path), 185 | _ => result += &format!(" @RequestMapping(value = \"{}\", method = RequestMethod.{})\n", rest_path, http_method), 186 | } 187 | 188 | // Method signature 189 | let return_type = if method.res.is_some() { 190 | let response_ty = get_response_name(ctx, method_name); 191 | response_ty 192 | } else { 193 | "void".to_string() 194 | }; 195 | 196 | result += &format!(" public {} {}(", return_type, java_method_name); 197 | 198 | let mut method_params = Vec::new(); 199 | 200 | // handle extra parameters 201 | if let Some(extra_params) = self.get_gen_option(ctx) 202 | .and_then(|opt| opt.extra_method_parameters.as_ref()) 203 | { 204 | for param in extra_params { 205 | method_params.push(param.clone()); 206 | } 207 | } 208 | 209 | if let Some(req) = &method.req { 210 | // Handle path parameters 211 | if let Some(path_params) = &path_params { 212 | for param in path_params { 213 | let prop_schema = req.properties.as_ref().unwrap().get(param).unwrap(); 214 | let param_type = self.get_java_type(prop_schema)?; 215 | method_params.push(format!("@PathVariable {} {}", param_type, param.to_case(Case::Camel))); 216 | } 217 | } 218 | 219 | // Handle query parameters 220 | if let Some(query_params) = &query_params { 221 | for param in query_params { 222 | let prop_schema = req.properties.as_ref().unwrap().get(param).unwrap(); 223 | let param_type = self.get_java_type(prop_schema)?; 224 | let required = prop_schema.required.unwrap_or(false); 225 | if required { 226 | method_params.push(format!("@RequestParam {} {}", param_type, param.to_case(Case::Camel))); 227 | } else { 228 | method_params.push(format!("@RequestParam(required = false) {} {}", param_type, param.to_case(Case::Camel))); 229 | } 230 | } 231 | } 232 | 233 | // Handle request body 234 | if let Some(body_params) = &body_params { 235 | if !body_params.is_empty() { 236 | if is_multipart { 237 | // Handle multipart form data 238 | for param in body_params { 239 | let prop_schema = req.properties.as_ref().unwrap().get(param).unwrap(); 240 | let param_type = self.get_java_type(prop_schema)?; 241 | method_params.push(format!("@RequestParam {} {}", param_type, param.to_case(Case::Camel))); 242 | } 243 | } else { 244 | // Handle JSON request body 245 | let (dto_name, dto_decl) = self.gen_body_dto(&method_name, method.req.as_ref().unwrap(), body_params)?; 246 | dto_result += &dto_decl; 247 | method_params.push(format!("@RequestBody {} body", dto_name)); 248 | } 249 | } 250 | } 251 | } 252 | 253 | result += &method_params.join(", "); 254 | result += ") throws Exception {\n"; 255 | 256 | // Method body 257 | 258 | // Prepare domain request if needed 259 | if let Some(req) = &method.req { 260 | let request_ty = get_request_name(ctx, method_name); 261 | 262 | 263 | // Build request object from parameters 264 | result += &format!(" {} request = new {}();\n", request_ty, request_ty); 265 | 266 | for (prop_name, prop_schema) in req.properties.as_ref().unwrap() { 267 | // Skip properties that are excluded 268 | if prop_schema.option.as_ref() 269 | .and_then(|o| o.java_springweb.as_ref()) 270 | .and_then(|j| j.exclude) 271 | .unwrap_or(false) { 272 | continue; 273 | } 274 | 275 | 276 | let java_prop_name = prop_name.to_case(Case::Camel); 277 | let setter_name = format!("set{}", prop_name.to_case(Case::UpperCamel)); 278 | if body_params.as_ref().is_some_and(|bp| bp.contains(prop_name)) { 279 | // If it's a body parameter, use the body object 280 | result += &format!(" request.{}(body.get{}());\n", setter_name, prop_name.to_case(Case::UpperCamel)); 281 | } else { 282 | result += &format!(" request.{}({});\n", setter_name, java_prop_name); 283 | 284 | } 285 | } 286 | 287 | 288 | // Add extra request statements 289 | let mut extra_stmts: Vec = Vec::new(); 290 | if let Some(extra_request_statements) = self 291 | .get_gen_option(ctx) 292 | .as_ref() 293 | .and_then(|opt| opt.extra_request_statements.as_ref()) 294 | { 295 | extra_stmts.extend(extra_request_statements.iter().cloned()); 296 | } 297 | 298 | 299 | for stmt in extra_stmts { 300 | result += &format!(" {};\n", stmt); 301 | } 302 | } 303 | 304 | // Call service method 305 | let service_method_name = method_name.to_case(Case::Camel); 306 | if method.res.is_some() { 307 | let response_ty = get_response_name(ctx, method_name); 308 | result += &format!(" {} response = {}.{}(", response_ty, service_field, service_method_name); 309 | } else { 310 | result += &format!(" {}.{}(", service_field, service_method_name); 311 | } 312 | 313 | if method.req.is_some() { 314 | result += "request"; 315 | } 316 | result += ");\n"; 317 | 318 | // Return response 319 | if method.res.is_some() { 320 | result += " return response;\n"; 321 | } 322 | 323 | result += " }\n"; 324 | 325 | Ok(dto_result + &result) 326 | } 327 | 328 | fn gen_body_dto( 329 | &self, 330 | method_name: &str, 331 | schema: &RawSchema, 332 | props: &HashSet, 333 | ) -> Result<(String, String)> { 334 | let dto_name = (method_name.to_owned() + "BodyDto").to_case(Case::UpperCamel); 335 | 336 | let mut result = String::new(); 337 | let annotations = vec![ 338 | "@Data", // Lombok annotation for getters/setters 339 | "@AllArgsConstructor", // Lombok annotation for all-args constructor 340 | "@NoArgsConstructor", // Lombok annotation for no-args constructor 341 | ]; 342 | for annotation in annotations { 343 | result += &format!("{}\n", annotation); 344 | } 345 | result += &format!("public class {} {{\n", dto_name); 346 | for prop in props { 347 | if let Some(prop_schema) = schema.properties.as_ref().and_then(|props| props.get(prop)) { 348 | let java_type = self.get_java_type(prop_schema)?; 349 | let java_prop_name = prop.to_case(Case::Camel); 350 | result += &format!(" private {} {};\n", java_type, java_prop_name); 351 | } else { 352 | bail!("Property {} not found in schema", prop); 353 | } 354 | } 355 | result += "\n}\n"; 356 | 357 | 358 | return Ok((dto_name, result)); 359 | } 360 | 361 | fn get_java_type(&self, prop_schema: &RawSchema) -> Result { 362 | if let Some(ty) = prop_schema.ty.as_ref() { 363 | if let Some(builtin_ty) = spec_ty_to_java_builtin_ty(ty) { 364 | Ok(builtin_ty) 365 | } else { 366 | Ok(ty.to_case(Case::UpperCamel)) 367 | } 368 | } else if prop_schema.items.is_some() { 369 | let item_type = self.get_java_type(prop_schema.items.as_ref().unwrap())?; 370 | Ok(format!("List<{}>", item_type)) 371 | } else { 372 | bail!("Cannot determine Java type for property") 373 | } 374 | } 375 | 376 | fn get_gen_option<'a>(&self, ctx: &'a Ctxt) -> Option<&'a JavaSpringWebGeneratorOption> { 377 | ctx.spec.option.as_ref().and_then(|go| { 378 | go.generator 379 | .as_ref() 380 | .and_then(|gen| gen.java_springweb.as_ref()) 381 | }) 382 | } 383 | 384 | fn dst_dir(&self, ctx: &Ctxt) -> String { 385 | let default_dir = "."; 386 | 387 | self.get_gen_option(ctx) 388 | .and_then(|gen| { 389 | Some(get_path_from_optional_parent( 390 | gen.def_loc.file.parent(), 391 | gen.dir.as_ref(), 392 | default_dir, 393 | )) 394 | }) 395 | .unwrap_or_else(|| default_dir.into()) 396 | } 397 | } 398 | 399 | fn common_imports() -> String { 400 | let imports = vec![ 401 | "org.springframework.web.bind.annotation.*", 402 | "org.springframework.beans.factory.annotation.Autowired", 403 | "lombok.Data", 404 | "lombok.NoArgsConstructor", 405 | "lombok.AllArgsConstructor", 406 | "java.util.List", 407 | "java.util.Map", 408 | ]; 409 | imports 410 | .iter() 411 | .map(|import| format!("import {};", import)) 412 | .collect::>() 413 | .join("\n") 414 | } -------------------------------------------------------------------------------- /lib/generator/src/rust.rs: -------------------------------------------------------------------------------- 1 | use std::{any::type_name, cell::RefCell, collections::HashSet, fmt::format, path::PathBuf}; 2 | 3 | use convert_case::{Case, Casing}; 4 | use cronus_spec::{RawSchema, RustGeneratorOption}; 5 | 6 | use crate::{ 7 | utils::{self, get_path_from_optional_parent, get_request_name, get_response_name, get_schema_by_name, get_usecase_name, spec_ty_to_rust_builtin_ty}, Ctxt, Generator 8 | }; 9 | use tracing::{self, debug, span, Level}; 10 | use anyhow::{Ok, Result}; 11 | 12 | pub struct RustGenerator { 13 | generated_tys: RefCell> 14 | } 15 | 16 | 17 | impl RustGenerator { 18 | pub fn new() -> Self { 19 | Self { 20 | generated_tys: Default::default() 21 | } 22 | } 23 | } 24 | 25 | impl Generator for RustGenerator { 26 | fn name(&self) -> &'static str { 27 | "rust" 28 | } 29 | 30 | fn before_all(&self, ctx: &Ctxt) -> Result<()> { 31 | 32 | let common_uses = vec!["use serde::{Deserialize, Serialize};","use async_trait::async_trait;"]; 33 | let common_uses_str = common_uses.join("\n") + "\n"; 34 | ctx.append_file(self.name(), &self.dst(ctx), &common_uses_str); 35 | 36 | // custom uses 37 | match self.get_gen_option(ctx) { 38 | Some(rust_gen) => { 39 | match &rust_gen.uses { 40 | Some(uses) => { 41 | let use_stmts:Vec = uses.iter().map(|u| format!("use {};", u).to_string()).collect(); 42 | 43 | let str = use_stmts.join("\n") + "\n"; 44 | ctx.append_file(self.name(), &self.dst(ctx), &str); 45 | 46 | }, 47 | None => {}, 48 | } 49 | }, 50 | None => {}, 51 | } 52 | 53 | Ok(()) 54 | 55 | } 56 | 57 | fn generate_schema(&self, ctx: &Ctxt, schema_name:&str, schema: &RawSchema) -> Result<()> { 58 | self.generate_struct(ctx, schema, Some(schema_name.to_owned()), None); 59 | Ok(()) 60 | } 61 | 62 | 63 | /// Generate the Rust trait for the usecase 64 | /// 65 | /// trait { 66 | /// fn (&self, request) -> response; 67 | /// } 68 | /// 69 | fn generate_usecase(&self, ctx: &Ctxt, name: &str, usecase: &cronus_spec::RawUsecase) -> Result<()> { 70 | let span = span!(Level::TRACE, "generate_usecase", "usecase" = name); 71 | // Enter the span, returning a guard object. 72 | let _enter = span.enter(); 73 | let trait_name = get_usecase_name(ctx, name); 74 | // TODO: customized error type 75 | let default_error_ty: &str = "Box"; 76 | let mut result = String::new(); 77 | 78 | // handle async trait 79 | match self.get_gen_option(ctx) { 80 | Some(rust_gen) => { 81 | match rust_gen.async_trait { 82 | Some(flag) => { 83 | if flag { 84 | result += "#[async_trait]\n"; 85 | } 86 | }, 87 | _ => {} 88 | } 89 | }, 90 | _ => {} 91 | } 92 | result += &format!("pub trait {} {{\n", trait_name); 93 | for (method_name, method) in &usecase.methods { 94 | 95 | // handle async fn 96 | match self.get_gen_option(ctx) { 97 | Some(rust_gen) => { 98 | match rust_gen.async_flag { 99 | Some(flag) => { 100 | if flag { 101 | result += " async"; 102 | } 103 | }, 104 | _ => {} 105 | } 106 | }, 107 | _ => {} 108 | } 109 | result += " fn "; 110 | result += &method_name.to_case(Case::Snake); 111 | result += "(&self"; 112 | 113 | if let Some(req) = &method.req { 114 | let request_ty = get_request_name(ctx, method_name); 115 | self.generate_struct(ctx, &req, Some(request_ty.clone()), None); 116 | result += ", request: "; 117 | result += &request_ty; 118 | } 119 | result += ")"; 120 | 121 | let mut result_t_type: String = "()".to_string(); 122 | let mut result_f_type: Option = Some(default_error_ty.to_string()); 123 | 124 | if let Some(res) = &method.res { 125 | let response_ty = get_response_name(ctx, method_name); 126 | self.generate_struct(ctx, &res, Some(response_ty.clone()), None); 127 | result_t_type = response_ty; 128 | } 129 | 130 | // handle result false type 131 | match self.get_gen_option(ctx) { 132 | Some(rust_gen) => { 133 | if let Some(no_error_type) = rust_gen.no_error_type { 134 | if no_error_type { 135 | result_f_type = None 136 | } 137 | } 138 | else if let Some(error_type) = &rust_gen.error_type { 139 | result_f_type = Some(error_type.clone()) 140 | } 141 | 142 | }, 143 | _ => {} 144 | } 145 | 146 | if result_f_type.is_some() { 147 | result += &format!(" -> Result<{}, {}>", result_t_type, result_f_type.unwrap()); 148 | } else { 149 | result += &format!(" -> Result<{}>", result_t_type); 150 | } 151 | 152 | result += ";\n"; 153 | } 154 | result += "}\n"; 155 | 156 | ctx.append_file(self.name(), &self.dst(ctx), &result); 157 | 158 | Ok(()) 159 | } 160 | 161 | 162 | 163 | 164 | 165 | } 166 | 167 | impl RustGenerator { 168 | 169 | 170 | 171 | 172 | /// Generate the Rust struct definition 173 | /// 174 | fn generate_struct( 175 | &self, 176 | ctx: &Ctxt, 177 | schema: &RawSchema, 178 | override_ty: Option, 179 | root_schema_ty: Option 180 | ) -> String { 181 | let type_name: String; 182 | 183 | // find out the correct type name 184 | if let Some(ty) = &override_ty { 185 | type_name = ty.to_case(Case::UpperCamel); 186 | } 187 | else if schema.items.is_some() { 188 | 189 | type_name = self.generate_struct(ctx, schema.items.as_ref().unwrap(), None, root_schema_ty.clone()); 190 | 191 | return format!("Vec<{}>", type_name).to_owned() 192 | } 193 | else { 194 | type_name = schema.ty.as_ref().unwrap().clone(); 195 | } 196 | 197 | 198 | 199 | 200 | let span = span!(Level::TRACE, "generate_struct", "type" = type_name); 201 | // Enter the span, returning a guard object. 202 | let _enter = span.enter(); 203 | 204 | // if type name belongs to built-in type, return directly 205 | if let Some(ty) = spec_ty_to_rust_builtin_ty(&type_name) { 206 | return ty; 207 | } 208 | 209 | if self.generated_tys.borrow().contains(&type_name) { 210 | if let Some(root_schema_ty) = root_schema_ty { 211 | if root_schema_ty == type_name { 212 | return format!("Box<{type_name}>") 213 | } 214 | } 215 | return type_name; 216 | } 217 | 218 | 219 | 220 | // if it is referenced to a custom type, find and return 221 | if let Some(ref_schema) = get_schema_by_name(&ctx, &type_name) { 222 | // check whether schema is a type referencing another user type 223 | if schema.properties.is_none() && schema.enum_items.is_none() && schema.items.is_none() { 224 | return self.generate_struct(ctx, ref_schema, Some(type_name.to_string()), Some(type_name.to_string())); 225 | } 226 | } 227 | let mut attrs: Vec = vec![]; 228 | 229 | if let Some(gen_opt) = self.get_gen_option(ctx) { 230 | let default_derive = match &gen_opt.default_derive { 231 | Some(default_derive) => default_derive.clone(), 232 | None => vec!["Debug", "Clone", "Serialize", "Deserialize", "PartialEq", "Eq"].iter().map(|s|s.to_string()).collect(), 233 | }; 234 | 235 | let no_default_derive = match gen_opt.no_default_derive { 236 | Some(no_default) => { 237 | no_default 238 | }, 239 | None => { false }, // default value is false 240 | }; 241 | 242 | if !no_default_derive { 243 | let derive_attr = format!("#[derive({})]", default_derive.join(", ")); 244 | attrs.push(derive_attr); 245 | 246 | } 247 | } 248 | 249 | 250 | 251 | match &schema.option { 252 | Some(option) => { 253 | match &option.rust { 254 | Some(rust_opt) => { 255 | 256 | match &rust_opt.attrs { 257 | Some(custom_attrs) => { 258 | attrs.extend(custom_attrs.iter().map(|attr| format!("#[{}]", attr).to_string()) ); 259 | }, 260 | None => {}, 261 | } 262 | }, 263 | None => {} 264 | } 265 | }, 266 | None => {}, 267 | } 268 | self.generated_tys.borrow_mut().insert(type_name.clone()); 269 | 270 | let mut result = format!("{}\npub struct {} {{\n", attrs.join("\n"), type_name).to_string(); 271 | 272 | 273 | if let Some(properties) = &schema.properties { 274 | for (prop_name, prop_schema) in properties { 275 | 276 | let mut attrs: Vec = vec![]; 277 | match &prop_schema.option { 278 | Some(option) => { 279 | match &option.rust { 280 | Some(rust_opt) => { 281 | match &rust_opt.attrs { 282 | Some(custom_attrs) => { 283 | attrs.extend(custom_attrs.iter().map(|attr| format!("#[{}]", attr).to_string()) ); 284 | }, 285 | None => {}, 286 | } 287 | }, 288 | None => {} 289 | } 290 | }, 291 | None => {}, 292 | } 293 | 294 | if !attrs.is_empty() { 295 | result += &format!(" {}\n", attrs.join("\n")); 296 | } 297 | 298 | result += " pub "; 299 | result += prop_name; 300 | result += ": "; 301 | 302 | let optional = match prop_schema.required { 303 | Some(req) => !req, 304 | None => false 305 | }; 306 | 307 | let prop_ty = self.generate_struct(ctx, &prop_schema, None, Some(type_name.clone())); 308 | 309 | if optional { 310 | result += &format!("Option<{}>", prop_ty); 311 | 312 | } else { 313 | result += &prop_ty; 314 | } 315 | result += ",\n"; 316 | } 317 | } 318 | 319 | result += "}\n"; 320 | ctx.append_file(self.name(), &self.dst(ctx), &result); 321 | 322 | 323 | 324 | type_name 325 | } 326 | 327 | fn get_gen_option<'a>(&self, ctx: &'a Ctxt) -> Option<&'a RustGeneratorOption> { 328 | ctx.spec.option.as_ref().and_then(|go| go.generator.as_ref().and_then(|gen| gen.rust.as_ref())) 329 | } 330 | 331 | fn dst(&self, ctx: &Ctxt) -> String { 332 | let default_file = "types.rs"; 333 | 334 | match &ctx.spec.option { 335 | Some(go) => { 336 | match &go.generator { 337 | Some(gen) => { 338 | match &gen.rust { 339 | Some(rust_gen) => { 340 | get_path_from_optional_parent(rust_gen.def_loc.file.parent(), rust_gen.file.as_ref(), default_file) 341 | }, 342 | None => default_file.into(), 343 | } 344 | }, 345 | None => { 346 | default_file.into() 347 | }, 348 | } 349 | }, 350 | None => { 351 | default_file.into() 352 | }, 353 | } 354 | 355 | } 356 | } 357 | 358 | 359 | #[cfg(test)] 360 | mod test { 361 | use std::path::PathBuf; 362 | 363 | use cronus_parser::api_parse; 364 | 365 | use crate::{run_generator, Ctxt, Generator}; 366 | use anyhow::{Ok, Result}; 367 | use super::RustGenerator; 368 | 369 | #[test] 370 | fn custom_struct() -> Result<()>{ 371 | let api_file: &'static str = r#" 372 | struct hello { 373 | a: string 374 | } 375 | "#; 376 | 377 | let spec = api_parse::parse(PathBuf::from(""), api_file)?; 378 | let ctx = Ctxt::new(spec, None); 379 | let g = RustGenerator::new(); 380 | run_generator(&g, &ctx)?; 381 | let gfs = ctx.get_gfs("rust"); 382 | let gfs_borrow = gfs.borrow(); 383 | let file_content = gfs_borrow.get("types.rs").unwrap(); 384 | 385 | assert!(file_content.find("a: String").is_some()); 386 | 387 | Ok(()) 388 | } 389 | 390 | #[test] 391 | fn ref_custom_struct() -> Result<()>{ 392 | let api_file: &'static str = r#" 393 | struct Hello { 394 | a: Hello1 395 | } 396 | 397 | struct Hello1 { 398 | b: string 399 | } 400 | "#; 401 | 402 | let spec = api_parse::parse(PathBuf::from(""), api_file)?; 403 | 404 | let ctx = Ctxt::new(spec, None); 405 | let g = RustGenerator::new(); 406 | run_generator(&g, &ctx)?; 407 | let gfs = ctx.get_gfs("rust"); 408 | let gfs_borrow = gfs.borrow(); 409 | let file_content = gfs_borrow.get("types.rs").unwrap(); 410 | 411 | assert!(file_content.find("a: Hello1").is_some()); 412 | assert!(file_content.find("b: String").is_some()); 413 | 414 | Ok(()) 415 | } 416 | 417 | #[test] 418 | fn array_ty() -> Result<()>{ 419 | let api_file: &'static str = r#" 420 | struct hello { 421 | a: string[] 422 | } 423 | "#; 424 | 425 | let spec = api_parse::parse(PathBuf::from(""), api_file)?; 426 | let ctx = Ctxt::new(spec, None); 427 | let g = RustGenerator::new(); 428 | run_generator(&g, &ctx)?; 429 | let gfs = ctx.get_gfs("rust"); 430 | let gfs_borrow = gfs.borrow(); 431 | let file_content = gfs_borrow.get("types.rs").unwrap(); 432 | 433 | assert!(file_content.find("a: Vec").is_some()); 434 | 435 | Ok(()) 436 | } 437 | 438 | #[test] 439 | fn map_ty() -> Result<()>{ 440 | let api_file: &'static str = r#" 441 | struct hello { 442 | a: map 443 | } 444 | "#; 445 | 446 | let spec = api_parse::parse(PathBuf::from(""), api_file)?; 447 | let ctx = Ctxt::new(spec, None); 448 | let g = RustGenerator::new(); 449 | run_generator(&g, &ctx)?; 450 | let gfs = ctx.get_gfs("rust"); 451 | let gfs_borrow = gfs.borrow(); 452 | let file_content = gfs_borrow.get("types.rs").unwrap(); 453 | 454 | assert!(file_content.find("a: HashMap").is_some()); 455 | 456 | Ok(()) 457 | } 458 | 459 | #[test] 460 | fn map_custom_ty() -> Result<()>{ 461 | let api_file: &'static str = r#" 462 | struct world { 463 | b: string 464 | } 465 | struct hello { 466 | a: map 467 | } 468 | "#; 469 | 470 | let spec = api_parse::parse(PathBuf::from(""), api_file)?; 471 | let ctx = Ctxt::new(spec, None); 472 | let g = RustGenerator::new(); 473 | run_generator(&g, &ctx)?; 474 | let gfs = ctx.get_gfs("rust"); 475 | let gfs_borrow = gfs.borrow(); 476 | let file_content = gfs_borrow.get("types.rs").unwrap(); 477 | 478 | assert!(file_content.find("a: HashMap").is_some()); 479 | 480 | Ok(()) 481 | } 482 | 483 | #[test] 484 | fn custom_uses() -> Result<()>{ 485 | let api_file: &'static str = r#" 486 | global [generator.rust.uses = ("anyhow::Result")] 487 | 488 | usecase abc { 489 | create_abcd { 490 | in { 491 | a: string 492 | } 493 | out { 494 | b: string 495 | } 496 | } 497 | } 498 | "#; 499 | 500 | let spec = api_parse::parse(PathBuf::from(""), api_file)?; 501 | let ctx = Ctxt::new(spec, None); 502 | let g = RustGenerator::new(); 503 | run_generator(&g, &ctx)?; 504 | let gfs = ctx.get_gfs("rust"); 505 | let gfs_borrow = gfs.borrow(); 506 | let file_content = gfs_borrow.get("types.rs").unwrap(); 507 | assert!(file_content.find("use anyhow::Result;").is_some()); 508 | assert!(file_content.find("struct CreateAbcdRequest").is_some()); 509 | assert!(file_content.find("a: String").is_some()); 510 | Ok(()) 511 | 512 | } 513 | } -------------------------------------------------------------------------------- /lib/generator/src/rust_axum.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, collections::HashMap, hash::Hash, path::PathBuf}; 2 | 3 | use anyhow::{ bail, Ok, Result}; 4 | use convert_case::{Case, Casing}; 5 | use cronus_spec::{RawUsecase, RawMethod, RawMethodRestOption, RustAxumGeneratorOption}; 6 | use tracing::{span, Level}; 7 | 8 | use crate::{utils::{self, get_path_from_optional_parent, get_request_name, get_usecase_name}, Ctxt, Generator}; 9 | 10 | 11 | 12 | pub struct RustAxumGenerator { 13 | /// routes to register in function init_router 14 | /// path => http type, route function 15 | routes: RefCell>> 16 | } 17 | 18 | impl RustAxumGenerator { 19 | pub fn new() -> Self { 20 | Self { 21 | routes: Default::default() 22 | } 23 | } 24 | } 25 | 26 | impl Generator for RustAxumGenerator { 27 | fn name(&self) -> &'static str { 28 | "rust_axum" 29 | } 30 | 31 | fn before_all(&self, ctx: &Ctxt) -> Result<()> { 32 | ctx.append_file(self.name(), &self.dst(ctx), self.axum_dependencies()); 33 | Ok(()) 34 | } 35 | 36 | fn generate_usecase(&self, ctx: &Ctxt, usecase_name: &str, usecase: &cronus_spec::RawUsecase) -> anyhow::Result<()> { 37 | 38 | for (method_name, method) in &usecase.methods { 39 | match method.option { 40 | Some(ref option) => { 41 | if let Some(rest) = &option.rest { 42 | self.generate_method(ctx, usecase_name,usecase,&method_name, method, rest)?; 43 | } 44 | }, 45 | None => {}, 46 | } 47 | } 48 | 49 | Ok(()) 50 | } 51 | 52 | fn after_all(&self, ctx: &Ctxt) -> Result<()> { 53 | // generate app state trait 54 | ctx.append_file(self.name(), &self.dst(ctx), &self.gen_app_state_trait(ctx)); 55 | self.generate_router_init(ctx); 56 | Ok(()) 57 | } 58 | 59 | } 60 | 61 | fn gen_method_query_struct(method: &RawMethod, query_type:&str) -> Option { 62 | let mut query_params: Vec = Vec::new(); 63 | if let Some(req) = &method.req { 64 | if let Some(properties) = &req.properties { 65 | for (name, schema) in properties { 66 | if let Some(option) = &schema.option { 67 | if let Some(rest_option) = &option.rest { 68 | if rest_option.query.unwrap_or(false) { 69 | let ty = if let Some(t) = utils::spec_ty_to_rust_builtin_ty(schema.ty.as_ref().unwrap()) { 70 | t 71 | } else { 72 | schema.ty.as_ref().unwrap().clone() 73 | }; 74 | query_params.push(format!("pub {}: {}", name, ty)); 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | if !query_params.is_empty() { 83 | let struct_def = format!( 84 | "#[derive(Debug, Serialize, Deserialize)]\n\ 85 | pub struct {} {{\n {}\n}}\n", 86 | query_type, 87 | query_params.join(",\n ") 88 | ); 89 | Some(struct_def) 90 | } else { 91 | None 92 | } 93 | } 94 | 95 | fn get_method_path_names_and_tys(method: &RawMethod) -> Result, Vec)>> { 96 | let path_params = utils::get_path_params(method); 97 | let mut struct_fields: Vec = Vec::new(); 98 | let mut struct_tys: Vec = Vec::new(); 99 | match path_params { 100 | Some(path_params) => { 101 | for prop in &path_params { 102 | let prop_schema = method.req.as_ref().unwrap().properties.as_ref().unwrap() 103 | .get(prop).unwrap(); 104 | 105 | let ty: String; 106 | 107 | if prop_schema.items.is_some() { 108 | bail!("array property cannot be used as path variable") 109 | } 110 | 111 | if let Some(t) = utils::spec_ty_to_rust_builtin_ty(prop_schema.ty.as_ref().unwrap()) { 112 | ty = t; 113 | } else { 114 | ty = prop_schema.ty.as_ref().unwrap().clone(); 115 | } 116 | 117 | 118 | struct_fields.push(prop.clone()); 119 | struct_tys.push(ty); 120 | } 121 | 122 | Ok(Some((struct_fields, struct_tys))) 123 | }, 124 | None =>Ok(None), 125 | } 126 | 127 | } 128 | 129 | fn geenerate_usecase_method_query_type(usecase_name:&str, method_name: &str) -> String { 130 | format!("{}_{}_Query", usecase_name, method_name,).to_case(convert_case::Case::UpperCamel) 131 | } 132 | 133 | 134 | 135 | impl RustAxumGenerator { 136 | 137 | 138 | /// Generate the axum handler for the usecase's method with http option 139 | /// 140 | /// pub async fn (State(state): State>, Json(request): Json) 141 | /// -> Result)> { 142 | /// ... 143 | /// } 144 | /// 145 | /// Except for the state and request, query parameter is also needed if there is a corresponding config in rest option 146 | /// Query 147 | /// 148 | /// Path parameter is also need if there is path templating 149 | /// Ex. PathExtractor(post_id): PathExtractor 150 | fn generate_method(&self, ctx: &Ctxt, usecase_name:&str, usecase: &RawUsecase, method_name: &str, method: &cronus_spec::RawMethod, rest: &cronus_spec::RawMethodRestOption) -> Result<()> { 151 | 152 | let mut result = "pub async fn ".to_owned(); 153 | let fn_name = method_name.to_case(convert_case::Case::Snake); 154 | result += &fn_name; 155 | result += &format!("(State(state): State>"); 156 | 157 | let mut has_path_or_query = false; 158 | // handle path parameters 159 | match get_method_path_names_and_tys(method)? { 160 | Some((props, tys)) => { 161 | result += ", "; 162 | result += &format!(" axum::extract::Path(({})): axum::extract::Path<({})>", 163 | props.join(","), 164 | tys.join(",") 165 | ); 166 | has_path_or_query = true; 167 | }, 168 | None => {}, 169 | } 170 | 171 | // handle query parameters 172 | let query_ty = geenerate_usecase_method_query_type(usecase_name, method_name); 173 | match gen_method_query_struct(method, &query_ty) { 174 | Some(query_struct) => { 175 | // add struct definition to file 176 | ctx.append_file(self.name(), &self.dst(ctx), &query_struct); 177 | result += &format!(", query: axum::extract::Query<{}>", query_ty); 178 | has_path_or_query = true; 179 | }, 180 | None => {}, 181 | } 182 | 183 | if method.req.is_some() { 184 | // If method request has some and method is not get 185 | if rest.method != "get" { 186 | if has_path_or_query { 187 | result += ", Json(mut request): Json<"; 188 | 189 | } else { 190 | result += ", Json(request): Json<"; 191 | } 192 | result += &get_request_name(ctx, method_name); 193 | result += ">" 194 | } 195 | 196 | // if method is get, since no http body, so we have to create our own request 197 | 198 | 199 | 200 | } 201 | 202 | result += ") -> Result)> {\n"; 203 | if method.req.is_some() && rest.method == "get" { 204 | // creating the request by our own 205 | result += &format!("let request = {} {{\n", &get_request_name(ctx, method_name)); 206 | 207 | } 208 | 209 | // handle request's path & query assignment 210 | // request.xxx = xxx 211 | match get_method_path_names_and_tys(method)? { 212 | Some((props, tys)) => { 213 | if rest.method != "get" { 214 | let stmts:Vec = props.iter() 215 | .map(|prop| { 216 | let required = method.req.as_ref().unwrap().properties.as_ref().unwrap().get(prop).unwrap().required.unwrap_or(false); 217 | 218 | if required { 219 | format!("request.{} = {};", prop, prop).to_string() 220 | } else { 221 | format!("request.{} = Some({});", prop, prop).to_string() 222 | } 223 | 224 | }) 225 | .collect(); 226 | result += &stmts.join("\n"); 227 | } else { 228 | for prop in &props { 229 | // using the short hand style to assign object prop (since our path name and prop name are the same) 230 | result += &format!("{},\n", prop) 231 | } 232 | } 233 | 234 | }, 235 | None => {} 236 | }; 237 | 238 | // request.xxx = query.xxx 239 | match utils::get_query_params(method) { 240 | Some(params) => { 241 | if rest.method != "get" { 242 | let stmts:Vec = params.iter() 243 | .map(|prop| format!("request.{} = Some(query.{});", prop, prop).to_string()) 244 | .collect(); 245 | result += &stmts.join("\n"); 246 | } else { 247 | for prop in params.iter() { 248 | result += &format!("{}:query.{},\n", prop, prop); 249 | } 250 | } 251 | }, 252 | None => {}, 253 | } 254 | 255 | // if http method is get, we need to add close to our created request object 256 | if method.req.is_some() && rest.method == "get" { 257 | result += "};\n"; 258 | } 259 | 260 | let req_var = if method.req.is_some() { "request" } else { ""}; 261 | result += &format!(r#" 262 | match state.{}.{}({}).await {{ 263 | Ok(res) => {{ 264 | Ok(Json(res)) 265 | }}, 266 | Err(err) => {{ 267 | let mut err_obj = serde_json::Map::new(); 268 | err_obj.insert("message".to_owned(), serde_json::Value::from(err.to_string())); 269 | Err((StatusCode::BAD_REQUEST, Json(serde_json::Value::Object(err_obj)))) 270 | }}, 271 | }} 272 | "#, usecase_name.to_case(Case::Snake), fn_name, req_var); 273 | result += "}\n"; 274 | ctx.append_file(self.name(), &self.dst(ctx), &result); 275 | 276 | // prepare routes 277 | let usecase_prefix = utils::get_usecase_rest_path_prefix(usecase); 278 | if let Some(options) = &method.option { 279 | if let Some(rest_option) = &options.rest { 280 | let method_path = rest_option.path.as_ref().map(|p| if usecase_prefix.ends_with("/") { format!("{}{}", usecase_prefix, p)} else { format!("{}/{}", usecase_prefix, p)} ).unwrap_or(usecase_prefix.clone()); 281 | if self.routes.borrow().get(&method_path).is_some() { 282 | self.routes.borrow_mut().get_mut(&method_path).unwrap().push((rest_option.method.clone(), fn_name)) 283 | } else { 284 | self.routes.borrow_mut() 285 | .insert(method_path, vec![(rest_option.method.clone(), fn_name)]); 286 | 287 | } 288 | 289 | } 290 | } 291 | 292 | 293 | Ok(()) 294 | 295 | } 296 | 297 | fn generate_router_init(&self, ctx: &Ctxt) { 298 | let mut result = "pub fn router_init(usecases: std::sync::Arc) -> Router {\n".to_owned(); 299 | result += " Router::new()\n"; 300 | 301 | let routes = self.routes.borrow(); 302 | 303 | 304 | for (idx, (path, methods)) in routes.iter().enumerate() { 305 | result += &" .route(".to_owned(); 306 | 307 | let axum_routes = methods.iter() 308 | .map(|(ty, func)| { 309 | format!("{}({})", ty, func) 310 | }) 311 | .collect::>() 312 | .join("."); 313 | 314 | result += &format!("\"{}\", axum::routing::{}", path, axum_routes); 315 | result += ")"; 316 | if idx < routes.len()-1 { 317 | result += "\n" 318 | } else { 319 | 320 | result += "\n .with_state(usecases)\n" 321 | } 322 | } 323 | 324 | result += "}\n"; 325 | ctx.append_file(self.name(), &self.dst(ctx), &result); 326 | 327 | } 328 | 329 | fn axum_dependencies(&self) -> &'static str { 330 | return r#" 331 | use axum::{ 332 | extract::State, 333 | http::{header, Response, StatusCode}, 334 | response::IntoResponse, 335 | Extension, Json, 336 | Router 337 | }; 338 | "#; 339 | } 340 | 341 | fn gen_app_state_trait(&self, ctx: &Ctxt) -> String { 342 | let mut result = "#[derive(Clone)]\npub struct Usecases {\n".to_string(); 343 | // find which use case is http 344 | ctx 345 | .spec 346 | .usecases 347 | .iter() 348 | .flat_map(|m| m.iter()) 349 | .filter(|(name, usecase)| { 350 | for (method_name, method) in &usecase.methods { 351 | match method.option { 352 | Some(ref option) => { 353 | if let Some(rest) = &option.rest { 354 | return true; 355 | } 356 | }, 357 | None => {}, 358 | } 359 | } 360 | return false; 361 | }) 362 | .for_each(|(name, usecase)|{ 363 | // usecase that contains at least one method that is open to REST 364 | let usecase_name = get_usecase_name(ctx, name); 365 | result += &format!(" pub {}: std::sync::Arc,\n", name.to_case(Case::Snake), usecase_name); 366 | }); 367 | 368 | result += "}\n"; 369 | 370 | return result; 371 | } 372 | 373 | fn get_gen_option<'a>(&self, ctx: &'a Ctxt) -> Option<&'a RustAxumGeneratorOption> { 374 | ctx.spec.option.as_ref().and_then(|go| go.generator.as_ref().and_then(|gen| gen.rust_axum.as_ref())) 375 | } 376 | 377 | fn dst(&self, ctx: &Ctxt) -> String { 378 | let default_file = "handler.rs"; 379 | 380 | self.get_gen_option(ctx) 381 | .and_then(|gen_opt| { 382 | Some(get_path_from_optional_parent( 383 | gen_opt.def_loc.file.parent(), 384 | gen_opt.file.as_ref(), 385 | default_file) 386 | ) 387 | 388 | 389 | }) 390 | .unwrap_or(default_file.to_string()) 391 | 392 | } 393 | 394 | 395 | } 396 | 397 | 398 | #[cfg(test)] 399 | mod tests { 400 | use crate::run_generator; 401 | 402 | use super::*; 403 | use std::{collections::HashMap, path::PathBuf}; 404 | use anyhow::{Ok, Result}; 405 | use cronus_parser::api_parse; 406 | 407 | #[test] 408 | fn test_axum_path_var() -> Result<()> { 409 | let api_file: &'static str = r#" 410 | usecase abc { 411 | [rest.path = "abcd/:a"] 412 | [rest.method = "post"] 413 | create_abcd { 414 | in { 415 | a: string 416 | } 417 | out { 418 | b: string 419 | } 420 | } 421 | } 422 | "#; 423 | 424 | let spec = api_parse::parse(PathBuf::from(""), api_file)?; 425 | let ctx = Ctxt::new(spec, None); 426 | let g = RustAxumGenerator::new(); 427 | run_generator(&g, &ctx)?; 428 | let gfs = ctx.get_gfs(g.name()); 429 | let gfs_borrow = gfs.borrow(); 430 | let file_content = gfs_borrow.get("handler.rs").unwrap(); 431 | assert!(file_content.contains("axum::extract::Path((a))")); 432 | Ok(()) 433 | } 434 | 435 | #[test] 436 | fn test_axum_no_path_var() -> Result<()> { 437 | let api_file: &'static str = r#" 438 | usecase abc { 439 | [rest.path = "abcd"] 440 | [rest.method = "post"] 441 | create_abcd { 442 | in { 443 | a: string 444 | } 445 | out { 446 | b: string 447 | } 448 | } 449 | } 450 | "#; 451 | 452 | let spec = api_parse::parse(PathBuf::from(""), api_file)?; 453 | let ctx = Ctxt::new(spec, None); 454 | let g = RustAxumGenerator::new(); 455 | run_generator(&g, &ctx)?; 456 | let gfs = ctx.get_gfs(g.name()); 457 | let gfs_borrow = gfs.borrow(); 458 | let file_content = gfs_borrow.get("handler.rs").unwrap(); 459 | assert!(!file_content.contains("axum::extract::Path(")); 460 | Ok(()) 461 | } 462 | 463 | #[test] 464 | fn test_axum_query_var() -> Result<()> { 465 | let api_file: &'static str = r#" 466 | usecase abc { 467 | [rest.path = "abcd"] 468 | [rest.method = "post"] 469 | create_abcd { 470 | in { 471 | [rest.query] 472 | a: string 473 | } 474 | out { 475 | b: string 476 | } 477 | } 478 | } 479 | "#; 480 | 481 | let spec = api_parse::parse(PathBuf::from(""), api_file)?; 482 | let ctx = Ctxt::new(spec, None); 483 | let g = RustAxumGenerator::new(); 484 | run_generator(&g, &ctx)?; 485 | let gfs = ctx.get_gfs(g.name()); 486 | let gfs_borrow = gfs.borrow(); 487 | let file_content = gfs_borrow.get("handler.rs").unwrap(); 488 | assert!(file_content.contains("axum::extract::Query")); 489 | Ok(()) 490 | } 491 | } -------------------------------------------------------------------------------- /lib/spec/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::{HashMap, VecDeque}, error::Error, fs, path::{Path, PathBuf}, sync::Arc}; 2 | use anyhow::{bail, Result}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Debug, Serialize, Deserialize, Clone)] 6 | pub struct RawSchemaEnumItem { 7 | pub name: String, 8 | pub value: Option, 9 | } 10 | 11 | 12 | #[derive(Debug, Serialize, Deserialize)] 13 | #[serde(deny_unknown_fields)] 14 | pub struct GlobalOption { 15 | #[serde(skip_serializing_if = "Option::is_none")] 16 | pub generator: Option, 17 | 18 | /// Default: "usecase", other popular choices are: "service", etc.. 19 | #[serde(skip_serializing_if = "Option::is_none")] 20 | pub usecase_suffix: Option, 21 | 22 | /// Default: "request", other popular choices are: "input", "req", etc.. 23 | #[serde(skip_serializing_if = "Option::is_none")] 24 | pub usecase_request_suffix: Option, 25 | 26 | /// Default: "response", other popular choices are: "output", "res", etc.. 27 | #[serde(skip_serializing_if = "Option::is_none")] 28 | pub usecase_response_suffix: Option, 29 | 30 | /// If given type(s) is/are mentioned in the spec, do not generate 31 | pub skip_types: Option> 32 | } 33 | 34 | 35 | #[derive(Debug, Serialize, Deserialize)] 36 | #[serde(deny_unknown_fields)] 37 | pub struct GeneratorOption { 38 | pub rust: Option, 39 | pub golang: Option, 40 | pub golang_gin: Option, 41 | pub python: Option, 42 | pub python_fastapi: Option, 43 | pub python_redis: Option, 44 | pub rust_axum: Option, 45 | pub openapi: Option, 46 | pub typescript: Option, 47 | pub typescript_nestjs: Option, 48 | pub java: Option, 49 | pub java_springweb: Option 50 | } 51 | 52 | #[derive(Debug, Serialize, Deserialize)] 53 | #[serde(deny_unknown_fields)] 54 | pub struct JavaGeneratorOption { 55 | #[serde(skip)] 56 | pub def_loc: Arc, 57 | 58 | /// Output directory for .java files 59 | #[serde(skip_serializing_if = "Option::is_none")] 60 | pub dir: Option, 61 | 62 | /// Java Package Name 63 | #[serde(skip_serializing_if = "Option::is_none")] 64 | pub package: Option, 65 | 66 | /// Custom imports 67 | #[serde(skip_serializing_if = "Option::is_none")] 68 | pub imports: Option>, 69 | 70 | /// Custom exception type for usecase methods 71 | #[serde(skip_serializing_if = "Option::is_none")] 72 | pub exception_type: Option, 73 | 74 | #[serde(skip_serializing_if = "Option::is_none")] 75 | pub extra_method_parameters: Option>, 76 | 77 | /// Do not add throws clauses to usecase methods 78 | #[serde(skip_serializing_if = "Option::is_none")] 79 | pub no_exceptions: Option, 80 | 81 | #[serde(skip_serializing_if = "Option::is_none")] 82 | pub exclude_types: Option>, 83 | } 84 | 85 | #[derive(Debug, Serialize, Deserialize)] 86 | #[serde(deny_unknown_fields)] 87 | pub struct JavaSpringWebGeneratorOption { 88 | #[serde(skip)] 89 | pub def_loc: Arc, 90 | 91 | /// Output directory for .java files 92 | #[serde(skip_serializing_if = "Option::is_none")] 93 | pub dir: Option, 94 | 95 | /// Java Package Name 96 | #[serde(skip_serializing_if = "Option::is_none")] 97 | pub package: Option, 98 | 99 | 100 | /// Java Domain Generated Package Import 101 | #[serde(skip_serializing_if = "Option::is_none")] 102 | pub domain_import: Option, 103 | 104 | #[serde(skip_serializing_if = "Option::is_none")] 105 | pub extra_request_statements: Option>, 106 | 107 | #[serde(skip_serializing_if = "Option::is_none")] 108 | pub extra_method_parameters: Option>, 109 | 110 | #[serde(skip_serializing_if = "Option::is_none")] 111 | pub extra_imports: Option>, 112 | } 113 | 114 | #[derive(Debug, Serialize, Deserialize)] 115 | #[serde(deny_unknown_fields)] 116 | pub struct GolangGinGeneratorOption { 117 | #[serde(skip)] 118 | pub def_loc: Arc, 119 | 120 | /// Output .go file 121 | #[serde(skip_serializing_if = "Option::is_none")] 122 | pub file: Option, 123 | 124 | /// Golang Package Name 125 | #[serde(skip_serializing_if = "Option::is_none")] 126 | pub package: Option, 127 | 128 | /// Golang Domain Generated Package Name 129 | #[serde(skip_serializing_if = "Option::is_none")] 130 | pub domain_package: Option, 131 | 132 | /// Golang Domain Generated Package Import 133 | #[serde(skip_serializing_if = "Option::is_none")] 134 | pub domain_import: Option, 135 | 136 | #[serde(skip_serializing_if = "Option::is_none")] 137 | pub extra_request_fields: Option> 138 | } 139 | 140 | #[derive(Debug, Serialize, Deserialize)] 141 | #[serde(deny_unknown_fields)] 142 | pub struct PythonRedisGeneratorOption { 143 | #[serde(skip)] 144 | pub def_loc: Arc, 145 | 146 | /// Output .py file 147 | #[serde(skip_serializing_if = "Option::is_none")] 148 | pub file: Option, 149 | 150 | #[serde(rename = "async", skip_serializing_if = "Option::is_none")] 151 | pub async_flag: Option, 152 | 153 | #[serde(skip_serializing_if = "Option::is_none")] 154 | pub usecase_from: Option, 155 | 156 | } 157 | 158 | #[derive(Debug, Serialize, Deserialize)] 159 | #[serde(deny_unknown_fields)] 160 | pub struct GolangGeneratorOption { 161 | #[serde(skip)] 162 | pub def_loc: Arc, 163 | 164 | /// Output .go file 165 | #[serde(skip_serializing_if = "Option::is_none")] 166 | pub file: Option, 167 | 168 | /// Golang Package Name 169 | #[serde(skip_serializing_if = "Option::is_none")] 170 | pub package: Option, 171 | 172 | } 173 | 174 | #[derive(Debug, Serialize, Deserialize)] 175 | #[serde(deny_unknown_fields)] 176 | pub struct PythonFastApiGeneratorOption { 177 | #[serde(skip)] 178 | pub def_loc: Arc, 179 | 180 | /// Output .py file 181 | #[serde(skip_serializing_if = "Option::is_none")] 182 | pub file: Option, 183 | 184 | #[serde(rename = "async", skip_serializing_if = "Option::is_none")] 185 | pub async_flag: Option, 186 | 187 | #[serde(skip_serializing_if = "Option::is_none")] 188 | pub get_ctx_from: Option, 189 | 190 | /// where the python usecase types are defined (which module) 191 | #[serde(skip_serializing_if = "Option::is_none")] 192 | pub usecase_from: Option, 193 | 194 | pub extra_imports: Option>, 195 | 196 | #[serde(skip_serializing_if = "Option::is_none")] 197 | pub extra_method_args: Option>, 198 | 199 | #[serde(skip_serializing_if = "Option::is_none")] 200 | pub extra_request_fields: Option> 201 | } 202 | 203 | 204 | 205 | 206 | 207 | #[derive(Debug, Serialize, Deserialize)] 208 | #[serde(deny_unknown_fields)] 209 | pub struct TypescriptNestjsGeneratorOption { 210 | #[serde(skip)] 211 | pub def_loc: Arc, 212 | 213 | /// Output .ts file 214 | pub file: Option 215 | } 216 | 217 | #[derive(Debug, Serialize, Deserialize)] 218 | #[serde(deny_unknown_fields)] 219 | pub struct TypescriptGeneratorOption { 220 | #[serde(skip)] 221 | pub def_loc: Arc, 222 | 223 | /// Output .ts file 224 | pub file: Option, 225 | 226 | } 227 | 228 | #[derive(Debug, Serialize, Deserialize)] 229 | #[serde(deny_unknown_fields)] 230 | pub struct PythonGeneratorOption { 231 | #[serde(skip)] 232 | pub def_loc: Arc, 233 | 234 | /// Output .py file 235 | #[serde(skip_serializing_if = "Option::is_none")] 236 | pub file: Option, 237 | 238 | #[serde(rename = "async", skip_serializing_if = "Option::is_none")] 239 | pub async_flag: Option, 240 | 241 | 242 | } 243 | 244 | #[derive(Debug, Serialize, Deserialize)] 245 | #[serde(deny_unknown_fields)] 246 | pub struct RustGeneratorOption { 247 | #[serde(skip)] 248 | pub def_loc: Arc, 249 | 250 | /// Output .rs file 251 | #[serde(skip_serializing_if = "Option::is_none")] 252 | pub file: Option, 253 | 254 | /// Do not place default derive for struct 255 | #[serde(skip_serializing_if = "Option::is_none")] 256 | pub no_default_derive: Option, 257 | 258 | /// Override the built-in default derive for struct 259 | #[serde(skip_serializing_if = "Option::is_none")] 260 | pub default_derive: Option>, 261 | 262 | /// Custom extra uses 263 | #[serde(skip_serializing_if = "Option::is_none")] 264 | pub uses: Option>, 265 | 266 | 267 | 268 | #[serde(skip_serializing_if = "Option::is_none")] 269 | pub no_error_type: Option, 270 | 271 | #[serde(skip_serializing_if = "Option::is_none")] 272 | pub error_type: Option, 273 | 274 | #[serde(rename = "async", skip_serializing_if = "Option::is_none")] 275 | pub async_flag: Option, 276 | 277 | #[serde(skip_serializing_if = "Option::is_none")] 278 | pub async_trait: Option, 279 | } 280 | 281 | #[derive(Debug, Serialize, Deserialize)] 282 | #[serde(deny_unknown_fields)] 283 | pub struct RustAxumGeneratorOption { 284 | #[serde(skip)] 285 | pub def_loc: Arc, 286 | 287 | /// Output .rs file 288 | pub file: Option 289 | } 290 | 291 | #[derive(Debug, Serialize, Deserialize)] 292 | #[serde(deny_unknown_fields)] 293 | pub enum Case { 294 | #[serde(rename = "camel")] 295 | Camel, 296 | #[serde(rename = "snake")] 297 | Snake 298 | } 299 | 300 | 301 | #[derive(Debug, Serialize, Deserialize)] 302 | pub struct OpenapiGeneratorOption { 303 | #[serde(skip)] 304 | pub def_loc: Arc, 305 | 306 | /// Output .rs file 307 | pub file: Option, 308 | 309 | /// Case for the fields of request, response etc. in OpenAPI spec 310 | #[serde(skip_serializing_if = "Option::is_none")] 311 | pub field_case: Option 312 | } 313 | 314 | 315 | #[derive(Debug)] 316 | pub struct DefLoc { 317 | pub file: PathBuf 318 | } 319 | 320 | impl DefLoc { 321 | pub fn new(file: PathBuf) -> Self { 322 | Self { 323 | file 324 | } 325 | } 326 | } 327 | 328 | impl Default for DefLoc { 329 | fn default() -> Self { 330 | Self { 331 | file: PathBuf::new() 332 | } 333 | } 334 | } 335 | 336 | 337 | 338 | 339 | 340 | 341 | #[derive(Debug, Serialize, Deserialize, Clone)] 342 | pub struct RawSchema { 343 | #[serde(skip)] 344 | pub def_loc: Arc, 345 | 346 | #[serde(rename = "type", skip_serializing_if = "Option::is_none")] 347 | pub ty: Option, // 'type' is a reserved keyword in Rust, hence the rename 348 | 349 | #[serde(skip_serializing_if = "Option::is_none")] 350 | pub items: Option>, 351 | 352 | #[serde(skip_serializing_if = "Option::is_none")] 353 | pub properties: Option>, 354 | 355 | #[serde(skip_serializing_if = "Option::is_none")] 356 | pub required: Option, 357 | 358 | #[serde(skip_serializing_if = "Option::is_none")] 359 | pub namespace: Option, 360 | 361 | #[serde(skip_serializing_if = "Option::is_none")] 362 | pub enum_items: Option>, 363 | 364 | #[serde(skip_serializing_if = "Option::is_none")] 365 | pub option: Option, 366 | 367 | #[serde(skip_serializing_if = "Option::is_none")] 368 | pub extends: Option>, 369 | 370 | #[serde(skip_serializing_if = "Option::is_none")] 371 | pub flat_extends: Option>, 372 | } 373 | 374 | impl RawSchema { 375 | pub fn new(def_loc: Arc, ty: String) -> Self { 376 | Self { 377 | def_loc, 378 | ty: Some(ty), 379 | items: None, 380 | properties: None, 381 | required: None, 382 | namespace: None, 383 | enum_items: None, 384 | option: None, 385 | extends: None, 386 | flat_extends: None, 387 | } 388 | } 389 | 390 | pub fn new_array_ty(def_loc: Arc, items_ty: String) -> Self { 391 | Self { 392 | def_loc: def_loc.clone(), 393 | ty: None, 394 | items: Some(Box::new(RawSchema::new(def_loc, items_ty))), 395 | properties: None, 396 | required: None, 397 | namespace: None, 398 | enum_items: None, 399 | option: None, 400 | extends: None, 401 | flat_extends: None, 402 | } 403 | } 404 | } 405 | 406 | #[derive(Debug, Serialize, Deserialize, Clone)] 407 | #[serde(deny_unknown_fields)] 408 | pub struct RawSchemaPropertyOption { 409 | #[serde(skip_serializing_if = "Option::is_none")] 410 | pub rest: Option, 411 | 412 | #[serde(skip_serializing_if = "Option::is_none")] 413 | pub python: Option, 414 | 415 | #[serde(skip_serializing_if = "Option::is_none")] 416 | pub rust: Option, 417 | 418 | #[serde(skip_serializing_if = "Option::is_none")] 419 | pub openapi: Option, 420 | 421 | #[serde(skip_serializing_if = "Option::is_none")] 422 | pub python_fastapi: Option, 423 | 424 | #[serde(skip_serializing_if = "Option::is_none")] 425 | pub golang_gin: Option, 426 | 427 | #[serde(skip_serializing_if = "Option::is_none")] 428 | pub java_springweb: Option, 429 | 430 | #[serde(skip_serializing_if = "Option::is_none")] 431 | pub description: Option 432 | 433 | } 434 | 435 | #[derive(Debug, Serialize, Deserialize, Clone)] 436 | #[serde(deny_unknown_fields)] 437 | pub struct RawSchemaPropertyPythonOption { 438 | 439 | } 440 | 441 | #[derive(Debug, Serialize, Deserialize, Clone)] 442 | #[serde(deny_unknown_fields)] 443 | pub struct RawSchemaPropertyOpenApiOption { 444 | #[serde(skip_serializing_if = "Option::is_none")] 445 | pub exclude: Option 446 | } 447 | 448 | #[derive(Debug, Serialize, Deserialize, Clone)] 449 | #[serde(deny_unknown_fields)] 450 | pub struct RawSchemaPropertyGolangGinOption { 451 | #[serde(skip_serializing_if = "Option::is_none")] 452 | pub exclude: Option 453 | } 454 | 455 | #[derive(Debug, Serialize, Deserialize, Clone)] 456 | #[serde(deny_unknown_fields)] 457 | pub struct RawSchemaPropertyPythonFastApiOption { 458 | #[serde(skip_serializing_if = "Option::is_none")] 459 | pub exclude: Option 460 | } 461 | 462 | #[derive(Debug, Serialize, Deserialize, Clone)] 463 | #[serde(deny_unknown_fields)] 464 | pub struct RawSchemaPropertyJavaSpringWebOption { 465 | #[serde(skip_serializing_if = "Option::is_none")] 466 | pub exclude: Option 467 | } 468 | 469 | 470 | 471 | #[derive(Debug, Serialize, Deserialize, Clone)] 472 | #[serde(deny_unknown_fields)] 473 | pub struct RawSchemaPropertyRestOption { 474 | #[serde(skip_serializing_if = "Option::is_none")] 475 | pub query: Option 476 | } 477 | 478 | #[derive(Debug, Serialize, Deserialize, Clone)] 479 | #[serde(deny_unknown_fields)] 480 | pub struct RawSchemaPropertyRustOption { 481 | #[serde(skip_serializing_if = "Option::is_none")] 482 | pub attrs: Option> 483 | } 484 | #[derive(Debug, Serialize, Deserialize)] 485 | pub struct RawMethod { 486 | #[serde(skip_serializing_if = "Option::is_none")] 487 | pub req: Option, 488 | #[serde(skip_serializing_if = "Option::is_none")] 489 | pub res: Option, 490 | 491 | #[serde(skip_serializing_if = "Option::is_none")] 492 | pub option: Option 493 | } 494 | 495 | 496 | #[derive(Debug, Serialize, Deserialize)] 497 | pub struct RawUsecase { 498 | #[serde(skip)] 499 | pub def_loc: Arc, 500 | 501 | pub methods: HashMap, 502 | 503 | #[serde(skip_serializing_if = "Option::is_none")] 504 | pub option: Option 505 | } 506 | 507 | #[derive(Debug, Serialize, Deserialize)] 508 | #[serde(deny_unknown_fields)] 509 | pub struct RawUsecaseOption { 510 | 511 | #[serde(skip_serializing_if = "Option::is_none")] 512 | pub rest: Option 513 | } 514 | 515 | #[derive(Debug, Serialize, Deserialize)] 516 | #[serde(deny_unknown_fields)] 517 | pub struct RawUsecaseRestOption { 518 | /// Http endpoint prefix 519 | #[serde(skip_serializing_if = "Option::is_none")] 520 | pub path: Option 521 | } 522 | 523 | 524 | #[derive(Debug, Serialize, Deserialize)] 525 | #[serde(deny_unknown_fields)] 526 | pub struct RawMethodOption { 527 | #[serde(skip_serializing_if = "Option::is_none")] 528 | pub rest: Option, 529 | 530 | #[serde(skip_serializing_if = "Option::is_none")] 531 | pub redis: Option, 532 | 533 | #[serde(skip_serializing_if = "Option::is_none")] 534 | pub python_fastapi: Option, 535 | 536 | #[serde(skip_serializing_if = "Option::is_none")] 537 | pub golang_gin: Option, 538 | 539 | #[serde(skip_serializing_if = "Option::is_none")] 540 | pub description: Option, 541 | 542 | 543 | } 544 | 545 | 546 | 547 | #[derive(Debug, Serialize, Deserialize)] 548 | #[serde(deny_unknown_fields)] 549 | pub struct RawMethodRedisOption { 550 | 551 | #[serde(skip_serializing_if = "Option::is_none")] 552 | pub queue_name: Option, 553 | 554 | #[serde(skip_serializing_if = "Option::is_none")] 555 | pub ack_queue_name: Option 556 | } 557 | 558 | #[derive(Debug, Serialize, Deserialize)] 559 | pub struct RawMethodGolangGinOption { 560 | 561 | #[serde(skip_serializing_if = "Option::is_none")] 562 | pub extra_request_fields: Option> 563 | } 564 | 565 | #[derive(Debug, Serialize, Deserialize)] 566 | pub struct RawMethodPythonFastApiOption { 567 | #[serde(skip_serializing_if = "Option::is_none")] 568 | pub extra_method_args: Option>, 569 | 570 | #[serde(skip_serializing_if = "Option::is_none")] 571 | pub extra_request_fields: Option> 572 | } 573 | 574 | #[derive(Clone, Debug, Serialize, Deserialize)] 575 | pub struct RawMethodRestOption { 576 | pub method: String, 577 | pub path: Option, 578 | pub content_type: Option, // e.g. Default is "application/json" 579 | } 580 | 581 | 582 | #[derive(Debug, Serialize, Deserialize)] 583 | pub struct RawRepository { 584 | #[serde(skip)] 585 | pub def_loc: Arc, 586 | 587 | // #[serde(skip_serializing_if = "Option::is_none")] 588 | // pub option: Option, 589 | 590 | #[serde(skip_serializing_if = "Option::is_none")] 591 | pub methods: Option>, 592 | } 593 | 594 | /// The schema for a spec 595 | /// 596 | #[derive(Debug, Serialize, Deserialize)] 597 | #[serde(deny_unknown_fields)] 598 | pub struct RawSpec { 599 | 600 | 601 | #[serde(rename = "types", skip_serializing_if = "Option::is_none")] 602 | pub ty: Option>, 603 | 604 | #[serde(skip_serializing_if = "Option::is_none")] 605 | pub usecases: Option>, 606 | 607 | #[serde(skip_serializing_if = "Option::is_none")] 608 | pub repositories: Option>, 609 | 610 | #[serde(skip_serializing_if = "Option::is_none")] 611 | pub option: Option, 612 | 613 | 614 | #[serde(skip_serializing_if = "Option::is_none")] 615 | pub imports: Option>, 616 | 617 | } 618 | 619 | 620 | impl RawSpec { 621 | 622 | 623 | pub fn merge(&mut self, to_merge: RawSpec)-> Result<()> { 624 | if let Some(to_merge_ty) = to_merge.ty { 625 | let ty_map = self.ty.get_or_insert_with(HashMap::new); 626 | for (key, value) in to_merge_ty { 627 | if ty_map.contains_key(&key) { 628 | bail!("Conflict for key '{}' in 'ty' hashmap", key); 629 | } 630 | ty_map.insert(key, value); 631 | } 632 | } 633 | 634 | // Merge 'usecase' HashMap 635 | if let Some(to_merge_usecase) = to_merge.usecases { 636 | let usecase_map = self.usecases.get_or_insert_with(HashMap::new); 637 | for (key, value) in to_merge_usecase { 638 | if usecase_map.contains_key(&key) { 639 | bail!("Conflict for key '{}' in 'usecase' hashmap", key) 640 | } 641 | usecase_map.insert(key, value); 642 | } 643 | } 644 | 645 | // Global option has to be putted into the entry .api or .yaml file 646 | 647 | // Merge 'imports' Vec 648 | if let Some(to_merge_imports) = to_merge.imports { 649 | if let Some(imports) = &mut self.imports { 650 | for import in &to_merge_imports { 651 | if imports.contains(import) { 652 | continue; 653 | } 654 | } 655 | imports.extend(to_merge_imports); 656 | } else { 657 | self.imports = Some(to_merge_imports); 658 | } 659 | } 660 | 661 | Ok(()) 662 | } 663 | 664 | pub fn new() -> Self { 665 | Self { 666 | ty: Default::default(), 667 | usecases: Default::default(), 668 | repositories: Default::default(), 669 | option: Default::default(), 670 | imports: Default::default() 671 | } 672 | } 673 | } 674 | 675 | --------------------------------------------------------------------------------