diff --git a/core/template/rust/src/lib.rs b/core/template/rust/src/lib.rs index c52dff3..73568ae 100644 --- a/core/template/rust/src/lib.rs +++ b/core/template/rust/src/lib.rs @@ -2,4 +2,5 @@ #![allow(unused)] pub mod model; pub mod request; +pub use httpclient::{Error, Result, InMemoryResponseExt}; use crate::model::*; diff --git a/hir/Cargo.toml b/hir/Cargo.toml new file mode 100644 index 0000000..adde088 --- /dev/null +++ b/hir/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "libninja_hir" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "hir" +path = "src/lib.rs" + +[dependencies] +anyhow = "1.0.75" +convert_case = "0.6.0" +serde_json = "1.0.108" +openapiv3-extended = "3.0.0" +clap = { version = "4.4.10", features = ["derive"] } diff --git a/hir/src/lib.rs b/hir/src/lib.rs index 4a2b210..b26b36a 100644 --- a/hir/src/lib.rs +++ b/hir/src/lib.rs @@ -8,8 +8,10 @@ use std::string::{String, ToString}; use anyhow::Result; use convert_case::{Case, Casing}; pub use doc::*; + mod doc; mod lang; + pub use lang::*; use openapiv3 as oa; @@ -324,6 +326,7 @@ pub enum ServerStrategy { /// There's multiple choices Env, } + impl ServerStrategy { pub fn env_var_for_strategy(&self, service_name: &str) -> Option { match self { @@ -374,6 +377,10 @@ impl HirSpec { pub fn has_security(&self) -> bool { !self.security.is_empty() } + + pub fn has_basic_auth(&self) -> bool { + self.security.iter().any(|s| s.fields.iter().any(|p| matches!(p.location, AuthLocation::Basic))) + } } #[derive(Debug, Clone)] diff --git a/libninja/src/command/meta.rs b/libninja/src/command/meta.rs index 3418d7b..8d43a0f 100644 --- a/libninja/src/command/meta.rs +++ b/libninja/src/command/meta.rs @@ -4,11 +4,24 @@ use anyhow::Result; use clap::Args; use crate::read_spec; use ln_core::child_schemas::ChildSchemas; +use ln_core::extract_spec; +use ln_core::extractor::add_operation_models; +use hir::Language; +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 { @@ -20,6 +33,10 @@ impl Meta { 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/rust.rs b/libninja/src/rust.rs index cb9499c..de03c47 100644 --- a/libninja/src/rust.rs +++ b/libninja/src/rust.rs @@ -1,3 +1,4 @@ +use std::hash::Hash; use std::path::Path; use std::thread::current; @@ -17,11 +18,12 @@ use ln_core::fs; use hir::{HirSpec, IntegerSerialization, DateSerialization}; use crate::{add_operation_models, extract_spec, LibraryOptions, OutputOptions, util}; +use crate::rust::client::build_Client_authenticate; pub use crate::rust::codegen::generate_example; -use crate::rust::codegen::{sanitize_filename, ToRustCode}; +use crate::rust::codegen::{codegen_function, sanitize_filename, ToRustCode}; use crate::rust::io::write_rust_file_to_path; use crate::rust::lower_mir::{generate_model_rs, generate_single_model_file}; -use crate::rust::request::{build_request_struct, generate_request_model_rs}; +use crate::rust::request::{build_request_struct, build_request_struct_builder_methods, build_url, generate_request_model_rs}; pub mod client; pub mod codegen; @@ -31,12 +33,14 @@ pub mod request; mod io; mod serde; +#[derive(Debug)] pub struct Extras { null_as_zero: bool, option_i64_str: bool, date_serialization: bool, currency: bool, integer_date_serialization: bool, + basic_auth: bool } impl Extras { @@ -75,12 +79,14 @@ pub fn calculate_extras(spec: &HirSpec) -> Extras { } } } + let basic_auth = spec.security.iter().any(|f| f.fields.iter().any(|f| matches!(f.location, hir::AuthLocation::Basic))); Extras { null_as_zero, date_serialization, integer_date_serialization, currency, option_i64_str, + basic_auth, } } @@ -93,23 +99,23 @@ pub fn generate_rust_library(spec: OpenAPI, opts: OutputOptions) -> Result<()> { fs::create_dir_all(&src_path)?; // Prepare the MIR Spec. - let mir_spec = extract_spec(&spec)?; - let mir_spec = add_operation_models(opts.library_options.language, mir_spec)?; - let extras = calculate_extras(&mir_spec); + let hir_spec = extract_spec(&spec)?; + let hir_spec = add_operation_models(opts.library_options.language, hir_spec)?; + let extras = calculate_extras(&hir_spec); - write_model_module(&mir_spec, &opts)?; + write_model_module(&hir_spec, &opts)?; - write_request_module(&mir_spec, &opts)?; + write_request_module(&hir_spec, &opts)?; - write_lib_rs(&mir_spec, &extras, &spec, &opts)?; + write_lib_rs(&hir_spec, &extras, &spec, &opts)?; write_serde_module_if_needed(&extras, &opts)?; let tera = prepare_templates(); - let mut context = create_context(&opts, &mir_spec); + let mut context = create_context(&opts, &hir_spec); if opts.library_options.build_examples { - let example = write_examples(&mir_spec, &opts)?; + let example = write_examples(&hir_spec, &opts)?; context.insert("code_sample", &example); } else { context.insert("code_sample", "// Examples were skipped. Run libninja with `--examples true` flag to create them."); @@ -147,19 +153,9 @@ fn write_lib_rs(mir_spec: &HirSpec, extras: &Extras, spec: &OpenAPI, opts: &Outp let mut struct_Client = client::struct_Client(mir_spec, &opts.library_options); let impl_Client = client::impl_Client(mir_spec, spec, &opts.library_options); - let security = if mir_spec.has_security() { - let struct_ServiceAuthentication = client::struct_Authentication(mir_spec, &opts.library_options); - let impl_ServiceAuthentication = client::impl_Authentication(mir_spec, spec, &opts.library_options); - quote! { - #struct_ServiceAuthentication - #impl_ServiceAuthentication - } - } else { - quote! {} - }; - - let client_name = struct_Client.name.to_string(); - let template_path = opts.dest_path.join("template").join("src").join("../../mir"); + let client_name = struct_Client.name.clone(); + let template_path = opts.dest_path.join("template").join("src").join("lib.rs"); + dbg!(&template_path); let lib_rs_template = if template_path.exists() { fs::read_to_string(template_path)? } else { @@ -169,8 +165,9 @@ fn write_lib_rs(mir_spec: &HirSpec, extras: &Extras, spec: &OpenAPI, opts: &Outp //! [`{client}`](struct.{client}.html) is the main entry point for this library. //! //! Library created with [`libninja`](https://www.libninja.com). - {s}"#, - client = client_name + {s} + "#, + client = client_name.0 ) }; let template_has_from_env = lib_rs_template.contains("from_env"); @@ -179,21 +176,46 @@ fn write_lib_rs(mir_spec: &HirSpec, extras: &Extras, spec: &OpenAPI, opts: &Outp struct_Client.class_methods.retain(|m| m.name.0 != "from_env"); } let struct_Client = struct_Client.to_rust_code(); - let serde = if extras.needs_serde() { + + let serde = extras.needs_serde().then(|| { quote! { mod serde; } - } else { - TokenStream::new() + }).unwrap_or_default(); + + let fluent_request = quote! { + pub struct FluentRequest<'a, T> { + pub(crate) client: &'a #client_name, + pub params: T, + } }; + let base64_import = extras.basic_auth.then(|| { + quote! { + use base64::{Engine, engine::general_purpose::STANDARD_NO_PAD}; + } + }).unwrap_or_default(); + + let security = mir_spec.has_security().then(|| { + let struct_ServiceAuthentication = client::struct_Authentication(mir_spec, &opts.library_options); + let impl_ServiceAuthentication = (!template_has_from_env).then(|| { + client::impl_Authentication(mir_spec, spec, &opts.library_options) + }).unwrap_or_default(); + + quote! { + #struct_ServiceAuthentication + #impl_ServiceAuthentication + } + }).unwrap_or_default(); + let code = quote! { + #base64_import #serde + #fluent_request #struct_Client #impl_Client #security }; - - io::write_rust_to_path(&src_path.join("../../mir"), code, &lib_rs_template)?; + io::write_rust_to_path(&src_path.join("lib.rs"), code, &lib_rs_template)?; Ok(()) } @@ -209,27 +231,49 @@ fn write_request_module(spec: &HirSpec, opts: &OutputOptions) -> Result<()> { let request_structs = build_request_struct(operation, spec, &opts.library_options); let struct_name = request_structs[0].name.clone(); let response = operation.ret.to_rust_type(); + let method = syn::Ident::new(&operation.method, proc_macro2::Span::call_site()); let struct_names = request_structs.iter().map(|s| s.name.to_string()).collect::>(); let request_structs = request_structs.into_iter().map(|s| s.to_rust_code()).collect::>(); + let url = build_url(&operation); modules.push(fname.clone()); let mut import = Import::new(&fname, struct_names); import.vis = Visibility::Public; imports.push(import); + let builder_methods = build_request_struct_builder_methods(&operation); + let builder_methods = builder_methods + .into_iter() + .map(|s| codegen_function(s, quote! { mut self , })); let file = quote! { use crate::#client_name; #(#request_structs)* - impl<'a> ::std::future::IntoFuture for #struct_name<'a> { + impl FluentRequest<'_, #struct_name> { + #(#builder_methods)* + } + + impl<'a> ::std::future::IntoFuture for FluentRequest<'a, #struct_name> { type Output = httpclient::InMemoryResult<#response>; type IntoFuture = ::futures::future::BoxFuture<'a, Self::Output>; fn into_future(self) -> Self::IntoFuture { - Box::pin(self.send()) + Box::pin(async { + let url = #url; + let mut r = self.client.client.#method(url); + r = r.set_query(self.params); + r = self.client.authenticate(r); + let res = r.await?; + res.json().map_err(Into::into) + }) } } }; - io::write_rust_to_path(&src_path.join(format!("request/{}.rs", fname)), file, "use serde_json::json; -use crate::model::*;")?; + let template = "\ +use serde_json::json; +use crate::model::*; +use crate::FluentRequest; +use serde::{Serialize, Deserialize}; +use httpclient::InMemoryResponseExt;"; + io::write_rust_to_path(&src_path.join(format!("request/{}.rs", fname)), file, template)?; } let file = File { imports, @@ -277,7 +321,15 @@ fn bump_version_and_update_deps(extras: &Extras, opts: &OutputOptions) -> anyhow ..cargo_toml::DependencyDetail::default() })); } - + if extras.basic_auth { + manifest.dependencies.entry("base64".to_string()) + .or_insert(cargo_toml::Dependency::Simple("0.21.0".to_string())); + } + // delete any examples that no longer exist + manifest.example.retain(|e| { + let Some(p) = &e.path else { return true; }; + opts.dest_path.join("examples").join(p).exists() + }); let content = toml::to_string(&manifest).unwrap(); fs::write_file(&cargo, &content) } diff --git a/libninja/src/rust/client.rs b/libninja/src/rust/client.rs index 4a6fdc9..dc52f54 100644 --- a/libninja/src/rust/client.rs +++ b/libninja/src/rust/client.rs @@ -291,19 +291,29 @@ pub fn struct_Authentication(mir_spec: &HirSpec, opt: &LibraryOptions) -> TokenS } } -fn build_Authentication_from_env(mir_spec: &HirSpec, spec: &OpenAPI, opt: &LibraryOptions) -> TokenStream { - let first_variant = mir_spec.security.first() +fn build_Authentication_from_env(hir_spec: &HirSpec, spec: &OpenAPI, service_name: &str) -> TokenStream { + let first_variant = hir_spec.security.first() .unwrap(); let fields = first_variant .fields .iter() .map(|f| { + let basic = matches!(f.location, AuthLocation::Basic); let field = syn::Ident::new(&f.name.to_case(Case::Snake), proc_macro2::Span::call_site()); let expect = format!("Environment variable {} is not set.", f.env_var); - let env_var = &f.env_var_for_service(&opt.service_name); - quote! { - #field: std::env::var(#env_var).expect(#expect) + let env_var = &f.env_var_for_service(service_name); + if basic { + quote! { + #field: { + let value = std::env::var(#env_var).expect(#expect); + STANDARD_NO_PAD.encode(value) + } + } + } else { + quote! { + #field: std::env::var(#env_var).expect(#expect) + } } }) .collect::>(); @@ -322,7 +332,7 @@ fn build_Authentication_from_env(mir_spec: &HirSpec, spec: &OpenAPI, opt: &Libra pub fn impl_Authentication(mir_spec: &HirSpec, spec: &OpenAPI, opt: &LibraryOptions) -> TokenStream { let auth_struct_name = opt.authenticator_name().to_rust_struct(); - let from_env = build_Authentication_from_env(mir_spec, spec, opt); + let from_env = build_Authentication_from_env(mir_spec, spec, &opt.service_name); quote! { impl #auth_struct_name { diff --git a/libninja/src/rust/codegen.rs b/libninja/src/rust/codegen.rs index f4f1074..166c4e6 100644 --- a/libninja/src/rust/codegen.rs +++ b/libninja/src/rust/codegen.rs @@ -113,7 +113,7 @@ impl ToRustCode for Visibility { } } -fn codegen_function(func: Function, self_arg: TokenStream) -> TokenStream { +pub fn codegen_function(func: Function, self_arg: TokenStream) -> TokenStream { let name = func.name; let args = func.args.into_iter().map(|a| { let name = a.name.unwrap_ident(); diff --git a/libninja/src/rust/request.rs b/libninja/src/rust/request.rs index aad9439..750cf0f 100644 --- a/libninja/src/rust/request.rs +++ b/libninja/src/rust/request.rs @@ -185,19 +185,19 @@ pub fn build_request_struct_builder_methods( let mut body = if a.ty.is_reference_type() { quote! { - self.#name = Some(#name.to_owned()); + self.params.#name = Some(#name.to_owned()); self } } else { quote! { - self.#name = Some(#name); + self.params.#name = Some(#name); self } }; if let Some(Ty::String) = a.ty.inner_iterable() { arg_type = quote!( impl IntoIterator> ); body = quote! { - self.#name = Some(#name.into_iter().map(|s| s.as_ref().to_owned()).collect()); + self.params.#name = Some(#name.into_iter().map(|s| s.as_ref().to_owned()).collect()); self }; } @@ -227,21 +227,21 @@ pub fn build_request_struct( opt: &LibraryOptions, ) -> Vec> { let mut instance_fields = build_struct_fields(&operation.parameters, false); - instance_fields.insert( - 0, - Field { - name: "http_client".to_string(), - ty: { - let c = opt.client_name().to_rust_struct(); - quote! { &'a #c } - }, - visibility: Visibility::Crate, - ..Field::default() - }, - ); + // instance_fields.insert( + // 0, + // Field { + // name: "http_client".to_string(), + // ty: { + // let c = opt.client_name().to_rust_struct(); + // quote! { &'a #c } + // }, + // visibility: Visibility::Crate, + // ..Field::default() + // }, + // ); - let mut instance_methods = vec![build_send_function(operation, spec)]; - let mut_self_instance_methods = build_request_struct_builder_methods(operation); + // let mut instance_methods = vec![build_send_function(operation, spec)]; + // let mut_self_instance_methods = build_request_struct_builder_methods(operation); let doc = doc("Create this with the associated client method. @@ -250,12 +250,13 @@ That method takes required values as arguments. Set optional values using builde name: operation.request_struct_name().to_rust_struct(), doc, instance_fields, - instance_methods, - mut_self_instance_methods, + instance_methods: Vec::new(), + mut_self_instance_methods: Vec::new(), // We need this lifetime because we hold a ref to the client. - lifetimes: vec!["'a".to_string()], + // lifetimes: vec!["'a".to_string()], + lifetimes: vec![], public: true, - decorators: vec![quote! {#[derive(Clone)]}], + decorators: vec![quote! {#[derive(Debug, Clone, Serialize, Deserialize)]}], ..Class::default() }];