Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
kurtbuilds committed Nov 29, 2023
1 parent f27ab3a commit a6ee882
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 63 deletions.
1 change: 1 addition & 0 deletions core/template/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
#![allow(unused)]
pub mod model;
pub mod request;
pub use httpclient::{Error, Result, InMemoryResponseExt};
use crate::model::*;
16 changes: 16 additions & 0 deletions hir/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
7 changes: 7 additions & 0 deletions hir/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> {
match self {
Expand Down Expand Up @@ -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)]
Expand Down
17 changes: 17 additions & 0 deletions libninja/src/command/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Language>,

#[clap(long)]
pub repo: Option<String>,

#[clap(short, long)]
pub output: Option<String>,
}

impl Meta {
Expand All @@ -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(())
}
Expand Down
122 changes: 87 additions & 35 deletions libninja/src/rust.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::hash::Hash;
use std::path::Path;
use std::thread::current;

Expand All @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
}
}

Expand All @@ -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.");
Expand Down Expand Up @@ -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 {
Expand All @@ -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");
Expand All @@ -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(())
}

Expand All @@ -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::<Vec<_>>();
let request_structs = request_structs.into_iter().map(|s| s.to_rust_code()).collect::<Vec<_>>();
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,
Expand Down Expand Up @@ -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)
}
Expand Down
22 changes: 16 additions & 6 deletions libninja/src/rust/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<_>>();
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion libninja/src/rust/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ impl ToRustCode for Visibility {
}
}

fn codegen_function(func: Function<TokenStream>, self_arg: TokenStream) -> TokenStream {
pub fn codegen_function(func: Function<TokenStream>, self_arg: TokenStream) -> TokenStream {
let name = func.name;
let args = func.args.into_iter().map(|a| {
let name = a.name.unwrap_ident();
Expand Down
Loading

0 comments on commit a6ee882

Please sign in to comment.