diff --git a/Cargo.lock b/Cargo.lock index 7654362..f9ec232 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,9 +91,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.76" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" dependencies = [ "backtrace", ] @@ -162,15 +162,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" -[[package]] -name = "cargo_toml" -version = "0.17.2" -source = "git+https://github.com/kurtbuilds/cargo_toml#cedc8cf967e4a23ec181055e1326f477d0cb5817" -dependencies = [ - "serde", - "toml", -] - [[package]] name = "cc" version = "1.0.83" @@ -188,14 +179,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", - "windows-targets 0.48.5", + "wasm-bindgen", + "windows-targets 0.52.0", ] [[package]] @@ -260,6 +253,20 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +[[package]] +name = "codegen_rust" +version = "0.1.0" +dependencies = [ + "anyhow", + "convert_case", + "libninja_hir", + "libninja_mir", + "libninja_mir_rust", + "proc-macro2", + "quote", + "regex", +] + [[package]] name = "colorchoice" version = "1.0.0" @@ -631,8 +638,9 @@ name = "libninja" version = "0.1.10" dependencies = [ "anyhow", - "cargo_toml", + "chrono", "clap", + "codegen_rust", "convert_case", "env_logger", "http", @@ -640,12 +648,8 @@ dependencies = [ "include_dir", "indexmap", "indoc", - "libninja_commercial", - "libninja_core", "libninja_hir", - "libninja_macro", "libninja_mir", - "libninja_mir_rust", "log", "openapiv3-extended", "pretty_assertions", @@ -670,35 +674,6 @@ dependencies = [ "url", ] -[[package]] -name = "libninja_commercial" -version = "0.1.0" - -[[package]] -name = "libninja_core" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "convert_case", - "http", - "include_dir", - "indexmap", - "libninja_hir", - "libninja_mir", - "openapiv3-extended", - "proc-macro2", - "quote", - "regex-lite", - "serde", - "serde_json", - "serde_yaml", - "syn", - "tera", - "tracing", - "tracing-ez", -] - [[package]] name = "libninja_hir" version = "0.1.0" @@ -719,6 +694,7 @@ dependencies = [ "pretty_assertions", "proc-macro2", "quote", + "syn", ] [[package]] @@ -1010,9 +986,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -1067,9 +1043,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", @@ -1088,12 +1064,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-lite" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b661b2f27137bdbc16f00eda72866a92bb28af1753ffbd56744fb6e2e9cd8e" - [[package]] name = "regex-syntax" version = "0.8.2" @@ -1274,9 +1244,9 @@ checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" [[package]] name = "syn" -version = "2.0.48" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -1471,16 +1441,6 @@ dependencies = [ "valuable", ] -[[package]] -name = "tracing-ez" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde38bb862b421d16f4fe66ec800c898c863911143d4854001d9ab681d2938b6" -dependencies = [ - "tracing", - "tracing-subscriber", -] - [[package]] name = "tracing-log" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index daae5f0..6b1fdf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,11 +3,7 @@ resolver = "2" members = [ "libninja", "macro", - "core", "mir", "hir", - "mir_rust", -] -exclude = [ - "commercial", -] + "codegen_rust", +] \ No newline at end of file diff --git a/Justfile b/Justfile index dbf3261..ee63e1f 100644 --- a/Justfile +++ b/Justfile @@ -61,27 +61,6 @@ clean MODE='debug': rust: clean cargo run -- gen --name PetStore --output-dir gen/rust --generator rust data/openapi-spec/petstore/petstore.yaml --github libninjacom/petstore-rs --version 0.1.0 -python: clean - cargo run -- gen --name PetStore --output-dir gen/python --generator python --version 0.1.0 --github libninjacom/petstore-py spec/petstore.yaml - -python-example: - #!/bin/bash -euxo pipefail - cd gen/python - eval "$(pdm --pep582)" - python3 -m examples.list_pets - -typescript: clean - cargo run -- gen --name PetStore --output-dir gen/typescript --generator typescript data/openapi-spec/petstore/petstore.yaml - -java: - just gen/java/build - just gen/java/run - -go: - rm -rf gen/petstore-go - checkexec ${CARGO_TARGET_DIR:-target}/debug/ocg $(fd . ocg/template) -- cargo clean --package ocg - cargo run -- gen --name PetStore --output-dir gen/petstore-go --generator go spec/petstore.yaml --github libninjacom/petstore-go --version 0.1.0 - generate: #!/bin/bash -euxo pipefail if [ -n "${LIBRARY:-}" ]; then @@ -94,14 +73,9 @@ generate: cargo run -- gen --name $SERVICE --output-dir $REPO_DIR --generator $SOURCEGEN --github $REPO --version $VERSION $LIBRARY $SPEC test *ARGS: - checkexec commercial -- just dummy_commercial cargo test -- "$ARGS" alias t := test -integration *ARGS: - cd libninja && cargo test -F integration -- "$@" -alias int := integration - # Test the library we just generated test_lib: #!/bin/bash -euxo pipefail @@ -118,16 +92,4 @@ clean-gen: echo "DIR is empty" exit 1 fi - rm -rf $DIR/* - -delete *ARG: - gh repo delete $REPO {{ARG}} - -commercial: - rm -rf commercial - git clone https://github.com/kurtbuilds/libninja-commercial commercial - -# Create a dummy commercial repo that lets the workspace work -# without the commericial code -dummy_commercial: - cargo new --lib commercial --name libninja_commercial + rm -rf $DIR/* \ No newline at end of file diff --git a/README.md b/README.md index 68229b1..342298b 100644 --- a/README.md +++ b/README.md @@ -5,19 +5,20 @@ The best way to see it in action is to see what it produces. [`plaid-rs`](https://github.com/libninjacom/plaid-rs) is generated entirely by Libninja. This includes: + - The client library itself -- Idiomatic interface, where required arguments are passed directly or as part of a struct, and optional arguments are included via method chaining. +- Idiomatic interface, where required arguments are passed directly or as part of a struct, and optional arguments are + included via method chaining. - Documentation is published online (docs.rs), and crate is published to registry (crates.io) - `examples/` folder containing an example for every API endpoint - The API client has the ability to record/replay requests, greatly aiding development for end users. -- API documentation is included in function docstrings, so it's available inline in the editor. The docstrings also include links to plaid's hosted API documentation. -- Github Action .yaml files to run tests and publish the package to package registries -- README that includes badges (that showcase a Green passing build) and usage examples +- API documentation is included in function docstrings, so it's available inline in the editor. The docstrings also + include links to plaid's hosted API documentation. All of that is created with this command: ```bash -libninja gen --lang rust --repo libninjacom/plaid-rs -o . Plaid ~/path/to/plaid/openapi.yaml +libninja gen Plaid ~/path/to/plaid/openapi.yaml ``` # Installation @@ -28,16 +29,14 @@ cargo install --git https://github.com/kurtbuilds/libninja Use the command line help to see required arguments & options when generating libraries. -The open source version builds client libraries for Rust. Libninja also supports other languages with a commercial license. Reach out at the email in author Github profile. - -# Advanced usage +# Usage ## Deriving traits for generated structs You can derive traits for the generated structs by passing them using one (or many) `--derive` arguments: ```bash -libninja gen --lang rust --repo libninjacom/plaid-rs --derive oasgen::OaSchema -o . Plaid ~/path/to/plaid/openapi.yaml +libninja gen --derive oasgen::OaSchema --derive faker::Dummy Plaid ~/path/to/plaid/openapi.yaml ``` Make sure to add the referenced crate(s) (and any necessary features) to your `Cargo.toml`: @@ -47,6 +46,7 @@ cargo add oasgen --features chrono ``` Then, the traits will be added to the `derive` attribute on the generated `model` and `request` structs: + ```rust use serde::{Serialize, Deserialize}; use super::Glossary; @@ -57,26 +57,32 @@ pub struct ListGlossariesResponse { } ``` -## Customizing generation further +## Customizing Files -There are two ways to customize codegen, first by modifying the OpenAPI spec, and second, using a file template system. +During codegen, `libninja` will examine the target directory for files or content it should keep (effectively, using the +existing crate as a template). It looks for two directives, `libninja: static` and `libninja: after`. -During codegen, `libninja` will look for a directory called `template`, and use files there to customize the generated code. +If `libninja` encounters `libninja: static`, it will skip generation entirely, and keep the existing file as-is. -For example, if libninja generates `src/model/user.rs`, it will check for `template/src/model/user.rs`. +If `libninja` encounters `libninja: after`, it will overwrite any code encountered after that directive, replacing +it with the generated code. Generally, use this when you want to customize the imports or add additional structs or +functions to the file. -If it's found, `libninja` will try to intelligently interpolate generated code with the templated file. The specific order of items in the output file will be: +Importantly, libninja removes outdated code, so any handwritten file is not marked with `libninja: static` will be +removed. -1. codegen docstring -2. codegen imports -3. template imports -4. template items (structs, enums, traits, impl, etc) -5. codegen items +### Customize the OpenAPI spec -Alternatively, if the string `libninja: static` is found in the file template, it will ignore all codegen for that file, and pass the template file through as-is. +Most OpenAPI specs you encounter in the real world are not perfect, and sometimes are entirely broken. You can manually +modify the script or write a script to do so. -# Development +The preferred way is to write a script to modify the spec. You can use `serde` to deserialize the spec, modify it, and +then serialize it back to disk. This way, you can rerun the same modifications every time the spec changes. This script +can be a standalone crate (which can live in the same repo), or part of build.rs. -If you run into errors about a missing `commericial` package, run the command `just dummy_commercial` to create a dummy -package. +If the spec is invalid and doesn't deserialize, deserialize it as a `serde_json::Value` to make it compliant, and then +deserialize again (serde_json::from_value) into a OpenAPI spec for further processing. +Alternatively, manually modifying the script is great for one-off changes, but your target spec might be evolving over +time. You can use `git` diffings to partially address this, but it's not ideal. +If you go this route, the [openapi](https://github.com/kurtbuilds/openapiv3_cli) cli tool can help. \ No newline at end of file diff --git a/codegen_rust/Cargo.toml b/codegen_rust/Cargo.toml new file mode 100644 index 0000000..93736de --- /dev/null +++ b/codegen_rust/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "codegen_rust" +version = "0.1.0" +edition = "2021" + +[dependencies] +libninja_hir = { path = "../hir" } +libninja_mir_rust = { path = "../mir_rust" } +anyhow = "1.0.89" +quote = "1.0.35" +libninja_mir = { path = "../mir" } +proc-macro2 = "1.0.86" +convert_case = "0.6.0" +regex = "1.10.6" \ No newline at end of file diff --git a/codegen_rust/src/example.rs b/codegen_rust/src/example.rs new file mode 100644 index 0000000..4cfc279 --- /dev/null +++ b/codegen_rust/src/example.rs @@ -0,0 +1,71 @@ +use hir::HirSpec; + +pub fn generate_example( + operation: &Operation, + opt: &Config, + spec: &HirSpec, +) -> anyhow::Result { + let args = operation.function_args(Language::Rust); + let declarations = args + .iter() + .map(|p| { + let ident = p.name.to_rust_ident(); + let value = to_rust_example_value(&p.ty, &p.name, spec, true)?; + Ok(quote! { + let #ident = #value; + }) + }) + .collect::, anyhow::Error>>()?; + let fn_args = args.iter().map(|p| p.name.to_rust_ident()); + let optionals = operation + .optional_args() + .into_iter() + .map(|p| { + let ident = p.name.to_rust_ident(); + let value = to_rust_example_value(&p.ty, &p.name, spec, true)?; + Ok(quote! { + .#ident(#value) + }) + }) + .collect::, anyhow::Error>>()?; + let qualified_client = format!( + "{}::{}", + opt.package_name, + opt.client_name().to_rust_struct() + ); + let mut imports = vec![ + Import::package(&qualified_client), + Import::package(&format!("{}::model::*", opt.package_name)), + ]; + if operation.use_required_struct(Language::Rust) { + let struct_name = operation + .required_struct_name() + .to_rust_struct() + .to_string(); + imports.push(Import::package(format!( + "{}::request::{}", + opt.package_name, struct_name + ))); + } + let operation = operation.name.to_rust_ident(); + let client = opt.client_name().to_rust_struct(); + let mut main = function!(async main()); + main.body = quote! { + let client = #client::from_env(); + #(#declarations)* + let response = client.#operation(#(#fn_args),*) + #(#optionals)* + .await + .unwrap(); + println!("{:#?}", response); + }; + main.annotations.push("tokio::main".to_string()); + + let example = File { + imports, + functions: vec![main], + ..File::default() + }; + let code = example.to_rust_code(); + Ok(format_code(code)) +} diff --git a/codegen_rust/src/lib.rs b/codegen_rust/src/lib.rs new file mode 100644 index 0000000..8d05f5f --- /dev/null +++ b/codegen_rust/src/lib.rs @@ -0,0 +1,83 @@ +mod example; +pub mod request; + +use hir::Config; +use hir::{HirSpec, Language}; +use std::collections::HashMap; +use std::fs; +use std::fs::File; +use std::path::Path; +use proc_macro2::TokenStream; +use anyhow::Result; +use mir_rust::format_code; + +pub fn generate_rust_library(spec: HirSpec, config: Config) -> Result<()> { + let src_path = opts.dest_path.join("src"); + + // Prepare the HIR Spec. + let extras = calculate_extras(&spec); + + // if src doesn't exist that's fine + let _ = fs::remove_dir_all(&src_path); + fs::create_dir_all(&src_path)?; + + // If there's nothing in cargo.toml, you want to prompt for it here. + // Then pass it back in. + // But you only need it if you're generating the README and/or Cargo.toml + let mut context = HashMap::::new(); + if !opts.dest_path.join("README.md").exists() || !opts.dest_path.join("Cargo.toml").exists() { + if let Some(github_repo) = &opts.github_repo { + context.insert("github_repo".to_string(), github_repo.to_string()); + } else { + println!( + "Because this is a first-time generation, please provide additional information." + ); + print!("Please provide a Github repo name (e.g. libninja/plaid-rs): "); + let github_repo: String = read!("{}\n"); + context.insert("github_repo".to_string(), github_repo); + } + } + let version = cargo_toml::update_cargo_toml(&extras, &opts, &context)?; + let build_examples = opts.build_examples; + let opts = Config { + package_name: opts.package_name, + service_name: opts.service_name, + language: opts.language, + package_version: version, + config: opts.config, + dest: opts.dest_path, + derives: opts.derive, + }; + write_model_module(&spec, &opts)?; + write_request_module(&spec, &opts)?; + write_lib_rs(&spec, &extras, &opts)?; + write_serde_module_if_needed(&extras, &opts.dest)?; + + let spec = add_operation_models(opts.language, spec)?; + + if build_examples { + write_examples(&spec, &opts)?; + } + + let tera = prepare_templates(); + let mut template_context = create_context(&opts, &spec); + template_context.insert( + "client_docs_url", + &format!("https://docs.rs/{}", opts.package_name), + ); + if let Some(github_repo) = context.get("github_repo") { + template_context.insert("github_repo", github_repo); + } + copy_builtin_files(&opts.dest, &opts.language.to_string(), &["src"])?; + copy_builtin_templates(&opts, &tera, &template_context)?; + copy_from_target_templates(&opts.dest)?; + Ok(()) +} + + +pub fn write_rust(path: &Path, tokens: TokenStream) -> Result<()> { + let code = format_code(tokens); + let existing_content = fs::read_to_string(path).unwrap_or_default(); + if existing_content.st + +} \ No newline at end of file diff --git a/libninja/src/rust/request.rs b/codegen_rust/src/request.rs similarity index 91% rename from libninja/src/rust/request.rs rename to codegen_rust/src/request.rs index 2d2eed5..524b7ea 100644 --- a/libninja/src/rust/request.rs +++ b/codegen_rust/src/request.rs @@ -6,17 +6,14 @@ use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use regex::Captures; -use hir::{HirSpec, Operation}; -use hir::{Language, Location, Parameter}; -use ln_core::PackageConfig; -use mir::Doc; -use mir::Ty; -use mir::{Class, Field, FnArg2, Function, Ident, Visibility}; -use mir_rust::{ToRustCode, ToRustIdent}; +use hir::{Config, HirSpec, Language, Location, Operation, Parameter}; +use mir::{Arg, Class, Doc, Field, Function, Ident, Ty, Visibility}; +use mir_rust::ToRustCode; -use crate::rust::codegen::ToRustType; +use mir_rust::ToRustType; use mir_rust::derives_to_tokens; +use mir_rust::ToRustIdent; pub fn assign_inputs_to_request(inputs: &[Parameter]) -> TokenStream { let params_except_path: Vec<&Parameter> = inputs @@ -173,8 +170,8 @@ pub fn build_request_struct_builder_methods(operation: &Operation) -> Vec Vec Vec> { let mut instance_fields = build_struct_fields(&operation.parameters, false); @@ -212,10 +209,10 @@ On request success, this will return a [`{response}`]."#, let mut result = vec![Class { name: operation.request_struct_name().to_rust_struct(), doc, - instance_fields, + fields: instance_fields, lifetimes: vec![], vis: Visibility::Public, - decorators: vec![quote! {#[derive(Debug, Clone, Serialize, Deserialize #derives)]}], + attributes: vec![quote! {#[derive(Debug, Clone, Serialize, Deserialize #derives)]}], ..Class::default() }]; @@ -231,7 +228,7 @@ On request success, this will return a [`{response}`]."#, }; result.push(Class { name: operation.required_struct_name().to_rust_struct(), - instance_fields: { + fields: { let required = operation .parameters .iter() @@ -248,7 +245,7 @@ On request success, this will return a [`{response}`]."#, result } -pub fn build_request_structs(spec: &HirSpec, opt: &PackageConfig) -> Vec> { +pub fn build_request_structs(spec: &HirSpec, opt: &Config) -> Vec> { let mut result = vec![]; for operation in &spec.operations { result.extend(build_request_struct(operation, spec, opt)); @@ -256,7 +253,7 @@ pub fn build_request_structs(spec: &HirSpec, opt: &PackageConfig) -> Vec TokenStream { +pub fn generate_request_model_rs(spec: &HirSpec, opt: &Config) -> TokenStream { let classes = build_request_structs(spec, opt); let mut request_structs = classes .into_iter() diff --git a/core/Cargo.toml b/core/Cargo.toml deleted file mode 100644 index 2abebc4..0000000 --- a/core/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "libninja_core" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[lib] -name = "libninja_core" - -[dependencies] -anyhow = "1.0.71" -clap = { version = "4.3.11", features = ["derive"] } -convert_case = "0.6.0" -openapiv3-extended = "6" -serde = { version = "1.0.166", features = ["derive"] } -quote = "1.0.29" -serde_json = "1.0.100" -proc-macro2 = "1.0.63" -indexmap = "2.0" -syn = "2.0" -libninja_mir = { path = "../mir" } -include_dir = "0.7.3" -tera = "1.19.0" -libninja_hir = { path = "../hir" } -tracing = "0.1.40" -tracing-ez = "0.3.0" -regex-lite = "0.1.5" - -[dev-dependencies] -serde_yaml = "0.9.25" -http = "1.1.0" diff --git a/core/Justfile b/core/Justfile deleted file mode 100644 index 1befe41..0000000 --- a/core/Justfile +++ /dev/null @@ -1,12 +0,0 @@ - -run: - cargo run - -test *ARGS: - cargo test -- $(ARGS) - -build: - cargo build - -install: - cargo install --path . diff --git a/core/src/lib.rs b/core/src/lib.rs deleted file mode 100644 index f3bd6ad..0000000 --- a/core/src/lib.rs +++ /dev/null @@ -1,13 +0,0 @@ -#![allow(unused)] - -pub use extractor::extract_spec; -pub use fs::*; -pub use options::*; -pub use template::*; - -pub mod extractor; -pub mod fs; -mod options; -pub mod sanitize; -mod template; -pub mod util; diff --git a/core/src/options.rs b/core/src/options.rs deleted file mode 100644 index 6f72b51..0000000 --- a/core/src/options.rs +++ /dev/null @@ -1,87 +0,0 @@ -use convert_case::{Case, Casing}; -use hir::Language; -use mir::{literal, Literal}; -use proc_macro2::TokenStream; -use quote::quote; -use std::path::PathBuf; - -#[derive(Debug, Clone, Default)] -pub struct ConfigFlags { - /// Only for Rust. Adds ormlite::TableMeta flags to the code. - pub ormlite: bool, - /// Only for Rust (for now). Adds fake::Dummy flags to the code. - pub fake: bool, -} - -#[derive(Debug, Clone)] -pub struct PackageConfig { - // e.g. petstore-api - pub package_name: String, - // eg PetStore - pub service_name: String, - - pub language: Language, - - pub package_version: String, - - pub config: ConfigFlags, - - pub dest: PathBuf, - - pub derives: Vec, -} - -impl PackageConfig { - pub fn user_agent(&self) -> Literal { - literal(format!( - "{}/{}/{}", - self.package_name, - self.language.to_string(), - self.package_version - )) - } - - pub fn client_name(&self) -> String { - format!("{} Client", self.service_name) - } - - pub fn async_client_name(&self) -> String { - format!("Async {} Client", self.service_name) - } - - pub fn authenticator_name(&self) -> String { - format!("{} Auth", self.service_name) - } - - pub fn env_var(&self, name: &str) -> Literal { - literal(format!( - "{}_{}", - self.service_name.to_case(Case::ScreamingSnake), - name.to_case(Case::ScreamingSnake) - )) - } - - pub fn get_file_template(&self, path: &str) -> Option { - let path = self.dest.join("template").join(path); - std::fs::read_to_string(path).ok() - } -} - -pub struct OutputConfig { - pub dest_path: PathBuf, - pub build_examples: bool, - // e.g. petstore-api - pub package_name: String, - // eg PetStore - pub service_name: String, - - pub language: Language, - - pub config: ConfigFlags, - - pub github_repo: Option, - - pub version: Option, - - pub derive: Vec, -} diff --git a/core/src/sanitize.rs b/core/src/sanitize.rs deleted file mode 100644 index c00800e..0000000 --- a/core/src/sanitize.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::ops::Deref; -use std::sync::OnceLock; - -use regex_lite::Regex; - -fn is_restricted(s: &str) -> bool { - ["type", "use", "ref", "self", "match"].contains(&s) -} - -pub fn sanitize(s: &str) -> String { - let r = if s.contains('(') { - static NO_PAREN: OnceLock = OnceLock::new(); - let re = NO_PAREN.get_or_init(|| Regex::new(r"\([^)]*\)").unwrap()); - re.replace_all(s, "") - } else { - s.into() - }; - let s = r.trim().to_string(); - // if is_restricted(&s) { - // format!("{}_", s) - if s.chars().next().unwrap().is_numeric() { - format!("_{}", s) - } else { - s - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_sanitize() { - assert_eq!(sanitize("coupon type (foo)"), "coupon type"); - } -} diff --git a/core/src/template.rs b/core/src/template.rs deleted file mode 100644 index d059c9f..0000000 --- a/core/src/template.rs +++ /dev/null @@ -1,78 +0,0 @@ -use tera::Context; - -use hir::HirSpec; - -use crate::{write_file, PackageConfig}; - -pub static TEMPLATE_DIR: include_dir::Dir<'_> = - include_dir::include_dir!("$CARGO_MANIFEST_DIR/template"); - -pub fn copy_builtin_templates( - opts: &PackageConfig, - tera: &tera::Tera, - context: &Context, -) -> anyhow::Result<()> { - let project_template = opts.language.to_string(); - TEMPLATE_DIR - .get_dir(&project_template) - .unwrap() - .files() - .filter(|f| f.path().extension().unwrap_or_default() == "j2") - .for_each(|f| { - let path = opts.dest.join( - f.path() - .strip_prefix(&project_template) - .unwrap() - .with_extension(""), - ); - if path.exists() { - return; - } - let content = tera.render(f.path().to_str().unwrap(), context).unwrap(); - write_file(&path, &content).unwrap(); - }); - Ok(()) -} - -pub fn add_templates(tera: &mut tera::Tera, dir: &include_dir::Dir<'static>) { - for dir in dir.dirs() { - for file in dir.files() { - let path = file.path(); - tera.add_raw_template(path.to_str().unwrap(), file.contents_utf8().unwrap()) - .unwrap(); - } - } -} - -pub fn prepare_templates() -> tera::Tera { - let mut tera = tera::Tera::default(); - add_templates(&mut tera, &TEMPLATE_DIR); - tera -} - -/// Create context for j2 files. -pub fn create_context(opts: &PackageConfig, spec: &HirSpec) -> Context { - let mut context = Context::new(); - context.insert("package_name", &opts.package_name); - context.insert("lang", &opts.language.to_string()); - context.insert( - "short_description", - &format!( - "{name} client, generated from the OpenAPI spec.", - name = opts.service_name - ), - ); - context.insert("env_vars", &spec.env_vars(&opts.service_name)); - if let Some(url) = &spec.api_docs_url { - context.insert("api_docs_url", url); - } - context -} - -pub fn get_template_file(path: &str) -> &'static str { - TEMPLATE_DIR - .get_file(path) - .expect(&format!("{} not found in TEMPLATE_DIR", path)) - .contents_utf8() - .unwrap() -} diff --git a/core/template/LICENSE b/core/template/LICENSE deleted file mode 100644 index f9ee781..0000000 --- a/core/template/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License - -Copyright (c) 2010-2018 Stripe (http://stripe.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/core/template/base/README.md.j2 b/core/template/base/README.md.j2 deleted file mode 100644 index 116d515..0000000 --- a/core/template/base/README.md.j2 +++ /dev/null @@ -1,43 +0,0 @@ -
- -

- - Stars - - - Build Status - - {% block additional_shields %} - {% endblock %} -

- -{{ short_description }} - -# Usage - -See examples/ directory for usage. - -This example loads configuration from environment variables, specifically: -{% for var in env_vars %} -* `{{ var }}` -{% endfor %} - -{% block installation %} -{% endblock %} - -# Documentation - -{% if api_docs_url %} -* [API Documentation]({{ api_docs_url }}) -{% endif %} -{% if client_docs_url %} -* [Client Library Documentation]({{ client_docs_url }}) -{% endif %} - -You can see working examples of every API call in the `examples/` directory. - -# Contributing - -Contributions are welcome! - -*Library created with [Libninja](https://www.libninja.com).* \ No newline at end of file diff --git a/core/template/rust/.github/workflows/ci.yaml b/core/template/rust/.github/workflows/ci.yaml deleted file mode 100644 index 45f8f1f..0000000 --- a/core/template/rust/.github/workflows/ci.yaml +++ /dev/null @@ -1,41 +0,0 @@ -name: ci - -on: - workflow_dispatch: { } - push: { } - -jobs: - - test: - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - uses: Swatinem/rust-cache@v1 - - run: cargo install just - - run: just test - - publish: - runs-on: ubuntu-latest - permissions: - contents: read - - if: >- - ((github.event_name == 'workflow_dispatch') || (github.event_name == 'push')) && - startsWith(github.ref, 'refs/tags/v') - needs: [ "test" ] - - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - - uses: katyo/publish-crates@v1 - with: - registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} \ No newline at end of file diff --git a/core/template/rust/.gitignore b/core/template/rust/.gitignore deleted file mode 100644 index ffa3bbd..0000000 --- a/core/template/rust/.gitignore +++ /dev/null @@ -1 +0,0 @@ -Cargo.lock \ No newline at end of file diff --git a/core/template/rust/CODE_OF_CONDUCT.md b/core/template/rust/CODE_OF_CONDUCT.md deleted file mode 100644 index 4e9f121..0000000 --- a/core/template/rust/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,63 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to make participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies within all project spaces, and it also applies when -an individual is representing the project or its community in public spaces. -Examples of representing a project or community include using an official -project e-mail address, posting via an official social media account, or acting -as an appointed representative at an online or offline event. Representation of -a project may be further defined and clarified by project maintainers. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq \ No newline at end of file diff --git a/core/template/rust/Cargo.toml.j2 b/core/template/rust/Cargo.toml.j2 deleted file mode 100644 index 25ec4f3..0000000 --- a/core/template/rust/Cargo.toml.j2 +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "{{ package_name }}" -edition = "2021" -description = "{{ short_description }}" -readme = "README.md" -license = "MIT" -documentation = "https://docs.rs/{{ package_name }}" -homepage = "https://github.com/{{ github_repo }}" -repository = "https://github.com/{{ github_repo }}" - -[workspace] - -[lib] -doctest = false - -[dependencies] -httpclient = "0.19.0" -serde = { version = "1.0.137", features = ["derive"] } -serde_json = "1.0.81" -futures = "0.3.25" -chrono = { version = "0.4.26", features = ["serde"] } - -[dev-dependencies] -tokio = { version = "1.18.2", features = ["full"] } diff --git a/core/template/rust/Justfile b/core/template/rust/Justfile deleted file mode 100644 index 55cc5df..0000000 --- a/core/template/rust/Justfile +++ /dev/null @@ -1,58 +0,0 @@ -set dotenv-load := true - -help: - @just --list --unsorted - -build: - cargo build -alias b := build - -run *args: - cargo run {{args}} -alias r := run - -release: - cargo build --release - -install: - cargo install --path . - -bootstrap: - cargo install cargo-edit - -test *args: - cargo test {{args}} - -check: - cargo check -alias c := check - -fix: - cargo clippy --fix - -# Bump version. level=major,minor,patch -version level: - git diff-index --exit-code HEAD > /dev/null || ! echo You have untracked changes. Commit your changes before bumping the version. - cargo set-version --bump {{level}} - cargo update # This bumps Cargo.lock - VERSION=$(rg "version = \"([0-9.]+)\"" -or '$1' Cargo.toml | head -n1) && \ - git commit -am "Bump version {{level}} to $VERSION" && \ - git tag v$VERSION && \ - git push origin v$VERSION - git push - -publish: - cargo publish - -patch: test - just version patch - just publish - -doc: - cargo doc --no-deps --open - -test-full: - #!/usr/bin/env bash -euxo pipefail - for file in $(ls examples); do - cargo run --example "$(basename "$file" .rs)" - done \ No newline at end of file diff --git a/core/template/rust/LICENSE b/core/template/rust/LICENSE deleted file mode 100644 index f9ee781..0000000 --- a/core/template/rust/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License - -Copyright (c) 2010-2018 Stripe (http://stripe.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/core/template/rust/README.md.j2 b/core/template/rust/README.md.j2 deleted file mode 100644 index e73a411..0000000 --- a/core/template/rust/README.md.j2 +++ /dev/null @@ -1,20 +0,0 @@ -{% extends "base/README.md.j2" %} -{% block additional_shields %} - - Downloads - - - Crates.io - -{% endblock %} - -{% block installation %} -# Installation - -Add this to your Cargo.toml: - -```toml -[dependencies] -{{ package_name }} = ".." -``` -{% endblock %} diff --git a/core/template/rust/src/lib.rs b/core/template/rust/src/lib.rs deleted file mode 100644 index f29668a..0000000 --- a/core/template/rust/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -#![allow(non_camel_case_types)] -#![allow(unused)] -pub mod model; -pub mod request; -pub use httpclient::{Error, Result, InMemoryResponseExt}; -use std::sync::{Arc, OnceLock}; -use std::borrow::Cow; -use crate::model::*; diff --git a/core/template/rust/src/model.rs b/core/template/rust/src/model.rs deleted file mode 100644 index 4b8e757..0000000 --- a/core/template/rust/src/model.rs +++ /dev/null @@ -1 +0,0 @@ -use serde::{Serialize, Deserialize}; \ No newline at end of file diff --git a/core/template/rust/src/request.rs b/core/template/rust/src/request.rs deleted file mode 100644 index 1b175fa..0000000 --- a/core/template/rust/src/request.rs +++ /dev/null @@ -1,2 +0,0 @@ -use serde_json::json; -use crate::model::*; \ No newline at end of file diff --git a/core/tests/test_extractor.rs b/core/tests/test_extractor.rs deleted file mode 100644 index e810287..0000000 --- a/core/tests/test_extractor.rs +++ /dev/null @@ -1,37 +0,0 @@ -use openapiv3 as oa; - -use hir::Record; -use libninja_core::extract_spec; -use mir::Ty; - -#[test] -fn test_post_translate() { - let s = include_str!("../../test_specs/deepl.yaml"); - let mut openapi: oa::OpenAPI = serde_yaml::from_str(s).unwrap(); - openapi.paths.paths.retain(|k, _| k == "/translate"); - - let hir = extract_spec(&openapi).unwrap(); - let op = hir.get_operation("translateText").unwrap(); - let Ty::Model(name) = &op.ret else { - panic!("Expected model type"); - }; - let Record::Struct(s) = hir.get_record(name).unwrap() else { - panic!("Expected struct"); - }; - let z = &s.fields["translations"]; - let Ty::Array(ty) = &z.ty else { - panic!("Expected array: {:?}", z.ty); - }; - dbg!(&hir); - assert!( - matches!(ty.as_ref(), Ty::Model(m) if m == "Translation"), - "Return value of translateText operation should be Vec. Instead: {:?}", - ty - ); - let p = op - .parameters - .iter() - .find(|p| p.name == "target_lang") - .unwrap(); - assert!(matches!(&p.ty, Ty::Model(m) if m == "TargetLanguageText")); -} diff --git a/hir/src/config.rs b/hir/src/config.rs new file mode 100644 index 0000000..11bc197 --- /dev/null +++ b/hir/src/config.rs @@ -0,0 +1,43 @@ +use crate::Language; +use convert_case::{Case, Casing}; +use mir::Literal; +use std::path::PathBuf; + +#[derive(Debug, Clone)] +pub struct Config { + pub name: String, + pub language: Language, + pub dest: PathBuf, + pub derives: Vec, +} + +impl Config { + pub fn user_agent(&self) -> Literal { + Literal(format!( + "{}/{}", + self.name, + self.language.to_string(), + // self.package_version + )) + } + + pub fn client_name(&self) -> String { + format!("{}Client", self.name) + } + + pub fn async_client_name(&self) -> String { + format!("Async{}Client", self.name) + } + + pub fn authenticator_name(&self) -> String { + format!("{}Auth", self.name) + } + + pub fn env_var(&self, name: &str) -> Literal { + Literal(format!( + "{}_{}", + self.name.to_case(Case::ScreamingSnake), + name.to_case(Case::ScreamingSnake) + )) + } +} diff --git a/hir/src/lang.rs b/hir/src/lang.rs index 4849fe1..53e5552 100644 --- a/hir/src/lang.rs +++ b/hir/src/lang.rs @@ -5,18 +5,18 @@ use std::fmt::Display; #[derive(Eq, PartialEq, Copy, Clone, Debug, ValueEnum)] pub enum Language { Rust, - Python, - Typescript, - Golang, + // Python, + // Typescript, + // Golang, } impl Display for Language { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let str = match self { Language::Rust => "rust", - Language::Python => "python", - Language::Typescript => "typescript", - Language::Golang => "go", + // Language::Python => "python", + // Language::Typescript => "typescript", + // Language::Golang => "go", } .to_string(); write!(f, "{}", str) @@ -29,9 +29,9 @@ impl std::str::FromStr for Language { fn from_str(s: &str) -> Result { match s { "rust" => Ok(Language::Rust), - "python" => Ok(Language::Python), - "typescript" => Ok(Language::Typescript), - "go" => Ok(Language::Golang), + // "python" => Ok(Language::Python), + // "typescript" => Ok(Language::Typescript), + // "go" => Ok(Language::Golang), _ => Err(anyhow::anyhow!("Unknown generator: {}", s)), } } diff --git a/hir/src/lib.rs b/hir/src/lib.rs index 5cb564f..4503de9 100644 --- a/hir/src/lib.rs +++ b/hir/src/lib.rs @@ -9,11 +9,16 @@ use anyhow::Result; use convert_case::{Case, Casing}; use openapiv3 as oa; +pub use config::Config; pub use lang::*; +use mir::parameter::ParamKey; +use mir::Doc; use mir::Ty; -use mir::{Doc, ParamKey}; +pub use operation::*; +mod config; mod lang; +mod operation; /// Parameter is an input to an OpenAPI operation. #[derive(Debug, Clone)] @@ -362,138 +367,6 @@ impl HirSpec { } } -#[derive(Debug, Clone)] -pub struct Operation { - pub name: String, - pub doc: Option, - pub parameters: Vec, - pub ret: Ty, - pub path: String, - pub method: String, -} - -impl Operation { - // Mostly for Go - pub fn flat_package_name(&self) -> String { - self.name.to_case(Case::Flat) - } - - pub fn file_name(&self) -> String { - self.name.to_case(Case::Snake) - } - - pub fn request_struct_name(&self) -> String { - format!("{}Request", self.name) - } - - pub fn required_struct_name(&self) -> String { - format!("{}Required", self.name) - } - - pub fn crowded_args(&self) -> bool { - self.parameters.iter().filter(|p| !p.optional).count() > 3 - } - - pub fn has_response(&self) -> bool { - !matches!(self.ret, Ty::Unit) - } - - pub fn optional_args(&self) -> Vec<&Parameter> { - self.parameters.iter().filter(|p| p.optional).collect() - } - - pub fn required_args(&self) -> Vec<&Parameter> { - self.parameters.iter().filter(|p| !p.optional).collect() - } - - pub fn parameters_by_header_query_body( - &self, - ) -> (Vec<&Parameter>, Vec<&Parameter>, Vec<&Parameter>) { - let mut header = Vec::new(); - let mut query = Vec::new(); - let mut body = Vec::new(); - self.parameters.iter().for_each(|p| match p.location { - Location::Header => header.push(p), - Location::Query => query.push(p), - Location::Body => body.push(p), - _ => {} - }); - (header, query, body) - } - - pub fn use_required_struct(&self, sourcegen: Language) -> bool { - matches!(sourcegen, Language::Rust | Language::Golang | Language::Typescript if self.crowded_args()) - } - - /// Returns the params that are used as function arguments. - pub fn function_args(&self, generator: Language) -> Vec { - match generator { - Language::Golang if self.crowded_args() => { - vec![Parameter { - name: "args".to_string(), - ty: Ty::model("Required"), - location: Location::Body, - optional: false, - doc: None, - example: None, - }] - } - _ if self.use_required_struct(generator) => { - vec![Parameter { - name: "args".to_string(), - ty: Ty::Model(self.required_struct_name()), - location: Location::Body, - optional: false, - doc: None, - example: None, - }] - } - _ => self - .parameters - .iter() - .filter(|p| !p.optional) - .cloned() - .collect(), - } - } - - pub fn required_struct(&self, sourcegen: Language) -> Struct { - let fields = match sourcegen { - Language::Typescript => self - .parameters - .iter() - .map(|p| (p.name.clone(), p.into())) - .collect(), - Language::Rust | Language::Golang => self - .parameters - .iter() - .filter(|p| !p.optional) - .map(|p| (p.name.clone(), p.into())) - .collect(), - _ => unimplemented!(), - }; - Struct { - nullable: false, - name: self.required_struct_name(), - fields, - docs: None, - } - } -} - -impl Default for Operation { - fn default() -> Self { - Self { - name: "".to_string(), - doc: None, - parameters: Vec::new(), - ret: Ty::Unit, - path: "".to_string(), - method: "".to_string(), - } - } -} - impl From<&Parameter> for HirField { fn from(p: &Parameter) -> Self { Self { diff --git a/hir/src/operation.rs b/hir/src/operation.rs new file mode 100644 index 0000000..f9e0a41 --- /dev/null +++ b/hir/src/operation.rs @@ -0,0 +1,138 @@ +use crate::{Language, Location, Parameter, Struct}; +use convert_case::{Case, Casing}; +use mir::{Doc, Ty}; + +#[derive(Debug, Clone)] +pub struct Operation { + pub name: String, + pub doc: Option, + pub parameters: Vec, + pub ret: Ty, + pub path: String, + pub method: String, +} + +impl Operation { + // Mostly for Go + pub fn flat_package_name(&self) -> String { + self.name.to_case(Case::Flat) + } + + pub fn file_name(&self) -> String { + self.name.to_case(Case::Snake) + } + + pub fn request_struct_name(&self) -> String { + format!("{}Request", self.name) + } + + pub fn required_struct_name(&self) -> String { + format!("{}Required", self.name) + } + + pub fn crowded_args(&self) -> bool { + self.parameters.iter().filter(|p| !p.optional).count() > 3 + } + + pub fn has_response(&self) -> bool { + !matches!(self.ret, Ty::Unit) + } + + pub fn optional_args(&self) -> Vec<&Parameter> { + self.parameters.iter().filter(|p| p.optional).collect() + } + + pub fn required_args(&self) -> Vec<&Parameter> { + self.parameters.iter().filter(|p| !p.optional).collect() + } + + pub fn parameters_by_header_query_body( + &self, + ) -> (Vec<&Parameter>, Vec<&Parameter>, Vec<&Parameter>) { + let mut header = Vec::new(); + let mut query = Vec::new(); + let mut body = Vec::new(); + self.parameters.iter().for_each(|p| match p.location { + Location::Header => header.push(p), + Location::Query => query.push(p), + Location::Body => body.push(p), + _ => {} + }); + (header, query, body) + } + + pub fn use_required_struct(&self, sourcegen: Language) -> bool { + // matches!(sourcegen, Language::Rust | Language::Golang | Language::Typescript if self.crowded_args()) + matches!(sourcegen, Language::Rust) + } + + /// Returns the params that are used as function arguments. + pub fn function_args(&self, generator: Language) -> Vec { + match generator { + // Language::Golang if self.crowded_args() => { + // vec![Parameter { + // name: "args".to_string(), + // ty: Ty::model("Required"), + // location: Location::Body, + // optional: false, + // doc: None, + // example: None, + // }] + // } + _ if self.use_required_struct(generator) => { + vec![Parameter { + name: "args".to_string(), + ty: Ty::Model(self.required_struct_name()), + location: Location::Body, + optional: false, + doc: None, + example: None, + }] + } + _ => self + .parameters + .iter() + .filter(|p| !p.optional) + .cloned() + .collect(), + } + } + + pub fn required_struct(&self, sourcegen: Language) -> Struct { + let fields = match sourcegen { + // Language::Typescript => self + // .parameters + // .iter() + // .map(|p| (p.name.clone(), p.into())) + // .collect(), + Language::Rust + // | Language::Golang + => self + .parameters + .iter() + .filter(|p| !p.optional) + .map(|p| (p.name.clone(), p.into())) + .collect(), + // _ => unimplemented!(), + }; + Struct { + nullable: false, + name: self.required_struct_name(), + fields, + docs: None, + } + } +} + +impl Default for Operation { + fn default() -> Self { + Self { + name: "".to_string(), + doc: None, + parameters: Vec::new(), + ret: Ty::Unit, + path: "".to_string(), + method: "".to_string(), + } + } +} diff --git a/libninja/Cargo.toml b/libninja/Cargo.toml index 7b5846c..e4fc9b6 100644 --- a/libninja/Cargo.toml +++ b/libninja/Cargo.toml @@ -9,9 +9,6 @@ default-run = "libninja" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[features] -commercial = ["libninja_commercial"] - [dependencies] anyhow = { version = "1.0.71", features = ["backtrace"] } log = "0.4.19" @@ -30,7 +27,6 @@ tera = "1.19.0" include_dir = "0.7.3" regex = "1.9.0" indoc = "2.0.2" -cargo_toml = { git = "https://github.com/kurtbuilds/cargo_toml" } toml = "0.8.8" topo_sort = "0.4.0" url = "2.4.0" @@ -38,16 +34,14 @@ http = "1.0.0" strum = "0.26.1" semver = "1.0.17" indexmap = "2.0" -libninja_macro = { path = "../macro" } -ln_core = { path = "../core", package = "libninja_core" } libninja_mir = { path = "../mir" } libninja_hir = { path = "../hir" } -libninja_mir_rust = { path = "../mir_rust" } -libninja_commercial = { path = "../commercial", optional = true } +codegen_rust = { path = "../codegen_rust" } ignore = "0.4.21" text_io = "0.1.12" tracing-subscriber = "0.3.18" tracing = "0.1.40" +chrono = "0.4.38" [dev-dependencies] env_logger = "0.11.2" diff --git a/libninja/src/bin/libninja.rs b/libninja/src/bin/libninja.rs deleted file mode 100644 index 46fb3a2..0000000 --- a/libninja/src/bin/libninja.rs +++ /dev/null @@ -1,87 +0,0 @@ -#![allow(non_snake_case)] -#![allow(unused)] - -use anyhow::Result; -use convert_case::{Case, Casing}; -use clap::{Args, Parser, Subcommand}; -use ln_core::{OutputConfig, PackageConfig}; -use hir::Language; -use libninja::rust::generate_rust_library; -use std::path::Path; -use tracing::Level; -use libninja::command::*; -use tracing_subscriber::util::SubscriberInitExt; -use tracing_subscriber::layer::SubscriberExt; - -fn warn_if_not_found(command: &str) { - if std::process::Command::new(command) - .stderr(std::process::Stdio::null()) - .stdout(std::process::Stdio::null()) - .spawn().is_err() - { - eprintln!("Warning: {} not found. Some commands may fail.", command); - } -} - -#[derive(Parser, Debug)] -#[command(author, version, about)] -struct Cli { - #[command(subcommand)] - command: Command, - - #[clap(short, long, global = true)] - verbose: bool, -} - -#[derive(Subcommand, Debug)] -pub enum Command { - Gen(Generate), - /// OpenAPI specs can be split into multiple files. This command takes a path to the spec root, - /// and examines all files in its parent directory to coalesce the spec into one single file. - /// `gen` will not work if the spec is split into multiple files, so use this step first if the - /// spec is split. - Coalesce(Resolve), - /// Analyze the OpenAPI spec - Meta(Meta), -} - -fn main() -> Result<()> { - let cli = Cli::parse(); - let level = if cli.verbose { Level::DEBUG } else { Level::INFO }; - tracing_subscriber::registry() - .with(tracing_subscriber::fmt::layer() - .without_time() - ) - .with(tracing_subscriber::filter::Targets::new() - .with_target(env!("CARGO_BIN_NAME"), level) - .with_target("libninja_mir", level) - .with_target("libninja_hir", level) - .with_target("ln_core", level) - .with_target("ln_macro", level) - ) - .init(); - - match cli.command { - Command::Gen(generate) => { - use Language::*; - match generate.language { - Rust => { - }, - Python => { - warn_if_not_found("pdm"); - warn_if_not_found("black"); - }, - Typescript => { - warn_if_not_found("pnpm"); - warn_if_not_found("prettier") - }, - Golang => { - warn_if_not_found("gofmt"); - }, - } - generate.run() - }, - Command::Coalesce(resolve) => resolve.run(), - Command::Meta(meta) => meta.run(), - } -} diff --git a/libninja/src/command.rs b/libninja/src/command.rs deleted file mode 100644 index 6f5e69a..0000000 --- a/libninja/src/command.rs +++ /dev/null @@ -1,22 +0,0 @@ -mod generate; -mod resolve; -mod meta; - -use anyhow::anyhow; -pub use generate::*; -pub use resolve::*; -pub use meta::*; - -pub trait Success { - fn ok(&self) -> anyhow::Result<()>; -} - -impl Success for std::process::ExitStatus { - fn ok(&self) -> anyhow::Result<()> { - if self.success() { - Ok(()) - } else { - Err(anyhow!("Process exited with code: {:?}", self)) - } - } -} diff --git a/libninja/src/command/generate.rs b/libninja/src/command/generate.rs index 1767501..21f3a6d 100644 --- a/libninja/src/command/generate.rs +++ b/libninja/src/command/generate.rs @@ -1,69 +1,34 @@ -use crate::{generate_library, read_spec, Language, OutputConfig, PackageConfig}; -use anyhow::Result; -use clap::{Args, ValueEnum}; +use crate::{rust, Language}; +use anyhow::{anyhow, Result}; +use clap::Args; use convert_case::{Case, Casing}; -use ln_core::ConfigFlags; +use openapiv3::{OpenAPI, VersionedOpenAPI}; +use std::fs::File; use std::path::{Path, PathBuf}; -use std::process::Output; -use tracing::debug; - -#[derive(ValueEnum, Debug, Clone, Copy)] -pub enum Config { - /// Only used by Rust. Adds ormlite::TableMeta flags to the code. - Ormlite, - /// Only used by Rust (for now). Adds fake::Dummy flags to the code. - Fake, -} - -fn build_config(configs: &[Config]) -> ConfigFlags { - let mut config = ConfigFlags::default(); - for c in configs { - match c { - Config::Ormlite => config.ormlite = true, - Config::Fake => config.fake = true, - } - } - config -} #[derive(Args, Debug)] pub struct Generate { /// Service name. - #[clap(short, long = "lang")] + #[clap(short, long = "lang", default_value = "rust")] pub language: Language, /// Toggle whether to generate examples. /// Defaults to true - #[clap(long)] - examples: Option, + #[clap(long, default_value = "true")] + examples: bool, #[clap(short, long)] output_dir: Option, + /// List of additional namespaced traits to derive on generated structs. #[clap(long)] - version: Option, - - /// config options - #[clap(short, long)] - config: Vec, - - /// Repo (e.g. libninjacom/plaid-rs) - #[clap(long)] - repo: Option, - - /// Package name. Defaults to the service name. - #[clap(short, long = "package")] - package_name: Option, + derive: Vec, /// The "service" name. E.g. if we want to generate a library for the Stripe API, this would be "Stripe". name: String, /// Path to the OpenAPI spec file. spec_filepath: String, - - /// List of additional namespaced traits to derive on generated structs. - #[clap(long)] - derive: Vec, } impl Generate { @@ -77,7 +42,7 @@ impl Generate { let spec = read_spec(&path)?; generate_library( spec, - OutputConfig { + Config { dest_path: PathBuf::from(output_dir), config: build_config(&self.config), language: self.language, @@ -91,3 +56,28 @@ impl Generate { ) } } + +pub fn read_spec(path: &Path) -> Result { + let file = File::open(path).map_err(|_| anyhow!("{:?}: File not found.", path))?; + let ext = path + .extension() + .map(|s| s.to_str().expect("Extension isn't utf8")) + .unwrap_or_else(|| "yaml"); + let openapi: VersionedOpenAPI = match ext { + "yaml" => serde_yaml::from_reader(file)?, + "json" => serde_json::from_reader(file)?, + _ => panic!("Unknown file extension"), + }; + let openapi = openapi.upgrade(); + Ok(openapi) +} + +pub fn generate_library(spec: OpenAPI, opts: Config) -> Result<()> { + let spec = extract_spec(&spec)?; + match opts.language { + Language::Rust => rust::generate_rust_library(spec, opts), + // Language::Python => python::generate_library(spec, opts), + // Language::Typescript => typescript::generate_library(spec, opts), + // Language::Golang => go::generate_library(spec, opts), + } +} diff --git a/libninja/src/command/init.rs b/libninja/src/command/init.rs new file mode 100644 index 0000000..a3d8268 --- /dev/null +++ b/libninja/src/command/init.rs @@ -0,0 +1,26 @@ +use clap::Parser; +use convert_case::{Case, Casing}; +use std::env::set_current_dir; +use std::process::Command; + +#[derive(Parser, Debug)] +pub struct Init { + /// The "service" name. E.g. if we want to generate a library for the Stripe API, this would be "Stripe". + name: String, +} + +impl Init { + pub fn run(self) -> Result<()> { + let name = self.name.to_case(Case::Snake); + Command::new("cargo").arg("new").arg(&name).output()?; + set_current_dir(&name)?; + Command::new("cargo") + .arg("add") + .arg("httpclient") + .output()?; + Command::new("cargo") + .args(["add", "serde", "-F", "derive"]) + .output()?; + Ok(()) + } +} diff --git a/libninja/src/command/meta.rs b/libninja/src/command/meta.rs deleted file mode 100644 index 43412f7..0000000 --- a/libninja/src/command/meta.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::path::PathBuf; - -use anyhow::Result; -use clap::Args; - -use hir::Language; -use ln_core::extract_spec; -use ln_core::extractor::add_operation_models; - -use crate::read_spec; -// use ln_core::child_schemas::ChildSchemas; -use crate::rust::calculate_extras; - -#[derive(Args, Debug)] -pub struct Meta { - service_name: String, - spec_filepath: String, - - #[clap(short, long = "lang")] - pub language: Option, - - #[clap(long)] - pub repo: Option, - - #[clap(short, long)] - pub output: Option, -} - -impl Meta { - pub fn run(self) -> Result<()> { - let path = PathBuf::from(self.spec_filepath); - let spec = read_spec(&path)?; - // let mut schema_lookup = HashMap::new(); - // spec.add_child_schemas(&mut schema_lookup); - // for (name, schema) in schema_lookup { - // println!("{}", name); - // } - let spec = extract_spec(&spec)?; - let spec = add_operation_models(Language::Rust, spec)?; - let extras = calculate_extras(&spec); - println!("{:#?}", extras); - // println!("{}", serde_json::to_string_pretty(&spec)?); - Ok(()) - } -} diff --git a/libninja/src/command/mod.rs b/libninja/src/command/mod.rs new file mode 100644 index 0000000..1e41d31 --- /dev/null +++ b/libninja/src/command/mod.rs @@ -0,0 +1,5 @@ +mod generate; +mod init; + +pub use generate::Generate; +pub use init::Init; diff --git a/libninja/src/command/resolve.rs b/libninja/src/command/resolve.rs deleted file mode 100644 index 8eee0dd..0000000 --- a/libninja/src/command/resolve.rs +++ /dev/null @@ -1,129 +0,0 @@ -use clap::Args; -use std::convert::Infallible; -use std::fmt::Formatter; -use std::path::{Path, PathBuf}; -use std::str::FromStr; - -use anyhow::{anyhow, Result}; -use serde_json::{json, Value}; - -fn walk(value: &mut Value, callback: impl Fn(&mut Value, String)) -> Result<()> { - fn _walk(value: &mut Value, callback: &impl Fn(&mut Value, String)) -> Result<()> { - match value { - Value::Array(arr) => { - for item in arr { - _walk(item, callback)?; - } - } - Value::Object(ref o) if o.len() == 1 && o.contains_key("$ref") => { - let path = o.get("$ref").unwrap().as_str().unwrap().to_string(); - callback(value, path) - } - Value::Object(o) => { - for (_, mut value) in o { - _walk(value, callback)?; - } - } - _ => {} - } - Ok(()) - } - _walk(value, &callback) -} - -#[derive(Debug)] -struct PathWithAnchor { - path: String, - anchor: Option, -} - -impl std::fmt::Display for PathWithAnchor { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.path)?; - if let Some(anchor) = &self.anchor { - write!(f, "#{}", anchor)?; - } - Ok(()) - } -} - -impl FromStr for PathWithAnchor { - type Err = Infallible; - - fn from_str(s: &str) -> std::result::Result { - let mut strs = s.splitn(2, '#') - .map(|s| s.to_string()); - Ok(Self { - path: strs.next().unwrap(), - anchor: strs.next(), - }) - } -} - -fn resolve_json_path(mut val: Value, path: &str) -> Result { - let mut split = path.split('/').skip(1); - for key in split { - match val { - Value::Object(ref mut o) => { - val = o.remove(key).ok_or_else(|| anyhow!("Failed to resolve JSON Pointer, key not found: {}, {}", path, key))?; - } - Value::Array(ref mut a) => { - let index = key.parse::()?; - if index >= a.len() { - return Err(anyhow!("Failed to resolve JSON Pointer, index out of range: {}, {}", path, key)); - } - val = a.remove(index); - } - _ => { - return Err(anyhow!("not an object or array")); - } - } - } - Ok(val) -} - - -//components/schemas -// schemas -//components/responses -// components/parameters -fn reroute_refs(mut val: Value) -> Result { - walk(&mut val, |value, path| { - let path = PathWithAnchor::from_str(&path).unwrap(); - let obj_type = Path::new(&path.path).file_stem().unwrap().to_str().unwrap(); // schemas, properties, etc. - let obj_name = &path.anchor.unwrap()[1..]; - *value = json!({ - "$ref": Value::String(format!("#/components/{}/{}", obj_type, obj_name)) - }); - })?; - Ok(val) -} - -#[derive(Debug, Args)] -pub struct Resolve { - pub path: String, -} - -impl Resolve { - pub fn run(self) -> Result<()> { - let path = PathBuf::from(&self.path).canonicalize().unwrap(); - let oa_dir = path.parent().unwrap(); - let mut doc = serde_json::from_reader::<_, Value>(std::fs::File::open(&path).unwrap()).unwrap(); - - walk(&mut doc, |val, path| { - let path = PathWithAnchor::from_str(&path).unwrap(); - let child_doc = serde_json::from_reader::<_, Value>(std::fs::File::open(oa_dir.join(&path.path)).unwrap()).unwrap(); - if path.anchor.is_some() { - let anchor = path.anchor.unwrap(); - let child_doc = resolve_json_path(child_doc, &anchor).unwrap(); - let child_doc = reroute_refs(child_doc).unwrap(); - *val = child_doc; - } else { - let child_doc = reroute_refs(child_doc).unwrap(); - *val = child_doc; - } - }).unwrap(); - println!("{}", serde_yaml::to_string(&doc).unwrap()); - Ok(()) - } -} \ No newline at end of file diff --git a/libninja/src/commercial.rs b/libninja/src/commercial.rs deleted file mode 100644 index 01b1951..0000000 --- a/libninja/src/commercial.rs +++ /dev/null @@ -1,62 +0,0 @@ -use openapiv3::OpenAPI; -use anyhow::{anyhow, Result}; -use ln_core::{PackageConfig, OutputConfig}; -use hir::{HirSpec, Operation}; - -#[cfg(feature = "commercial")] -pub mod python { - pub use ln_commercial::python::*; -} - -#[cfg(not(feature = "commercial"))] -pub mod python { - use super::*; - - pub fn generate_library(spec: OpenAPI, opts: OutputConfig) -> Result<()> { - Err(anyhow!("Commercial features are not enabled")) - } - - pub fn generate_sync_example(operation: &Operation, opt: &PackageConfig, spec: &HirSpec) -> Result { - Err(anyhow!("Commercial features are not enabled")) - } - - pub fn generate_async_example(operation: &Operation, opt: &PackageConfig, spec: &HirSpec) -> Result { - Err(anyhow!("Commercial features are not enabled")) - } -} - -#[cfg(feature = "commercial")] -pub mod go { - pub use ln_commercial::go::*; -} - -#[cfg(not(feature = "commercial"))] -pub mod go { - use super::*; - - pub fn generate_library(spec: OpenAPI, opts: OutputConfig) -> Result<()> { - Err(anyhow!("Commercial features are not enabled")) - } - - pub fn generate_example(operation: &Operation, opt: &PackageConfig, spec: &HirSpec) -> Result { - Err(anyhow!("Commercial features are not enabled")) - } -} - -#[cfg(feature = "commercial")] -pub mod typescript { - pub use ln_commercial::typescript::*; -} - -#[cfg(not(feature = "commercial"))] -pub mod typescript { - use super::*; - - pub fn generate_library(spec: OpenAPI, opts: OutputConfig) -> Result<()> { - Err(anyhow!("Commercial features are not enabled")) - } - - pub fn generate_example(operation: &Operation, opt: &PackageConfig, spec: &HirSpec) -> Result { - Err(anyhow!("Commercial features are not enabled")) - } -} \ No newline at end of file diff --git a/libninja/src/config.rs b/libninja/src/config.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/libninja/src/config.rs @@ -0,0 +1 @@ + diff --git a/libninja/src/custom.rs b/libninja/src/custom.rs deleted file mode 100644 index adb46cc..0000000 --- a/libninja/src/custom.rs +++ /dev/null @@ -1,37 +0,0 @@ -use openapiv3::{OpenAPI, SchemaKind, Type}; -use serde_yaml::Value; - -pub fn modify_sendgrid(mut yaml: Value) -> OpenAPI { - let mut spec: OpenAPI = - serde_yaml::from_value(yaml).expect("Could not structure OpenAPI file."); - spec.paths.paths.get_mut("/v3/contactdb/recipients/search") - .unwrap() - .as_mut() - .unwrap() - .get = None; - spec -} - -pub fn modify_recurly(mut yaml: Value) -> OpenAPI { - println!("modifying recurly:\n{}", serde_json::to_string(&yaml).unwrap()); - yaml["paths"]["/invoices/{invoice_id}/apply_credit_balance"]["put"]["parameters"].as_sequence_mut().unwrap().retain(|param| { - param["$ref"].as_str().unwrap() != "#/components/parameters/site_id" - }); - serde_yaml::from_value(yaml).unwrap() -} - -pub fn modify_openai(mut yaml: Value) -> OpenAPI { - let mut spec: OpenAPI = - serde_yaml::from_value(yaml).expect("Could not structure OpenAPI file."); - spec.security = vec![{ - let mut map = indexmap::IndexMap::new(); - map.insert("Bearer".to_string(), vec![]); - map - }]; - spec.security_schemes.insert("Bearer".to_string(), openapiv3::ReferenceOr::Item(openapiv3::SecurityScheme::HTTP { - scheme: "bearer".to_string(), - bearer_format: None, - description: None, - })); - spec -} \ No newline at end of file diff --git a/core/src/extractor.rs b/libninja/src/extractor/mod.rs similarity index 99% rename from core/src/extractor.rs rename to libninja/src/extractor/mod.rs index f70db3d..fd2ed18 100644 --- a/core/src/extractor.rs +++ b/libninja/src/extractor/mod.rs @@ -3,24 +3,24 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use anyhow::{anyhow, Result}; use convert_case::{Case, Casing}; use openapiv3 as oa; -use openapiv3::{APIKeyLocation, OpenAPI, ReferenceOr, RefOr, Schema, SecurityScheme}; +use openapiv3::{APIKeyLocation, OpenAPI, RefOr, ReferenceOr, Schema, SecurityScheme}; use tracing_ez::{debug, span, warn}; use hir::{ AuthLocation, AuthParam, AuthStrategy, HirSpec, Language, Location, Oauth2Auth, Operation, Parameter, Record, TokenAuth, }; -use mir::{Doc, DocFormat, NewType}; use mir::Ty; +use mir::{Doc, DocFormat, NewType}; pub use record::*; pub use ty::*; pub use ty::{schema_ref_to_ty, schema_ref_to_ty2, schema_to_ty}; use crate::extractor::operation::extract_operation; use crate::sanitize::sanitize; -use crate::util::{is_plural, singular}; mod operation; +pub mod plural; mod record; mod ty; diff --git a/core/src/extractor/operation.rs b/libninja/src/extractor/operation.rs similarity index 98% rename from core/src/extractor/operation.rs rename to libninja/src/extractor/operation.rs index 3dcd956..cb1d0fd 100644 --- a/core/src/extractor/operation.rs +++ b/libninja/src/extractor/operation.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Result}; use convert_case::{Case, Casing}; use openapiv3::{ - ArrayType, OpenAPI, Operation, Parameter, PathItem, ReferenceOr, RefOr, Schema, SchemaKind, + ArrayType, OpenAPI, Operation, Parameter, PathItem, RefOr, ReferenceOr, Schema, SchemaKind, Type, }; use tracing_ez::span; @@ -10,10 +10,10 @@ use hir::{HirSpec, Location}; use mir::{Doc, DocFormat, Ty}; use crate::extractor; -use crate::extractor::{is_primitive, schema_ref_to_ty, schema_ref_to_ty2, schema_to_ty}; use crate::extractor::record::extract_schema; +use crate::extractor::{is_primitive, schema_ref_to_ty, schema_ref_to_ty2, schema_to_ty}; -// make a name for hir::Operation +/// make a name for hir::Operation fn make_name(operation_id: Option<&String>, method: &str, path: &str) -> String { if let Some(name) = operation_id { return name.replace(".", "_"); diff --git a/core/src/extractor/pet_tag.yaml b/libninja/src/extractor/pet_tag.yaml similarity index 100% rename from core/src/extractor/pet_tag.yaml rename to libninja/src/extractor/pet_tag.yaml diff --git a/core/src/util.rs b/libninja/src/extractor/plural.rs similarity index 100% rename from core/src/util.rs rename to libninja/src/extractor/plural.rs diff --git a/core/src/extractor/record.rs b/libninja/src/extractor/record.rs similarity index 98% rename from core/src/extractor/record.rs rename to libninja/src/extractor/record.rs index 3815961..85df1ad 100644 --- a/core/src/extractor/record.rs +++ b/libninja/src/extractor/record.rs @@ -11,11 +11,11 @@ use hir::{Enum, HirField, HirSpec, NewType, Record, Struct, Variant}; use mir::{Doc, Ty}; use crate::extractor; +use crate::extractor::plural::{is_plural, singular}; use crate::extractor::{ is_optional, is_primitive, schema_ref_to_ty, schema_ref_to_ty2, schema_to_ty, }; use crate::sanitize::sanitize; -use crate::util::{is_plural, singular}; fn extract_fields( properties: &RefOrMap, @@ -274,7 +274,7 @@ mod tests { #[test] fn test_all_of_required_set_correctly() { let mut hir = HirSpec::default(); - let mut schema: Schema = from_str(include_str!("./pet_tag.yaml")).unwrap(); + let mut schema: Schema = from_str(include_str!("pet_tag.yaml")).unwrap(); let SchemaKind::AllOf { all_of } = &schema.kind else { panic!() }; diff --git a/core/src/extractor/ty.rs b/libninja/src/extractor/ty.rs similarity index 93% rename from core/src/extractor/ty.rs rename to libninja/src/extractor/ty.rs index 29070f0..f76c3b9 100644 --- a/core/src/extractor/ty.rs +++ b/libninja/src/extractor/ty.rs @@ -1,5 +1,5 @@ use openapiv3 as oa; -use openapiv3::{ArrayType, OpenAPI, ReferenceOr, Schema, SchemaKind, SchemaReference}; +use openapiv3::{ArrayType, OpenAPI, RefOr, ReferenceOr, Schema, SchemaKind, SchemaReference}; use serde_json::Value; use tracing::warn; @@ -7,12 +7,12 @@ use mir::Ty; use crate::sanitize::sanitize; -pub fn schema_ref_to_ty(schema_ref: &ReferenceOr, spec: &OpenAPI) -> Ty { +pub fn schema_ref_to_ty(schema_ref: &RefOr, spec: &OpenAPI) -> Ty { let schema = schema_ref.resolve(spec); schema_ref_to_ty2(schema_ref, spec, schema) } -pub fn schema_ref_to_ty2(schema_ref: &ReferenceOr, spec: &OpenAPI, schema: &Schema) -> Ty { +pub fn schema_ref_to_ty2(schema_ref: &RefOr, spec: &OpenAPI, schema: &Schema) -> Ty { if is_primitive(schema, spec) { schema_to_ty(schema, spec) } else { diff --git a/core/src/fs.rs b/libninja/src/fs.rs similarity index 90% rename from core/src/fs.rs rename to libninja/src/fs.rs index c3550d6..bc32ba9 100644 --- a/core/src/fs.rs +++ b/libninja/src/fs.rs @@ -1,9 +1,8 @@ -use std::path::Path; -use std::io::Write; -use std::io; use std::fs::File; -pub use std::fs::{create_dir_all, remove_dir_all, read_to_string}; -use crate::{fs, TEMPLATE_DIR}; +pub use std::fs::{create_dir_all, read_to_string, remove_dir_all}; +use std::io; +use std::io::Write; +use std::path::Path; pub fn write_file(path: &Path, text: &str) -> anyhow::Result<()> { let mut f = open(path)?; @@ -65,7 +64,11 @@ fn copy_files_recursive( } /// Copy static files to the destination path. -pub fn copy_builtin_files(dest_path: &Path, project_template: &str, ignore: &[&str]) -> anyhow::Result<()> { +pub fn copy_builtin_files( + dest_path: &Path, + project_template: &str, + ignore: &[&str], +) -> anyhow::Result<()> { copy_files_recursive( dest_path, TEMPLATE_DIR.get_dir(project_template).unwrap(), diff --git a/libninja/src/lib.rs b/libninja/src/lib.rs index 70af1f6..c2e57dd 100644 --- a/libninja/src/lib.rs +++ b/libninja/src/lib.rs @@ -1,116 +1,12 @@ -#![allow(non_snake_case)] -#![allow(deprecated)] -#![allow(unused)] - -use std::collections::HashMap; -use std::fs::File; -use std::path::Path; - -pub use ::openapiv3::OpenAPI; -use anyhow::{anyhow, Context, Result}; +use anyhow::Context; pub use openapiv3; -use openapiv3::VersionedOpenAPI; +pub use ::openapiv3::OpenAPI; use serde::{Deserialize, Serialize}; -use commercial::*; use hir::Language; -use ln_core::{OutputConfig, PackageConfig}; -use ln_core::extractor::add_operation_models; -use ln_core::extractor::extract_spec; - -pub mod command; -mod commercial; -pub mod custom; -pub mod rust; - -pub fn read_spec(path: &Path) -> Result { - let file = File::open(path).map_err(|_| anyhow!("{:?}: File not found.", path))?; - let ext = path - .extension() - .map(|s| s.to_str().expect("Extension isn't utf8")) - .unwrap_or_else(|| "yaml"); - let openapi: VersionedOpenAPI = match ext { - "yaml" => serde_yaml::from_reader(file)?, - "json" => serde_json::from_reader(file)?, - _ => panic!("Unknown file extension"), - }; - let openapi = openapi.upgrade(); - Ok(openapi) -} - -pub fn generate_library(spec: OpenAPI, opts: OutputConfig) -> Result<()> { - match opts.language { - Language::Rust => rust::generate_rust_library(spec, opts), - Language::Python => python::generate_library(spec, opts), - Language::Typescript => typescript::generate_library(spec, opts), - Language::Golang => go::generate_library(spec, opts), - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Examples { - pub rust: String, - pub python: String, - pub python_async: String, - pub typescript: String, - pub go: String, -} - -pub fn generate_examples( - spec: OpenAPI, - mut opt: PackageConfig, -) -> Result> { - let mut map = HashMap::new(); - let spec = extract_spec(&spec)?; - for operation in &spec.operations { - let rust = { - let generator = Language::Rust; - let opt = PackageConfig { - language: generator, - ..opt.clone() - }; - let spec = add_operation_models(generator, spec.clone())?; - rust::generate_example(operation, &opt, &spec)? - }; - let python = { - let opt = PackageConfig { - language: Language::Python, - ..opt.clone() - }; - python::generate_sync_example(operation, &opt, &spec)? - }; - let python_async = { - let opt = PackageConfig { - language: Language::Python, - ..opt.clone() - }; - python::generate_async_example(operation, &opt, &spec)? - }; - let typescript = { - let generator = Language::Rust; - let opt = PackageConfig { - language: generator, - ..opt.clone() - }; - let spec = add_operation_models(generator, spec.clone())?; - typescript::generate_example(operation, &opt, &spec)? - }; - let go = { - let opt = PackageConfig { - language: Language::Golang, - ..opt.clone() - }; - go::generate_example(operation, &opt, &spec)? - }; - let examples = Examples { - rust, - python, - python_async, - typescript, - go, - }; - map.insert(operation.name.clone(), examples); - } - Ok(map) -} +mod command; +mod config; +mod extractor; +mod fs; +mod rust; diff --git a/libninja/src/main.rs b/libninja/src/main.rs new file mode 100644 index 0000000..44a3d26 --- /dev/null +++ b/libninja/src/main.rs @@ -0,0 +1,77 @@ +#![allow(non_snake_case)] +#![allow(unused)] + +use anyhow::Result; +use clap::{Args, Parser, Subcommand}; +use hir::Language; +use libninja::command::*; +use tracing::Level; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; + +fn warn_if_not_found(command: &str) { + if std::process::Command::new(command) + .stderr(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .spawn() + .is_err() + { + eprintln!("Warning: {} not found. Some commands may fail.", command); + } +} + +#[derive(Parser, Debug)] +#[command(author, version, about)] +struct Cli { + #[command(subcommand)] + command: Command, + + #[clap(short, long, global = true)] + verbose: bool, +} + +#[derive(Subcommand, Debug)] +pub enum Command { + Gen(Generate), +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + let level = if cli.verbose { + Level::DEBUG + } else { + Level::INFO + }; + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer().without_time()) + .with( + tracing_subscriber::filter::Targets::new() + .with_target(env!("CARGO_BIN_NAME"), level) + .with_target("libninja_mir", level) + .with_target("libninja_hir", level) + .with_target("ln_core", level) + .with_target("ln_macro", level), + ) + .init(); + + match cli.command { + Command::Gen(generate) => { + use Language::*; + match generate.language { + Rust => {} + // Python => { + // warn_if_not_found("pdm"); + // warn_if_not_found("black"); + // }, + // Typescript => { + // warn_if_not_found("pnpm"); + // warn_if_not_found("prettier") + // }, + // Golang => { + // warn_if_not_found("gofmt"); + // }, + } + generate.run() + } + } +} diff --git a/libninja/src/rust/cargo_toml.rs b/libninja/src/rust/cargo_toml.rs deleted file mode 100644 index dcc50f6..0000000 --- a/libninja/src/rust/cargo_toml.rs +++ /dev/null @@ -1,168 +0,0 @@ -use std::collections::btree_map::Entry; -use std::collections::HashMap; -use std::path::Path; -use std::process::Output; -use cargo_toml::{Inheritable, Manifest, Package, Dependency, DependencyDetail, DepsSet}; -use ln_core::{fs, get_template_file, OutputConfig, PackageConfig}; -use crate::rust::Extras; - -pub fn update_cargo_toml(extras: &Extras, opts: &OutputConfig, context: &HashMap) -> anyhow::Result { - let cargo = opts.dest_path.join("Cargo.toml"); - - let mut m = Manifest::from_path(&cargo).ok().unwrap_or_else(|| { - let mut m = default_manifest(); - let p = m.package.as_mut().unwrap(); - if let Some(name) = context.get("github_repo") { - p.set_homepage(Some(name.clone())); - p.set_repository(Some(name.clone())); - } - p.name = opts.package_name.clone(); - p.set_documentation(Some(format!("https://docs.rs/{}", &opts.package_name))); - let lib = m.lib.as_mut().expect("Cargo.toml must have a lib section"); - lib.doctest = false; - m - }); - let package = m.package.as_mut().expect("Cargo.toml must have a package section"); - - if let Some(v) = &opts.version { - package.version = Inheritable::Set(v.clone()); - } else if let Inheritable::Set(t) = &mut package.version { - if t == "" { - *t = "0.1.0".to_string(); - } else { - let mut ver = semver::Version::parse(t).unwrap(); - if ver.major == 0 { - ver.minor += 1; - ver.patch = 0; - } else { - ver.major += 1; - ver.minor = 0; - ver.patch = 0; - } - *t = ver.to_string(); - } - } - let package_version = package.version().to_string(); - - ensure_dependency(&mut m.dependencies, "httpclient", "0.20.2", &[]); - ensure_dependency(&mut m.dependencies, "serde", "1.0.137", &["derive"]); - ensure_dependency(&mut m.dependencies, "serde_json", "1.0.81", &[]); - ensure_dependency(&mut m.dependencies, "futures", "0.3.25", &[]); - ensure_dependency(&mut m.dependencies, "chrono", "0.4.26", &["serde"]); - ensure_dependency(&mut m.dev_dependencies, "tokio", "1.18.2", &["full"]); - if extras.currency { - ensure_dependency(&mut m.dependencies, "rust_decimal", "1.33.0", &["serde-with-str"]); - ensure_dependency(&mut m.dependencies, "rust_decimal_macros", "1.33.0", &[]); - } - if extras.date_serialization { - m.dependencies.entry("chrono".to_string()) - .or_insert(Dependency::Detailed(DependencyDetail { - version: Some("0.4.23".to_string()), - features: vec!["serde".to_string()], - default_features: true, - ..DependencyDetail::default() - })); - } - if opts.config.ormlite { - ensure_dependency(&mut m.dependencies, "ormlite", "0.16.0", &["decimal"]); - let d = m.dependencies.get_mut("ormlite").unwrap(); - d.detail_mut().optional = true; - } - if opts.config.fake { - ensure_dependency(&mut m.dependencies, "fake", "2.9", &["derive", "chrono", "rust_decimal", "http", "uuid"]); - let d = m.dependencies.get_mut("fake").unwrap(); - d.detail_mut().optional = true; - } - if extras.basic_auth { - ensure_dependency(&mut m.dependencies, "base64", "0.21.0", &[]); - } - if extras.oauth2 { - ensure_dependency(&mut m.dependencies, "httpclient_oauth2", "0.1.3", &[]); - } - m.example = vec![]; - fs::write_file(&cargo, &toml::to_string(&m).unwrap())?; - Ok(package_version) -} - -fn detailed(version: &str, features: &[&str]) -> Dependency { - Dependency::Detailed(DependencyDetail { - version: Some(version.to_string()), - features: features.iter().map(|f| f.to_string()).collect(), - default_features: true, - ..DependencyDetail::default() - }) -} - -fn simple(version: &str) -> Dependency { - Dependency::Simple(version.to_string()) -} - -fn ensure_dependency(deps: &mut DepsSet, name: &str, version: &str, features: &[&str]) { - deps.entry(name.to_string()) - .and_modify(|dep| { - let current_version = dep.req().to_string(); - let mut current_features = dep.req_features().to_vec(); - let version = if version > current_version.as_str() { - version - } else { - ¤t_version - }; - if !features.is_empty() { - let mut features = features.into_iter().map(|s| s.to_string()).collect::>(); - features.retain(|f| !current_features.contains(f)); - current_features.extend(features); - } - if current_features.is_empty() { - *dep = Dependency::Simple(version.to_string()); - } else { - let detail = dep.detail_mut(); - detail.version = Some(version.to_string()); - detail.features = current_features; - } - }) - .or_insert_with(|| if features.is_empty() { - simple(version) - } else { - detailed(version, features) - }); -} - -fn default_manifest() -> Manifest { - let package: Package = serde_json::from_str(r#"{ - "name": "", - "edition": "2021", - "readme": "README.md", - "license": "MIT", - "version": "" - }"#).unwrap(); - Manifest { - package: Some(package), - workspace: None, - dependencies: Default::default(), - dev_dependencies: Default::default(), - build_dependencies: Default::default(), - target: Default::default(), - features: Default::default(), - replace: Default::default(), - patch: Default::default(), - lib: Some(Default::default()), - profile: Default::default(), - badges: Default::default(), - bin: vec![], - bench: vec![], - test: vec![], - example: vec![], - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// We're really just testing that it doesn't panic, since Default on package doesn't work and - /// it's non-exhaustive - #[test] - fn test_default_manifest() { - let manifest = default_manifest(); - } -} \ No newline at end of file diff --git a/libninja/src/rust/codegen.rs b/libninja/src/rust/codegen.rs deleted file mode 100644 index d4df2af..0000000 --- a/libninja/src/rust/codegen.rs +++ /dev/null @@ -1,72 +0,0 @@ -use convert_case::Casing; -use proc_macro2::TokenStream; -use quote::{quote, TokenStreamExt}; - -pub use example::*; -pub use ident::*; -use mir::Ident; -use mir_rust::{ToRustCode, ToRustIdent}; -pub use ty::*; - -mod example; -mod ident; -mod ty; - -#[cfg(test)] -mod tests { - use mir::{import, Import}; - - use crate::rust::codegen::{ToRustCode, ToRustIdent}; - - #[test] - fn test_to_ident() { - assert_eq!("meta/root".to_rust_ident().0, "meta_root"); - } - - #[test] - fn test_to_ident1() { - assert_eq!( - "get-phone-checks-v0.1".to_rust_ident().0, - "get_phone_checks_v0_1" - ); - } - - #[test] - fn test_star() { - let i = import!("super::*"); - assert_eq!(i.to_rust_code().to_string(), "use super :: * ;"); - let i = Import::new("super", vec!["*"]); - assert_eq!(i.to_rust_code().to_string(), "use super :: { * } ;"); - } - - #[test] - fn test_import() { - let import = import!("plaid::model::LinkTokenCreateRequestUser"); - assert_eq!( - import.to_rust_code().to_string(), - "use plaid :: model :: LinkTokenCreateRequestUser ;" - ); - let import = import!("plaid::model", LinkTokenCreateRequestUser, Foobar); - assert_eq!( - import.to_rust_code().to_string(), - "use plaid :: model :: { LinkTokenCreateRequestUser , Foobar } ;" - ); - - let import = Import::alias("plaid::model", "foobar"); - assert_eq!( - import.to_rust_code().to_string(), - "use plaid :: model as foobar ;" - ); - - let import = Import::package("foo_bar"); - assert_eq!(import.to_rust_code().to_string(), "use foo_bar ;"); - } -} - -pub fn serde_rename(value: &str, ident: &Ident) -> TokenStream { - if ident.0 != value { - quote!(#[serde(rename = #value)]) - } else { - TokenStream::new() - } -} diff --git a/libninja/src/rust/codegen/example.rs b/libninja/src/rust/codegen/example.rs deleted file mode 100644 index bf5ab15..0000000 --- a/libninja/src/rust/codegen/example.rs +++ /dev/null @@ -1,200 +0,0 @@ -use convert_case::{Case, Casing}; -use proc_macro2::TokenStream; -use quote::quote; - -use hir::{Enum, HirField, HirSpec, Language, NewType, Operation, Parameter, Record, Struct}; -use ln_macro::rfunction; -use mir::{File, Import, Ty}; -use mir_rust::format_code; - -use crate::rust::codegen::ToRustIdent; -use crate::rust::codegen::{ToRustCode, ToRustType}; -use crate::PackageConfig; - -pub trait ToRustExample { - fn to_rust_example(&self, spec: &HirSpec) -> anyhow::Result; -} - -impl ToRustExample for Parameter { - fn to_rust_example(&self, spec: &HirSpec) -> anyhow::Result { - to_rust_example_value(&self.ty, &self.name, spec, false) - } -} - -pub fn generate_example( - operation: &Operation, - opt: &PackageConfig, - spec: &HirSpec, -) -> anyhow::Result { - let args = operation.function_args(Language::Rust); - let declarations = args - .iter() - .map(|p| { - let ident = p.name.to_rust_ident(); - let value = to_rust_example_value(&p.ty, &p.name, spec, true)?; - Ok(quote! { - let #ident = #value; - }) - }) - .collect::, anyhow::Error>>()?; - let fn_args = args.iter().map(|p| p.name.to_rust_ident()); - let optionals = operation - .optional_args() - .into_iter() - .map(|p| { - let ident = p.name.to_rust_ident(); - let value = to_rust_example_value(&p.ty, &p.name, spec, true)?; - Ok(quote! { - .#ident(#value) - }) - }) - .collect::, anyhow::Error>>()?; - let qualified_client = format!( - "{}::{}", - opt.package_name, - opt.client_name().to_rust_struct() - ); - let mut imports = vec![ - Import::package(&qualified_client), - Import::package(&format!("{}::model::*", opt.package_name)), - ]; - if operation.use_required_struct(Language::Rust) { - let struct_name = operation - .required_struct_name() - .to_rust_struct() - .to_string(); - imports.push(Import::package(&format!( - "{}::request::{}", - opt.package_name, struct_name - ))); - } - let operation = operation.name.to_rust_ident(); - let client = opt.client_name().to_rust_struct(); - let mut main = rfunction!(async main() { - let client = #client::from_env(); - #(#declarations)* - let response = client.#operation(#(#fn_args),*) - #(#optionals)* - .await - .unwrap(); - println!("{:#?}", response); - }); - main.annotations.push("tokio::main".to_string()); - - let example = File { - imports, - functions: vec![main], - ..File::default() - }; - let code = example.to_rust_code(); - Ok(format_code(code)) -} - -impl ToRustExample for hir::Enum { - fn to_rust_example(&self, spec: &HirSpec) -> anyhow::Result { - todo!() - } -} - -pub fn to_rust_example_value( - ty: &Ty, - name: &str, - spec: &HirSpec, - use_ref_value: bool, -) -> anyhow::Result { - let s = match ty { - Ty::String => { - let s = format!("your {}", name.to_case(Case::Lower)); - if use_ref_value { - quote!(#s) - } else { - quote!(#s.to_owned()) - } - } - Ty::Integer { .. } => quote!(1), - Ty::Float => quote!(1.0), - Ty::Boolean => quote!(true), - Ty::Array(inner) => { - let use_ref_value = if !inner.is_reference_type() { - false - } else { - use_ref_value - }; - let inner = to_rust_example_value(inner, name, spec, use_ref_value)?; - if use_ref_value { - quote!(&[#inner]) - } else { - quote!(vec![#inner]) - } - } - Ty::Model(model) => { - let record = spec.get_record(model)?; - let force_ref = model.ends_with("Required"); - match record { - Record::Struct(Struct { - name: _name, - fields, - nullable, - docs: _docs, - }) => { - let fields = fields - .iter() - .map(|(name, field)| { - let not_ref = !force_ref || field.optional; - let mut value = to_rust_example_value(&field.ty, name, spec, !not_ref)?; - let name = name.to_rust_ident(); - if field.optional { - value = quote!(Some(#value)); - } - Ok(quote!(#name: #value)) - }) - .collect::, anyhow::Error>>()?; - let model = model.to_rust_struct(); - quote!(#model{#(#fields),*}) - } - Record::NewType(NewType { - name, - fields, - doc: _docs, - }) => { - let fields = fields - .iter() - .map(|f| to_rust_example_value(&f.ty, name, spec, false)) - .collect::, _>>()?; - let name = name.to_rust_struct(); - quote!(#name(#(#fields),*)) - } - Record::Enum(Enum { - name, - variants, - doc: _docs, - }) => { - let variant = variants.first().unwrap(); - let variant = if let Some(a) = &variant.alias { - a.to_rust_struct() - } else { - variant.value.to_rust_struct() - }; - let model = model.to_rust_struct(); - quote!(#model::#variant) - } - Record::TypeAlias(name, HirField { ty, optional, .. }) => { - let not_ref = !force_ref || !optional; - let ty = to_rust_example_value(ty, name, spec, not_ref)?; - if *optional { - quote!(Some(#ty)) - } else { - quote!(#ty) - } - } - } - } - Ty::Unit => quote!(()), - Ty::Any(_) => quote!(serde_json::json!({})), - Ty::Date { .. } => quote!(chrono::Utc::now().date_naive()), - Ty::DateTime { .. } => quote!(chrono::Utc::now()), - Ty::Currency { .. } => quote!(rust_decimal_macros::dec!(100.01)), - Ty::HashMap(_) => quote!(std::collections::HashMap::new()), - }; - Ok(s) -} diff --git a/libninja/src/rust/codegen/ident.rs b/libninja/src/rust/codegen/ident.rs deleted file mode 100644 index e69de29..0000000 diff --git a/libninja/src/rust/format.rs b/libninja/src/rust/format.rs deleted file mode 100644 index 533f529..0000000 --- a/libninja/src/rust/format.rs +++ /dev/null @@ -1,35 +0,0 @@ -use proc_macro2::TokenStream; -use std::io::Write; - -#[cfg(test)] -mod tests { - use super::*; - use proc_macro2::TokenStream; - use quote::quote; - use mir_rust::format_code; - - fn codegen_example() -> TokenStream { - quote! { - use tokio; - - pub async fn main() { - println!("Hello, world!"); - } - } - } - - #[test] - fn test_codegen() { - let code = codegen_example(); - let code = format_code(code); - assert_eq!( - code, - r#" -use tokio; -pub async fn main() { - println!("Hello, world!"); -} -"#.trim() - ); - } -} diff --git a/libninja/src/rust/io.rs b/libninja/src/rust/io.rs index c94dc53..2129bf5 100644 --- a/libninja/src/rust/io.rs +++ b/libninja/src/rust/io.rs @@ -16,7 +16,7 @@ pub fn write_rust_code_to_path(path: &Path, code: TokenStream) -> anyhow::Result write_rust_to_path(path, code, "") } -pub fn write_rust_to_path(path: &Path, code: TokenStream, template: &str) -> anyhow::Result<()> { +pub fn write_rust_to_path(path: &Path, code: TokenStream) -> anyhow::Result<()> { let code = format_code(code); let mut f = fs::open(path)?; let mut s = template.to_string(); diff --git a/libninja/src/rust/lower_hir.rs b/libninja/src/rust/lower_hir.rs index 5f01967..faa8dd2 100644 --- a/libninja/src/rust/lower_hir.rs +++ b/libninja/src/rust/lower_hir.rs @@ -4,14 +4,15 @@ use convert_case::Casing; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; -use crate::rust::codegen::ToRustType; use hir::{HirField, HirSpec, NewType, Record, Struct}; -use ln_core::{ConfigFlags, PackageConfig}; +use ln_core::{Config, ConfigFlags}; use mir::{import, Field, File, Ident, Import, Visibility}; use mir::{DateSerialization, DecimalSerialization, IntegerSerialization, Ty}; +use mir_rust::ident::ToRustIdent; +use mir_rust::ty::ToRustType; use mir_rust::ToRustCode; use mir_rust::{derives_to_tokens, lower_enum}; -use mir_rust::{sanitize_filename, RustExtra, ToRustIdent}; +use mir_rust::{sanitize_filename, RustExtra}; pub trait FieldExt { fn decorators(&self, name: &str, config: &ConfigFlags) -> Vec; @@ -224,7 +225,7 @@ pub fn generate_single_model_file( name: &str, record: &Record, spec: &HirSpec, - config: &PackageConfig, + config: &Config, ) -> File { let mut imports = vec![import!("serde", Serialize, Deserialize)]; if let Some(import) = record.imports("super") { @@ -337,7 +338,7 @@ pub fn create_typealias(name: &str, schema: &HirField) -> TokenStream { } } -pub fn create_struct(record: &Record, config: &PackageConfig, hir: &HirSpec) -> TokenStream { +pub fn create_struct(record: &Record, config: &Config, hir: &HirSpec) -> TokenStream { match record { Record::Struct(s) => create_sumtype_struct(s, &config.config, hir, &config.derives), Record::NewType(nt) => create_newtype_struct(nt, hir, &config.derives), diff --git a/libninja/src/rust.rs b/libninja/src/rust/mod.rs similarity index 82% rename from libninja/src/rust.rs rename to libninja/src/rust/mod.rs index 7ecb0ee..62e0934 100644 --- a/libninja/src/rust.rs +++ b/libninja/src/rust/mod.rs @@ -13,7 +13,7 @@ use syn::Item; use text_io::read; use tracing::debug; -use codegen::ToRustType; +use codegen_rust::file::client; use hir::{qualified_env_var, AuthStrategy, HirSpec, Location, Oauth2Auth, Parameter}; use ln_core::fs; use ln_core::{ @@ -23,27 +23,26 @@ use ln_core::{ use mir::Ident; use mir::{DateSerialization, IntegerSerialization}; use ::mir::{File, Import, Visibility}; -use mir_rust::ToRustIdent; +use mir_rust::ident::ToRustIdent; +use mir_rust::ty::ToRustType; use mir_rust::{format_code, RustExtra}; use mir_rust::{sanitize_filename, ToRustCode}; -use crate::rust::client::{build_Client_authenticate, server_url}; pub use crate::rust::codegen::generate_example; use crate::rust::io::write_rust_file_to_path; use crate::rust::lower_hir::{generate_model_rs, generate_single_model_file}; -use crate::rust::request::{ +use crate::{add_operation_models, extract_spec, Config, Config}; +use codegen_rust::file::client::{build_Client_authenticate, server_url}; +use codegen_rust::file::request::{ assign_inputs_to_request, build_request_struct, build_request_struct_builder_methods, build_url, generate_request_model_rs, }; -use crate::{add_operation_models, extract_spec, OutputConfig, PackageConfig}; mod cargo_toml; -pub mod client; pub mod codegen; pub mod format; mod io; pub mod lower_hir; -pub mod request; mod serde; #[derive(Debug)] @@ -137,70 +136,6 @@ pub fn copy_from_target_templates(dest: &Path) -> Result<()> { Ok(()) } -pub fn generate_rust_library(spec: OpenAPI, opts: OutputConfig) -> Result<()> { - let src_path = opts.dest_path.join("src"); - - // Prepare the HIR Spec. - let spec = extract_spec(&spec)?; - let extras = calculate_extras(&spec); - - // if src doesn't exist that's fine - let _ = fs::remove_dir_all(&src_path); - fs::create_dir_all(&src_path)?; - - // If there's nothing in cargo.toml, you want to prompt for it here. - // Then pass it back in. - // But you only need it if you're generating the README and/or Cargo.toml - let mut context = HashMap::::new(); - if !opts.dest_path.join("README.md").exists() || !opts.dest_path.join("Cargo.toml").exists() { - if let Some(github_repo) = &opts.github_repo { - context.insert("github_repo".to_string(), github_repo.to_string()); - } else { - println!( - "Because this is a first-time generation, please provide additional information." - ); - print!("Please provide a Github repo name (e.g. libninja/plaid-rs): "); - let github_repo: String = read!("{}\n"); - context.insert("github_repo".to_string(), github_repo); - } - } - let version = cargo_toml::update_cargo_toml(&extras, &opts, &context)?; - let build_examples = opts.build_examples; - let opts = PackageConfig { - package_name: opts.package_name, - service_name: opts.service_name, - language: opts.language, - package_version: version, - config: opts.config, - dest: opts.dest_path, - derives: opts.derive, - }; - write_model_module(&spec, &opts)?; - write_request_module(&spec, &opts)?; - write_lib_rs(&spec, &extras, &opts)?; - write_serde_module_if_needed(&extras, &opts.dest)?; - - let spec = add_operation_models(opts.language, spec)?; - - if build_examples { - write_examples(&spec, &opts)?; - } - - let tera = prepare_templates(); - let mut template_context = create_context(&opts, &spec); - template_context.insert( - "client_docs_url", - &format!("https://docs.rs/{}", opts.package_name), - ); - if let Some(github_repo) = context.get("github_repo") { - template_context.insert("github_repo", github_repo); - } - copy_builtin_files(&opts.dest, &opts.language.to_string(), &["src"])?; - copy_builtin_templates(&opts, &tera, &template_context)?; - copy_from_target_templates(&opts.dest)?; - Ok(()) -} - fn write_file_with_template( mut file: File, template: Option, @@ -238,7 +173,7 @@ fn write_file_with_template( fs::write_file(path, &code) } -fn write_model_module(spec: &HirSpec, opts: &PackageConfig) -> Result<()> { +fn write_model_module(spec: &HirSpec, opts: &Config) -> Result<()> { let config = &opts.config; let src_path = opts.dest.join("src"); @@ -258,7 +193,7 @@ fn write_model_module(spec: &HirSpec, opts: &PackageConfig) -> Result<()> { Ok(()) } -fn static_shared_http_client(spec: &HirSpec, opt: &PackageConfig) -> TokenStream { +fn static_shared_http_client(spec: &HirSpec, opt: &Config) -> TokenStream { let url = server_url(spec, opt); quote! { static SHARED_HTTPCLIENT: OnceLock = OnceLock::new(); @@ -287,7 +222,7 @@ fn static_shared_http_client(spec: &HirSpec, opt: &PackageConfig) -> TokenStream } } -fn shared_oauth2_flow(auth: &Oauth2Auth, spec: &HirSpec, opts: &PackageConfig) -> TokenStream { +fn shared_oauth2_flow(auth: &Oauth2Auth, spec: &HirSpec, opts: &Config) -> TokenStream { let service_name = opts.service_name.as_str(); let client_id = qualified_env_var(service_name, "client id"); @@ -321,7 +256,7 @@ fn shared_oauth2_flow(auth: &Oauth2Auth, spec: &HirSpec, opts: &PackageConfig) - } /// Generates the client code for a given OpenAPI specification. -fn write_lib_rs(spec: &HirSpec, extras: &Extras, opts: &PackageConfig) -> Result<()> { +fn write_lib_rs(spec: &HirSpec, extras: &Extras, opts: &Config) -> Result<()> { let src_path = opts.dest.join("src"); let name = &opts.service_name; let mut struct_Client = client::struct_Client(spec, &opts); @@ -413,7 +348,7 @@ fn write_lib_rs(spec: &HirSpec, extras: &Extras, opts: &PackageConfig) -> Result Ok(()) } -fn write_request_module(spec: &HirSpec, opts: &PackageConfig) -> Result<()> { +fn write_request_module(spec: &HirSpec, opts: &Config) -> Result<()> { let src_path = opts.dest.join("src"); let client_name = opts.client_name().to_rust_struct(); let mut imports = vec![]; @@ -504,7 +439,7 @@ use httpclient::InMemoryResponseExt;"; Ok(()) } -fn write_examples(spec: &HirSpec, opts: &PackageConfig) -> Result<()> { +fn write_examples(spec: &HirSpec, opts: &Config) -> Result<()> { let example_path = opts.dest.join("examples"); let _ = fs::remove_dir_all(&example_path); fs::create_dir_all(&example_path)?; diff --git a/libninja/src/rust/serde.rs b/libninja/src/rust/serde.rs deleted file mode 100644 index 084e0d4..0000000 --- a/libninja/src/rust/serde.rs +++ /dev/null @@ -1,155 +0,0 @@ -use proc_macro2::TokenStream; -use quote::quote; - -pub fn option_i64_null_as_zero_module() -> TokenStream { - quote! { - pub mod option_i64_null_as_zero { - use std::fmt; - use serde::de::{Error, Unexpected, Deserializer}; - - struct IntVisitor; - - impl<'de> serde::de::Visitor<'de> for IntVisitor { - type Value = Option; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!(formatter, "an integer") - } - - fn visit_i64(self, value: i64) -> Result { - if value == 0 { - Ok(None) - } else { - Ok(Some(value)) - } - } - - fn visit_u64(self, value: u64) -> Result { - if value == 0 { - Ok(None) - } else { - Ok(Some(value as i64)) - } - } - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - deserializer.deserialize_i64(IntVisitor) - } - - pub fn serialize(value: &Option, serializer: S) -> Result - where - S: serde::Serializer, - { - if let Some(i) = value { - serializer.serialize_i64(*i) - } else { - serializer.serialize_i64(0) - } - } - } - } -} - -pub fn option_i64_str_module() -> TokenStream { - quote! { - pub mod option_i64_str { - use std::fmt; - use serde::de::{Error, Unexpected, Deserializer}; - - struct StrVisitor; - - impl<'de> serde::de::Visitor<'de> for StrVisitor { - type Value = Option; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!(formatter, "an integer") - } - - fn visit_str(self, value: &str) -> Result { - if value.is_empty() { - Ok(None) - } else { - value.parse::().map(Some).map_err(|_| { - Error::invalid_value(Unexpected::Str(value), &self) - }) - } - } - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - deserializer.deserialize_str(StrVisitor) - } - - pub fn serialize(value: &Option, serializer: S) -> Result - where - S: serde::Serializer, - { - if let Some(i) = value { - serializer.serialize_str(&i.to_string()) - } else { - serializer.serialize_str("") - } - } - } - } -} - -pub fn option_chrono_naive_date_as_int_module() -> TokenStream { - quote! { - pub mod option_chrono_naive_date_as_int { - use std::fmt; - use serde::de::{Error, Unexpected, Deserializer}; - use chrono::Datelike; - - struct NaiveDateVisitor; - - impl<'de> serde::de::Visitor<'de> for NaiveDateVisitor { - type Value = Option; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!(formatter, "an integer that looks like a date") - } - - fn visit_u64(self, value: u64) -> Result { - if value == 0 { - Ok(None) - } else { - let day = value % 100; - let month = (value / 100) % 100; - let year = value / 10000; - Ok(chrono::NaiveDate::from_ymd_opt(year as i32, month as u32, day as u32)) - } - } - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - deserializer.deserialize_u64(NaiveDateVisitor) - } - - pub fn serialize(value: &Option, serializer: S) -> Result - where - S: serde::Serializer, - { - if let Some(value) = value { - let day = value.day() as i32; - let month = value.month() as i32; - let year = value.year(); - let value = year * 10000 + month * 100 + day; - serializer.serialize_i64(value as i64) - } else { - serializer.serialize_i64(0) - } - } - } - } -} \ No newline at end of file diff --git a/libninja/tests/all_of/main.rs b/libninja/tests/all_of/main.rs index 9ce4da6..e6dd3c9 100644 --- a/libninja/tests/all_of/main.rs +++ b/libninja/tests/all_of/main.rs @@ -6,12 +6,12 @@ use serde_yaml::from_str; use hir::{HirSpec, Record}; use libninja::rust::lower_hir::create_struct; -use ln_core::{ConfigFlags, PackageConfig}; use ln_core::extractor::{extract_schema, extract_without_treeshake}; +use ln_core::{Config, ConfigFlags}; use mir_rust::format_code; fn formatted_code(record: &Record, spec: &HirSpec) -> String { - let config = PackageConfig { + let config = Config { package_name: "test".to_string(), service_name: "service".to_string(), language: hir::Language::Rust, diff --git a/libninja/tests/basic/main.rs b/libninja/tests/basic/main.rs index c675850..36a2cbf 100644 --- a/libninja/tests/basic/main.rs +++ b/libninja/tests/basic/main.rs @@ -5,10 +5,10 @@ use openapiv3::OpenAPI; use pretty_assertions::assert_eq; use serde_yaml::from_str; +use hir::Config; use hir::Language; use libninja::generate_library; use libninja::rust::generate_example; -use ln_core::{OutputConfig, PackageConfig}; use ln_core::extractor::extract_spec; const EXAMPLE: &str = include_str!("link_create_token.rs"); @@ -20,7 +20,7 @@ const RECURLY: &str = include_str!("../../../test_specs/recurly.yaml"); fn test_generate_example() { let spec: OpenAPI = from_str(BASIC).unwrap(); - let config = PackageConfig { + let config = Config { package_name: "plaid".to_string(), service_name: "Plaid".to_string(), language: Language::Rust, @@ -46,7 +46,7 @@ pub fn test_build_full_library_recurly() { let temp = tempfile::tempdir().unwrap(); - let opts = OutputConfig { + let opts = Config { dest_path: temp.path().to_path_buf(), build_examples: false, package_name: "recurly".to_string(), diff --git a/libninja/tests/test_example_gen.rs b/libninja/tests/test_example_gen.rs index b91c177..2548f7c 100644 --- a/libninja/tests/test_example_gen.rs +++ b/libninja/tests/test_example_gen.rs @@ -1,7 +1,7 @@ use hir::Language; use libninja::rust; use ln_core::extractor::add_operation_models; -use ln_core::{extract_spec, PackageConfig}; +use ln_core::{extract_spec, Config}; use openapiv3::OpenAPI; use pretty_assertions::assert_eq; @@ -13,7 +13,7 @@ fn test_example_generation_with_refs() { let spec = add_operation_models(Language::Rust, spec).unwrap(); let op = spec.operations.iter().next().unwrap(); - let opt = PackageConfig { + let opt = Config { package_name: "plaid".to_string(), service_name: "Plaid".to_string(), language: Language::Rust, @@ -34,7 +34,7 @@ fn test_example_generation_with_refs2() { let spec = add_operation_models(Language::Rust, spec).unwrap(); let op = spec.operations.iter().next().unwrap(); - let opt = PackageConfig { + let opt = Config { package_name: "plaid".to_string(), service_name: "Plaid".to_string(), language: Language::Rust, diff --git a/macro/Cargo.toml b/macro/Cargo.toml index 5ebfc62..d194aa0 100644 --- a/macro/Cargo.toml +++ b/macro/Cargo.toml @@ -8,13 +8,13 @@ license = "MIT" edition = "2021" [lib] -name = "ln_macro" proc-macro = true [dependencies] proc-macro2 = { version = "1.0", features = ["span-locations"] } quote = "1.0.29" -libninja_mir = { path = "../mir" } +syn = "2.0.77" [dev-dependencies] pretty_assertions = "1.3.0" +libninja_mir = { path = "../mir" } diff --git a/macro/src/body.rs b/macro/src/body.rs index e4b4010..fec939e 100644 --- a/macro/src/body.rs +++ b/macro/src/body.rs @@ -20,7 +20,6 @@ fn closing(delim: Delimiter) -> &'static str { } } - /// Use this to create a binding for the given ident. /// E.g. if we encounter #foo while tokenizing, get the idx of foo, returning it as the literal string r#"{idx}"# /// If foo is not already captured, then we push to captured, returning that new idx. @@ -33,12 +32,19 @@ fn interpolation_binding(ident: &str, captured: &mut Vec, escape: bool) } Some(idx) => idx, }; - format!("{{{}{}}}", interpolation_idx, if escape { ":?" } else { "" }) + format!( + "{{{}{}}}", + interpolation_idx, + if escape { ":?" } else { "" } + ) } - /// Call this after we encounter a # in tokenization. -pub fn pull_interpolation(toks: &mut impl Iterator, captured: &mut Vec, escape: bool) -> String { +pub fn pull_interpolation( + toks: &mut impl Iterator, + captured: &mut Vec, + escape: bool, +) -> String { let ident = match toks.next() { Some(TokenTree::Ident(ident)) => ident.to_string(), other => panic!("Expected ident after #, got {:?}", other), @@ -46,8 +52,12 @@ pub fn pull_interpolation(toks: &mut impl Iterator, captured: &m interpolation_binding(&ident, captured, escape) } - -fn body_recurse(body: TokenStream, captured: &mut Vec, lines: &mut Vec, indent: usize) { +fn body_recurse( + body: TokenStream, + captured: &mut Vec, + lines: &mut Vec, + indent: usize, +) { let mut toks = body.into_iter().peekable(); loop { match toks.next() { @@ -58,8 +68,7 @@ fn body_recurse(body: TokenStream, captured: &mut Vec, lines: &mut Vec { + Some(TokenTree::Punct(punct)) if ['#', '=', ':'].contains(&punct.as_char()) => { lines.last_mut().unwrap().push(' '); } _ => {} @@ -76,7 +85,10 @@ fn body_recurse(body: TokenStream, captured: &mut Vec, lines: &mut Vec { let n_lines = lines.len(); - lines.last_mut().unwrap().push_str(opening(group.delimiter())); + lines + .last_mut() + .unwrap() + .push_str(opening(group.delimiter())); let group_indent = indent + 4; if group.stream().to_string().contains(';') { lines.push(" ".repeat(group_indent)); @@ -86,7 +98,10 @@ fn body_recurse(body: TokenStream, captured: &mut Vec, lines: &mut Vec, lines: &mut Vec { lines.last_mut().unwrap().push_str(&ident.to_string()); match toks.peek() { - Some(TokenTree::Punct(punct)) if ['.', ';', ','].contains(&punct.as_char()) => {} + Some(TokenTree::Punct(punct)) if ['.', ';', ','].contains(&punct.as_char()) => { + } Some(TokenTree::Group(g)) if g.delimiter() != Delimiter::Brace => {} - Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace && !g.to_string().contains(';') => {} + Some(TokenTree::Group(g)) + if g.delimiter() == Delimiter::Brace && !g.to_string().contains(';') => {} None => {} _ => { lines.last_mut().unwrap().push(' '); @@ -107,7 +124,7 @@ fn body_recurse(body: TokenStream, captured: &mut Vec, lines: &mut Vec', '<', '=', '*'].contains(&punct.as_char()) => {} + if ['>', '<', '=', '*'].contains(&punct.as_char()) => {} Some(TokenTree::Group(_)) => {} None => {} _ => { diff --git a/macro/src/function.rs b/macro/src/function.rs index 4d00f7e..9906094 100644 --- a/macro/src/function.rs +++ b/macro/src/function.rs @@ -1,198 +1,17 @@ -use proc_macro::{Delimiter, Ident, TokenStream, TokenTree}; -use std::iter::Peekable; +use syn::parse::Parse; -use proc_macro2::{Ident as Ident2, TokenStream as TokenStream2}; -use quote::quote; +use syn::{Signature, Visibility}; -use mir::Visibility; - -use crate::body::pull_interpolation; - -pub struct Tags { - pub asyn: bool, +pub struct FnHeader { pub vis: Visibility, - pub fn_name: TokenStream2, -} - -/// Capture $(async)? $(pub)? fn_name -pub fn parse_intro(toks: &mut impl Iterator) -> Tags { - let mut asyn = false; - let mut vis = Visibility::Private; - let mut captured = vec![]; - let fn_name = loop { - let next = toks - .next() - .expect("Unexpectedly reached end of token stream in function! macro"); - match next { - TokenTree::Ident(ident) if ident.to_string() == "async" => { - asyn = true; - } - TokenTree::Ident(ident) if ident.to_string() == "pub" => { - vis = Visibility::Public; - } - TokenTree::Ident(ident) => { - break ident.to_string(); - } - TokenTree::Punct(punct) if punct.as_char() == '#' => { - break pull_interpolation(toks, &mut captured, false); - } - _ => panic!( - "Expected one of: async, pub, or the function's name. Got: {:?}", - next - ), - } - }; - let fn_name = if captured.is_empty() { - quote!( ::mir::Ident(#fn_name.to_string()) ) - } else { - let captured = captured - .into_iter() - .map(|name| Ident2::new(&name, proc_macro2::Span::call_site())); - - quote!(::mir::Ident(format!("{}", #( #captured ),*))) - }; - Tags { asyn, vis, fn_name } -} - -pub struct Arg { - pub name: String, - pub arg_type: TokenStream2, - pub default: TokenStream2, + pub sig: Signature, } -pub fn parse_args(arg_toks: TokenStream) -> Vec { - let mut args = Vec::new(); - let mut arg_toks = arg_toks.into_iter().peekable(); - loop { - let arg_name = match arg_toks.next() { - Some(TokenTree::Ident(ident)) => ident.to_string(), - Some(other) => panic!("Expected an argument name. Got: {:?}", other), - None => break, - }; - - match arg_toks.next() { - Some(TokenTree::Punct(punct)) if punct.as_char() == ':' => (), - Some(other) => panic!("Expected a colon. Got: {:?}", other), - None => panic!("Expected a colon. Got: end of token stream"), - } - - let arg_type = match arg_toks.next() { - // Matches the ident arg_type, e.g. - // str - // Dict[str, str] - // requests.PreparedRequest - Some(TokenTree::Ident(ident)) => parse_type(ident, &mut arg_toks), - // Matches a arg_type binding, e.g. - // let arg_type = "int"; - // function!(add(a: #arg_type, b: #arg_type) {}) - Some(TokenTree::Punct(punct)) if punct.as_char() == '#' => { - let mut captured = vec![]; - let placeholder = pull_interpolation(&mut arg_toks, &mut captured, false); - - let captured = captured - .into_iter() - .map(|name| Ident2::new(&name, proc_macro2::Span::call_site())); - - quote! { - format!(#placeholder, #( #captured ),*) - } - } - other => panic!("Expected an argument type. Got: {:?}", other), - }; - - let default = match arg_toks.peek() { - Some(TokenTree::Punct(punct)) if punct.as_char() == '=' => { - arg_toks.next(); // we peaked, so we need to consume the = - match arg_toks.next() { - Some(TokenTree::Literal(lit)) => { - let lit = lit.to_string(); - quote!(Some(#lit.to_string())) - } - Some(other) => panic!("Expected a default value. Got: {:?}", other), - None => panic!("Expected a default value. Got: end of token stream"), - } - } - Some(TokenTree::Punct(punct)) if [',', ')'].contains(&punct.as_char()) => { - quote!(None) - } - Some(other) => panic!("Expected one of: , or ) or =. Got: {:?}", other), - None => quote!(None), - }; - - args.push(Arg { - name: arg_name, - arg_type, - default, - }); - - match arg_toks.next() { - Some(TokenTree::Punct(punct)) if punct.as_char() == ',' => (), - None => break, - Some(other) => panic!( - "Expected a comma or a closing parenthesis. Got: {:?}", - other - ), - } - } - args -} - -/// Matches a ident arg_type, e.g. -/// str -/// Dict[str, str] -/// requests.PreparedRequest -pub fn parse_type( - ident: Ident, - toks: &mut Peekable>, -) -> TokenStream2 { - let mut ident = ident.to_string(); - // Matches path-ed types, e.g. requests.PreparedRequest - while matches!(toks.peek(), Some(TokenTree::Punct(punct)) if punct.as_char() == '.') { - ident += &toks.next().unwrap().to_string(); - if !matches!(toks.peek(), Some(TokenTree::Ident(_))) { - panic!("Expected an identifier after a dot. Got: {:?}", toks.peek()); - } - ident += &toks.next().unwrap().to_string(); - } - // Matches python generics, e.g. Dict[str, Any] - if matches!(toks.peek(), Some(TokenTree::Group(group)) - if matches!(group.delimiter(), Delimiter::Bracket)) - { - ident += &toks.next().unwrap().to_string(); - } - quote! { - #ident.to_string() - } -} - -pub fn parse_return(toks: &mut Peekable>) -> TokenStream2 { - loop { - match toks.peek() { - Some(TokenTree::Punct(punct)) if punct.as_char() == '-' => { - toks.next(); - match toks.next() { - Some(TokenTree::Punct(punct)) if punct.as_char() == '>' => match toks.next() { - Some(TokenTree::Ident(ident)) => { - break parse_type(ident, toks); - } - Some(TokenTree::Punct(punct)) if punct.as_char() == '#' => { - let mut captured = vec![]; - let placeholder = pull_interpolation(toks, &mut captured, false); - - let captured = captured - .into_iter() - .map(|name| Ident2::new(&name, proc_macro2::Span::call_site())); - break quote!(format!(#placeholder, #( #captured ),*)); - } - next => panic!("Expected the return type. Got: {:?}", next), - }, - next => panic!("Expected ->. Got: {:?}", next), - } - } - Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace => { - break quote!("".to_string()); - } - next => panic!("Expected -> or {{. Got: {:?}", next), - } +impl Parse for FnHeader { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + Ok(FnHeader { + vis: input.parse()?, + sig: input.parse()?, + }) } } diff --git a/macro/src/lib.rs b/macro/src/lib.rs index c13d55d..8eb8691 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -1,22 +1,11 @@ -mod rfunction; mod function; mod body; -use proc_macro::{Delimiter, TokenStream, TokenTree}; -use proc_macro2::{TokenStream as TokenStream2}; -use quote::quote; -use body::body_callable; -use function::{Arg, Tags}; -use mir::Visibility; -use crate::function::{parse_intro, parse_args, parse_return} ; - -fn vis_to_token(vis: Visibility) -> TokenStream2 { - match vis { - Visibility::Public => quote!(::mir::Visibility::Public), - Visibility::Private => quote!(::mir::Visibility::Private), - Visibility::Crate => quote!(::mir::Visibility::Crate), - } -} +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens}; +use syn::{parse_macro_input, ReturnType, Visibility}; +use crate::body::body_callable; /// Define a function where the body is a string. The fn interface definition is reminiscent of Python, /// but because it creates a mir::Function, it will compile down into whatever language we target. @@ -24,101 +13,57 @@ fn vis_to_token(vis: Visibility) -> TokenStream2 { /// nor would making one make sense (languages don't have mutually compatible ASTs) #[proc_macro] pub fn function(item: TokenStream) -> TokenStream { - let mut toks = item.into_iter().peekable(); - let Tags { asyn, vis, fn_name } = parse_intro(&mut toks); - // 2. Argument groups - let arg_toks = match toks.next() { - Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Parenthesis => group, - _ => panic!("Expected a group of arguments"), + let item = parse_macro_input!(item as function::FnHeader); + let vis = match item.vis { + Visibility::Public(_) => quote!(::mir::Visibility::Public), + Visibility::Restricted(_) => quote!(::mir::Visibility::Private), + Visibility::Inherited => quote!(::mir::Visibility::Private), }; - let args = parse_args(arg_toks.stream()).into_iter().map(|arg| { - let Arg { name, arg_type, default } = arg; - quote! { - ::mir::FnArg2::Basic { - name: ::mir::Ident::new(#name), - ty: #arg_type, - default: #default, - } - } - }).collect::>(); - - let ret = parse_return(&mut toks); - - // 4. Body - let body = match toks.next() { - Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace => { - body_callable(group.stream()) - } - other => panic!("Expected a function body. Got: {:?}", other), + let is_async = if item.sig.asyncness.is_some() { + quote!(true) + } else { + quote!(false) }; - - let vis = vis_to_token(vis); - quote! { - ::mir::Function { - name: #fn_name, - async_: #asyn, - vis: #vis, - args: vec![#(#args),*], - ret: #ret, - body: #body, - ..::mir::Function::default() - } - }.into() -} - - -/// like function, but for Rust -#[proc_macro] -pub fn rfunction(item: TokenStream) -> TokenStream { - let mut toks = item.into_iter().peekable(); - - let Tags { asyn, vis, fn_name } = parse_intro(&mut toks); - // 2. Argument groups - let arg_toks = match toks.next() { - Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Parenthesis => group, - _ => panic!("Expected a group of arguments"), + let name = item.sig.ident.to_string(); + let ret = match item.sig.output { + ReturnType::Default => TokenStream2::new(), + ReturnType::Type(_, t) => t.to_token_stream(), }; - let args = rfunction::parse_args2(arg_toks.stream()).into_iter().map(|arg| { - let Arg { name, arg_type, default } = arg; + let args = item.sig.inputs.iter().map(|arg| { + let (name, ty) = match arg { + syn::FnArg::Receiver(_) => panic!("Self arguments are not supported"), + syn::FnArg::Typed(pat) => { + let name = match &*pat.pat { + syn::Pat::Ident(ident) => ident.ident.to_string(), + _ => panic!("Only simple identifiers are supported"), + }; + let ty = pat.ty.to_token_stream(); + (name, ty) + } + }; quote! { - ::mir::FnArg2::Basic { - name: ::mir::Ident::new(#name), - ty: #arg_type, - default: #default, + ::mir::Arg::Basic { + name: Ident(#name.to_string()), + ty: #ty, + default: None, } } - }).collect::>(); - - let ret = rfunction::parse_return2(&mut toks); + }); - let body = match toks.next() { - Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace => { - let toks = TokenStream2::from(group.stream()); - let toks = quote! { - ::quote::quote!(#toks) - }; - toks - } - other => panic!("Expected function body. Got: {:?}", other), - }; - - let vis = vis_to_token(vis); quote! { ::mir::Function { - name: #fn_name, - async_: #asyn, + name: Ident(#name.to_string()), + is_async: #is_async, vis: #vis, args: vec![#(#args),*], ret: #ret, - body: #body, ..::mir::Function::default() } }.into() } - #[proc_macro] pub fn body(body: TokenStream) -> TokenStream { body_callable(body).into() -} +} \ No newline at end of file diff --git a/macro/src/rfunction.rs b/macro/src/rfunction.rs deleted file mode 100644 index 4131fb8..0000000 --- a/macro/src/rfunction.rs +++ /dev/null @@ -1,92 +0,0 @@ -use proc_macro::{Delimiter, TokenStream, TokenTree}; -use proc_macro2::TokenStream as TokenStream2; -use std::iter::Peekable; -use quote::quote; -use crate::Arg; - -pub fn parse_args2(arg_toks: TokenStream) -> Vec { - let mut args = Vec::new(); - let mut arg_toks = arg_toks.into_iter().peekable(); - loop { - let arg_name = match arg_toks.next() { - Some(TokenTree::Ident(ident)) => ident.to_string(), - Some(other) => panic!("Expected an argument name. Got: {:?}", other), - None => break, - }; - - match arg_toks.next() { - Some(TokenTree::Punct(punct)) if punct.as_char() == ':' => (), - Some(other) => panic!("Expected a colon. Got: {:?}", other), - None => panic!("Expected a colon. Got: end of token stream"), - } - - let arg_type = match arg_toks.peek() { - Some(TokenTree::Ident(_)) => { - let toks = vec![ - arg_toks.next().unwrap() - ]; - let toks = TokenStream2::from(TokenStream::from_iter(toks.into_iter())); - quote! { - ::quote::quote!(#toks) - } - } - Some(TokenTree::Punct(punct)) if punct.as_char() == '#' => { - let toks = vec![ - arg_toks.next().unwrap(), - arg_toks.next().expect("Expected an ident after #"), - ]; - let toks = TokenStream2::from(TokenStream::from_iter(toks.into_iter())); - quote! { - ::quote::quote!(#toks) - } - } - other => panic!("Expected an argument type. Got: {:?}", other), - }; - - args.push(Arg { - name: arg_name, - arg_type, - default: quote!(None), - }); - - match arg_toks.next() { - Some(TokenTree::Punct(punct)) if punct.as_char() == ',' => (), - None => break, - Some(other) => panic!( - "Expected a comma or a closing parenthesis. Got: {:?}", - other - ), - } - } - args -} - -// for parsing rust function, retuns a TokenStream2 OF a TokenStream2 -pub fn parse_return2(toks: &mut Peekable>) -> TokenStream2 { - let mut muncher = vec![]; - match toks.peek() { - Some(TokenTree::Punct(punct)) if punct.as_char() == '-' => { - toks.next(); // skip the - - match toks.next() { - Some(TokenTree::Punct(punct)) if punct.as_char() == '>' => { - loop { - match toks.peek() { - Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace => break, - Some(_) => { - muncher.push(toks.next().unwrap()) - } - None => panic!("Expected return type. Got end of stream."), - } - } - let toks = TokenStream2::from(TokenStream::from_iter(muncher.into_iter())); - quote!(::quote::quote!(#toks)) - } - next => panic!("Expected ->. Got: {:?}", next), - } - } - Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace => { - quote!(::proc_macro2::TokenStream::new()) - } - next => panic!("Expected -> or {{. Got: {:?}", next), - } -} diff --git a/macro/tests/body.rs b/macro/tests/body.rs index 7a2a524..d234a89 100644 --- a/macro/tests/body.rs +++ b/macro/tests/body.rs @@ -1,4 +1,4 @@ -use ln_macro::body; +use libninja_macro::body; use pretty_assertions::assert_eq; // #[test] @@ -65,5 +65,8 @@ fn test_go_assignment_spacing() { fn test_go_doesnt_wrap_brace() { let inside = "\"api\" : \"v1\""; let b = body!(postBody, _ := json.Marshal(map[string]string{#inside})); - assert_eq!(b, "postBody, _ := json.Marshal(map[string]string{\"api\" : \"v1\"})"); -} \ No newline at end of file + assert_eq!( + b, + "postBody, _ := json.Marshal(map[string]string{\"api\" : \"v1\"})" + ); +} diff --git a/macro/tests/function.rs b/macro/tests/function.rs index 9a86e3e..4e7cc48 100644 --- a/macro/tests/function.rs +++ b/macro/tests/function.rs @@ -1,6 +1,6 @@ use pretty_assertions::assert_eq; -use ln_macro::function; +use libninja_macro::function; use mir::{Function, Visibility}; #[test] @@ -18,9 +18,9 @@ fn test_function_args() { assert_eq!(s.async_, false); assert_eq!(s.vis, Visibility::Private); assert_eq!(s.args.len(), 2); - assert_eq!(s.args[0].name().unwrap(), "s"); + assert_eq!(s.args[0].ident().unwrap(), "s"); assert_eq!(s.args[0].ty().unwrap(), "str"); - assert_eq!(s.args[1].name().unwrap(), "n"); + assert_eq!(s.args[1].ident().unwrap(), "n"); assert_eq!(s.args[1].ty().unwrap(), "int"); assert_eq!(s.ret, "".to_string()); } diff --git a/macro/tests/rfunction.rs b/macro/tests/rfunction.rs index 43b2e84..08dbff5 100644 --- a/macro/tests/rfunction.rs +++ b/macro/tests/rfunction.rs @@ -1,14 +1,13 @@ -use proc_macro2::TokenStream; use quote::quote; -use ln_macro::rfunction; -use mir::Function; +use libninja_macro::function; #[test] fn test_quote_body() { - let s: Function = rfunction!(add(a: i32, b: i32) -> i32 { + let mut s = function!(fn add(a: i32, b: i32) -> i32); + s.body = quote! { println!("Hello, World!") - }); + }; assert_eq!(s.name.0, "add"); assert_eq!(s.body.to_string(), "println ! (\"Hello, World!\")"); assert_eq!(s.ret.to_string(), "i32"); @@ -23,7 +22,8 @@ fn test_regression1() { let declarations = vec![quote!(let a = 1), quote!(let b = 2), quote!(let c = 3)]; let operation = quote!(link_token_create); let fn_args = vec![quote!(a), quote!(b), quote!(c)]; - let main = rfunction!(main() { + let mut main = function!(fn main()); + main.body = quote! { let client = #client::from_env(); #(#declarations)* let response = client.#operation(#(#fn_args),*) @@ -31,6 +31,6 @@ fn test_regression1() { .await .unwrap(); println!("{:#?}", response); - }); + }; assert_eq!(main.body.to_string(), "let client = Client :: from_env () ; let a = 1 let b = 2 let c = 3 let response = client . link_token_create (a , b , c) . send () . await . unwrap () ; println ! (\"{:#?}\" , response) ;"); } diff --git a/macro/tests/test_only_need_single_braces.rs b/macro/tests/test_only_need_single_braces.rs index 962e4be..7769310 100644 --- a/macro/tests/test_only_need_single_braces.rs +++ b/macro/tests/test_only_need_single_braces.rs @@ -1,23 +1,28 @@ +use libninja_macro::{body, function}; use pretty_assertions::assert_eq; -use ln_macro::function; #[test] fn test_only_need_single_braces() { let client_name = "foobar"; - let s = function!(pub NewClientFromEnv() -> #client_name { + let s = function!(pub fn NewClientFromEnv() -> #client_name); + s.body = body! { baseUrl, exists := os.LookupEnv("PET_STORE_BASE_URL"); if !exists { fmt.Fprintln(os.Stderr, "Environment variable PET_STORE_BASE_URL is not set."); os.Exit(1); } return Client{baseUrl: baseUrl} - }); - assert_eq!(s.body, r#" + }; + assert_eq!( + s.body, + r#" baseUrl, exists := os.LookupEnv("PET_STORE_BASE_URL") if !exists { fmt.Fprintln(os.Stderr, "Environment variable PET_STORE_BASE_URL is not set.") os.Exit(1) } return Client{baseUrl : baseUrl} -"#.trim()); -} \ No newline at end of file +"# + .trim() + ); +} diff --git a/mir/src/class.rs b/mir/src/class.rs index 33e781e..a21b817 100644 --- a/mir/src/class.rs +++ b/mir/src/class.rs @@ -1,25 +1,13 @@ -use std::fmt::{Debug, Formatter}; use crate::{Doc, Function, Ident, Visibility}; +use std::fmt::{Debug, Formatter}; pub struct Class { + pub vis: Visibility, pub name: Ident, pub doc: Option, - /// `code` is for Python, where we need code like this: - /// class Account(BaseModel): - /// class Config: - /// this_is_a_config_for_pydantic = True - pub code: Option, - pub instance_fields: Vec>, - pub static_fields: Vec>, - pub constructors: Vec>, - /// Use `class_methods` in Rust. - pub class_methods: Vec>, - pub static_methods: Vec>, - pub vis: Visibility, - - pub lifetimes: Vec, - pub decorators: Vec, - pub superclasses: Vec, + pub fields: Vec>, + pub methods: Vec>, + pub attributes: Vec, } #[derive(Debug, Default)] @@ -30,23 +18,16 @@ pub struct Field { pub vis: Visibility, pub doc: Option, pub optional: bool, - pub decorators: Vec, + pub attributes: Vec, } - impl Debug for Class { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("Class") .field("name", &self.name) .field("doc", &self.doc) - .field("instance_fields", &self.instance_fields) - .field("static_fields", &self.static_fields) - .field("constructors", &self.constructors) - .field("class_methods", &self.class_methods) - .field("static_methods", &self.static_methods) + .field("instance_fields", &self.fields) .field("vis", &self.vis) - .field("lifetimes", &self.lifetimes) - .field("superclasses", &self.superclasses) .finish() } } @@ -54,18 +35,12 @@ impl Debug for Class { impl Default for Class { fn default() -> Self { Self { - name: Ident("".to_string()), - code: None, + name: Ident::empty(), doc: None, - instance_fields: vec![], - static_fields: vec![], - constructors: vec![], - class_methods: vec![], - static_methods: vec![], + fields: vec![], vis: Visibility::Private, - lifetimes: vec![], - decorators: vec![], - superclasses: vec![], + attributes: vec![], + methods: vec![], } } } diff --git a/mir/src/enum.rs b/mir/src/enum.rs index 22e3f78..51829ed 100644 --- a/mir/src/enum.rs +++ b/mir/src/enum.rs @@ -1,20 +1,20 @@ use crate::{Doc, Function, Ident, Visibility}; #[derive(Debug, Default)] -pub struct Enum { +pub struct Enum { pub name: Ident, pub doc: Option, - pub variants: Vec>, + pub variants: Vec>, pub vis: Visibility, pub methods: Vec>, - pub extra: E, + pub attributes: Vec, } #[derive(Debug)] -pub struct Variant { +pub struct Variant { pub ident: Ident, pub doc: Option, // in rust, value is like enum { Error = 0 } - pub value: Option, - pub extra: E, + pub value: Option, + pub attributes: Vec, } diff --git a/mir/src/file.rs b/mir/src/file.rs index b1ea01f..4df79ba 100644 --- a/mir/src/file.rs +++ b/mir/src/file.rs @@ -1,19 +1,31 @@ use crate::{Class, Doc, Enum, Function, Import}; -pub struct File { +/// A file is a collection of imports, classes, enums, functions, and code. +/// Layout: +/// +/// {docs} +/// {imports} +/// {declaration} +/// {classes} +/// {enums} +/// {functions} +/// {code} +/// +/// TODO wheres package go? +pub struct File { pub imports: Vec, pub doc: Option, /// Code that is before function and class declarations pub declaration: Option, pub classes: Vec>, - pub enums: Vec>, + pub enums: Vec>, pub functions: Vec>, /// Code that follows after the function and class declarations pub code: Option, pub package: Option, } -impl Default for File +impl Default for File where T: Default, { diff --git a/mir/src/function.rs b/mir/src/function.rs index 59bd569..03fdfb8 100644 --- a/mir/src/function.rs +++ b/mir/src/function.rs @@ -2,66 +2,7 @@ use std::fmt::{Debug, Formatter}; use crate::{Doc, Ident, Visibility}; -/// Localized -pub enum ArgIdent { - Ident(String), - // parallel to Ident - Unpack(Vec), -} - -impl ArgIdent { - pub fn force_string(&self) -> String { - match self { - ArgIdent::Ident(s) => s.clone(), - ArgIdent::Unpack(_) => panic!("cannot force unpacked arg name to string"), - } - } - - pub fn is_empty(&self) -> bool { - match self { - ArgIdent::Ident(s) => s.is_empty(), - ArgIdent::Unpack(v) => v.is_empty(), - } - } - - pub fn unwrap_ident(self) -> Ident { - match self { - ArgIdent::Ident(s) => Ident(s), - ArgIdent::Unpack(_) => panic!("cannot unwrap unpacked arg name"), - } - } -} - -impl From for ArgIdent { - fn from(s: String) -> Self { - ArgIdent::Ident(s) - } -} - -impl From<&str> for ArgIdent { - fn from(s: &str) -> Self { - ArgIdent::Ident(s.to_string()) - } -} - - -impl From for ArgIdent { - fn from(ident: Ident) -> Self { - ArgIdent::Ident(ident.0) - } -} - - -// // IR form. Therefore it's localized -// pub struct FnArg { -// pub name: ArgIdent, -// pub ty: T, -// // T is a String (for Rust, TokenStream) -// pub default: Option, -// pub treatment: Option, -// } - -pub enum FnArg2 { +pub enum Arg { /// fn foo(a: i32) Basic { name: Ident, @@ -70,109 +11,52 @@ pub enum FnArg2 { }, /// For typescript /// function foo({foo, bar}: FooProps) - Unpack { - names: Vec, - ty: T, - }, - /// For rust - /// fn foo(&self) + Unpack { names: Vec, ty: T }, + /// rust: fn foo(&self) SelfArg { mutable: bool, reference: bool }, - /// For python - /// def foo(**kwargs) - Kwargs { - name: Ident, - ty: T, - }, - /// For python - /// def foo(*args) - Variadic { - name: Ident, - ty: T, - }, + /// python: def foo(**kwargs) + Kwargs { name: Ident, ty: T }, + /// python: def foo(*args) + Variadic { name: Ident, ty: T }, } -impl FnArg2 { - pub fn name(&self) -> Option<&Ident> { +impl Arg { + pub fn ident(&self) -> Option<&Ident> { let name = match self { - FnArg2::Basic { name, .. } => name, - FnArg2::Unpack { .. } => return None, - FnArg2::SelfArg { .. } => return None, - FnArg2::Kwargs { name, .. } => name, - FnArg2::Variadic { name, .. } => name, + Arg::Basic { name, .. } => name, + Arg::Unpack { .. } => return None, + Arg::SelfArg { .. } => return None, + Arg::Kwargs { name, .. } => name, + Arg::Variadic { name, .. } => name, }; Some(name) } pub fn ty(&self) -> Option<&T> { let ty = match self { - FnArg2::Basic { ty, .. } => ty, - FnArg2::Unpack { ty, .. } => ty, - FnArg2::SelfArg { .. } => return None, - FnArg2::Kwargs { ty, .. } => ty, - FnArg2::Variadic { ty, .. } => ty, + Arg::Basic { ty, .. } => ty, + Arg::Unpack { ty, .. } => ty, + Arg::SelfArg { .. } => return None, + Arg::Kwargs { ty, .. } => ty, + Arg::Variadic { ty, .. } => ty, }; Some(ty) } } -// impl FnArg { -// pub fn new(name: String, ty: T) -> Self { -// FnArg { -// name: ArgIdent::Ident(name), -// ty, -// default: None, -// treatment: None, -// } -// } -// -// pub fn from_ident(name: Ident, ty: T) -> Self { -// FnArg { -// name: ArgIdent::Ident(name.0), -// ty, -// default: None, -// treatment: None, -// } -// } -// } - -// impl FnArg { -// /// Used by python for dividing required vs optional args -// pub fn empty_variadic() -> Self { -// FnArg { -// name: ArgIdent::Ident("".to_string()), -// ty: "".to_string(), -// default: None, -// treatment: Some(FnArgTreatment::Variadic), -// } -// } -// } - -// pub enum FnArgTreatment { -// /// python: **kwargs -// Kwargs, -// /// python: *args -// /// golang: ...opt -// Variadic, -// } - pub struct Function { pub name: Ident, - pub args: Vec>, - /// This *is* localized to the programming language. + pub args: Vec>, pub ret: T, pub body: T, pub doc: Option, - pub async_: bool, + pub is_async: bool, pub vis: Visibility, - /// #[...] in Rust - /// @... in Python - pub annotations: Vec, - pub generic: Vec, } impl Debug for Function - where - T: Debug, +where + T: Debug, { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("Function") @@ -185,8 +69,8 @@ impl Debug for Function } impl Default for Function - where - T: Default, +where + T: Default, { fn default() -> Self { Self { @@ -195,25 +79,14 @@ impl Default for Function ret: T::default(), body: T::default(), doc: None, - async_: false, + is_async: false, vis: Default::default(), - annotations: vec![], - generic: vec![], - } - } -} - -impl std::fmt::Display for ArgIdent { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - ArgIdent::Ident(name) => write!(f, "{}", name), - ArgIdent::Unpack(vec) => write!(f, "{}", build_struct(vec.iter())), } } } /// Build something wrapped in braces, { A, B, C } -pub fn build_struct(mut s: impl Iterator>) -> String { +pub fn build_struct(mut s: impl Iterator>) -> String { let mut r = String::from("{"); let mut t = s.next(); while let Some(u) = &t { @@ -228,6 +101,6 @@ pub fn build_struct(mut s: impl Iterator>) -> String { } /// Build keys wrapped in braces, e.g. {"a": 1, "b": 2} -pub fn build_dict<'a>(s: impl Iterator) -> String { +pub fn build_dict<'a>(s: impl Iterator) -> String { build_struct(s.map(|(k, v)| format!("\"{}\": {}", k, v))) -} \ No newline at end of file +} diff --git a/mir/src/ident.rs b/mir/src/ident.rs index 919cc71..9dbd0e3 100644 --- a/mir/src/ident.rs +++ b/mir/src/ident.rs @@ -10,6 +10,10 @@ impl Ident { pub fn new(s: &'static str) -> Self { Ident(s.into()) } + + pub fn empty() -> Self { + Ident("".into()) + } } impl std::fmt::Display for Ident { diff --git a/mir/src/import.rs b/mir/src/import.rs index 0afcfe0..a777c73 100644 --- a/mir/src/import.rs +++ b/mir/src/import.rs @@ -10,14 +10,13 @@ pub struct Import { /// If a wildcard import and if we want to alias, then alias pub alias: Option, pub vis: Visibility, - pub feature: Option + pub feature: Option, } - impl Import { - pub fn package(path: &str) -> Self { + pub fn package(path: impl AsRef) -> Self { Self { - path: path.to_string(), + path: path.as_ref().to_string(), imports: vec![], alias: None, vis: Visibility::Private, @@ -25,13 +24,10 @@ impl Import { } } - pub fn new(path: &str, imports: impl IntoIterator>) -> Self { + pub fn new(path: &str, imports: impl IntoIterator>) -> Self { Self { path: path.to_string(), - imports: imports - .into_iter() - .map(|s| s.into()) - .collect(), + imports: imports.into_iter().map(|s| s.into()).collect(), alias: None, vis: Visibility::Private, feature: None, @@ -54,7 +50,6 @@ impl Import { } } - pub struct ImportItem { /// This might not conform to standard ident rules for the language, so its a string, not an ident. pub name: String, @@ -63,7 +58,10 @@ pub struct ImportItem { impl ImportItem { pub fn alias(name: &str, alias: &str) -> Self { - Self { name: name.to_string(), alias: Some(alias.to_string()) } + Self { + name: name.to_string(), + alias: Some(alias.to_string()), + } } pub fn validate(&self) -> Result<(), String> { @@ -79,7 +77,10 @@ impl ImportItem { impl From<&String> for ImportItem { fn from(s: &String) -> Self { - let r = Self { name: s.clone(), alias: None }; + let r = Self { + name: s.clone(), + alias: None, + }; r.validate().unwrap(); r } @@ -87,7 +88,10 @@ impl From<&String> for ImportItem { impl From for ImportItem { fn from(s: String) -> Self { - let r = Self { name: s, alias: None }; + let r = Self { + name: s, + alias: None, + }; r.validate().unwrap(); r } @@ -95,7 +99,10 @@ impl From for ImportItem { impl From<&str> for ImportItem { fn from(s: &str) -> Self { - let r = Self { name: s.to_string(), alias: None }; + let r = Self { + name: s.to_string(), + alias: None, + }; r.validate().unwrap(); r } @@ -103,7 +110,9 @@ impl From<&str> for ImportItem { impl From for ImportItem { fn from(s: Ident) -> Self { - Self { name: s.0, alias: None } + Self { + name: s.0, + alias: None, + } } } - diff --git a/mir/src/interface.rs b/mir/src/interface.rs new file mode 100644 index 0000000..7514bcf --- /dev/null +++ b/mir/src/interface.rs @@ -0,0 +1,9 @@ +use crate::{Doc, Field, Function}; + +pub struct Interface { + pub name: String, + pub doc: Option, + pub fields: Vec>, + pub public: bool, + pub instance_methods: Vec>, +} diff --git a/mir/src/lib.rs b/mir/src/lib.rs index af6097e..9d79bb3 100644 --- a/mir/src/lib.rs +++ b/mir/src/lib.rs @@ -1,9 +1,7 @@ -use core::fmt::Formatter; - pub use class::*; pub use doc::{Doc, DocFormat}; pub use file::File; -pub use function::{ArgIdent, build_dict, build_struct, FnArg2, Function}; +pub use function::{build_dict, build_struct, Arg, Function}; pub use ident::*; pub use import::*; pub use r#enum::*; @@ -17,56 +15,12 @@ mod file; mod function; mod ident; mod import; +mod interface; +mod literal; mod r#macro; +mod newtype; +pub mod parameter; mod ty; mod visibility; -pub struct Interface { - pub name: String, - pub doc: Option, - pub fields: Vec>, - pub public: bool, - pub instance_methods: Vec>, -} - -pub struct NewType { - pub name: String, - pub doc: Option, - pub ty: T, - pub public: bool, -} - -pub struct Literal(pub T); - -#[allow(unused)] -pub struct Grave(String); - -#[allow(unused)] -pub struct FString(String); - -pub fn literal(s: impl Into) -> Literal { - Literal(s.into()) -} - -pub fn grave(s: &str) -> Literal { - Literal(Grave(s.to_string())) -} - -pub fn f_string(s: &str) -> Literal { - Literal(FString(s.to_string())) -} - -/// Specifically represents a parameter in Location::Query. We need special treatment for repeated keys. -pub enum ParamKey { - Key(String), - RepeatedKey(String), -} - -impl std::fmt::Display for ParamKey { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - ParamKey::Key(s) => write!(f, "\"{}\"", s), - ParamKey::RepeatedKey(s) => write!(f, "\"{}[]\"", s), - } - } -} +pub use literal::Literal; diff --git a/mir/src/literal.rs b/mir/src/literal.rs new file mode 100644 index 0000000..3203597 --- /dev/null +++ b/mir/src/literal.rs @@ -0,0 +1 @@ +pub struct Literal(pub T); diff --git a/mir/src/newtype.rs b/mir/src/newtype.rs new file mode 100644 index 0000000..41a79a9 --- /dev/null +++ b/mir/src/newtype.rs @@ -0,0 +1,6 @@ +pub struct NewType { + pub name: String, + pub doc: Option, + pub ty: T, + pub public: bool, +} diff --git a/mir/src/parameter.rs b/mir/src/parameter.rs new file mode 100644 index 0000000..48c1fc2 --- /dev/null +++ b/mir/src/parameter.rs @@ -0,0 +1,16 @@ +use std::fmt::Formatter; + +/// Specifically represents a parameter in Location::Query. We need special treatment for repeated keys. +pub enum ParamKey { + Key(String), + RepeatedKey(String), +} + +impl std::fmt::Display for ParamKey { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ParamKey::Key(s) => write!(f, "\"{}\"", s), + ParamKey::RepeatedKey(s) => write!(f, "\"{}[]\"", s), + } + } +} diff --git a/mir_rust/Cargo.toml b/mir_rust/Cargo.toml index cd468b4..ca781d6 100644 --- a/mir_rust/Cargo.toml +++ b/mir_rust/Cargo.toml @@ -13,7 +13,7 @@ path = "src/lib.rs" libninja_mir = { path = "../mir" } proc-macro2 = "1.0.78" quote = "1.0.9" -syn = "2.0.48" +syn = { version = "2.0.48", features = ["full"] } convert_case = "0.6.0" regex = "1.10.3" prettyplease = "0.2.16" diff --git a/mir_rust/src/class.rs b/mir_rust/src/class.rs index d584747..e9898b3 100644 --- a/mir_rust/src/class.rs +++ b/mir_rust/src/class.rs @@ -4,31 +4,28 @@ use quote::quote; use crate::ToRustCode; -impl ToRustCode for Class { +pub struct RustClass { + pub class: Class, + pub lifetimes: Vec, +} + +impl ToRustCode for RustClass { fn to_rust_code(self) -> TokenStream { - let Class { - name, - doc, - code, - instance_fields, - static_fields, - constructors, - class_methods, - static_methods, - vis, + let RustClass { + class: + Class { + vis, + name, + doc, + fields, + methods, + attributes, + }, lifetimes, - decorators, - superclasses } = self; - assert!(superclasses.is_empty(), "superclasses not supported in Rust"); - assert!(static_fields.is_empty(), "static fields not supported in Rust"); - assert!(constructors.is_empty(), "constructors not supported in Rust"); - assert!(code.is_none(), "code in class body not supported in Rust"); - assert!(static_methods.is_empty(), "static methods not supported in Rust"); - let vis = vis.to_rust_code(); - let fields = instance_fields.into_iter().map(|f| f.to_rust_code()); - let class_methods = class_methods.into_iter().map(|m| m.to_rust_code()); + let fields = fields.into_iter().map(|f| f.to_rust_code()); + let class_methods = methods.into_iter().map(|m| m.into().to_rust_code()); let doc = doc.to_rust_code(); let lifetimes = if lifetimes.is_empty() { @@ -42,9 +39,7 @@ impl ToRustCode for Class { }; quote! { #doc - #( - #decorators - )* + #(#attributes)* #vis struct #name #lifetimes { #(#fields,)* } @@ -75,4 +70,4 @@ impl ToRustCode for Field { #vis #name: #ty } } -} \ No newline at end of file +} diff --git a/mir_rust/src/enum.rs b/mir_rust/src/enum.rs index f968f98..89689c9 100644 --- a/mir_rust/src/enum.rs +++ b/mir_rust/src/enum.rs @@ -1,9 +1,10 @@ -use crate::{derives_to_tokens, serde_rename2, RustExtra, ToRustCode, ToRustIdent}; +use crate::ident::ToRustIdent; +use crate::{derives_to_tokens, serde_rename2, ToRustCode}; use mir::{Enum, Variant, Visibility}; use proc_macro2::TokenStream; use quote::quote; -pub fn lower_enum(e: &hir::Enum, derives: &[String]) -> Enum { +pub fn lower_enum(e: &hir::Enum, derives: &[String]) -> Enum { let variants = e .variants .iter() @@ -22,9 +23,7 @@ pub fn lower_enum(e: &hir::Enum, derives: &[String]) -> Enum Enum { +impl ToRustCode for Enum { fn to_rust_code(self) -> TokenStream { let Enum { name, @@ -50,13 +47,12 @@ impl ToRustCode for Enum { vis, variants, methods, - extra, + attributes, } = self; let vis = vis.to_rust_code(); let doc = doc.to_rust_code(); let variants = variants.into_iter().map(|v| v.to_rust_code()); let methods = methods.into_iter().map(|m| m.to_rust_code()); - let attributes = extra.attributes; quote! { #doc #(#attributes)* @@ -70,16 +66,15 @@ impl ToRustCode for Enum { } } -impl ToRustCode for Variant { +impl ToRustCode for Variant { fn to_rust_code(self) -> TokenStream { let Variant { ident, doc, value, - extra, + attributes, } = self; let doc = doc.to_rust_code(); - let attributes = extra.attributes; let value = value.map(|v| quote!(= #v)).unwrap_or_default(); quote! { #doc diff --git a/mir_rust/src/example.rs b/mir_rust/src/example.rs new file mode 100644 index 0000000..5488fb2 --- /dev/null +++ b/mir_rust/src/example.rs @@ -0,0 +1,115 @@ +use crate::ident::ToRustIdent; +use crate::ty::ToRustType; +use convert_case::{Case, Casing}; +use hir::{Enum, HirField, HirSpec, NewType, Parameter, Record, Struct}; +use mir::Ty; +use proc_macro2::TokenStream; +use quote::quote; + +pub trait ToRustExample { + fn to_rust_example(&self, spec: &HirSpec) -> TokenStream; +} + +impl ToRustExample for Parameter { + fn to_rust_example(&self, spec: &HirSpec) -> TokenStream { + to_rust_example_value(&self.ty, &self.name, spec, false) + } +} + +pub fn to_rust_example_value( + ty: &Ty, + name: &str, + spec: &HirSpec, + use_ref_value: bool, +) -> TokenStream { + match ty { + Ty::String => { + let s = format!("your {}", name.to_case(Case::Lower)); + if use_ref_value { + quote!(#s) + } else { + quote!(#s.to_owned()) + } + } + Ty::Integer { .. } => quote!(1), + Ty::Float => quote!(1.0), + Ty::Boolean => quote!(true), + Ty::Array(inner) => { + let use_ref_value = if !inner.is_reference_type() { + false + } else { + use_ref_value + }; + let inner = to_rust_example_value(inner, name, spec, use_ref_value); + if use_ref_value { + quote!(&[#inner]) + } else { + quote!(vec![#inner]) + } + } + Ty::Model(model) => { + let record = spec.get_record(model).expect("record not found"); + let force_ref = model.ends_with("Required"); + match record { + Record::Struct(Struct { + name: _name, + fields, + nullable, + docs: _docs, + }) => { + let fields = fields.iter().map(|(name, field)| { + let not_ref = !force_ref || field.optional; + let mut value = to_rust_example_value(&field.ty, name, spec, !not_ref); + let name = name.to_rust_ident(); + if field.optional { + value = quote!(Some(#value)); + } + Ok(quote!(#name: #value)) + }); + let model = model.to_rust_struct(); + quote!(#model{#(#fields),*}) + } + Record::NewType(NewType { + name, + fields, + doc: _docs, + }) => { + let fields = fields + .iter() + .map(|f| to_rust_example_value(&f.ty, name, spec, false)); + let name = name.to_rust_struct(); + quote!(#name(#(#fields),*)) + } + Record::Enum(Enum { + name, + variants, + doc: _docs, + }) => { + let variant = variants.first().unwrap(); + let variant = if let Some(a) = &variant.alias { + a.to_rust_struct() + } else { + variant.value.to_rust_struct() + }; + let model = model.to_rust_struct(); + quote!(#model::#variant) + } + Record::TypeAlias(name, HirField { ty, optional, .. }) => { + let not_ref = !force_ref || !optional; + let ty = to_rust_example_value(ty, name, spec, not_ref); + if *optional { + quote!(Some(#ty)) + } else { + quote!(#ty) + } + } + } + } + Ty::Unit => quote!(()), + Ty::Any(_) => quote!(serde_json::json!({})), + Ty::Date { .. } => quote!(chrono::Utc::now().date_naive()), + Ty::DateTime { .. } => quote!(chrono::Utc::now()), + Ty::Currency { .. } => quote!(rust_decimal_macros::dec!(100.01)), + Ty::HashMap(_) => quote!(std::collections::HashMap::new()), + } +} diff --git a/libninja/src/rust/client.rs b/mir_rust/src/file/client.rs similarity index 91% rename from libninja/src/rust/client.rs rename to mir_rust/src/file/client.rs index 953c676..7406748 100644 --- a/libninja/src/rust/client.rs +++ b/mir_rust/src/file/client.rs @@ -2,17 +2,16 @@ use convert_case::{Case, Casing}; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; -use hir::{ - qualified_env_var, AuthLocation, AuthStrategy, HirSpec, Language, Operation, ServerStrategy, -}; -use ln_core::PackageConfig; +use hir::operation::Operation; +use hir::{qualified_env_var, AuthLocation, AuthStrategy, HirSpec, Language, ServerStrategy}; +use ln_core::Config; +use mir::{Arg, Function, Ident}; use mir::{Class, Field, Visibility}; -use mir::{FnArg2, Function, Ident}; -use mir_rust::{ToRustCode, ToRustIdent}; +use mir_rust::ident::ToRustIdent; +use mir_rust::ty::ToRustType; +use mir_rust::ToRustCode; -use crate::rust::codegen::ToRustType; - -pub fn server_url(spec: &HirSpec, opt: &PackageConfig) -> TokenStream { +pub fn server_url(spec: &HirSpec, opt: &Config) -> TokenStream { match spec.server_strategy() { ServerStrategy::Single(url) => quote!(#url), ServerStrategy::Env => { @@ -28,7 +27,7 @@ pub fn server_url(spec: &HirSpec, opt: &PackageConfig) -> TokenStream { } } -fn build_Client_from_env(spec: &HirSpec, opt: &PackageConfig) -> Function { +fn build_Client_from_env(spec: &HirSpec, opt: &Config) -> Function { let auth_struct = opt.authenticator_name().to_rust_struct(); let body = if spec.has_security() { let auth_struct = opt.authenticator_name().to_rust_struct(); @@ -55,7 +54,7 @@ fn build_Client_from_env(spec: &HirSpec, opt: &PackageConfig) -> Function Function { +fn build_Client_with_auth(spec: &HirSpec, opt: &Config) -> Function { let auth_struct = opt.authenticator_name().to_rust_struct(); let body = quote! { Self { @@ -68,7 +67,7 @@ fn build_Client_with_auth(spec: &HirSpec, opt: &PackageConfig) -> Function Function Function { +fn build_Client_new_with(spec: &HirSpec, opt: &Config) -> Function { let auth_struct = opt.authenticator_name().to_rust_struct(); let body = quote! { Self { @@ -91,12 +90,12 @@ fn build_Client_new_with(spec: &HirSpec, opt: &PackageConfig) -> Function Function Class { +pub fn struct_Client(spec: &HirSpec, opt: &Config) -> Class { let auth_struct_name = opt.authenticator_name().to_rust_struct(); let mut instance_fields = vec![Field { @@ -143,7 +142,7 @@ pub fn struct_Client(spec: &HirSpec, opt: &PackageConfig) -> Class } Class { name: opt.client_name().to_rust_struct(), - instance_fields, + fields: instance_fields, class_methods, vis: Visibility::Public, ..Class::default() @@ -219,7 +218,7 @@ pub fn impl_ServiceClient_paths(spec: &HirSpec) -> Vec { result } -pub fn authenticate_variant(req: &AuthStrategy, opt: &PackageConfig) -> TokenStream { +pub fn authenticate_variant(req: &AuthStrategy, opt: &Config) -> TokenStream { let auth_struct = opt.authenticator_name().to_rust_struct(); match req { @@ -277,7 +276,7 @@ pub fn authenticate_variant(req: &AuthStrategy, opt: &PackageConfig) -> TokenStr } } -pub fn build_Client_authenticate(spec: &HirSpec, opt: &PackageConfig) -> TokenStream { +pub fn build_Client_authenticate(spec: &HirSpec, opt: &Config) -> TokenStream { let authenticate_variant = spec .security .iter() @@ -294,7 +293,7 @@ pub fn build_Client_authenticate(spec: &HirSpec, opt: &PackageConfig) -> TokenSt } } -pub fn impl_Client(spec: &HirSpec, opt: &PackageConfig) -> TokenStream { +pub fn impl_Client(spec: &HirSpec, opt: &Config) -> TokenStream { let client_struct_name = opt.client_name().to_rust_struct(); let path_fns = impl_ServiceClient_paths(spec); @@ -311,7 +310,7 @@ pub fn impl_Client(spec: &HirSpec, opt: &PackageConfig) -> TokenStream { } } -pub fn struct_Authentication(mir_spec: &HirSpec, opt: &PackageConfig) -> TokenStream { +pub fn struct_Authentication(mir_spec: &HirSpec, opt: &Config) -> TokenStream { let auth_struct_name = opt.authenticator_name().to_rust_struct(); let variants = mir_spec.security.iter().map(|strategy| match strategy { @@ -409,7 +408,7 @@ fn build_Authentication_from_env(spec: &HirSpec, service_name: &str) -> TokenStr } } -pub fn impl_Authentication(spec: &HirSpec, opt: &PackageConfig) -> TokenStream { +pub fn impl_Authentication(spec: &HirSpec, opt: &Config) -> TokenStream { let auth_struct_name = opt.authenticator_name().to_rust_struct(); let from_env = build_Authentication_from_env(spec, &opt.service_name); let oauth2 = spec diff --git a/mir_rust/src/file.rs b/mir_rust/src/file/mod.rs similarity index 98% rename from mir_rust/src/file.rs rename to mir_rust/src/file/mod.rs index 9457edc..4b63278 100644 --- a/mir_rust/src/file.rs +++ b/mir_rust/src/file/mod.rs @@ -3,6 +3,8 @@ use mir::File; use proc_macro2::TokenStream; use quote::quote; +pub mod client; + impl ToRustCode for File { fn to_rust_code(self) -> TokenStream { let File { diff --git a/mir_rust/src/file/serde/mod.rs b/mir_rust/src/file/serde/mod.rs new file mode 100644 index 0000000..d42add8 --- /dev/null +++ b/mir_rust/src/file/serde/mod.rs @@ -0,0 +1,19 @@ +mod null_as_zero; +mod option_chrono_naive_date_as_int; +mod option_i64_null_as_zero; +mod option_i64_str; + +use proc_macro2::TokenStream; +use std::str::FromStr; + +pub fn option_i64_null_as_zero_module() -> TokenStream { + TokenStream::from_str(include_str!("option_i64_null_as_zero.rs")).unwrap() +} + +pub fn option_i64_str_module() -> TokenStream { + TokenStream::from_str(include_str!("option_i64_str.rs")).unwrap() +} + +pub fn option_chrono_naive_date_as_int_module() -> TokenStream { + TokenStream::from_str(include_str!("option_chrono_naive_date_as_int.rs")).unwrap() +} diff --git a/core/template/rust/src/serde.rs b/mir_rust/src/file/serde/null_as_zero.rs similarity index 74% rename from core/template/rust/src/serde.rs rename to mir_rust/src/file/serde/null_as_zero.rs index 544e6c6..8debfee 100644 --- a/core/template/rust/src/serde.rs +++ b/mir_rust/src/file/serde/null_as_zero.rs @@ -1,18 +1,16 @@ pub mod null_as_zero { - use super::*; - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: serde::de::Deserializer<'de>, + where + D: serde::de::Deserializer<'de>, { deserializer.deserialize_str(DecimalVisitor) } pub fn serialize(value: &Decimal, serializer: S) -> Result - where - S: serde::Serializer, + where + S: serde::Serializer, { let value = crate::str::to_str_internal(value, true, None); serializer.serialize_str(value.0.as_ref()) } -} \ No newline at end of file +} diff --git a/mir_rust/src/file/serde/option_chrono_naive_date_as_int.rs b/mir_rust/src/file/serde/option_chrono_naive_date_as_int.rs new file mode 100644 index 0000000..33c9713 --- /dev/null +++ b/mir_rust/src/file/serde/option_chrono_naive_date_as_int.rs @@ -0,0 +1,52 @@ +pub mod option_chrono_naive_date_as_int { + use chrono::Datelike; + use serde::de::{Deserializer, Error}; + use std::fmt; + + struct NaiveDateVisitor; + + impl<'de> serde::de::Visitor<'de> for NaiveDateVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "an integer that looks like a date") + } + + fn visit_u64(self, value: u64) -> Result { + if value == 0 { + Ok(None) + } else { + let day = value % 100; + let month = (value / 100) % 100; + let year = value / 10000; + Ok(chrono::NaiveDate::from_ymd_opt( + year as i32, + month as u32, + day as u32, + )) + } + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + deserializer.deserialize_u64(NaiveDateVisitor) + } + + pub fn serialize(value: &Option, serializer: S) -> Result + where + S: serde::Serializer, + { + if let Some(value) = value { + let day = value.day() as i32; + let month = value.month() as i32; + let year = value.year(); + let value = year * 10000 + month * 100 + day; + serializer.serialize_i64(value as i64) + } else { + serializer.serialize_i64(0) + } + } +} diff --git a/mir_rust/src/file/serde/option_i64_null_as_zero.rs b/mir_rust/src/file/serde/option_i64_null_as_zero.rs new file mode 100644 index 0000000..16fffd4 --- /dev/null +++ b/mir_rust/src/file/serde/option_i64_null_as_zero.rs @@ -0,0 +1,48 @@ +pub mod option_i64_null_as_zero { + use serde::de::{Deserializer, Error}; + use std::fmt; + + struct IntVisitor; + + impl<'de> serde::de::Visitor<'de> for IntVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "an integer") + } + + fn visit_i64(self, value: i64) -> Result { + if value == 0 { + Ok(None) + } else { + Ok(Some(value)) + } + } + + fn visit_u64(self, value: u64) -> Result { + if value == 0 { + Ok(None) + } else { + Ok(Some(value as i64)) + } + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + deserializer.deserialize_i64(IntVisitor) + } + + pub fn serialize(value: &Option, serializer: S) -> Result + where + S: serde::Serializer, + { + if let Some(i) = value { + serializer.serialize_i64(*i) + } else { + serializer.serialize_i64(0) + } + } +} diff --git a/mir_rust/src/file/serde/option_i64_str.rs b/mir_rust/src/file/serde/option_i64_str.rs new file mode 100644 index 0000000..73fc1e9 --- /dev/null +++ b/mir_rust/src/file/serde/option_i64_str.rs @@ -0,0 +1,43 @@ +pub mod option_i64_str { + use serde::de::{Deserializer, Error, Unexpected}; + use std::fmt; + + struct StrVisitor; + + impl<'de> serde::de::Visitor<'de> for StrVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "an integer") + } + + fn visit_str(self, value: &str) -> Result { + if value.is_empty() { + Ok(None) + } else { + value + .parse::() + .map(Some) + .map_err(|_| Error::invalid_value(Unexpected::Str(value), &self)) + } + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(StrVisitor) + } + + pub fn serialize(value: &Option, serializer: S) -> Result + where + S: serde::Serializer, + { + if let Some(i) = value { + serializer.serialize_str(&i.to_string()) + } else { + serializer.serialize_str("") + } + } +} diff --git a/mir_rust/src/function.rs b/mir_rust/src/function.rs index c7dd82b..1b6b3f8 100644 --- a/mir_rust/src/function.rs +++ b/mir_rust/src/function.rs @@ -1,24 +1,52 @@ +use crate::{FluentBool, ToRustCode}; +use mir::Arg; use proc_macro2::TokenStream; use quote::quote; -use mir::{FnArg2, Function}; -use crate::{FluentBool, ToRustCode}; +use std::ops::{Deref, DerefMut}; + +#[derive(Debug)] +pub struct RustFunction { + pub inner: mir::Function, + pub annotations: Vec, +} + +impl From> for RustFunction { + fn from(inner: mir::Function) -> Self { + Self { + inner, + annotations: vec![], + } + } +} -impl ToRustCode for Function { +impl Deref for RustFunction { + type Target = mir::Function; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for RustFunction { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl ToRustCode for RustFunction { fn to_rust_code(self) -> TokenStream { - let Function { - name, - args, - body, - doc, - async_, + let RustFunction { annotations, - ret, - vis, - .. + inner: + mir::Function { + name, + args, + ret, + body, + doc, + is_async: async_, + vis, + }, } = self; - let annotations = annotations - .into_iter() - .map(|a| syn::parse_str::(&a).unwrap()); let doc = doc.to_rust_code(); let vis = vis.to_rust_code(); let async_ = async_.to_value(|| quote!(async)); @@ -34,22 +62,24 @@ impl ToRustCode for Function { } } -impl ToRustCode for FnArg2 { +impl ToRustCode for Arg { fn to_rust_code(self) -> TokenStream { match self { - FnArg2::Basic { name, ty, default } => { + Arg::Basic { name, ty, default } => { if default.is_some() { panic!("No default args in Rust") } quote!(#name: #ty) } - FnArg2::Unpack { .. } => panic!("unpack args not yet supported in Rust"), - FnArg2::SelfArg { reference, mutable } => { + Arg::Unpack { .. } => panic!("unpack args not yet supported in Rust"), + Arg::SelfArg { reference, mutable } => { let mutability = mutable.then(|| quote!(mut)).unwrap_or_default(); let reference = reference.then(|| quote!(&)).unwrap_or_default(); quote!(#reference #mutability self) } - FnArg2::Variadic { .. } | FnArg2::Kwargs { .. } => panic!("No variadic or kwargs args in Rust"), + Arg::Variadic { .. } | Arg::Kwargs { .. } => { + panic!("No variadic or kwargs args in Rust") + } } } } diff --git a/mir_rust/src/ident.rs b/mir_rust/src/ident.rs new file mode 100644 index 0000000..26ec00f --- /dev/null +++ b/mir_rust/src/ident.rs @@ -0,0 +1,26 @@ +use mir::Ident; + +pub trait ToRustIdent { + fn to_rust_struct(&self) -> Ident; + fn to_rust_ident(&self) -> Ident; +} + +impl ToRustIdent for String { + fn to_rust_struct(&self) -> Ident { + crate::sanitize_struct(self) + } + + fn to_rust_ident(&self) -> Ident { + crate::sanitize_ident(self) + } +} + +impl ToRustIdent for &str { + fn to_rust_struct(&self) -> Ident { + crate::sanitize_struct(self) + } + + fn to_rust_ident(&self) -> Ident { + crate::sanitize_ident(self) + } +} diff --git a/mir_rust/src/lib.rs b/mir_rust/src/lib.rs index 8a5ffec..a7531f1 100644 --- a/mir_rust/src/lib.rs +++ b/mir_rust/src/lib.rs @@ -3,14 +3,21 @@ use proc_macro2::TokenStream; use quote::quote; use regex::{Captures, Regex}; -use mir::{Doc, Ident, Literal, ParamKey, Visibility}; +use mir::parameter::ParamKey; +use mir::Literal; +use mir::{Doc, Ident, Visibility}; mod class; mod r#enum; +mod example; mod file; mod function; +mod ident; mod import; +mod ty; + pub use r#enum::lower_enum; +pub use ty::ToRustType; pub fn serde_rename2(value: &str, ident: &Ident) -> Option { if ident.0 != value { @@ -33,11 +40,6 @@ pub fn derives_to_tokens(derives: &[String]) -> TokenStream { .collect() } -#[derive(Debug, Clone)] -pub struct RustExtra { - pub attributes: Vec, -} - /// Use this for codegen structs: Function, Class, etc. pub trait ToRustCode { fn to_rust_code(self) -> TokenStream; @@ -98,31 +100,6 @@ impl ToRustCode for ParamKey { } } -pub trait ToRustIdent { - fn to_rust_struct(&self) -> Ident; - fn to_rust_ident(&self) -> Ident; -} - -impl ToRustIdent for String { - fn to_rust_struct(&self) -> Ident { - sanitize_struct(self) - } - - fn to_rust_ident(&self) -> Ident { - sanitize_ident(self) - } -} - -impl ToRustIdent for &str { - fn to_rust_struct(&self) -> Ident { - sanitize_struct(self) - } - - fn to_rust_ident(&self) -> Ident { - sanitize_ident(self) - } -} - pub fn sanitize_filename(s: &str) -> String { sanitize(s) } @@ -204,6 +181,7 @@ fn assert_valid_ident(s: &str, original: &str) { #[cfg(test)] mod tests { use super::*; + use ident::ToRustIdent; #[test] fn test_filename() { diff --git a/libninja/src/rust/codegen/ty.rs b/mir_rust/src/ty.rs similarity index 95% rename from libninja/src/rust/codegen/ty.rs rename to mir_rust/src/ty.rs index 8b6c3d2..f88aef4 100644 --- a/libninja/src/rust/codegen/ty.rs +++ b/mir_rust/src/ty.rs @@ -1,11 +1,9 @@ +use crate::ident::ToRustIdent; use hir::HirSpec; use mir::Ty; use proc_macro2::TokenStream; use quote::quote; -use crate::rust::codegen::ToRustIdent; -use crate::rust::lower_hir::HirFieldExt; - /// Use this to generate Rust code types. pub trait ToRustType { fn to_rust_type(&self) -> TokenStream; @@ -84,7 +82,7 @@ impl ToRustType for Ty { Ty::Array(_) => true, Ty::Model(name) => { let model = spec.get_record(name.as_str()).expect("Model not found"); - model.fields().count() > 0 && model.fields().all(|f| f.implements_default(spec)) + model.fields().count() > 0 && model.fields().all(|f| f.ty.implements_default(spec)) } Ty::Unit => true, Ty::Any(_) => true, @@ -104,7 +102,7 @@ impl ToRustType for Ty { Ty::Array(inner) => inner.implements_dummy(spec), Ty::Model(name) => { let model = spec.get_record(name.as_str()).expect("Model not found"); - model.fields().all(|f| f.ty.implements_dummy(spec)) + model.fields().count() > 0 && model.fields().all(|f| f.ty.implements_dummy(spec)) } Ty::Unit => true, Ty::Any(_) => false, diff --git a/mir_rust/tests/main.rs b/mir_rust/tests/main.rs new file mode 100644 index 0000000..c4ec26c --- /dev/null +++ b/mir_rust/tests/main.rs @@ -0,0 +1,47 @@ +use crate::rust::codegen::ToRustCode; +use mir::{import, Import}; +use mir_rust::ident::ToRustIdent; + +#[test] +fn test_to_ident() { + assert_eq!("meta/root".to_rust_ident().0, "meta_root"); +} + +#[test] +fn test_to_ident1() { + assert_eq!( + "get-phone-checks-v0.1".to_rust_ident().0, + "get_phone_checks_v0_1" + ); +} + +#[test] +fn test_star() { + let i = import!("super::*"); + assert_eq!(i.to_rust_code().to_string(), "use super :: * ;"); + let i = Import::new("super", vec!["*"]); + assert_eq!(i.to_rust_code().to_string(), "use super :: { * } ;"); +} + +#[test] +fn test_import() { + let import = import!("plaid::model::LinkTokenCreateRequestUser"); + assert_eq!( + import.to_rust_code().to_string(), + "use plaid :: model :: LinkTokenCreateRequestUser ;" + ); + let import = import!("plaid::model", LinkTokenCreateRequestUser, Foobar); + assert_eq!( + import.to_rust_code().to_string(), + "use plaid :: model :: { LinkTokenCreateRequestUser , Foobar } ;" + ); + + let import = Import::alias("plaid::model", "foobar"); + assert_eq!( + import.to_rust_code().to_string(), + "use plaid :: model as foobar ;" + ); + + let import = Import::package("foo_bar"); + assert_eq!(import.to_rust_code().to_string(), "use foo_bar ;"); +}