diff --git a/Cargo.lock b/Cargo.lock index 8ac7bcc..072ac94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1060,7 +1060,7 @@ checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "libninja" -version = "0.1.9" +version = "0.1.10" dependencies = [ "actix-web", "anyhow", diff --git a/core/src/child_schemas.rs b/core/src/child_schemas.rs new file mode 100644 index 0000000..2fd98a3 --- /dev/null +++ b/core/src/child_schemas.rs @@ -0,0 +1,108 @@ +use std::collections::HashMap; +use std::sync::atomic::AtomicBool; +use openapiv3::{OpenAPI, Operation, RequestBody, Response, Schema, SchemaKind, Type}; + +pub trait ChildSchemas { + fn add_child_schemas<'a>(&'a self, acc: &mut HashMap); +} + +impl ChildSchemas for Schema { + fn add_child_schemas<'a>(&'a self, acc: &mut HashMap) { + match &self.schema_kind { + SchemaKind::Type(Type::Array(a)) => { + let Some(items) = &a.items else { return; }; + let Some(items) = items.as_item() else { return; }; + let items = items.as_ref(); + if let Some(title) = &items.schema_data.title { + acc.entry(title.clone()).or_insert(items); + } + items.add_child_schemas(acc); + } + SchemaKind::Type(Type::Object(o)) => { + if let Some(title) = &self.schema_data.title { + acc.entry(title.clone()).or_insert(self); + } + for (_name, prop) in &o.properties { + let Some(prop) = prop.as_item() else { continue; }; + if let Some(title) = &prop.schema_data.title { + acc.entry(title.clone()).or_insert(prop); + } + prop.add_child_schemas(acc); + } + } + SchemaKind::Type(_) => {} + | SchemaKind::OneOf { one_of: schemas } + | SchemaKind::AllOf { all_of: schemas } + | SchemaKind::AnyOf { any_of: schemas} => { + for schema in schemas { + let Some(schema) = schema.as_item() else { continue; }; + if let Some(title) = &schema.schema_data.title { + acc.entry(title.clone()).or_insert(schema); + } + schema.add_child_schemas(acc); + } + } + SchemaKind::Not { .. } => {} + SchemaKind::Any(_) => {} + } + } +} + +impl ChildSchemas for Operation { + fn add_child_schemas<'a>(&'a self, acc: &mut HashMap) { + 'body: { + let Some(body) = &self.request_body else { break 'body; }; + let Some(body) = body.as_item() else { break 'body; }; + body.add_child_schemas(acc); + } + for par in &self.parameters { + let Some(par) = par.as_item() else { continue; }; + let Some(schema) = par.parameter_data_ref().schema() else { continue; }; + let Some(schema) = schema.as_item() else { continue; }; + schema.add_child_schemas(acc); + } + for (_code, response) in &self.responses.responses { + let Some(response) = response.as_item() else { continue; }; + response.add_child_schemas(acc); + } + } +} + +impl ChildSchemas for RequestBody { + fn add_child_schemas<'a>(&'a self, acc: &mut HashMap) { + for (_key, content) in &self.content { + let Some(schema) = &content.schema else { continue; }; + let Some(schema) = schema.as_item() else { continue; }; + if let Some(title) = &schema.schema_data.title { + acc.entry(title.clone()).or_insert(schema); + } + schema.add_child_schemas(acc); + } + } +} + +impl ChildSchemas for Response { + fn add_child_schemas<'a>(&'a self, acc: &mut HashMap) { + for (k, content) in &self.content { + let Some(schema) = &content.schema else { continue; }; + let Some(schema) = schema.as_item() else { continue; }; + if let Some(title) = &schema.schema_data.title { + acc.entry(title.clone()).or_insert(schema); + } + schema.add_child_schemas(acc); + } + } +} + +impl ChildSchemas for OpenAPI { + fn add_child_schemas<'a>(&'a self, acc: &mut HashMap) { + for (_path, _method, op, _item) in self.operations() { + op.add_child_schemas(acc); + } + for (name, schema) in self.schemas() { + let Some(schema) = schema.as_item() else { continue; }; + acc.entry(name.clone()).or_insert(schema); + schema.add_child_schemas(acc); + } + } +} \ No newline at end of file diff --git a/core/src/extractor.rs b/core/src/extractor.rs index 0b0748a..b92b77d 100644 --- a/core/src/extractor.rs +++ b/core/src/extractor.rs @@ -8,7 +8,7 @@ use tracing_ez::{span, warn}; use ln_mir::{Doc, Name, NewType}; pub use record::*; -pub use resolution::{concrete_schema_to_ty, schema_ref_to_ty, schema_ref_to_ty_already_resolved}; +pub use resolution::{schema_to_ty, schema_ref_to_ty, schema_ref_to_ty_already_resolved}; pub use resolution::*; use crate::{hir, Language, LibraryOptions}; @@ -18,12 +18,11 @@ mod resolution; mod record; /// You might need to call add_operation_models after this -pub fn extract_spec(spec: &OpenAPI, opt: &LibraryOptions) -> Result { +pub fn extract_spec(spec: &OpenAPI) -> Result { let operations = extract_api_operations(spec)?; - let schemas = record::extract_records(spec)?; - + let schemas = extract_records(spec)?; let servers = extract_servers(spec)?; - let security = extract_security_strategies(spec, opt); + let security = extract_security_strategies(spec); let api_docs_url = extract_api_docs_link(spec); @@ -351,7 +350,7 @@ pub fn spec_defines_auth(spec: &MirSpec) -> bool { !spec.security.is_empty() } -fn extract_security_fields(_name: &str, requirement: &SecurityRequirement, spec: &OpenAPI, opt: &LibraryOptions) -> Result> { +fn extract_security_fields(_name: &str, requirement: &SecurityRequirement, spec: &OpenAPI) -> Result> { let security_schemas = &spec.components.as_ref().unwrap().security_schemes; let mut fields = vec![]; for (name, _scopes) in requirement { @@ -403,25 +402,14 @@ fn extract_security_fields(_name: &str, requirement: &SecurityRequirement, spec: fields.push(AuthorizationParameter { name: name.to_string(), - env_var: if name - .to_lowercase() - .starts_with(&opt.service_name.to_lowercase()) - { - name.to_case(Case::ScreamingSnake) - } else { - format!( - "{}_{}", - opt.service_name.to_case(Case::ScreamingSnake), - name.to_case(Case::ScreamingSnake) - ) - }, + env_var: name.to_case(Case::ScreamingSnake), location, }); } Ok(fields) } -pub fn extract_security_strategies(spec: &OpenAPI, opt: &LibraryOptions) -> Vec { +pub fn extract_security_strategies(spec: &OpenAPI) -> Vec { let mut strats = vec![]; let security = match spec.security.as_ref() { None => return strats, @@ -429,7 +417,7 @@ pub fn extract_security_strategies(spec: &OpenAPI, opt: &LibraryOptions) -> Vec< }; for requirement in security { let (name, _scopes) = requirement.iter().next().unwrap(); - let fields = match extract_security_fields(name, requirement, spec, opt) { + let fields = match extract_security_fields(name, requirement, spec) { Ok(f) => f, Err(_e) => { continue; @@ -444,7 +432,7 @@ pub fn extract_security_strategies(spec: &OpenAPI, opt: &LibraryOptions) -> Vec< } pub fn extract_newtype(name: &str, schema: &Schema, spec: &OpenAPI) -> NewType { - let ty = concrete_schema_to_ty(schema, spec); + let ty = schema_to_ty(schema, spec); NewType { name: name.to_string(), diff --git a/core/src/extractor/record.rs b/core/src/extractor/record.rs index f19b690..aafa181 100644 --- a/core/src/extractor/record.rs +++ b/core/src/extractor/record.rs @@ -1,14 +1,15 @@ /// Records are the "model"s of the MIR world. model is a crazy overloaded word though. -use openapiv3::{ObjectType, OpenAPI, ReferenceOr, Schema, SchemaData, SchemaKind, SchemaReference, StringType, Type}; +use openapiv3::{ObjectType, OpenAPI, ReferenceOr, Schema, SchemaData, SchemaKind, SchemaReference, StatusCode, StringType, Type}; use ln_mir::{Doc, Name}; -use std::collections::{BTreeMap}; +use std::collections::{BTreeMap, HashMap}; use tracing_ez::warn; use crate::{extractor, hir}; -use crate::extractor::schema_ref_to_ty_already_resolved; +use crate::extractor::{schema_to_ty, schema_ref_to_ty_already_resolved}; use crate::hir::{MirField, Record, StrEnum, Struct}; use indexmap::IndexMap; use anyhow::Result; +use crate::child_schemas::ChildSchemas; fn properties_to_fields(properties: &IndexMap>, schema: &Schema, spec: &OpenAPI) -> BTreeMap { properties @@ -44,8 +45,7 @@ pub fn effective_length(all_of: &[ReferenceOr]) -> usize { length } -pub fn create_record(name: &str, schema_ref: &ReferenceOr, spec: &OpenAPI) -> Record { - let schema = schema_ref.resolve(spec); +pub fn create_record(name: &str, schema: &Schema, spec: &OpenAPI) -> Record { match &schema.schema_kind { // The base case, a regular object SchemaKind::Type(Type::Object(ObjectType { properties, .. })) => { @@ -80,7 +80,7 @@ pub fn create_record(name: &str, schema_ref: &ReferenceOr, spec: &OpenAP _ => Record::NewType(hir::NewType { name: Name::new(name), fields: vec![MirField { - ty: schema_ref_to_ty_already_resolved(schema_ref, spec, schema), + ty: schema_to_ty(schema, spec), optional: schema.schema_data.nullable, doc: None, example: None, @@ -142,19 +142,14 @@ fn create_record_from_all_of(name: &str, all_of: &[ReferenceOr], schema_ // records are data types: structs, newtypes pub fn extract_records(spec: &OpenAPI) -> Result> { - if spec.components.is_none() { - return Ok(BTreeMap::new()); + let mut result: BTreeMap = BTreeMap::new(); + let mut schema_lookup = HashMap::new(); + spec.add_child_schemas(&mut schema_lookup); + for (name, schema) in schema_lookup { + let rec = create_record(&name, schema, spec); + let name = rec.name().0.clone(); + result.insert(name, rec); } - let result: BTreeMap = spec.schemas() - .into_iter() - .map(|(name, schema)| { - create_record(name, schema, spec) - }) - .map(|r| { - let name = r.name().0.clone(); - Ok((name, r)) - }) - .collect::>()?; Ok(result) } diff --git a/core/src/extractor/resolution.rs b/core/src/extractor/resolution.rs index e71c5d0..30cf4f2 100644 --- a/core/src/extractor/resolution.rs +++ b/core/src/extractor/resolution.rs @@ -12,7 +12,7 @@ pub fn schema_ref_to_ty(schema_ref: &ReferenceOr, spec: &OpenAPI) -> Ty pub fn schema_ref_to_ty_already_resolved(schema_ref: &ReferenceOr, spec: &OpenAPI, schema: &Schema) -> Ty { if is_primitive(schema, spec) { - concrete_schema_to_ty(schema, spec) + schema_to_ty(schema, spec) } else { match schema_ref { ReferenceOr::Reference { reference } => { @@ -22,14 +22,14 @@ pub fn schema_ref_to_ty_already_resolved(schema_ref: &ReferenceOr, spec: SchemaReference::Property { schema: _, property: _ } => unimplemented!(), } } - ReferenceOr::Item(schema) => concrete_schema_to_ty(schema, spec) + ReferenceOr::Item(schema) => schema_to_ty(schema, spec) } } } /// You probably want schema_ref_to_ty, not this method. Reason being, you want /// to use the ref'd model if one exists (e.g. User instead of resolving to Ty::Any) -pub fn concrete_schema_to_ty(schema: &Schema, spec: &OpenAPI) -> Ty { +pub fn schema_to_ty(schema: &Schema, spec: &OpenAPI) -> Ty { match &schema.schema_kind { SchemaKind::Type(oa::Type::String(s)) => { match s.format.as_str() { @@ -59,7 +59,13 @@ pub fn concrete_schema_to_ty(schema: &Schema, spec: &OpenAPI) -> Ty { } } SchemaKind::Type(oa::Type::Boolean {}) => Ty::Boolean, - SchemaKind::Type(oa::Type::Object(_)) => Ty::Any, + SchemaKind::Type(oa::Type::Object(_)) => { + if let Some(title) = &schema.schema_data.title { + Ty::model(&title) + } else { + Ty::Any + } + }, SchemaKind::Type(oa::Type::Array(ArrayType { items: Some(item), .. })) => { diff --git a/core/src/hir.rs b/core/src/hir.rs index 6ac2527..6d77bd0 100644 --- a/core/src/hir.rs +++ b/core/src/hir.rs @@ -52,7 +52,6 @@ impl Default for Ty { fn default() -> Self { Ty::Any } - } impl Ty { @@ -177,6 +176,17 @@ pub struct AuthorizationParameter { pub location: AuthLocation, } +impl AuthorizationParameter { + pub fn env_var_for_service(&self, service_name: &str) -> String { + let service = service_name.to_case(Case::ScreamingSnake); + if self.env_var.starts_with(&service) { + self.env_var.clone() + } else { + format!("{}_{}", service, self.env_var) + } + } +} + #[derive(Debug, Clone)] pub enum AuthLocation { Header { key: String }, @@ -354,7 +364,7 @@ impl MirSpec { } for strategy in &self.security { for param in &strategy.fields { - env_vars.push(param.env_var.clone()); + env_vars.push(param.env_var_for_service(&opt.service_name)); } } env_vars diff --git a/core/src/lib.rs b/core/src/lib.rs index d7140cc..8a88a6c 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -4,6 +4,7 @@ mod lang; mod options; pub mod extractor; mod template; +pub mod child_schemas; pub use options::*; pub use lang::Language; diff --git a/libninja/src/bin/libninja.rs b/libninja/src/bin/libninja.rs index 682b912..e99dcd5 100644 --- a/libninja/src/bin/libninja.rs +++ b/libninja/src/bin/libninja.rs @@ -44,6 +44,8 @@ pub enum Command { /// `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<()> { @@ -72,5 +74,6 @@ fn main() -> Result<()> { generate.run() }, Command::Coalesce(resolve) => resolve.run(), + Command::Meta(meta) => meta.run(), } } diff --git a/libninja/src/command.rs b/libninja/src/command.rs index cc19eca..6f5e69a 100644 --- a/libninja/src/command.rs +++ b/libninja/src/command.rs @@ -1,9 +1,11 @@ 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<()>; diff --git a/libninja/src/command/meta.rs b/libninja/src/command/meta.rs new file mode 100644 index 0000000..3418d7b --- /dev/null +++ b/libninja/src/command/meta.rs @@ -0,0 +1,27 @@ +use std::collections::HashMap; +use std::path::PathBuf; +use anyhow::Result; +use clap::Args; +use crate::read_spec; +use ln_core::child_schemas::ChildSchemas; + +#[derive(Args, Debug)] +pub struct Meta { + service_name: String, + spec_filepath: String, +} + +impl Meta { + pub fn run(self) -> Result<()> { + let path = PathBuf::from(self.spec_filepath); + let spec = read_spec(path, &self.service_name)?; + let mut schema_lookup = HashMap::new(); + spec.add_child_schemas(&mut schema_lookup); + for (name, schema) in schema_lookup { + println!("{}", name); + } + // println!("{}", serde_json::to_string_pretty(&spec)?); + Ok(()) + } +} + diff --git a/libninja/src/lib.rs b/libninja/src/lib.rs index 8ee77e5..9aaa49e 100644 --- a/libninja/src/lib.rs +++ b/libninja/src/lib.rs @@ -70,7 +70,7 @@ pub fn generate_examples( mut opt: LibraryOptions, ) -> Result> { let mut map = HashMap::new(); - let spec = extract_spec(&spec, &opt)?; + let spec = extract_spec(&spec)?; for operation in &spec.operations { let rust = { diff --git a/libninja/src/rust.rs b/libninja/src/rust.rs index ee9bd76..f3cc6a7 100644 --- a/libninja/src/rust.rs +++ b/libninja/src/rust.rs @@ -92,7 +92,7 @@ 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, &opts.library_options)?; + let mir_spec = extract_spec(&spec)?; let mir_spec = add_operation_models(opts.library_options.language, mir_spec)?; let extras = calculate_extras(&mir_spec); @@ -104,7 +104,6 @@ pub fn generate_rust_library(spec: OpenAPI, opts: OutputOptions) -> Result<()> { write_serde_module_if_needed(&extras, &opts)?; - let tera = prepare_templates(); let mut context = create_context(&opts, &mir_spec); diff --git a/libninja/src/rust/client.rs b/libninja/src/rust/client.rs index 91f152d..6cb434f 100644 --- a/libninja/src/rust/client.rs +++ b/libninja/src/rust/client.rs @@ -306,7 +306,7 @@ fn build_Authentication_from_env(mir_spec: &MirSpec, spec: &OpenAPI, opt: &Libra 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; + let env_var = &f.env_var_for_service(&opt.service_name); quote! { #field: std::env::var(#env_var).expect(#expect) } diff --git a/libninja/src/rust/codegen/example.rs b/libninja/src/rust/codegen/example.rs index 061e63b..6ed11b7 100644 --- a/libninja/src/rust/codegen/example.rs +++ b/libninja/src/rust/codegen/example.rs @@ -15,7 +15,7 @@ pub trait ToRustExample { impl ToRustExample for Parameter { fn to_rust_example(&self, spec: &MirSpec) -> anyhow::Result { - codegen::to_rust_example_value(&self.ty, &self.name, spec, false) + to_rust_example_value(&self.ty, &self.name, spec, false) } } diff --git a/libninja/src/rust/mir.rs b/libninja/src/rust/mir.rs index e989746..c743808 100644 --- a/libninja/src/rust/mir.rs +++ b/libninja/src/rust/mir.rs @@ -82,9 +82,15 @@ impl FieldExt for MirField { } } Ty::Currency { serialization: hir::DecimalSerialization::String } => { - decorators.push(quote! { - #[serde(with = "rust_decimal::serde::str")] - }); + if self.optional { + decorators.push(quote! { + #[serde(with = "rust_decimal::serde::str_option")] + }); + } else { + decorators.push(quote! { + #[serde(with = "rust_decimal::serde::str")] + }); + } }, _ => {} } diff --git a/libninja/tests/all_of/main.rs b/libninja/tests/all_of/main.rs index 29a9678..49ffc13 100644 --- a/libninja/tests/all_of/main.rs +++ b/libninja/tests/all_of/main.rs @@ -13,8 +13,7 @@ const RESTRICTION_BACS_RS: &str = include_str!("restriction_bacs.rs"); fn record_for_schema(name: &str, schema: &str, spec: &OpenAPI) -> hir::Record { let schema = serde_yaml::from_str::(schema).unwrap(); - let schema_ref = ReferenceOr::Item(schema); - let mut record = ln_core::extractor::create_record(name, &schema_ref, spec); + let mut record = ln_core::extractor::create_record(name, &schema, spec); record.clear_docs(); record } diff --git a/libninja/tests/regression/main.rs b/libninja/tests/regression/main.rs index 94a2c9f..55b0782 100644 --- a/libninja/tests/regression/main.rs +++ b/libninja/tests/regression/main.rs @@ -7,8 +7,7 @@ const LINK_TOKEN_CREATE: &str = include_str!("link_token_create.yaml"); fn record_for_schema(name: &str, schema: &str, spec: &OpenAPI) -> hir::Record { let schema = serde_yaml::from_str::(schema).unwrap(); - let schema_ref = ReferenceOr::Item(schema); - let mut record = ln_core::extractor::create_record(name, &schema_ref, spec); + let mut record = ln_core::extractor::create_record(name, &schema, spec); record.clear_docs(); record }