├── opg ├── LICENSE ├── README.md ├── crates-io.md ├── Cargo.toml └── src │ ├── lib.rs │ └── macros.rs ├── opg_derive ├── LICENSE ├── README.md ├── crates-io.md ├── Cargo.toml └── src │ ├── lib.rs │ ├── dummy.rs │ ├── parsing_context.rs │ ├── symbol.rs │ ├── bound.rs │ ├── ast.rs │ ├── case.rs │ ├── attr.rs │ └── opg.rs ├── .gitignore ├── Cargo.toml ├── test_suite ├── Cargo.toml └── tests │ ├── repr.rs │ ├── api.rs │ └── models.rs ├── .github └── workflows │ └── master.yml ├── LICENSE └── README.md /opg/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /opg/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /opg/crates-io.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /opg_derive/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /opg_derive/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /opg_derive/crates-io.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["opg", "opg_derive", "test_suite"] 3 | -------------------------------------------------------------------------------- /test_suite/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "opg_test_suite" 3 | version = "0.0.0" 4 | authors = ["Ivan Kalinin "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | opg = { path = "../opg", features = [ "uuid", "chrono" ] } 9 | 10 | [dev-dependencies] 11 | chrono = { version = "0.4", features = ["serde"] } 12 | uuid = { version = "*" } 13 | serde = "1.0" 14 | serde_yaml = "0.8" 15 | serde_repr = "0.1" 16 | -------------------------------------------------------------------------------- /opg_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "opg_derive" 3 | description = "Rust OpenAPI 3.0 docs generator" 4 | authors = ["Ivan Kalinin "] 5 | license = "Apache-2.0" 6 | version = "0.1.0" 7 | repository = "https://github.com/Rexagon/opg" 8 | keywords = ["openapi", "documentation", "generator"] 9 | categories = ["encoding"] 10 | include = ["src/**/*.rs", "README.md", "LICENSE"] 11 | edition = "2018" 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | syn = { version = "1.0", features = ["visit"] } 18 | quote = "1.0" 19 | proc-macro2 = "1.0" 20 | either = "1.5" 21 | 22 | [dev-dependencies] 23 | opg = { version = "0.2", path = "../opg" } 24 | 25 | [package.metadata.docs.rs] 26 | targets = ["x86_64-unknown-linux-gnu"] 27 | -------------------------------------------------------------------------------- /opg_derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro2; 2 | extern crate quote; 3 | extern crate syn; 4 | 5 | mod ast; 6 | mod attr; 7 | mod bound; 8 | mod case; 9 | mod dummy; 10 | mod opg; 11 | mod parsing_context; 12 | mod symbol; 13 | 14 | use proc_macro::TokenStream; 15 | use quote::quote; 16 | 17 | use self::opg::*; 18 | 19 | #[proc_macro_derive(OpgModel, attributes(opg))] 20 | pub fn derive_opg_model(input: TokenStream) -> TokenStream { 21 | let input = syn::parse_macro_input!(input as syn::DeriveInput); 22 | impl_derive_opg_model(input) 23 | .unwrap_or_else(to_compile_errors) 24 | .into() 25 | } 26 | 27 | fn to_compile_errors(errors: Vec) -> proc_macro2::TokenStream { 28 | let compile_errors = errors.iter().map(syn::Error::to_compile_error); 29 | quote!(#(#compile_errors)*) 30 | } 31 | -------------------------------------------------------------------------------- /opg_derive/src/dummy.rs: -------------------------------------------------------------------------------- 1 | pub fn wrap_in_const( 2 | trait_: &str, 3 | ty: &syn::Ident, 4 | code: proc_macro2::TokenStream, 5 | ) -> proc_macro2::TokenStream { 6 | let dummy_const = if cfg!(underscore_consts) { 7 | quote::format_ident!("_") 8 | } else { 9 | quote::format_ident!("_IMPL_{}_FOR_{}", trait_, unraw(ty)) 10 | }; 11 | 12 | let use_opg = quote::quote! { 13 | #[allow(rust_2018_idioms, clippy::useless_attribute)] 14 | extern crate opg as _opg; 15 | }; 16 | 17 | quote::quote! { 18 | #[doc(hidden)] 19 | #[allow(non_upper_case_globals, unused_attributes, unused_qualifications)] 20 | const #dummy_const: () = { 21 | #use_opg 22 | #code 23 | }; 24 | } 25 | } 26 | 27 | pub fn unraw(ident: &syn::Ident) -> String { 28 | ident.to_string().trim_start_matches("r#").to_owned() 29 | } 30 | -------------------------------------------------------------------------------- /opg/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "opg" 3 | description = "Rust OpenAPI 3.0 docs generator" 4 | authors = ["Ivan Kalinin "] 5 | license = "Apache-2.0" 6 | version = "0.2.1" 7 | repository = "https://github.com/Rexagon/opg" 8 | keywords = ["openapi", "documentation", "generator"] 9 | categories = ["encoding"] 10 | include = ["src/**/*.rs", "README.md", "LICENSE"] 11 | edition = "2018" 12 | 13 | [dependencies] 14 | http = "0.2" 15 | opg_derive = { version = "=0.1.0", path = "../opg_derive" } 16 | serde = { version = "1", features = ["derive"] } 17 | either = "1.5" 18 | uuid = { version = "1", optional = true } 19 | chrono = { version = "0.4", optional = true } 20 | 21 | [dev-dependencies] 22 | opg_derive = { version = "0.1", path = "../opg_derive" } 23 | 24 | [features] 25 | default = ["const_generics"] 26 | const_generics = [] 27 | 28 | [package.metadata.docs.rs] 29 | targets = ["x86_64-unknown-linux-gnu"] 30 | -------------------------------------------------------------------------------- /opg_derive/src/parsing_context.rs: -------------------------------------------------------------------------------- 1 | use quote::ToTokens; 2 | use std::cell::RefCell; 3 | 4 | #[derive(Default)] 5 | pub struct ParsingContext { 6 | errors: RefCell>>, 7 | } 8 | 9 | impl ParsingContext { 10 | pub fn new() -> Self { 11 | Self { 12 | errors: RefCell::new(Some(Vec::new())), 13 | } 14 | } 15 | 16 | pub fn error_spanned_by(&self, object: O, message: T) 17 | where 18 | O: ToTokens, 19 | T: std::fmt::Display, 20 | { 21 | self.errors 22 | .borrow_mut() 23 | .as_mut() 24 | .unwrap() 25 | .push(syn::Error::new_spanned(object.into_token_stream(), message)) 26 | } 27 | 28 | pub fn syn_error(&self, err: syn::Error) { 29 | self.errors.borrow_mut().as_mut().unwrap().push(err) 30 | } 31 | 32 | pub fn check(self) -> Result<(), Vec> { 33 | let errors = self.errors.borrow_mut().take().unwrap(); 34 | if errors.is_empty() { 35 | Ok(()) 36 | } else { 37 | Err(errors) 38 | } 39 | } 40 | } 41 | 42 | impl Drop for ParsingContext { 43 | fn drop(&mut self) { 44 | if !std::thread::panicking() && self.errors.borrow().is_some() { 45 | panic!("forgot to check for errors"); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test_suite/tests/repr.rs: -------------------------------------------------------------------------------- 1 | #[allow(dead_code)] 2 | mod tests { 3 | 4 | use opg::*; 5 | use serde_repr::Serialize_repr; 6 | 7 | #[derive(Serialize_repr, OpgModel)] 8 | #[repr(i32)] 9 | pub enum LedgerAccountId { 10 | IssuedLoansAndCredits = 5586, 11 | LiabilitiesForSale = 4909, 12 | InterestRateOnIssuedLoansAndCredits = 55861, 13 | OtherIncomesFromCoreActivity = 819, 14 | FinesOnIssuedLoansAndCredits = 55862, 15 | InterestIncomes = 700, 16 | CurrentAccounts = 651, 17 | Pledge = 8, 18 | } 19 | 20 | #[test] 21 | fn repr_enum() { 22 | let cx = &mut Components::default(); 23 | assert_eq!( 24 | serde_yaml::to_string(&LedgerAccountId::get_schema(cx)).unwrap(), 25 | r##"--- 26 | oneOf: 27 | - description: IssuedLoansAndCredits variant 28 | type: integer 29 | example: "5586" 30 | - description: LiabilitiesForSale variant 31 | type: integer 32 | example: "4909" 33 | - description: InterestRateOnIssuedLoansAndCredits variant 34 | type: integer 35 | example: "55861" 36 | - description: OtherIncomesFromCoreActivity variant 37 | type: integer 38 | example: "819" 39 | - description: FinesOnIssuedLoansAndCredits variant 40 | type: integer 41 | example: "55862" 42 | - description: InterestIncomes variant 43 | type: integer 44 | example: "700" 45 | - description: CurrentAccounts variant 46 | type: integer 47 | example: "651" 48 | - description: Pledge variant 49 | type: integer 50 | example: "8" 51 | "## 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: master 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout sources 11 | uses: actions/checkout@v2 12 | 13 | - name: Install stable toolchain 14 | uses: actions-rs/toolchain@v1 15 | with: 16 | profile: minimal 17 | toolchain: stable 18 | override: true 19 | 20 | - name: Run cargo check 21 | uses: actions-rs/cargo@v1 22 | with: 23 | command: check 24 | 25 | test: 26 | name: Test Suite 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout sources 30 | uses: actions/checkout@v2 31 | 32 | - name: Install stable toolchain 33 | uses: actions-rs/toolchain@v1 34 | with: 35 | profile: minimal 36 | toolchain: stable 37 | override: true 38 | 39 | - name: Run cargo test 40 | uses: actions-rs/cargo@v1 41 | with: 42 | command: test 43 | 44 | lints: 45 | name: Lints 46 | runs-on: ubuntu-latest 47 | steps: 48 | - name: Checkout sources 49 | uses: actions/checkout@v2 50 | 51 | - name: Install stable toolchain 52 | uses: actions-rs/toolchain@v1 53 | with: 54 | profile: minimal 55 | toolchain: stable 56 | override: true 57 | components: rustfmt, clippy 58 | 59 | - name: Run cargo fmt 60 | uses: actions-rs/cargo@v1 61 | with: 62 | command: fmt 63 | args: --all -- --check 64 | 65 | - name: Run cargo clippy 66 | uses: actions-rs/cargo@v1 67 | with: 68 | command: clippy 69 | args: -- -D warnings 70 | -------------------------------------------------------------------------------- /opg_derive/src/symbol.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | use syn::{Ident, Path}; 3 | 4 | macro_rules! define_symbols( 5 | ($($name:ident => $value:literal),*,) => { 6 | $(pub const $name: Symbol = Symbol($value));*; 7 | }; 8 | ); 9 | 10 | define_symbols! { 11 | // main macro name 12 | OPG => "opg", 13 | 14 | // named values 15 | EXAMPLE => "example", 16 | EXAMPLE_WITH => "example_with", 17 | FORMAT => "format", 18 | DESCRIPTION => "description", 19 | 20 | // flags 21 | STRING => "string", 22 | NUMBER => "number", 23 | INTEGER => "integer", 24 | BOOLEAN => "boolean", 25 | ANY => "any", 26 | 27 | INLINE => "inline", 28 | OPTIONAL => "optional", 29 | NULLABLE => "nullable", 30 | 31 | // serde 32 | SERDE => "serde", 33 | UNTAGGED => "untagged", 34 | TRANSPARENT => "transparent", 35 | FLATTEN => "flatten", 36 | SKIP => "skip", 37 | SKIP_SERIALIZING => "skip_serializing", 38 | SKIP_SERIALIZING_IF => "skip_serializing_if", 39 | TAG => "tag", 40 | CONTENT => "content", 41 | RENAME => "rename", 42 | RENAME_ALL => "rename_all", 43 | SERIALIZE => "serialize", 44 | DESERIALIZE => "deserialize", 45 | 46 | // misc 47 | REPR => "repr", 48 | } 49 | 50 | #[derive(Copy, Clone)] 51 | pub struct Symbol(&'static str); 52 | 53 | impl Symbol { 54 | pub fn inner(&self) -> &'static str { 55 | self.0 56 | } 57 | } 58 | 59 | impl PartialEq for Ident { 60 | fn eq(&self, other: &Symbol) -> bool { 61 | self == other.0 62 | } 63 | } 64 | 65 | impl<'a> PartialEq for &'a Ident { 66 | fn eq(&self, other: &Symbol) -> bool { 67 | *self == other.0 68 | } 69 | } 70 | 71 | impl PartialEq for Path { 72 | fn eq(&self, other: &Symbol) -> bool { 73 | self.is_ident(other.0) 74 | } 75 | } 76 | 77 | impl<'a> PartialEq for &'a Path { 78 | fn eq(&self, other: &Symbol) -> bool { 79 | self.is_ident(other.0) 80 | } 81 | } 82 | 83 | impl Display for Symbol { 84 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 85 | f.write_str(self.0) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /opg_derive/src/bound.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use syn::punctuated::Pair; 4 | use syn::visit::{self, Visit}; 5 | 6 | use crate::ast::{Container, Data}; 7 | use crate::attr; 8 | 9 | pub fn without_default(generics: &syn::Generics) -> syn::Generics { 10 | syn::Generics { 11 | params: generics 12 | .params 13 | .iter() 14 | .map(|param| match param { 15 | syn::GenericParam::Type(param) => syn::GenericParam::Type(syn::TypeParam { 16 | eq_token: None, 17 | default: None, 18 | ..param.clone() 19 | }), 20 | _ => param.clone(), 21 | }) 22 | .collect(), 23 | ..generics.clone() 24 | } 25 | } 26 | 27 | pub fn with_bound( 28 | cont: &Container, 29 | generics: &syn::Generics, 30 | filter: fn(&attr::Field, Option<&attr::Variant>) -> bool, 31 | bound: &syn::Path, 32 | ) -> syn::Generics { 33 | struct FindTyParams<'ast> { 34 | all_type_params: HashSet, 35 | relevant_type_params: HashSet, 36 | associated_type_usage: Vec<&'ast syn::TypePath>, 37 | } 38 | 39 | impl<'ast> Visit<'ast> for FindTyParams<'ast> { 40 | fn visit_field(&mut self, field: &'ast syn::Field) { 41 | if let syn::Type::Path(ty) = ungroup(&field.ty) { 42 | if let Some(Pair::Punctuated(t, _)) = ty.path.segments.pairs().next() { 43 | if self.all_type_params.contains(&t.ident) { 44 | self.associated_type_usage.push(ty); 45 | } 46 | } 47 | } 48 | self.visit_type(&field.ty); 49 | } 50 | 51 | fn visit_macro(&mut self, _mac: &'ast syn::Macro) {} 52 | 53 | fn visit_path(&mut self, path: &'ast syn::Path) { 54 | if let Some(seg) = path.segments.last() { 55 | if seg.ident == "PhantomData" { 56 | return; 57 | } 58 | } 59 | if path.leading_colon.is_none() && path.segments.len() == 1 { 60 | let id = &path.segments[0].ident; 61 | if self.all_type_params.contains(id) { 62 | self.relevant_type_params.insert(id.clone()); 63 | } 64 | } 65 | visit::visit_path(self, path); 66 | } 67 | } 68 | 69 | let all_type_params = generics 70 | .type_params() 71 | .map(|param| param.ident.clone()) 72 | .collect(); 73 | 74 | let mut visitor = FindTyParams { 75 | all_type_params, 76 | relevant_type_params: HashSet::new(), 77 | associated_type_usage: Vec::new(), 78 | }; 79 | 80 | match &cont.data { 81 | Data::Enum(variants) => { 82 | for variant in variants.iter() { 83 | let relevant_fields = variant 84 | .fields 85 | .iter() 86 | .filter(|field| filter(&field.attrs, Some(&variant.attrs))); 87 | 88 | for field in relevant_fields { 89 | visitor.visit_field(field.original); 90 | } 91 | } 92 | } 93 | Data::Struct(_, fields) => { 94 | for field in fields.iter().filter(|field| filter(&field.attrs, None)) { 95 | visitor.visit_field(field.original); 96 | } 97 | } 98 | } 99 | 100 | let relevant_type_params = visitor.relevant_type_params; 101 | let associated_type_params = visitor.associated_type_usage; 102 | 103 | let new_predicates = generics 104 | .type_params() 105 | .map(|param| param.ident.clone()) 106 | .filter(|ident| relevant_type_params.contains(ident)) 107 | .map(|ident| syn::TypePath { 108 | qself: None, 109 | path: ident.into(), 110 | }) 111 | .chain(associated_type_params.into_iter().cloned()) 112 | .map(|bounded_ty| { 113 | syn::WherePredicate::Type(syn::PredicateType { 114 | lifetimes: None, 115 | bounded_ty: syn::Type::Path(bounded_ty), 116 | colon_token: ::default(), 117 | bounds: vec![syn::TypeParamBound::Trait(syn::TraitBound { 118 | paren_token: None, 119 | modifier: syn::TraitBoundModifier::None, 120 | lifetimes: None, 121 | path: bound.clone(), 122 | })] 123 | .into_iter() 124 | .collect(), 125 | }) 126 | }); 127 | 128 | let mut generics = generics.clone(); 129 | generics 130 | .make_where_clause() 131 | .predicates 132 | .extend(new_predicates); 133 | generics 134 | } 135 | 136 | fn ungroup(mut ty: &syn::Type) -> &syn::Type { 137 | while let syn::Type::Group(group) = ty { 138 | ty = &group.elem; 139 | } 140 | ty 141 | } 142 | -------------------------------------------------------------------------------- /opg_derive/src/ast.rs: -------------------------------------------------------------------------------- 1 | use either::*; 2 | use syn::punctuated::Punctuated; 3 | 4 | use crate::attr; 5 | use crate::parsing_context::*; 6 | 7 | pub struct Container<'a> { 8 | pub ident: syn::Ident, 9 | pub attrs: attr::Container, 10 | pub data: Data<'a>, 11 | pub generics: &'a syn::Generics, 12 | pub original: &'a syn::DeriveInput, 13 | } 14 | 15 | pub enum Data<'a> { 16 | Enum(Vec>), 17 | Struct(StructStyle, Vec>), 18 | } 19 | 20 | pub struct Variant<'a> { 21 | pub ident: syn::Ident, 22 | pub attrs: attr::Variant, 23 | pub style: StructStyle, 24 | pub fields: Vec>, 25 | pub original: &'a syn::Variant, 26 | } 27 | 28 | pub struct Field<'a> { 29 | pub member: syn::Member, 30 | pub attrs: attr::Field, 31 | pub ty: &'a syn::Type, 32 | pub original: &'a syn::Field, 33 | } 34 | 35 | impl<'a> Container<'a> { 36 | pub fn from_ast(cx: &ParsingContext, input: &'a syn::DeriveInput) -> Option> { 37 | let mut attrs = attr::Container::from_ast(cx, input); 38 | 39 | let mut data = match &input.data { 40 | syn::Data::Enum(data) => Data::Enum(enum_from_ast(cx, &data.variants)?), 41 | syn::Data::Struct(data) => { 42 | let (style, fields) = struct_from_ast(cx, &data.fields); 43 | Data::Struct(style, fields) 44 | } 45 | syn::Data::Union(_) => { 46 | cx.error_spanned_by(input, "union types are not supported"); 47 | return None; 48 | } 49 | }; 50 | 51 | let mut has_flatten = false; 52 | match &mut data { 53 | Data::Enum(variants) => { 54 | for variant in variants { 55 | variant.attrs.rename_by_rule(attrs.rename_rule); 56 | for field in &mut variant.fields { 57 | if field.attrs.flatten { 58 | has_flatten = true; 59 | } 60 | field.attrs.rename_by_rule(variant.attrs.rename_rule); 61 | } 62 | } 63 | } 64 | Data::Struct(_, fields) => { 65 | for field in fields { 66 | if field.attrs.flatten { 67 | has_flatten = true; 68 | } 69 | field.attrs.rename_by_rule(attrs.rename_rule); 70 | } 71 | } 72 | } 73 | 74 | if has_flatten { 75 | attrs.has_flatten = true; 76 | } 77 | 78 | let item = Self { 79 | ident: input.ident.clone(), 80 | attrs, 81 | data, 82 | generics: &input.generics, 83 | original: input, 84 | }; 85 | // TODO: check item 86 | Some(item) 87 | } 88 | } 89 | 90 | impl<'a> Data<'a> { 91 | #[allow(dead_code)] 92 | pub fn all_fields(&'a self) -> impl Iterator> { 93 | match self { 94 | Data::Enum(variants) => { 95 | Either::Left(variants.iter().flat_map(|variant| variant.fields.iter())) 96 | } 97 | Data::Struct(_, fields) => Either::Right(fields.iter()), 98 | } 99 | } 100 | } 101 | 102 | fn enum_from_ast<'a>( 103 | cx: &ParsingContext, 104 | variants: &'a Punctuated, 105 | ) -> Option>> { 106 | let has_consistent_discriminants = { 107 | let mut iter = variants.iter(); 108 | match iter.next() { 109 | Some(variant) => { 110 | iter.all(|item| item.discriminant.is_some() == variant.discriminant.is_some()) 111 | } 112 | None => true, 113 | } 114 | }; 115 | 116 | if !has_consistent_discriminants { 117 | cx.error_spanned_by(variants, "varant discriminants are not consistent"); 118 | return None; 119 | } 120 | 121 | Some( 122 | variants 123 | .iter() 124 | .map(|variant| { 125 | let attrs = attr::Variant::from_ast(cx, variant); 126 | let (style, fields) = struct_from_ast(cx, &variant.fields); 127 | Variant { 128 | ident: variant.ident.clone(), 129 | attrs, 130 | style, 131 | fields, 132 | original: variant, 133 | } 134 | }) 135 | .collect(), 136 | ) 137 | } 138 | 139 | fn struct_from_ast<'a>( 140 | cx: &ParsingContext, 141 | fields: &'a syn::Fields, 142 | ) -> (StructStyle, Vec>) { 143 | match fields { 144 | syn::Fields::Named(fields) => (StructStyle::Struct, fields_from_ast(cx, &fields.named)), 145 | syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { 146 | (StructStyle::NewType, fields_from_ast(cx, &fields.unnamed)) 147 | } 148 | syn::Fields::Unnamed(fields) => (StructStyle::Tuple, fields_from_ast(cx, &fields.unnamed)), 149 | syn::Fields::Unit => (StructStyle::Unit, Vec::new()), 150 | } 151 | } 152 | 153 | fn fields_from_ast<'a>( 154 | cx: &ParsingContext, 155 | fields: &'a Punctuated, 156 | ) -> Vec> { 157 | fields 158 | .iter() 159 | .enumerate() 160 | .map(|(i, field)| Field { 161 | member: match &field.ident { 162 | Some(ident) => syn::Member::Named(ident.clone()), 163 | None => syn::Member::Unnamed(i.into()), 164 | }, 165 | attrs: attr::Field::from_ast(cx, i, field), 166 | ty: &field.ty, 167 | original: field, 168 | }) 169 | .collect() 170 | } 171 | 172 | #[derive(Debug, Clone, Copy)] 173 | pub enum StructStyle { 174 | Struct, 175 | Tuple, 176 | NewType, 177 | Unit, 178 | } 179 | -------------------------------------------------------------------------------- /opg_derive/src/case.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use self::RenameRule::*; 4 | 5 | /// The different possible ways to change case of fields in a struct, or variants in an enum. 6 | #[allow(clippy::upper_case_acronyms)] 7 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 8 | pub enum RenameRule { 9 | /// Don't apply a default rename rule. 10 | None, 11 | /// Rename direct children to "lowercase" style. 12 | LowerCase, 13 | /// Rename direct children to "UPPERCASE" style. 14 | UPPERCASE, 15 | /// Rename direct children to "PascalCase" style, as typically used for 16 | /// enum variants. 17 | PascalCase, 18 | /// Rename direct children to "camelCase" style. 19 | CamelCase, 20 | /// Rename direct children to "snake_case" style, as commonly used for 21 | /// fields. 22 | SnakeCase, 23 | /// Rename direct children to "SCREAMING_SNAKE_CASE" style, as commonly 24 | /// used for constants. 25 | ScreamingSnakeCase, 26 | /// Rename direct children to "kebab-case" style. 27 | KebabCase, 28 | /// Rename direct children to "SCREAMING-KEBAB-CASE" style. 29 | ScreamingKebabCase, 30 | } 31 | 32 | impl RenameRule { 33 | /// Apply a renaming rule to an enum variant, returning the version expected in the source. 34 | pub fn apply_to_variant(self, variant: &str) -> String { 35 | match self { 36 | None | PascalCase => variant.to_owned(), 37 | LowerCase => variant.to_ascii_lowercase(), 38 | UPPERCASE => variant.to_ascii_uppercase(), 39 | CamelCase => variant[..1].to_ascii_lowercase() + &variant[1..], 40 | SnakeCase => { 41 | let mut snake = String::new(); 42 | for (i, ch) in variant.char_indices() { 43 | if i > 0 && ch.is_uppercase() { 44 | snake.push('_'); 45 | } 46 | snake.push(ch.to_ascii_lowercase()); 47 | } 48 | snake 49 | } 50 | ScreamingSnakeCase => SnakeCase.apply_to_variant(variant).to_ascii_uppercase(), 51 | KebabCase => SnakeCase.apply_to_variant(variant).replace('_', "-"), 52 | ScreamingKebabCase => ScreamingSnakeCase 53 | .apply_to_variant(variant) 54 | .replace('_', "-"), 55 | } 56 | } 57 | 58 | /// Apply a renaming rule to a struct field, returning the version expected in the source. 59 | pub fn apply_to_field(self, field: &str) -> String { 60 | match self { 61 | None | LowerCase | SnakeCase => field.to_owned(), 62 | UPPERCASE => field.to_ascii_uppercase(), 63 | PascalCase => { 64 | let mut pascal = String::new(); 65 | let mut capitalize = true; 66 | for ch in field.chars() { 67 | if ch == '_' { 68 | capitalize = true; 69 | } else if capitalize { 70 | pascal.push(ch.to_ascii_uppercase()); 71 | capitalize = false; 72 | } else { 73 | pascal.push(ch); 74 | } 75 | } 76 | pascal 77 | } 78 | CamelCase => { 79 | let pascal = PascalCase.apply_to_field(field); 80 | pascal[..1].to_ascii_lowercase() + &pascal[1..] 81 | } 82 | ScreamingSnakeCase => field.to_ascii_uppercase(), 83 | KebabCase => field.replace('_', "-"), 84 | ScreamingKebabCase => ScreamingSnakeCase.apply_to_field(field).replace('_', "-"), 85 | } 86 | } 87 | } 88 | 89 | impl FromStr for RenameRule { 90 | type Err = (); 91 | 92 | fn from_str(rename_all_str: &str) -> Result { 93 | match rename_all_str { 94 | "lowercase" => Ok(LowerCase), 95 | "UPPERCASE" => Ok(UPPERCASE), 96 | "PascalCase" => Ok(PascalCase), 97 | "camelCase" => Ok(CamelCase), 98 | "snake_case" => Ok(SnakeCase), 99 | "SCREAMING_SNAKE_CASE" => Ok(ScreamingSnakeCase), 100 | "kebab-case" => Ok(KebabCase), 101 | "SCREAMING-KEBAB-CASE" => Ok(ScreamingKebabCase), 102 | _ => Err(()), 103 | } 104 | } 105 | } 106 | 107 | #[test] 108 | fn rename_variants() { 109 | for &(original, lower, upper, camel, snake, screaming, kebab, screaming_kebab) in &[ 110 | ( 111 | "Outcome", "outcome", "OUTCOME", "outcome", "outcome", "OUTCOME", "outcome", "OUTCOME", 112 | ), 113 | ( 114 | "VeryTasty", 115 | "verytasty", 116 | "VERYTASTY", 117 | "veryTasty", 118 | "very_tasty", 119 | "VERY_TASTY", 120 | "very-tasty", 121 | "VERY-TASTY", 122 | ), 123 | ("A", "a", "A", "a", "a", "A", "a", "A"), 124 | ("Z42", "z42", "Z42", "z42", "z42", "Z42", "z42", "Z42"), 125 | ] { 126 | assert_eq!(None.apply_to_variant(original), original); 127 | assert_eq!(LowerCase.apply_to_variant(original), lower); 128 | assert_eq!(UPPERCASE.apply_to_variant(original), upper); 129 | assert_eq!(PascalCase.apply_to_variant(original), original); 130 | assert_eq!(CamelCase.apply_to_variant(original), camel); 131 | assert_eq!(SnakeCase.apply_to_variant(original), snake); 132 | assert_eq!(ScreamingSnakeCase.apply_to_variant(original), screaming); 133 | assert_eq!(KebabCase.apply_to_variant(original), kebab); 134 | assert_eq!( 135 | ScreamingKebabCase.apply_to_variant(original), 136 | screaming_kebab 137 | ); 138 | } 139 | } 140 | 141 | #[test] 142 | fn rename_fields() { 143 | for &(original, upper, pascal, camel, screaming, kebab, screaming_kebab) in &[ 144 | ( 145 | "outcome", "OUTCOME", "Outcome", "outcome", "OUTCOME", "outcome", "OUTCOME", 146 | ), 147 | ( 148 | "very_tasty", 149 | "VERY_TASTY", 150 | "VeryTasty", 151 | "veryTasty", 152 | "VERY_TASTY", 153 | "very-tasty", 154 | "VERY-TASTY", 155 | ), 156 | ("a", "A", "A", "a", "A", "a", "A"), 157 | ("z42", "Z42", "Z42", "z42", "Z42", "z42", "Z42"), 158 | ] { 159 | assert_eq!(None.apply_to_field(original), original); 160 | assert_eq!(UPPERCASE.apply_to_field(original), upper); 161 | assert_eq!(PascalCase.apply_to_field(original), pascal); 162 | assert_eq!(CamelCase.apply_to_field(original), camel); 163 | assert_eq!(SnakeCase.apply_to_field(original), original); 164 | assert_eq!(ScreamingSnakeCase.apply_to_field(original), screaming); 165 | assert_eq!(KebabCase.apply_to_field(original), kebab); 166 | assert_eq!(ScreamingKebabCase.apply_to_field(original), screaming_kebab); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /opg/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | pub use macros::*; 4 | pub use models::*; 5 | pub use opg_derive::OpgModel; 6 | 7 | pub mod macros; 8 | pub mod models; 9 | 10 | pub const OPENAPI_VERSION: &str = "3.0.3"; 11 | pub const SCHEMA_REFERENCE_PREFIX: &str = "#/components/schemas/"; 12 | 13 | impl_opg_model!(string(always_inline, "char"): char); 14 | impl_opg_model!(string(always_inline): str); 15 | impl_opg_model!(string(always_inline): String); 16 | 17 | impl_opg_model!(integer(always_inline, "int8"): i8); 18 | impl_opg_model!(integer(always_inline, "uint8"): u8); 19 | impl_opg_model!(integer(always_inline, "int16"): i16); 20 | impl_opg_model!(integer(always_inline, "uint16"): u16); 21 | impl_opg_model!(integer(always_inline, "int32"): i32); 22 | impl_opg_model!(integer(always_inline, "uint32"): u32); 23 | impl_opg_model!(integer(always_inline, "int64"): i64); 24 | impl_opg_model!(integer(always_inline, "uint64"): u64); 25 | impl_opg_model!(integer(always_inline): isize); 26 | impl_opg_model!(integer(always_inline): usize); 27 | 28 | impl_opg_model!(number(always_inline, "float"): f32); 29 | impl_opg_model!(number(always_inline, "double"): f64); 30 | 31 | impl_opg_model!(boolean(always_inline): bool); 32 | 33 | impl_opg_model!(integer(always_inline, "int8"): std::sync::atomic::AtomicI8); 34 | impl_opg_model!(integer(always_inline, "uint8"): std::sync::atomic::AtomicU8); 35 | impl_opg_model!(integer(always_inline, "int16"): std::sync::atomic::AtomicI16); 36 | impl_opg_model!(integer(always_inline, "uint16"): std::sync::atomic::AtomicU16); 37 | impl_opg_model!(integer(always_inline, "int32"): std::sync::atomic::AtomicI32); 38 | impl_opg_model!(integer(always_inline, "uint32"): std::sync::atomic::AtomicU32); 39 | impl_opg_model!(integer(always_inline, "int64"): std::sync::atomic::AtomicI64); 40 | impl_opg_model!(integer(always_inline, "uint64"): std::sync::atomic::AtomicU64); 41 | impl_opg_model!(integer(always_inline): std::sync::atomic::AtomicIsize); 42 | impl_opg_model!(integer(always_inline): std::sync::atomic::AtomicUsize); 43 | 44 | impl_opg_model!(boolean(always_inline): std::sync::atomic::AtomicBool); 45 | 46 | impl_opg_model!(generic_simple(?Sized): &T); 47 | impl_opg_model!(generic_simple(?Sized): &mut T); 48 | impl_opg_model!(generic_simple(?Sized): Box); 49 | impl_opg_model!(generic_simple(?Sized): std::rc::Rc); 50 | impl_opg_model!(generic_simple(?Sized): std::sync::Arc); 51 | impl_opg_model!(generic_simple(?Sized): std::cell::Cell); 52 | impl_opg_model!(generic_simple(?Sized): std::cell::RefCell); 53 | 54 | impl_opg_model!(generic_simple(nullable): Option); 55 | 56 | impl_opg_model!(generic_simple: (T,)); 57 | 58 | impl_opg_model!(generic_tuple: (T1, T2)); 59 | impl_opg_model!(generic_tuple: (T1, T2, T3)); 60 | impl_opg_model!(generic_tuple: (T1, T2, T3, T4)); 61 | impl_opg_model!(generic_tuple: (T1, T2, T3, T4, T5)); 62 | impl_opg_model!(generic_tuple: (T1, T2, T3, T4, T5, T6)); 63 | impl_opg_model!(generic_tuple: (T1, T2, T3, T4, T5, T6, T7)); 64 | impl_opg_model!(generic_tuple: (T1, T2, T3, T4, T5, T6, T7, T8)); 65 | impl_opg_model!(generic_tuple: (T1, T2, T3, T4, T5, T6, T7, T8, T9)); 66 | impl_opg_model!(generic_tuple: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10)); 67 | 68 | impl_opg_model!(generic_array: [T]); 69 | impl_opg_model!(generic_array: Vec); 70 | impl_opg_model!(generic_array: std::collections::HashSet); 71 | impl_opg_model!(generic_array: std::collections::LinkedList); 72 | impl_opg_model!(generic_array: std::collections::VecDeque); 73 | impl_opg_model!(generic_array: std::collections::BinaryHeap); 74 | 75 | #[cfg(not(feature = "const_generics"))] 76 | macro_rules! array_impls { 77 | ($($len:tt)+) => { 78 | $(impl_opg_model!(generic_array: [T; $len]);)* 79 | }; 80 | } 81 | #[cfg(not(feature = "const_generics"))] 82 | array_impls! { 83 | 01 02 03 04 05 06 07 08 09 10 84 | 11 12 13 14 15 16 17 18 19 20 85 | 21 22 23 24 25 26 27 28 29 30 86 | 31 32 87 | } 88 | 89 | #[cfg(feature = "const_generics")] 90 | impl OpgModel for [T; N] 91 | where 92 | T: OpgModel, 93 | { 94 | fn get_schema(cx: &mut Components) -> Model { 95 | Model { 96 | description: None, 97 | data: ModelData::Single(ModelType { 98 | nullable: false, 99 | type_description: ModelTypeDescription::Array(ModelArray { 100 | items: Box::new(cx.mention_schema::(false, &Default::default())), 101 | }), 102 | }), 103 | } 104 | } 105 | 106 | #[inline] 107 | fn type_name() -> Option> { 108 | None 109 | } 110 | 111 | #[inline] 112 | fn select_reference(cx: &mut Components, _: bool, params: &ContextParams) -> ModelReference { 113 | ModelReference::Inline(Self::get_schema(cx).apply_params(params)) 114 | } 115 | } 116 | 117 | impl_opg_model!(generic_dictionary: std::collections::HashMap); 118 | impl_opg_model!(generic_dictionary: std::collections::BTreeMap); 119 | 120 | impl OpgModel for () { 121 | fn get_schema(_: &mut Components) -> Model { 122 | Model { 123 | description: Some("Always `null`".to_owned()), 124 | data: ModelData::Single(ModelType { 125 | nullable: true, 126 | type_description: ModelTypeDescription::String(ModelString { 127 | variants: None, 128 | data: ModelSimple { 129 | format: Some("null".to_owned()), 130 | example: None, 131 | }, 132 | }), 133 | }), 134 | } 135 | } 136 | 137 | #[inline] 138 | fn type_name() -> Option> { 139 | None 140 | } 141 | 142 | #[inline] 143 | fn select_reference(cx: &mut Components, _: bool, params: &ContextParams) -> ModelReference { 144 | ModelReference::Inline(Self::get_schema(cx).apply_params(params)) 145 | } 146 | } 147 | 148 | #[cfg(feature = "uuid")] 149 | impl OpgModel for uuid::Uuid { 150 | fn get_schema(_: &mut Components) -> Model { 151 | Model { 152 | description: Some("UUID ver. 4 [rfc](https://tools.ietf.org/html/rfc4122)".to_owned()), 153 | data: ModelData::Single(ModelType { 154 | nullable: false, 155 | type_description: ModelTypeDescription::String(ModelString { 156 | variants: None, 157 | data: ModelSimple { 158 | format: Some("uuid".to_owned()), 159 | example: Some("00000000-0000-0000-0000-000000000000".to_owned()), 160 | }, 161 | }), 162 | }), 163 | } 164 | } 165 | 166 | #[inline] 167 | fn type_name() -> Option> { 168 | None 169 | } 170 | 171 | #[inline] 172 | fn select_reference(cx: &mut Components, _: bool, params: &ContextParams) -> ModelReference { 173 | ModelReference::Inline(Self::get_schema(cx).apply_params(params)) 174 | } 175 | } 176 | 177 | #[cfg(feature = "chrono")] 178 | impl OpgModel for chrono::NaiveDateTime { 179 | fn get_schema(_: &mut Components) -> Model { 180 | Model { 181 | description: Some("Datetime without timezone".to_owned()), 182 | data: ModelData::Single(ModelType { 183 | nullable: false, 184 | type_description: ModelTypeDescription::String(ModelString { 185 | variants: None, 186 | data: ModelSimple { 187 | format: Some("date".to_owned()), 188 | example: Some("2020-06-26T14:04:20.730045106".to_owned()), 189 | }, 190 | }), 191 | }), 192 | } 193 | } 194 | 195 | #[inline] 196 | fn type_name() -> Option> { 197 | None 198 | } 199 | 200 | #[inline] 201 | fn select_reference(cx: &mut Components, _: bool, params: &ContextParams) -> ModelReference { 202 | ModelReference::Inline(Self::get_schema(cx).apply_params(params)) 203 | } 204 | } 205 | 206 | #[cfg(feature = "chrono")] 207 | impl OpgModel for chrono::DateTime { 208 | fn get_schema(_: &mut Components) -> Model { 209 | Model { 210 | description: Some("Datetime with timezone".to_owned()), 211 | data: ModelData::Single(ModelType { 212 | nullable: false, 213 | type_description: ModelTypeDescription::String(ModelString { 214 | variants: None, 215 | data: ModelSimple { 216 | format: Some("date".to_owned()), 217 | example: Some("2020-06-26T14:04:20.730045106Z".to_owned()), 218 | }, 219 | }), 220 | }), 221 | } 222 | } 223 | 224 | #[inline] 225 | fn type_name() -> Option> { 226 | None 227 | } 228 | 229 | #[inline] 230 | fn select_reference(cx: &mut Components, _: bool, params: &ContextParams) -> ModelReference { 231 | ModelReference::Inline(Self::get_schema(cx).apply_params(params)) 232 | } 233 | } 234 | 235 | #[cfg(feature = "chrono")] 236 | impl OpgModel for chrono::NaiveDate { 237 | fn get_schema(_: &mut Components) -> Model { 238 | Model { 239 | description: Some("Date without timezone".to_owned()), 240 | data: ModelData::Single(ModelType { 241 | nullable: false, 242 | type_description: ModelTypeDescription::String(ModelString { 243 | variants: None, 244 | data: ModelSimple { 245 | format: Some("date".to_owned()), 246 | example: Some("2020-06-26".to_owned()), 247 | }, 248 | }), 249 | }), 250 | } 251 | } 252 | 253 | #[inline] 254 | fn type_name() -> Option> { 255 | None 256 | } 257 | 258 | #[inline] 259 | fn select_reference(cx: &mut Components, _: bool, params: &ContextParams) -> ModelReference { 260 | ModelReference::Inline(Self::get_schema(cx).apply_params(params)) 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /test_suite/tests/api.rs: -------------------------------------------------------------------------------- 1 | #[allow(dead_code)] 2 | mod tests { 3 | 4 | use opg::*; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Serialize, Deserialize, OpgModel)] 8 | #[serde(rename_all = "camelCase")] 9 | #[opg("Simple enum")] 10 | enum SimpleEnum { 11 | Test, 12 | Another, 13 | Yay, 14 | } 15 | 16 | mod request { 17 | use super::*; 18 | 19 | #[derive(Serialize, OpgModel)] 20 | pub struct InModule { 21 | field: String, 22 | second: Test, 23 | } 24 | 25 | #[derive(Serialize, OpgModel)] 26 | pub struct Test { 27 | another_field: Option, 28 | } 29 | } 30 | 31 | #[test] 32 | fn expands_normally() { 33 | let test_auth = ApiKeySecurityScheme { 34 | parameter_in: ParameterIn::Query, 35 | name: "X-MY-SUPER-API".to_string(), 36 | description: None, 37 | }; 38 | 39 | let title_as_variable = "My super API".to_owned(); 40 | let server_as_variable = "http://test/123"; 41 | 42 | let test = describe_api! { 43 | info: { 44 | title: title_as_variable, 45 | version: "0.0.0", 46 | }, 47 | tags: {internal, admin("Super admin methods")}, 48 | servers: { 49 | "https://my.super.server.com/v1", 50 | server_as_variable 51 | }, 52 | security_schemes: { 53 | (http "bearerAuth"): { 54 | scheme: Bearer, 55 | bearer_format: "JWT", 56 | description: "Test description", 57 | }, 58 | (http "basicAuth"): { 59 | scheme: Basic, 60 | description: "Another test description" 61 | }, 62 | (apiKey "ApiKeyAuth"): { 63 | parameter_in: Query, 64 | name: "X-API-KEY", 65 | description: "And another test description" 66 | } 67 | }, 68 | paths: { 69 | ("test" / uuid::Uuid): { 70 | POST: { 71 | operationId: "test", 72 | security: { 73 | test_auth && "basicAuth" 74 | }, 75 | deprecated: true, 76 | body: request::InModule, 77 | 200: std::vec::Vec, 78 | callbacks: { 79 | myCallback: { 80 | ("callback_url"): { 81 | POST: { 82 | body: request::InModule, 83 | 200: std::vec::Vec, 84 | } 85 | } 86 | } 87 | } 88 | } 89 | }, 90 | ("hello" / "world" / { paramTest: String }): { 91 | summary: "Some test group of requests", 92 | description: "Another test description", 93 | parameters: { 94 | (header "x-request-id"): { 95 | description: "Test", 96 | }, 97 | (query test: i32), 98 | (header "asd") 99 | }, 100 | GET: { 101 | operationId: "testGet", 102 | tags: {internal}, 103 | summary: "Small summary", 104 | description: "Small description", 105 | parameters: { 106 | (query someParam: u32): { 107 | description: "Test", 108 | deprecated: true 109 | }, 110 | }, 111 | 200("Custom response desc"): String 112 | }, 113 | POST: { 114 | operationId: "testPost", 115 | tags: {admin}, 116 | body: { 117 | description: "Some interesting description", 118 | schema: String, 119 | required: true 120 | }, 121 | 200: SimpleEnum, 122 | }, 123 | DELETE: { 124 | 200: None, 125 | }, 126 | OPTIONS: { 127 | 200: () 128 | } 129 | } 130 | } 131 | }; 132 | 133 | assert_eq!( 134 | serde_yaml::to_string(&test).unwrap(), 135 | r##"--- 136 | openapi: 3.0.3 137 | info: 138 | title: My super API 139 | version: 0.0.0 140 | tags: 141 | - name: admin 142 | description: Super admin methods 143 | - name: internal 144 | servers: 145 | - url: "https://my.super.server.com/v1" 146 | - url: "http://test/123" 147 | paths: 148 | "/test/{uuid}": 149 | post: 150 | operationId: test 151 | deprecated: true 152 | security: 153 | - basicAuth: [] 154 | test_auth: [] 155 | requestBody: 156 | required: true 157 | description: "" 158 | content: 159 | application/json: 160 | schema: 161 | $ref: "#/components/schemas/InModule" 162 | responses: 163 | 200: 164 | description: OK 165 | content: 166 | application/json: 167 | schema: 168 | type: array 169 | items: 170 | type: string 171 | callbacks: 172 | myCallback: 173 | /callback_url: 174 | post: 175 | requestBody: 176 | required: true 177 | description: "" 178 | content: 179 | application/json: 180 | schema: 181 | $ref: "#/components/schemas/InModule" 182 | responses: 183 | 200: 184 | description: OK 185 | content: 186 | application/json: 187 | schema: 188 | type: array 189 | items: 190 | type: string 191 | parameters: 192 | - name: uuid 193 | in: path 194 | required: true 195 | schema: 196 | description: "UUID ver. 4 [rfc](https://tools.ietf.org/html/rfc4122)" 197 | type: string 198 | format: uuid 199 | example: 00000000-0000-0000-0000-000000000000 200 | "/hello/world/{paramTest}": 201 | summary: Some test group of requests 202 | description: Another test description 203 | get: 204 | tags: 205 | - internal 206 | summary: Small summary 207 | operationId: testGet 208 | description: Small description 209 | responses: 210 | 200: 211 | description: Custom response desc 212 | content: 213 | application/json: 214 | schema: 215 | type: string 216 | parameters: 217 | - name: someParam 218 | description: Test 219 | in: query 220 | deprecated: true 221 | schema: 222 | type: integer 223 | format: uint32 224 | post: 225 | tags: 226 | - admin 227 | operationId: testPost 228 | requestBody: 229 | required: true 230 | description: Some interesting description 231 | content: 232 | application/json: 233 | schema: 234 | type: string 235 | responses: 236 | 200: 237 | description: OK 238 | content: 239 | application/json: 240 | schema: 241 | $ref: "#/components/schemas/SimpleEnum" 242 | delete: 243 | responses: 244 | 200: 245 | description: OK 246 | options: 247 | responses: 248 | 200: 249 | description: OK 250 | content: 251 | application/json: 252 | schema: 253 | description: "Always `null`" 254 | nullable: true 255 | type: string 256 | format: "null" 257 | parameters: 258 | - name: asd 259 | in: header 260 | required: true 261 | schema: 262 | type: string 263 | - name: paramTest 264 | in: path 265 | required: true 266 | schema: 267 | type: string 268 | - name: test 269 | in: query 270 | schema: 271 | type: integer 272 | format: int32 273 | - name: x-request-id 274 | description: Test 275 | in: header 276 | required: true 277 | schema: 278 | type: string 279 | components: 280 | schemas: 281 | InModule: 282 | type: object 283 | properties: 284 | field: 285 | type: string 286 | second: 287 | $ref: "#/components/schemas/Test" 288 | required: 289 | - field 290 | - second 291 | SimpleEnum: 292 | description: Simple enum 293 | type: string 294 | enum: 295 | - test 296 | - another 297 | - yay 298 | example: test 299 | Test: 300 | type: object 301 | properties: 302 | another_field: 303 | nullable: true 304 | type: string 305 | required: 306 | - another_field 307 | securitySchemes: 308 | ApiKeyAuth: 309 | type: apiKey 310 | in: query 311 | name: X-API-KEY 312 | description: And another test description 313 | basicAuth: 314 | type: http 315 | scheme: basic 316 | description: Another test description 317 | bearerAuth: 318 | type: http 319 | scheme: bearer 320 | bearerFormat: JWT 321 | description: Test description 322 | test_auth: 323 | type: apiKey 324 | in: query 325 | name: X-MY-SUPER-API 326 | "## 327 | ); 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Ivan Kalinin 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

opg

3 |

Rust OpenAPI 3.0 docs generator

4 |

5 | 6 | GitHub 7 | 8 | 9 | GitHub Workflow Status 10 | 11 | 12 | Crates.io Version 13 | 14 | 15 | Docs.rs 16 | 17 |

18 |

19 | 20 | #### Example: 21 | > Or see more [here](https://github.com/Rexagon/opg/tree/master/test_suite/tests) 22 | 23 | ```rust 24 | use opg::*; 25 | use serde::{Serialize, Deserialize}; 26 | 27 | #[derive(Serialize, Deserialize, OpgModel)] 28 | #[serde(rename_all = "camelCase")] 29 | #[opg("Simple enum")] 30 | enum SimpleEnum { 31 | Test, 32 | Another, 33 | Yay, 34 | } 35 | 36 | #[derive(Serialize, Deserialize, OpgModel)] 37 | #[opg("newtype string", format = "id", example = "abcd0001")] 38 | struct NewType(String); 39 | 40 | #[derive(Serialize, Deserialize, OpgModel)] 41 | struct SimpleStruct { 42 | first_field: i32, 43 | #[opg("Field description")] 44 | second: String, 45 | } 46 | 47 | #[derive(Serialize, Deserialize, OpgModel)] 48 | #[serde(rename_all = "kebab-case")] 49 | enum ExternallyTaggedEnum { 50 | Test(String), 51 | AnotherTest(String, #[opg("Second")] String), 52 | } 53 | 54 | #[derive(Serialize, Deserialize, OpgModel)] 55 | #[serde(untagged)] 56 | enum UntaggedEnum { 57 | First { 58 | value: NewType, 59 | }, 60 | #[opg("Variant description")] 61 | Second { 62 | #[opg("Inlined struct", inline)] 63 | another: SimpleStruct, 64 | }, 65 | } 66 | 67 | #[derive(Serialize, Deserialize, OpgModel)] 68 | #[serde(tag = "tag", rename_all = "lowercase")] 69 | enum InternallyTaggedEnum { 70 | First(SimpleStruct), 71 | Second { field: String }, 72 | } 73 | 74 | #[derive(Serialize, Deserialize, OpgModel)] 75 | #[serde(tag = "tag", content = "content", rename_all = "lowercase")] 76 | enum AdjacentlyTaggedEnum { 77 | First(String), 78 | Second(NewType, NewType), 79 | } 80 | 81 | #[derive(Serialize, Deserialize, OpgModel)] 82 | #[serde(rename_all = "camelCase")] 83 | struct TypeChangedStruct { 84 | #[serde(with = "chrono::naive::serde::ts_milliseconds")] 85 | #[opg("UTC timestamp in milliseconds", integer, format = "int64")] 86 | pub timestamp: chrono::NaiveDateTime, 87 | } 88 | 89 | #[derive(Serialize, Deserialize, OpgModel)] 90 | struct StructWithComplexObjects { 91 | #[serde(skip_serializing_if = "Option::is_none")] 92 | #[opg(optional)] 93 | super_optional: Option>, 94 | field: Option, 95 | boxed: Box>, 96 | } 97 | 98 | #[derive(Serialize, OpgModel)] 99 | struct GenericStructWithRef<'a, T> { 100 | message: &'a str, 101 | test: T, 102 | } 103 | 104 | #[derive(Serialize, OpgModel)] 105 | struct SuperResponse { 106 | simple_enum: SimpleEnum, 107 | #[serde(rename = "new_type")] 108 | newtype: NewType, 109 | externally_tagged_enum: ExternallyTaggedEnum, 110 | untagged_enum: UntaggedEnum, 111 | internally_tagged_enum: InternallyTaggedEnum, 112 | adjacently_tagged_enum: AdjacentlyTaggedEnum, 113 | type_changed_struct: TypeChangedStruct, 114 | struct_with_complex_objects: StructWithComplexObjects, 115 | } 116 | 117 | #[test] 118 | fn print_api() { 119 | let test = describe_api! { 120 | info: { 121 | title: "My super API", 122 | version: "0.0.0", 123 | }, 124 | tags: {internal, admin("Super admin methods")}, 125 | servers: { 126 | "https://my.super.server.com/v1", 127 | }, 128 | security_schemes: { 129 | (http "bearerAuth"): { 130 | scheme: Bearer, 131 | bearer_format: "JWT", 132 | }, 133 | }, 134 | paths: { 135 | ("hello" / "world" / { paramTest: String }): { 136 | summary: "Some test group of requests", 137 | description: "Another test description", 138 | parameters: { 139 | (header "x-request-id"): { 140 | description: "Test", 141 | required: true, 142 | }, 143 | }, 144 | GET: { 145 | tags: {internal}, 146 | summary: "Small summary", 147 | description: "Small description", 148 | deprecated: true, 149 | parameters: { 150 | (query someParam: u32): { 151 | description: "Test", 152 | } 153 | }, 154 | 200: String, 155 | 418 ("Optional response description"): String 156 | }, 157 | POST: { 158 | tags: {admin}, 159 | security: {"bearerAuth"}, 160 | body: { 161 | description: "Some interesting description", 162 | schema: GenericStructWithRef<'static, i64>, 163 | required: true, 164 | }, 165 | 200: SuperResponse, 166 | callbacks: { 167 | callbackUrl: { 168 | ("callbackUrl"): { 169 | POST: { 170 | 200: std::vec::Vec, 171 | } 172 | } 173 | } 174 | } 175 | } 176 | } 177 | } 178 | }; 179 | 180 | println!("{}", serde_yaml::to_string(&test).unwrap()); 181 | } 182 | ``` 183 | 184 |
Result: 185 |

186 | 187 | ```yaml 188 | --- 189 | openapi: 3.0.3 190 | info: 191 | title: My super API 192 | version: 0.0.0 193 | tags: 194 | - name: admin 195 | description: Super admin methods 196 | - name: internal 197 | servers: 198 | - url: "https://my.super.server.com/v1" 199 | paths: 200 | "/hello/world/{paramTest}": 201 | summary: Some test group of requests 202 | description: Another test description 203 | get: 204 | tags: 205 | - internal 206 | summary: Small summary 207 | description: Small description 208 | deprecated: true 209 | responses: 210 | 200: 211 | description: OK 212 | content: 213 | application/json: 214 | schema: 215 | type: string 216 | 418: 217 | description: Optional response description 218 | content: 219 | application/json: 220 | schema: 221 | type: string 222 | parameters: 223 | - name: someParam 224 | description: Test 225 | in: query 226 | schema: 227 | type: integer 228 | format: uint32 229 | post: 230 | tags: 231 | - admin 232 | security: 233 | - bearerAuth: [] 234 | requestBody: 235 | required: true 236 | description: Some interesting description 237 | content: 238 | application/json: 239 | schema: 240 | $ref: "#/components/schemas/GenericStructWithRef" 241 | responses: 242 | 200: 243 | description: OK 244 | content: 245 | application/json: 246 | schema: 247 | $ref: "#/components/schemas/SuperResponse" 248 | callbacks: 249 | callbackUrl: 250 | /callbackUrl: 251 | post: 252 | responses: 253 | 200: 254 | description: OK 255 | content: 256 | application/json: 257 | schema: 258 | type: array 259 | items: 260 | type: string 261 | parameters: 262 | - name: paramTest 263 | in: path 264 | required: true 265 | schema: 266 | type: string 267 | - name: x-request-id 268 | description: Test 269 | in: header 270 | required: true 271 | schema: 272 | type: string 273 | components: 274 | schemas: 275 | AdjacentlyTaggedEnum: 276 | type: object 277 | properties: 278 | content: 279 | oneOf: 280 | - type: string 281 | - type: array 282 | items: 283 | oneOf: 284 | - $ref: "#/components/schemas/NewType" 285 | - $ref: "#/components/schemas/NewType" 286 | tag: 287 | description: AdjacentlyTaggedEnum type variant 288 | type: string 289 | enum: 290 | - first 291 | - second 292 | example: first 293 | required: 294 | - tag 295 | - content 296 | ExternallyTaggedEnum: 297 | type: object 298 | additionalProperties: 299 | oneOf: 300 | - type: string 301 | - type: array 302 | items: 303 | oneOf: 304 | - type: string 305 | - description: Second 306 | type: string 307 | GenericStructWithRef: 308 | type: object 309 | properties: 310 | message: 311 | type: string 312 | test: 313 | type: integer 314 | format: int64 315 | required: 316 | - message 317 | - test 318 | InternallyTaggedEnum: 319 | oneOf: 320 | - type: object 321 | properties: 322 | first_field: 323 | type: integer 324 | format: int32 325 | second: 326 | description: Field description 327 | type: string 328 | tag: 329 | description: InternallyTaggedEnum type variant 330 | type: string 331 | enum: 332 | - first 333 | example: first 334 | required: 335 | - first_field 336 | - second 337 | - tag 338 | - type: object 339 | properties: 340 | field: 341 | type: string 342 | tag: 343 | description: InternallyTaggedEnum type variant 344 | type: string 345 | enum: 346 | - second 347 | example: second 348 | required: 349 | - field 350 | - tag 351 | NewType: 352 | description: newtype string 353 | type: string 354 | format: id 355 | example: abcd0001 356 | SimpleEnum: 357 | description: Simple enum 358 | type: string 359 | enum: 360 | - test 361 | - another 362 | - yay 363 | example: test 364 | StructWithComplexObjects: 365 | type: object 366 | properties: 367 | boxed: 368 | nullable: true 369 | type: integer 370 | format: int32 371 | field: 372 | nullable: true 373 | type: string 374 | super_optional: 375 | nullable: true 376 | type: string 377 | required: 378 | - field 379 | - boxed 380 | SuperResponse: 381 | type: object 382 | properties: 383 | adjacently_tagged_enum: 384 | $ref: "#/components/schemas/AdjacentlyTaggedEnum" 385 | externally_tagged_enum: 386 | $ref: "#/components/schemas/ExternallyTaggedEnum" 387 | internally_tagged_enum: 388 | $ref: "#/components/schemas/InternallyTaggedEnum" 389 | new_type: 390 | $ref: "#/components/schemas/NewType" 391 | simple_enum: 392 | $ref: "#/components/schemas/SimpleEnum" 393 | struct_with_complex_objects: 394 | $ref: "#/components/schemas/StructWithComplexObjects" 395 | type_changed_struct: 396 | $ref: "#/components/schemas/TypeChangedStruct" 397 | untagged_enum: 398 | $ref: "#/components/schemas/UntaggedEnum" 399 | required: 400 | - simple_enum 401 | - new_type 402 | - externally_tagged_enum 403 | - untagged_enum 404 | - internally_tagged_enum 405 | - adjacently_tagged_enum 406 | - type_changed_struct 407 | - struct_with_complex_objects 408 | TypeChangedStruct: 409 | type: object 410 | properties: 411 | timestamp: 412 | description: UTC timestamp in milliseconds 413 | type: integer 414 | format: int64 415 | required: 416 | - timestamp 417 | UntaggedEnum: 418 | oneOf: 419 | - type: object 420 | properties: 421 | value: 422 | $ref: "#/components/schemas/NewType" 423 | required: 424 | - value 425 | - description: Variant description 426 | type: object 427 | properties: 428 | another: 429 | description: Inlined struct 430 | type: object 431 | properties: 432 | first_field: 433 | type: integer 434 | format: int32 435 | second: 436 | description: Field description 437 | type: string 438 | required: 439 | - first_field 440 | - second 441 | required: 442 | - another 443 | securitySchemes: 444 | bearerAuth: 445 | type: http 446 | scheme: bearer 447 | bearerFormat: JWT 448 | ``` 449 |

450 |
451 | -------------------------------------------------------------------------------- /test_suite/tests/models.rs: -------------------------------------------------------------------------------- 1 | #[allow(dead_code)] 2 | mod tests { 3 | 4 | use opg::{Components, OpgModel}; 5 | use serde::Serialize; 6 | 7 | #[derive(Serialize, OpgModel)] 8 | #[opg("New type description", format = "uuid", example = "000-000")] 9 | struct NewType(String); 10 | 11 | #[derive(Serialize, OpgModel)] 12 | #[opg("Override description")] 13 | struct NewNewType(NewType); 14 | 15 | #[test] 16 | fn newtype() { 17 | let cx = &mut Components::new(); 18 | 19 | assert_eq!( 20 | serde_yaml::to_string(&NewType::get_schema(cx)).unwrap(), 21 | r##"--- 22 | description: New type description 23 | type: string 24 | format: uuid 25 | example: 000-000 26 | "## 27 | ); 28 | 29 | assert_eq!( 30 | serde_yaml::to_string(&NewNewType::get_schema(cx)).unwrap(), 31 | r##"--- 32 | description: Override description 33 | type: string 34 | format: uuid 35 | example: 000-000 36 | "## 37 | ); 38 | } 39 | 40 | #[derive(Serialize, OpgModel)] 41 | #[serde(rename_all = "camelCase")] 42 | struct SimpleStruct { 43 | asd: i32, 44 | #[opg(optional)] 45 | hello_camel_case: NewType, 46 | } 47 | 48 | #[test] 49 | fn simple_struct() { 50 | let cx = &mut Components::default(); 51 | assert_eq!( 52 | serde_yaml::to_string(&SimpleStruct::get_schema(cx)).unwrap(), 53 | r##"--- 54 | type: object 55 | properties: 56 | asd: 57 | type: integer 58 | format: int32 59 | helloCamelCase: 60 | $ref: "#/components/schemas/NewType" 61 | required: 62 | - asd 63 | "## 64 | ); 65 | } 66 | 67 | #[derive(Serialize, OpgModel)] 68 | #[serde(rename_all = "kebab-case")] 69 | #[opg("New type description")] 70 | enum StringEnumTest { 71 | First, 72 | Second, 73 | HelloWorld, 74 | } 75 | 76 | #[test] 77 | fn string_enum() { 78 | let cx = &mut Components::default(); 79 | assert_eq!( 80 | serde_yaml::to_string(&StringEnumTest::get_schema(cx)).unwrap(), 81 | r##"--- 82 | description: New type description 83 | type: string 84 | enum: 85 | - first 86 | - second 87 | - hello-world 88 | example: first 89 | "## 90 | ); 91 | } 92 | 93 | #[derive(Serialize, OpgModel)] 94 | #[serde(rename_all = "kebab-case")] 95 | enum ExternallyTaggedEnum { 96 | Test(String), 97 | AnotherTest(String, #[opg("Second")] String), 98 | } 99 | 100 | #[test] 101 | fn externally_enum() { 102 | let cx = &mut Components::default(); 103 | assert_eq!( 104 | serde_yaml::to_string(&ExternallyTaggedEnum::get_schema(cx)).unwrap(), 105 | r##"--- 106 | type: object 107 | additionalProperties: 108 | oneOf: 109 | - type: string 110 | - type: array 111 | items: 112 | oneOf: 113 | - type: string 114 | - description: Second 115 | type: string 116 | "## 117 | ); 118 | } 119 | 120 | #[derive(Serialize, OpgModel)] 121 | #[serde(untagged)] 122 | enum UntaggedEnumTest { 123 | First { 124 | value: NewType, 125 | }, 126 | #[opg("Very simple variant")] 127 | Second { 128 | #[opg("Very simple struct", inline)] 129 | another: SimpleStruct, 130 | }, 131 | } 132 | 133 | #[test] 134 | fn untagged_enum() { 135 | let cx = &mut Components::default(); 136 | assert_eq!( 137 | serde_yaml::to_string(&UntaggedEnumTest::get_schema(cx)).unwrap(), 138 | r##"--- 139 | oneOf: 140 | - type: object 141 | properties: 142 | value: 143 | $ref: "#/components/schemas/NewType" 144 | required: 145 | - value 146 | - description: Very simple variant 147 | type: object 148 | properties: 149 | another: 150 | description: Very simple struct 151 | type: object 152 | properties: 153 | asd: 154 | type: integer 155 | format: int32 156 | helloCamelCase: 157 | $ref: "#/components/schemas/NewType" 158 | required: 159 | - asd 160 | required: 161 | - another 162 | "## 163 | ); 164 | } 165 | 166 | #[derive(Serialize, OpgModel)] 167 | #[serde(tag = "tag", rename_all = "kebab-case")] 168 | enum InternallyTaggedEnum { 169 | Test(SimpleStruct), 170 | AnotherTest { field: String }, 171 | } 172 | 173 | #[test] 174 | fn internally_tagged_enum() { 175 | let cx = &mut Components::default(); 176 | assert_eq!( 177 | serde_yaml::to_string(&InternallyTaggedEnum::get_schema(cx)).unwrap(), 178 | r##"--- 179 | oneOf: 180 | - type: object 181 | properties: 182 | asd: 183 | type: integer 184 | format: int32 185 | helloCamelCase: 186 | $ref: "#/components/schemas/NewType" 187 | tag: 188 | description: InternallyTaggedEnum type variant 189 | type: string 190 | enum: 191 | - test 192 | example: test 193 | required: 194 | - asd 195 | - tag 196 | - type: object 197 | properties: 198 | field: 199 | type: string 200 | tag: 201 | description: InternallyTaggedEnum type variant 202 | type: string 203 | enum: 204 | - another-test 205 | example: another-test 206 | required: 207 | - field 208 | - tag 209 | "## 210 | ); 211 | } 212 | 213 | #[derive(Serialize, OpgModel)] 214 | #[serde(tag = "tag", content = "content", rename_all = "kebab-case")] 215 | enum AdjacentlyTaggedEnum { 216 | Test(String), 217 | AnotherTest(NewType, NewType), 218 | } 219 | 220 | #[test] 221 | fn adjacently_tagged_enum() { 222 | let cx = &mut Components::default(); 223 | assert_eq!( 224 | serde_yaml::to_string(&AdjacentlyTaggedEnum::get_schema(cx)).unwrap(), 225 | r##"--- 226 | type: object 227 | properties: 228 | content: 229 | oneOf: 230 | - type: string 231 | - type: array 232 | items: 233 | oneOf: 234 | - $ref: "#/components/schemas/NewType" 235 | - $ref: "#/components/schemas/NewType" 236 | tag: 237 | description: AdjacentlyTaggedEnum type variant 238 | type: string 239 | enum: 240 | - test 241 | - another-test 242 | example: test 243 | required: 244 | - tag 245 | - content 246 | "## 247 | ); 248 | } 249 | 250 | #[derive(Serialize, OpgModel)] 251 | #[serde(rename_all = "camelCase")] 252 | struct TypeChangedStruct { 253 | #[opg(integer)] 254 | asd: String, 255 | } 256 | 257 | #[test] 258 | fn type_changed_field() { 259 | let cx = &mut Components::default(); 260 | assert_eq!( 261 | serde_yaml::to_string(&TypeChangedStruct::get_schema(cx)).unwrap(), 262 | r##"--- 263 | type: object 264 | properties: 265 | asd: 266 | type: integer 267 | required: 268 | - asd 269 | "## 270 | ); 271 | } 272 | 273 | #[test] 274 | fn tuples() { 275 | let cx = &mut Components::default(); 276 | assert_eq!( 277 | serde_yaml::to_string(&<(String, u64)>::get_schema(cx)).unwrap(), 278 | r##"--- 279 | type: array 280 | items: 281 | oneOf: 282 | - type: string 283 | - type: integer 284 | format: uint64 285 | "## 286 | ); 287 | } 288 | 289 | #[derive(Serialize, OpgModel)] 290 | struct StructWithInner { 291 | field: Option, 292 | #[opg(optional)] 293 | super_optional: Option>, 294 | boxed: Box>, 295 | } 296 | 297 | #[test] 298 | fn inner_type() { 299 | let cx = &mut Components::default(); 300 | assert_eq!( 301 | serde_yaml::to_string(&StructWithInner::get_schema(cx)).unwrap(), 302 | r##"--- 303 | type: object 304 | properties: 305 | boxed: 306 | nullable: true 307 | type: integer 308 | format: int32 309 | field: 310 | nullable: true 311 | type: string 312 | super_optional: 313 | nullable: true 314 | type: string 315 | required: 316 | - field 317 | - boxed 318 | "## 319 | ); 320 | } 321 | 322 | #[test] 323 | fn hash_map() { 324 | let cx = &mut Components::default(); 325 | assert_eq!( 326 | serde_yaml::to_string(&std::collections::HashMap::<&str, i32>::get_schema(cx)).unwrap(), 327 | r##"--- 328 | type: object 329 | additionalProperties: 330 | type: integer 331 | format: int32 332 | "## 333 | ); 334 | } 335 | 336 | #[derive(Serialize, OpgModel)] 337 | struct NullableNewtype(Option); 338 | 339 | #[test] 340 | fn nullable_newtype() { 341 | let cx = &mut Components::default(); 342 | assert_eq!( 343 | serde_yaml::to_string(&NullableNewtype::get_schema(cx)).unwrap(), 344 | r##"--- 345 | nullable: true 346 | type: integer 347 | format: int32 348 | "## 349 | ); 350 | } 351 | 352 | #[derive(Serialize, OpgModel)] 353 | struct StructWithNullable { 354 | #[opg(nullable)] 355 | field: i32, 356 | } 357 | 358 | #[test] 359 | fn nullable_field() { 360 | let cx = &mut Components::default(); 361 | assert_eq!( 362 | serde_yaml::to_string(&StructWithNullable::get_schema(cx)).unwrap(), 363 | r##"--- 364 | type: object 365 | properties: 366 | field: 367 | nullable: true 368 | type: integer 369 | format: int32 370 | required: 371 | - field 372 | "## 373 | ); 374 | } 375 | 376 | #[derive(Serialize, OpgModel)] 377 | struct Recursive { 378 | recursive_field: Option>, 379 | } 380 | 381 | fn recursive_field() { 382 | let cx = &mut Components::default(); 383 | assert_eq!( 384 | serde_yaml::to_string(&Recursive::get_schema(cx)).unwrap(), 385 | r##"--- 386 | type: object 387 | properties: 388 | field: 389 | nullable: true 390 | type: integer 391 | required: 392 | - field 393 | "## 394 | ); 395 | } 396 | 397 | #[derive(Debug, Serialize, Hash, OpgModel)] 398 | #[serde(rename_all = "snake_case")] 399 | #[opg("Credit history kind")] 400 | pub enum CreditHistoryMetaResponse { 401 | #[serde(rename_all = "camelCase")] 402 | CreateCredit { 403 | pledge_currency: Option, 404 | pledge_amount: Option, 405 | credit_currency: String, 406 | credit_amount: String, 407 | }, 408 | CloseCredit, 409 | #[serde(rename_all = "camelCase")] 410 | FreezePledge { 411 | pledge_currency: String, 412 | pledge_amount: String, 413 | }, 414 | } 415 | 416 | #[derive(Debug, Default, Serialize, OpgModel)] 417 | #[opg(example_with = "now()")] 418 | struct ExampleWith(u64); 419 | 420 | fn now() -> u64 { 421 | use std::time::{Duration, SystemTime}; 422 | 423 | // let now = SystemTime::now(); 424 | let now = SystemTime::UNIX_EPOCH + Duration::from_secs(1599083980); 425 | 426 | now.duration_since(SystemTime::UNIX_EPOCH) 427 | .unwrap() 428 | .as_secs() 429 | } 430 | 431 | #[test] 432 | fn example_with() { 433 | let cx = &mut Components::default(); 434 | assert_eq!( 435 | serde_yaml::to_string(&ExampleWith::get_schema(cx)).unwrap(), 436 | r##"--- 437 | type: integer 438 | format: uint64 439 | example: "1599083980" 440 | "## 441 | ); 442 | } 443 | 444 | #[derive(Debug, Serialize, OpgModel)] 445 | #[opg("Test")] 446 | struct GenericStruct { 447 | body: T, 448 | } 449 | 450 | #[test] 451 | fn generic_struct() { 452 | let cx = &mut Components::default(); 453 | assert_eq!( 454 | serde_yaml::to_string(&GenericStruct::::get_schema(cx)).unwrap(), 455 | r##"--- 456 | description: Test 457 | type: object 458 | properties: 459 | body: 460 | type: integer 461 | format: int32 462 | required: 463 | - body 464 | "## 465 | ); 466 | } 467 | 468 | #[derive(Debug, Serialize, OpgModel)] 469 | struct GenericStructWithRef<'a, T> { 470 | message: &'a str, 471 | test: T, 472 | } 473 | 474 | #[test] 475 | fn generic_struct_with_ref() { 476 | let cx = &mut Components::default(); 477 | assert_eq!( 478 | serde_yaml::to_string(&GenericStructWithRef::::get_schema(cx)).unwrap(), 479 | r##"--- 480 | type: object 481 | properties: 482 | message: 483 | type: string 484 | test: 485 | type: integer 486 | format: int32 487 | required: 488 | - message 489 | - test 490 | "## 491 | ); 492 | } 493 | 494 | #[derive(Debug, Serialize, OpgModel)] 495 | struct StructWithAny { 496 | #[opg(any)] 497 | field: serde_yaml::Value, 498 | } 499 | 500 | #[test] 501 | fn struct_with_any_field() { 502 | let cx = &mut Components::default(); 503 | assert_eq!( 504 | serde_yaml::to_string(&StructWithAny::get_schema(cx)).unwrap(), 505 | r##"--- 506 | type: object 507 | properties: 508 | field: {} 509 | required: 510 | - field 511 | "## 512 | ); 513 | } 514 | 515 | #[test] 516 | fn null_response() { 517 | let cx = &mut Components::default(); 518 | assert_eq!( 519 | serde_yaml::to_string(&<()>::get_schema(cx)).unwrap(), 520 | r##"--- 521 | description: "Always `null`" 522 | nullable: true 523 | type: string 524 | format: "null" 525 | "## 526 | ); 527 | } 528 | 529 | #[test] 530 | fn complex_enum() { 531 | let cx = &mut Components::default(); 532 | assert_eq!( 533 | serde_yaml::to_string(&CreditHistoryMetaResponse::get_schema(cx)).unwrap(), 534 | r##"--- 535 | description: Credit history kind 536 | type: object 537 | additionalProperties: 538 | description: Credit history kind 539 | oneOf: 540 | - type: object 541 | properties: 542 | creditAmount: 543 | type: string 544 | creditCurrency: 545 | type: string 546 | pledgeAmount: 547 | nullable: true 548 | type: string 549 | pledgeCurrency: 550 | nullable: true 551 | type: string 552 | required: 553 | - pledgeCurrency 554 | - pledgeAmount 555 | - creditCurrency 556 | - creditAmount 557 | - type: string 558 | enum: 559 | - close_credit 560 | example: close_credit 561 | - type: object 562 | properties: 563 | pledgeAmount: 564 | type: string 565 | pledgeCurrency: 566 | type: string 567 | required: 568 | - pledgeCurrency 569 | - pledgeAmount 570 | "## 571 | ); 572 | } 573 | 574 | #[test] 575 | fn manual_serialization() { 576 | use opg::*; 577 | 578 | let model = Model { 579 | description: Some("Some type".to_owned()), 580 | data: ModelData::Single(ModelType { 581 | nullable: false, 582 | type_description: ModelTypeDescription::Object(ModelObject { 583 | properties: { 584 | let mut properties = std::collections::BTreeMap::new(); 585 | properties.insert( 586 | "id".to_owned(), 587 | ModelReference::Link("TransactionId".to_owned()), 588 | ); 589 | properties.insert( 590 | "amount".to_owned(), 591 | ModelReference::Inline(Model { 592 | description: None, 593 | data: ModelData::Single(ModelType { 594 | nullable: false, 595 | type_description: ModelTypeDescription::String(ModelString { 596 | variants: None, 597 | data: ModelSimple { 598 | format: None, 599 | example: None, 600 | }, 601 | }), 602 | }), 603 | }), 604 | ); 605 | 606 | properties 607 | }, 608 | additional_properties: Default::default(), 609 | required: vec![ 610 | "id".to_owned(), 611 | "amount".to_owned(), 612 | "currency".to_owned(), 613 | "paymentType".to_owned(), 614 | "status".to_owned(), 615 | ], 616 | }), 617 | }), 618 | }; 619 | 620 | assert_eq!( 621 | serde_yaml::to_string(&model).unwrap(), 622 | r##"--- 623 | description: Some type 624 | type: object 625 | properties: 626 | amount: 627 | type: string 628 | id: 629 | $ref: "#/components/schemas/TransactionId" 630 | required: 631 | - id 632 | - amount 633 | - currency 634 | - paymentType 635 | - status 636 | "## 637 | ); 638 | } 639 | 640 | #[test] 641 | fn describe_type_macro() { 642 | use opg::describe_type; 643 | 644 | let sub = describe_type!(string => { 645 | description: "Test" 646 | }); 647 | 648 | let model = describe_type!(object => { 649 | description: "Hello world" 650 | properties: { 651 | id[required]: (link => "TransactionId") 652 | test[required]: (object => { 653 | properties: { 654 | sub: (link => "TransactionId") 655 | } 656 | }) 657 | test_object: (string => { 658 | format: "uuid" 659 | variants: ["aaa", "bbb"] 660 | }) 661 | test_integer: (integer => { 662 | format: "timestamp" 663 | example: "1591956576404" 664 | }) 665 | test_boolean: (boolean => {}) 666 | test_array: (array => { 667 | items: (string => {}) 668 | }) 669 | test_raw_model: (raw_model => sub) 670 | } 671 | }); 672 | 673 | assert_eq!( 674 | serde_yaml::to_string(&model).unwrap(), 675 | r##"--- 676 | description: Hello world 677 | type: object 678 | properties: 679 | id: 680 | $ref: "#/components/schemas/TransactionId" 681 | test: 682 | type: object 683 | properties: 684 | sub: 685 | $ref: "#/components/schemas/TransactionId" 686 | test_array: 687 | type: array 688 | items: 689 | type: string 690 | test_boolean: 691 | type: boolean 692 | test_integer: 693 | type: integer 694 | format: timestamp 695 | example: "1591956576404" 696 | test_object: 697 | type: string 698 | enum: 699 | - aaa 700 | - bbb 701 | format: uuid 702 | test_raw_model: 703 | description: Test 704 | type: string 705 | required: 706 | - id 707 | - test 708 | "## 709 | ); 710 | } 711 | 712 | #[test] 713 | fn valid_models_context() { 714 | use opg::describe_type; 715 | 716 | let mut cx = Components::new(); 717 | 718 | cx.add_model( 719 | "TransactionId", 720 | describe_type!(string => { 721 | description: "Transaction UUID" 722 | format: "uuid" 723 | example: "000..000-000..000-00..00" 724 | }), 725 | ); 726 | 727 | cx.add_model( 728 | "SomeResponse", 729 | describe_type!(object => { 730 | properties: { 731 | id: (link => "TransactionId") 732 | } 733 | }), 734 | ); 735 | 736 | assert_eq!(cx.verify_schemas(), Ok(())); 737 | } 738 | 739 | #[test] 740 | fn invalid_models_context() { 741 | use opg::describe_type; 742 | 743 | let mut cx = Components::new(); 744 | 745 | let invalid_link = "TransactionId"; 746 | 747 | cx.add_model( 748 | "SomeResponse", 749 | describe_type!(object => { 750 | properties: { 751 | id: (link => invalid_link) 752 | } 753 | }), 754 | ); 755 | 756 | assert_eq!(cx.verify_schemas(), Err(invalid_link.to_owned())); 757 | } 758 | } 759 | -------------------------------------------------------------------------------- /opg_derive/src/attr.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use proc_macro2::{Group, Span, TokenStream, TokenTree}; 4 | use quote::ToTokens; 5 | use syn::punctuated::Punctuated; 6 | use syn::Meta::*; 7 | use syn::NestedMeta::*; 8 | 9 | use crate::case::*; 10 | use crate::dummy::*; 11 | use crate::parsing_context::*; 12 | use crate::symbol::*; 13 | 14 | pub struct Container { 15 | pub name: Name, 16 | pub rename_rule: RenameRule, 17 | pub transparent: bool, 18 | pub tag_type: TagType, 19 | pub has_flatten: bool, 20 | pub has_repr: bool, 21 | 22 | pub description: Option, 23 | pub format: Option, 24 | pub example: Option, 25 | pub inline: bool, 26 | pub nullable: bool, 27 | pub explicit_model_type: Option, 28 | pub model_type: ModelType, 29 | } 30 | 31 | impl Container { 32 | #[allow(clippy::cognitive_complexity)] 33 | pub fn from_ast(cx: &ParsingContext, input: &syn::DeriveInput) -> Self { 34 | let mut ser_name = Attr::none(cx, RENAME); 35 | let mut rename_rule = Attr::none(cx, RENAME_ALL); 36 | let mut transparent = BoolAttr::none(cx, TRANSPARENT); 37 | let mut untagged = BoolAttr::none(cx, UNTAGGED); 38 | let mut internal_tag = Attr::none(cx, TAG); 39 | let mut content = Attr::none(cx, CONTENT); 40 | let mut has_repr = BoolAttr::none(cx, REPR); 41 | 42 | let mut description = Attr::none(cx, DESCRIPTION); 43 | let mut format = Attr::none(cx, FORMAT); 44 | let mut example = Attr::none(cx, EXAMPLE); 45 | let mut inline = BoolAttr::none(cx, INLINE); 46 | let mut nullable = BoolAttr::none(cx, NULLABLE); 47 | let mut model_type = OneOfFlagsAttr::none(cx); 48 | 49 | for (from, meta_item) in input 50 | .attrs 51 | .iter() 52 | .flat_map(|attr| get_meta_items(cx, attr)) 53 | .flat_map(|item| item.into_iter()) 54 | { 55 | match (from, &meta_item) { 56 | (AttrFrom::Serde, Meta(NameValue(m))) if m.path == RENAME => { 57 | if let Ok(s) = get_lit_str(cx, RENAME, &m.lit) { 58 | ser_name.set(&m.path, s.value()); 59 | } 60 | } 61 | (AttrFrom::Serde, Meta(List(m))) if m.path == RENAME => { 62 | if let Ok(ser) = get_renames(cx, &m.nested) { 63 | ser_name.set_opt(&m.path, ser.map(syn::LitStr::value)); 64 | } 65 | } 66 | (AttrFrom::Serde, Meta(NameValue(m))) if m.path == RENAME_ALL => { 67 | if let Ok(rule) = get_lit_str(cx, RENAME_ALL, &m.lit) 68 | .and_then(|s| RenameRule::from_str(&s.value())) 69 | { 70 | rename_rule.set(&m.path, rule) 71 | } 72 | } 73 | (AttrFrom::Serde, Meta(List(m))) if m.path == RENAME_ALL => { 74 | if let Ok(Some(rule)) = get_renames(cx, &m.nested) { 75 | if let Ok(rule) = RenameRule::from_str(&rule.value()) { 76 | rename_rule.set(&m.path, rule) 77 | } 78 | } 79 | } 80 | (AttrFrom::Serde, Meta(Path(word))) if word == TRANSPARENT => { 81 | transparent.set_true(word); 82 | } 83 | (AttrFrom::Serde, Meta(Path(word))) if word == UNTAGGED => { 84 | if let syn::Data::Enum(_) = input.data { 85 | untagged.set_true(word); 86 | } 87 | } 88 | (AttrFrom::Serde, Meta(NameValue(m))) if m.path == TAG => { 89 | if let Ok(s) = get_lit_str_simple(&m.lit) { 90 | match &input.data { 91 | syn::Data::Enum(_) 92 | | syn::Data::Struct(syn::DataStruct { 93 | fields: syn::Fields::Named(_), 94 | .. 95 | }) => { 96 | internal_tag.set(&m.path, s.value()); 97 | } 98 | _ => {} 99 | } 100 | } 101 | } 102 | (AttrFrom::Serde, Meta(NameValue(m))) if m.path == CONTENT => { 103 | if let Ok(s) = get_lit_str_simple(&m.lit) { 104 | if let syn::Data::Enum(_) = &input.data { 105 | content.set(&m.path, s.value()); 106 | } 107 | } 108 | } 109 | (AttrFrom::Serde, _) => {} 110 | (AttrFrom::Opg, Lit(lit)) => { 111 | if let Ok(s) = get_lit_str_simple(lit) { 112 | description.set(lit, s.value().clone()); 113 | } 114 | } 115 | (AttrFrom::Opg, Meta(NameValue(m))) if m.path == FORMAT => { 116 | if let Ok(s) = get_lit_str(cx, FORMAT, &m.lit) { 117 | format.set(&m.path, s.value().clone()) 118 | } 119 | } 120 | (AttrFrom::Opg, Meta(NameValue(m))) if m.path == EXAMPLE => { 121 | if let Ok(s) = get_lit_str(cx, EXAMPLE, &m.lit) { 122 | example.set(&m.path, lit_str_expr(s)); 123 | } 124 | } 125 | 126 | (AttrFrom::Opg, Meta(NameValue(m))) if m.path == EXAMPLE_WITH => { 127 | if let Ok(expr) = parse_lit_into_expr(cx, EXAMPLE_WITH, &m.lit) { 128 | example.set(&m.path, expr); 129 | } 130 | } 131 | (AttrFrom::Opg, Meta(Path(word))) if word == INLINE => inline.set_true(word), 132 | (AttrFrom::Opg, Meta(Path(word))) if word == NULLABLE => nullable.set_true(word), 133 | (AttrFrom::Opg, Meta(Path(word))) => { 134 | if let Ok(t) = ExplicitModelType::from_path(word) { 135 | model_type.set(word, t); 136 | } else { 137 | cx.error_spanned_by(word, "unknown attribute") 138 | } 139 | } 140 | (AttrFrom::Opg, Meta(meta_item)) => { 141 | let path = meta_item 142 | .path() 143 | .into_token_stream() 144 | .to_string() 145 | .replace(' ', ""); 146 | cx.error_spanned_by( 147 | meta_item.path(), 148 | format!("unknown opg variant attribute `{}`", path), 149 | ); 150 | } 151 | (AttrFrom::Repr, item) => { 152 | has_repr.set_true(item); 153 | } 154 | } 155 | } 156 | 157 | let tag_type = decide_tag(untagged, internal_tag, content); 158 | let explicit_model_type = model_type.at_most_one(); 159 | let model_type = decide_model_type(cx, input, &tag_type).unwrap_or(ModelType::Object); 160 | 161 | Self { 162 | name: Name::from_attrs(unraw(&input.ident), ser_name), 163 | rename_rule: rename_rule.get().unwrap_or(RenameRule::None), 164 | transparent: transparent.get(), 165 | tag_type, 166 | has_flatten: false, 167 | has_repr: has_repr.get(), 168 | description: description.get(), 169 | format: format.get(), 170 | example: example.get(), 171 | inline: inline.get(), 172 | nullable: nullable.get(), 173 | explicit_model_type, 174 | model_type, 175 | } 176 | } 177 | } 178 | 179 | pub struct Variant { 180 | pub name: Name, 181 | pub rename_rule: RenameRule, 182 | pub skip_serializing: bool, 183 | 184 | pub description: Option, 185 | pub format: Option, 186 | pub example: Option, 187 | pub inline: bool, 188 | pub explicit_model_type: Option, 189 | } 190 | 191 | impl Variant { 192 | pub fn from_ast(cx: &ParsingContext, input: &syn::Variant) -> Self { 193 | let mut ser_name = Attr::none(cx, RENAME); 194 | let mut rename_rule = Attr::none(cx, RENAME_ALL); 195 | let mut skip_serializing = BoolAttr::none(cx, SKIP_SERIALIZING); 196 | 197 | let mut description = Attr::none(cx, DESCRIPTION); 198 | let mut format = Attr::none(cx, FORMAT); 199 | let mut example = Attr::none(cx, EXAMPLE); 200 | let mut inline = BoolAttr::none(cx, INLINE); 201 | let mut model_type = OneOfFlagsAttr::none(cx); 202 | 203 | for (from, meta_item) in input 204 | .attrs 205 | .iter() 206 | .flat_map(|attr| get_meta_items(cx, attr)) 207 | .flat_map(|item| item.into_iter()) 208 | { 209 | match (from, &meta_item) { 210 | (AttrFrom::Serde, Meta(NameValue(m))) if m.path == RENAME => { 211 | if let Ok(s) = get_lit_str(cx, RENAME, &m.lit) { 212 | ser_name.set(&m.path, s.value()); 213 | } 214 | } 215 | (AttrFrom::Serde, Meta(List(m))) if m.path == RENAME => { 216 | if let Ok(ser) = get_renames(cx, &m.nested) { 217 | ser_name.set_opt(&m.path, ser.map(syn::LitStr::value)); 218 | } 219 | } 220 | (AttrFrom::Serde, Meta(NameValue(m))) if m.path == RENAME_ALL => { 221 | if let Ok(rule) = get_lit_str(cx, RENAME_ALL, &m.lit) 222 | .and_then(|s| RenameRule::from_str(&s.value())) 223 | { 224 | rename_rule.set(&m.path, rule) 225 | } 226 | } 227 | (AttrFrom::Serde, Meta(List(m))) if m.path == RENAME_ALL => { 228 | if let Ok(Some(rule)) = get_renames(cx, &m.nested) { 229 | if let Ok(rule) = RenameRule::from_str(&rule.value()) { 230 | rename_rule.set(&m.path, rule) 231 | } 232 | } 233 | } 234 | (AttrFrom::Serde, Meta(Path(word))) if word == SKIP || word == SKIP_SERIALIZING => { 235 | skip_serializing.set_true(word); 236 | } 237 | (AttrFrom::Serde, _) => {} 238 | (AttrFrom::Opg, Lit(lit)) => { 239 | if let Ok(s) = get_lit_str_simple(lit) { 240 | description.set(lit, s.value().clone()); 241 | } 242 | } 243 | (AttrFrom::Opg, Meta(NameValue(m))) if m.path == FORMAT => { 244 | if let Ok(s) = get_lit_str(cx, FORMAT, &m.lit) { 245 | format.set(&m.path, s.value().clone()) 246 | } 247 | } 248 | (AttrFrom::Opg, Meta(NameValue(m))) if m.path == EXAMPLE => { 249 | if let Ok(s) = get_lit_str(cx, EXAMPLE, &m.lit) { 250 | example.set(&m.path, lit_str_expr(s)) 251 | } 252 | } 253 | 254 | (AttrFrom::Opg, Meta(NameValue(m))) if m.path == EXAMPLE_WITH => { 255 | if let Ok(expr) = parse_lit_into_expr(cx, EXAMPLE_WITH, &m.lit) { 256 | example.set(&m.path, expr); 257 | } 258 | } 259 | (AttrFrom::Opg, Meta(Path(word))) if word == INLINE => inline.set_true(word), 260 | (AttrFrom::Opg, Meta(Path(word))) => { 261 | if let Ok(t) = ExplicitModelType::from_path(word) { 262 | model_type.set(word, t); 263 | } else { 264 | cx.error_spanned_by(word, "unknown attribute") 265 | } 266 | } 267 | (AttrFrom::Opg, Meta(meta_item)) => { 268 | let path = meta_item 269 | .path() 270 | .into_token_stream() 271 | .to_string() 272 | .replace(' ', ""); 273 | cx.error_spanned_by( 274 | meta_item.path(), 275 | format!("unknown opg variant attribute `{}`", path), 276 | ); 277 | } 278 | (AttrFrom::Repr, _) => {} 279 | } 280 | } 281 | 282 | Variant { 283 | name: Name::from_attrs(unraw(&input.ident), ser_name), 284 | rename_rule: rename_rule.get().unwrap_or(RenameRule::None), 285 | skip_serializing: skip_serializing.get(), 286 | description: description.get(), 287 | format: format.get(), 288 | example: example.get(), 289 | inline: inline.get(), 290 | explicit_model_type: model_type.at_most_one(), 291 | } 292 | } 293 | 294 | pub fn rename_by_rule(&mut self, rule: RenameRule) { 295 | self.name.rename_as_variant(rule); 296 | } 297 | } 298 | 299 | pub struct Field { 300 | pub name: Name, 301 | pub skip_serializing: bool, 302 | pub flatten: bool, 303 | pub transparent: bool, 304 | 305 | pub optional: bool, 306 | pub description: Option, 307 | pub format: Option, 308 | pub example: Option, 309 | pub inline: bool, 310 | pub nullable: bool, 311 | pub explicit_model_type: Option, 312 | } 313 | 314 | impl Field { 315 | pub fn from_ast(cx: &ParsingContext, index: usize, input: &syn::Field) -> Self { 316 | let mut ser_name = Attr::none(cx, RENAME); 317 | let mut skip_serializing = BoolAttr::none(cx, SKIP_SERIALIZING); 318 | let mut skip_serializing_if = Attr::none(cx, SKIP_SERIALIZING_IF); 319 | let mut flatten = BoolAttr::none(cx, FLATTEN); 320 | 321 | let mut optional = BoolAttr::none(cx, OPTIONAL); 322 | let mut description = Attr::none(cx, DESCRIPTION); 323 | let mut format = Attr::none(cx, FORMAT); 324 | let mut example = Attr::none(cx, EXAMPLE); 325 | let mut inline = BoolAttr::none(cx, INLINE); 326 | let mut nullable = BoolAttr::none(cx, NULLABLE); 327 | let mut model_type = OneOfFlagsAttr::none(cx); 328 | 329 | let ident = match &input.ident { 330 | Some(ident) => unraw(ident), 331 | None => index.to_string(), 332 | }; 333 | 334 | for (from, meta_item) in input 335 | .attrs 336 | .iter() 337 | .flat_map(|attr| get_meta_items(cx, attr)) 338 | .flat_map(|item| item.into_iter()) 339 | { 340 | match (from, &meta_item) { 341 | (AttrFrom::Serde, Meta(NameValue(m))) if m.path == RENAME => { 342 | if let Ok(s) = get_lit_str(cx, RENAME, &m.lit) { 343 | ser_name.set(&m.path, s.value()); 344 | } 345 | } 346 | (AttrFrom::Serde, Meta(List(m))) if m.path == RENAME => { 347 | if let Ok(ser) = get_renames(cx, &m.nested) { 348 | ser_name.set_opt(&m.path, ser.map(syn::LitStr::value)); 349 | } 350 | } 351 | (AttrFrom::Serde, Meta(Path(word))) if word == SKIP || word == SKIP_SERIALIZING => { 352 | skip_serializing.set_true(word); 353 | } 354 | (AttrFrom::Serde, Meta(NameValue(m))) if m.path == SKIP_SERIALIZING_IF => { 355 | if let Ok(path) = parse_lit_into_expr_path(cx, SKIP_SERIALIZING_IF, &m.lit) { 356 | skip_serializing_if.set(&m.path, path); 357 | } 358 | } 359 | (AttrFrom::Serde, Meta(Path(word))) if word == FLATTEN => { 360 | flatten.set_true(word); 361 | } 362 | (AttrFrom::Serde, _) => {} 363 | (AttrFrom::Opg, Lit(lit)) => { 364 | if let Ok(s) = get_lit_str_simple(lit) { 365 | description.set(lit, s.value().clone()); 366 | } 367 | } 368 | (AttrFrom::Opg, Meta(NameValue(m))) if m.path == FORMAT => { 369 | if let Ok(s) = get_lit_str(cx, FORMAT, &m.lit) { 370 | format.set(&m.path, s.value().clone()) 371 | } 372 | } 373 | (AttrFrom::Opg, Meta(NameValue(m))) if m.path == EXAMPLE => { 374 | if let Ok(s) = get_lit_str(cx, EXAMPLE, &m.lit) { 375 | example.set(&m.path, lit_str_expr(s)) 376 | } 377 | } 378 | (AttrFrom::Opg, Meta(NameValue(m))) if m.path == EXAMPLE_WITH => { 379 | if let Ok(expr) = parse_lit_into_expr(cx, EXAMPLE_WITH, &m.lit) { 380 | example.set(&m.path, expr); 381 | } 382 | } 383 | (AttrFrom::Opg, Meta(Path(word))) if word == OPTIONAL => optional.set_true(word), 384 | (AttrFrom::Opg, Meta(Path(word))) if word == INLINE => inline.set_true(word), 385 | (AttrFrom::Opg, Meta(Path(word))) if word == NULLABLE => nullable.set_true(word), 386 | (AttrFrom::Opg, Meta(Path(word))) => { 387 | if let Ok(t) = ExplicitModelType::from_path(word) { 388 | model_type.set(word, t); 389 | } else { 390 | cx.error_spanned_by(word, "unknown attribute") 391 | } 392 | } 393 | (AttrFrom::Opg, Meta(meta_item)) => { 394 | let path = meta_item 395 | .path() 396 | .into_token_stream() 397 | .to_string() 398 | .replace(' ', ""); 399 | cx.error_spanned_by( 400 | meta_item.path(), 401 | format!("unknown opg variant attribute `{}`", path), 402 | ); 403 | } 404 | (AttrFrom::Repr, _) => {} 405 | } 406 | } 407 | 408 | Self { 409 | name: Name::from_attrs(ident, ser_name), 410 | skip_serializing: skip_serializing.get(), 411 | flatten: flatten.get(), 412 | transparent: false, 413 | optional: skip_serializing_if.get().is_some() || optional.get(), 414 | description: description.get(), 415 | format: format.get(), 416 | example: example.get(), 417 | inline: inline.get(), 418 | nullable: nullable.get(), 419 | explicit_model_type: model_type.at_most_one(), 420 | } 421 | } 422 | 423 | pub fn rename_by_rule(&mut self, rule: RenameRule) { 424 | self.name.rename_as_field(rule); 425 | } 426 | } 427 | 428 | #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] 429 | pub enum ExplicitModelType { 430 | String, 431 | Integer, 432 | Number, 433 | Boolean, 434 | Any, 435 | } 436 | 437 | impl ExplicitModelType { 438 | fn from_path(p: &syn::Path) -> Result { 439 | // can't use match here ;( 440 | if p == STRING { 441 | Ok(ExplicitModelType::String) 442 | } else if p == NUMBER { 443 | Ok(ExplicitModelType::Number) 444 | } else if p == INTEGER { 445 | Ok(ExplicitModelType::Integer) 446 | } else if p == BOOLEAN { 447 | Ok(ExplicitModelType::Boolean) 448 | } else if p == ANY { 449 | Ok(ExplicitModelType::Any) 450 | } else { 451 | Err(()) 452 | } 453 | } 454 | } 455 | 456 | #[derive(Copy, Clone)] 457 | pub enum ModelType { 458 | NewType, 459 | Object, 460 | OneOf, 461 | Dictionary, 462 | } 463 | 464 | pub enum TagType { 465 | External, 466 | Internal { tag: String }, 467 | Adjacent { tag: String, content: String }, 468 | None, 469 | } 470 | 471 | fn decide_tag(untagged: BoolAttr, internal_tag: Attr, content: Attr) -> TagType { 472 | match ( 473 | untagged.0.get_with_tokens(), 474 | internal_tag.get_with_tokens(), 475 | content.get_with_tokens(), 476 | ) { 477 | (None, None, None) => TagType::External, 478 | (Some(_), None, None) => TagType::None, 479 | (None, Some((_, tag)), None) => TagType::Internal { tag }, 480 | (None, Some((_, tag)), Some((_, content))) => TagType::Adjacent { tag, content }, 481 | _ => TagType::External, // should be an error, but serde will handle it 482 | } 483 | } 484 | 485 | fn decide_model_type( 486 | cx: &ParsingContext, 487 | input: &syn::DeriveInput, 488 | tag_type: &TagType, 489 | ) -> Result { 490 | Ok(match &input.data { 491 | syn::Data::Enum(variants) => { 492 | if variants 493 | .variants 494 | .iter() 495 | .all(|field| matches!(field.fields, syn::Fields::Unit)) 496 | { 497 | return match tag_type { 498 | TagType::None => { 499 | cx.error_spanned_by( 500 | &input.ident, 501 | "unit enums are not supported for untagged", 502 | ); 503 | Err(()) 504 | } 505 | _ => Ok(ModelType::NewType), 506 | }; 507 | } 508 | 509 | match tag_type { 510 | TagType::None | TagType::Internal { .. } => ModelType::OneOf, 511 | TagType::External => ModelType::Dictionary, 512 | TagType::Adjacent { .. } => ModelType::Object, 513 | } 514 | } 515 | syn::Data::Struct(syn::DataStruct { fields, .. }) => match fields { 516 | syn::Fields::Named(_) => ModelType::Object, 517 | syn::Fields::Unnamed(_) => ModelType::NewType, 518 | syn::Fields::Unit => { 519 | cx.error_spanned_by(&input.ident, "unit structs are not supported"); 520 | return Err(()); 521 | } 522 | }, 523 | _ => { 524 | cx.error_spanned_by(&input.ident, "unions are not supported"); 525 | return Err(()); 526 | } 527 | }) 528 | } 529 | 530 | fn get_renames<'a>( 531 | cx: &ParsingContext, 532 | items: &'a Punctuated, 533 | ) -> Result, ()> { 534 | get_ser(cx, RENAME, items)?.at_most_one() 535 | } 536 | 537 | fn get_ser<'c, 'm>( 538 | cx: &'c ParsingContext, 539 | attr_name: Symbol, 540 | metas: &'m Punctuated, 541 | ) -> Result, ()> { 542 | let mut ser_meta = VecAttr::none(cx, attr_name); 543 | 544 | for meta in metas { 545 | match meta { 546 | Meta(NameValue(m)) if m.path == SERIALIZE => { 547 | if let Ok(value) = get_lit_str_simple(&m.lit) { 548 | ser_meta.insert(&m.path, value); 549 | } 550 | } 551 | Meta(NameValue(m)) if m.path == DESERIALIZE => {} 552 | _ => return Err(()), 553 | } 554 | } 555 | 556 | Ok(ser_meta) 557 | } 558 | 559 | fn lit_str_expr(lit: &syn::LitStr) -> syn::Expr { 560 | syn::Expr::Lit(syn::ExprLit { 561 | attrs: Vec::new(), 562 | lit: syn::Lit::Str(lit.clone()), 563 | }) 564 | } 565 | 566 | fn parse_lit_into_expr( 567 | cx: &ParsingContext, 568 | attr_name: Symbol, 569 | lit: &syn::Lit, 570 | ) -> Result { 571 | let string = get_lit_str(cx, attr_name, lit)?; 572 | parse_lit_str(string).map_err(|_| { 573 | cx.error_spanned_by(lit, format!("failed to parse expr: {:?}", string.value())) 574 | }) 575 | } 576 | 577 | fn parse_lit_into_expr_path( 578 | cx: &ParsingContext, 579 | attr_name: Symbol, 580 | lit: &syn::Lit, 581 | ) -> Result { 582 | let string = get_lit_str(cx, attr_name, lit)?; 583 | parse_lit_str(string).map_err(|_| { 584 | cx.error_spanned_by( 585 | lit, 586 | format!("failed to parse path expr: {:?}", string.value()), 587 | ) 588 | }) 589 | } 590 | 591 | fn parse_lit_str(s: &syn::LitStr) -> syn::parse::Result 592 | where 593 | T: syn::parse::Parse, 594 | { 595 | let tokens = spanned_tokens(s)?; 596 | syn::parse2(tokens) 597 | } 598 | 599 | fn spanned_tokens(s: &syn::LitStr) -> syn::parse::Result { 600 | let stream = syn::parse_str(&s.value())?; 601 | Ok(respan_token_stream(stream, s.span())) 602 | } 603 | 604 | fn respan_token_stream(stream: TokenStream, span: Span) -> TokenStream { 605 | stream 606 | .into_iter() 607 | .map(|token| respan_token_tree(token, span)) 608 | .collect() 609 | } 610 | 611 | fn respan_token_tree(mut token: TokenTree, span: Span) -> TokenTree { 612 | if let TokenTree::Group(g) = &mut token { 613 | *g = Group::new(g.delimiter(), respan_token_stream(g.stream(), span)); 614 | } 615 | token.set_span(span); 616 | token 617 | } 618 | 619 | fn get_lit_str_simple(lit: &syn::Lit) -> Result<&syn::LitStr, ()> { 620 | if let syn::Lit::Str(lit) = lit { 621 | Ok(lit) 622 | } else { 623 | Err(()) 624 | } 625 | } 626 | 627 | fn get_lit_str<'a>( 628 | cx: &ParsingContext, 629 | attr_name: Symbol, 630 | lit: &'a syn::Lit, 631 | ) -> Result<&'a syn::LitStr, ()> { 632 | get_lit_str_special(cx, attr_name, attr_name, lit) 633 | } 634 | 635 | fn get_lit_str_special<'a>( 636 | cx: &ParsingContext, 637 | attr_name: Symbol, 638 | path_name: Symbol, 639 | lit: &'a syn::Lit, 640 | ) -> Result<&'a syn::LitStr, ()> { 641 | if let syn::Lit::Str(lit) = lit { 642 | Ok(lit) 643 | } else { 644 | cx.error_spanned_by( 645 | lit, 646 | format!( 647 | "expected {} attribute to be a string: `{} = \"...\"`", 648 | attr_name, path_name 649 | ), 650 | ); 651 | Err(()) 652 | } 653 | } 654 | 655 | fn get_meta_items( 656 | cx: &ParsingContext, 657 | attr: &syn::Attribute, 658 | ) -> Result, ()> { 659 | let attr_from = if attr.path == OPG { 660 | AttrFrom::Opg 661 | } else if attr.path == SERDE { 662 | AttrFrom::Serde 663 | } else if attr.path == REPR { 664 | AttrFrom::Repr 665 | } else { 666 | return Ok(Vec::new()); 667 | }; 668 | 669 | match attr.parse_meta() { 670 | Ok(List(meta)) => Ok(meta 671 | .nested 672 | .into_iter() 673 | .map(|meta| (attr_from, meta)) 674 | .collect()), 675 | Ok(other) => { 676 | cx.error_spanned_by(other, format!("expected #[{}(...)]", attr_from)); 677 | Err(()) 678 | } 679 | Err(err) => { 680 | cx.syn_error(err); 681 | Err(()) 682 | } 683 | } 684 | } 685 | 686 | #[derive(Copy, Clone)] 687 | enum AttrFrom { 688 | Serde, 689 | Opg, 690 | Repr, 691 | } 692 | 693 | impl std::fmt::Display for AttrFrom { 694 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 695 | match self { 696 | AttrFrom::Serde => f.write_str(SERDE.inner()), 697 | AttrFrom::Opg => f.write_str(OPG.inner()), 698 | AttrFrom::Repr => f.write_str(REPR.inner()), 699 | } 700 | } 701 | } 702 | 703 | struct Attr<'c, T> { 704 | cx: &'c ParsingContext, 705 | name: Symbol, 706 | tokens: TokenStream, 707 | value: Option, 708 | } 709 | 710 | impl<'c, T> Attr<'c, T> { 711 | fn none(cx: &'c ParsingContext, name: Symbol) -> Self { 712 | Attr { 713 | cx, 714 | name, 715 | tokens: TokenStream::new(), 716 | value: None, 717 | } 718 | } 719 | 720 | fn set(&mut self, object: A, value: T) { 721 | let tokens = object.into_token_stream(); 722 | 723 | if self.value.is_some() { 724 | self.cx 725 | .error_spanned_by(tokens, format!("duplicate opg attribute `{}`", self.name)); 726 | } else { 727 | self.tokens = tokens; 728 | self.value = Some(value); 729 | } 730 | } 731 | 732 | #[allow(dead_code)] 733 | fn set_opt(&mut self, object: A, value: Option) { 734 | if let Some(value) = value { 735 | self.set(object, value); 736 | } 737 | } 738 | 739 | #[allow(dead_code)] 740 | fn set_if_none(&mut self, value: T) { 741 | if self.value.is_none() { 742 | self.value = Some(value); 743 | } 744 | } 745 | 746 | fn get(self) -> Option { 747 | self.value 748 | } 749 | 750 | fn get_with_tokens(self) -> Option<(TokenStream, T)> { 751 | match self.value { 752 | Some(value) => Some((self.tokens, value)), 753 | None => None, 754 | } 755 | } 756 | } 757 | 758 | struct BoolAttr<'c>(Attr<'c, ()>); 759 | 760 | impl<'c> BoolAttr<'c> { 761 | fn none(cx: &'c ParsingContext, name: Symbol) -> Self { 762 | BoolAttr(Attr::none(cx, name)) 763 | } 764 | 765 | fn set_true(&mut self, object: A) { 766 | self.0.set(object, ()); 767 | } 768 | 769 | fn get(&self) -> bool { 770 | self.0.value.is_some() 771 | } 772 | } 773 | 774 | struct OneOfFlagsAttr<'c, T> { 775 | cx: &'c ParsingContext, 776 | first_dup_tokens: TokenStream, 777 | values: Vec, 778 | } 779 | 780 | #[allow(dead_code)] 781 | impl<'c, T> OneOfFlagsAttr<'c, T> { 782 | fn none(cx: &'c ParsingContext) -> Self { 783 | OneOfFlagsAttr { 784 | cx, 785 | first_dup_tokens: TokenStream::new(), 786 | values: Vec::new(), 787 | } 788 | } 789 | 790 | fn set(&mut self, object: A, value: T) { 791 | if self.values.len() == 1 { 792 | self.first_dup_tokens = object.into_token_stream(); 793 | } 794 | self.values.push(value) 795 | } 796 | 797 | fn at_most_one(mut self) -> Option { 798 | if self.values.len() > 1 { 799 | let dup_token = self.first_dup_tokens; 800 | self.cx 801 | .error_spanned_by(dup_token, "duplicate opg attribute"); 802 | } 803 | 804 | self.values.pop() 805 | } 806 | 807 | fn get(self) -> Vec { 808 | self.values 809 | } 810 | } 811 | 812 | struct VecAttr<'c, T> { 813 | cx: &'c ParsingContext, 814 | name: Symbol, 815 | first_dup_tokens: TokenStream, 816 | values: Vec, 817 | } 818 | 819 | #[allow(dead_code)] 820 | impl<'c, T> VecAttr<'c, T> { 821 | fn none(cx: &'c ParsingContext, name: Symbol) -> Self { 822 | VecAttr { 823 | cx, 824 | name, 825 | first_dup_tokens: TokenStream::new(), 826 | values: Vec::new(), 827 | } 828 | } 829 | 830 | fn insert(&mut self, object: A, value: T) { 831 | if self.values.len() == 1 { 832 | self.first_dup_tokens = object.into_token_stream(); 833 | } 834 | self.values.push(value) 835 | } 836 | 837 | fn at_most_one(mut self) -> Result, ()> { 838 | if self.values.len() > 1 { 839 | let dup_token = self.first_dup_tokens; 840 | self.cx.error_spanned_by( 841 | dup_token, 842 | format!("duplicate opg attribute `{}`", self.name), 843 | ); 844 | Err(()) 845 | } else { 846 | Ok(self.values.pop()) 847 | } 848 | } 849 | 850 | fn get(self) -> Vec { 851 | self.values 852 | } 853 | } 854 | 855 | pub struct Name { 856 | source_name: String, 857 | serialized_name: String, 858 | renamed: bool, 859 | } 860 | 861 | #[allow(dead_code)] 862 | impl Name { 863 | fn from_attrs(source_name: String, serialized_name: Attr) -> Self { 864 | let serialized_name = serialized_name.get(); 865 | let renamed = serialized_name.is_some(); 866 | 867 | Self { 868 | source_name: source_name.clone(), 869 | serialized_name: serialized_name.unwrap_or_else(|| source_name.clone()), 870 | renamed, 871 | } 872 | } 873 | 874 | pub fn rename_as_variant(&mut self, rename_rule: RenameRule) { 875 | if !self.renamed { 876 | self.serialized_name = rename_rule.apply_to_variant(&self.source_name); 877 | } 878 | } 879 | 880 | pub fn rename_as_field(&mut self, rename_rule: RenameRule) { 881 | if !self.renamed { 882 | self.serialized_name = rename_rule.apply_to_field(&self.source_name); 883 | } 884 | } 885 | 886 | pub fn raw(&self) -> String { 887 | self.source_name.clone() 888 | } 889 | 890 | pub fn serialized(&self) -> String { 891 | self.serialized_name.clone() 892 | } 893 | } 894 | -------------------------------------------------------------------------------- /opg_derive/src/opg.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Span; 2 | use quote::quote; 3 | 4 | use crate::ast::*; 5 | use crate::attr::{self, ExplicitModelType, ModelType, TagType}; 6 | use crate::bound; 7 | use crate::dummy; 8 | use crate::parsing_context::*; 9 | 10 | pub fn impl_derive_opg_model( 11 | input: syn::DeriveInput, 12 | ) -> Result> { 13 | let cx = ParsingContext::new(); 14 | let container = match Container::from_ast(&cx, &input) { 15 | Some(container) => container, 16 | None => return Err(cx.check().unwrap_err()), 17 | }; 18 | cx.check()?; 19 | 20 | let ident = &container.ident; 21 | let generics = build_generics(&container); 22 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 23 | 24 | let body = serialize_body(&container); 25 | 26 | let result = quote! { 27 | impl #impl_generics _opg::OpgModel for #ident #ty_generics #where_clause { 28 | #body 29 | } 30 | }; 31 | 32 | // println!("{}", result.to_string()); 33 | 34 | Ok(dummy::wrap_in_const("OPG_MODEL", ident, result)) 35 | } 36 | 37 | fn build_generics(cont: &Container) -> syn::Generics { 38 | let generics = bound::without_default(cont.generics); 39 | 40 | bound::with_bound( 41 | cont, 42 | &generics, 43 | |field, variant| { 44 | !field.skip_serializing && variant.map_or(true, |variant| !variant.skip_serializing) 45 | }, 46 | &syn::parse_quote!(_opg::OpgModel), 47 | ) 48 | } 49 | 50 | fn serialize_body(container: &Container) -> proc_macro2::TokenStream { 51 | match &container.data { 52 | Data::Enum(variants) => serialize_enum(container, variants), 53 | Data::Struct(StructStyle::Struct, fields) => serialize_struct(container, fields), 54 | Data::Struct(StructStyle::Tuple, fields) => serialize_tuple_struct(container, fields), 55 | Data::Struct(StructStyle::NewType, fields) => { 56 | serialize_newtype_struct(container, &fields[0]) 57 | } 58 | _ => unimplemented!(), 59 | } 60 | } 61 | 62 | fn serialize_enum(container: &Container, variants: &[Variant]) -> proc_macro2::TokenStream { 63 | match (container.attrs.model_type, &container.attrs.tag_type) { 64 | (ModelType::NewType, _) => serialize_newtype_enum(container, variants), 65 | (ModelType::Object, TagType::Adjacent { tag, content }) => { 66 | serialize_adjacent_tagged_enum(container, variants, tag, content) 67 | } 68 | (ModelType::Dictionary, _) => serialize_external_tagged_enum(container, variants), 69 | (ModelType::OneOf, TagType::None) => serialize_untagged_enum(container, variants), 70 | (ModelType::OneOf, TagType::Internal { tag }) => { 71 | serialize_internal_tagged_enum(container, variants, tag) 72 | } 73 | _ => unreachable!(), 74 | } 75 | } 76 | 77 | fn serialize_newtype_enum(container: &Container, variants: &[Variant]) -> proc_macro2::TokenStream { 78 | let description = option_string(container.attrs.description.as_deref()); 79 | 80 | let body = if container.attrs.has_repr { 81 | let variants = variants 82 | .iter() 83 | .filter(|variant| !variant.attrs.skip_serializing) 84 | .filter_map(|variant| { 85 | variant 86 | .original 87 | .discriminant 88 | .as_ref() 89 | .map(|(_, discriminant)| (variant.attrs.name.serialized(), discriminant)) 90 | }) 91 | .map(|(name, discriminant)| { 92 | let description = format!("{} variant", name); 93 | let example = quote::ToTokens::to_token_stream(discriminant).to_string(); 94 | 95 | quote! { 96 | _opg::Model { 97 | description: Some(#description.to_owned()), 98 | data: _opg::ModelData::Single(_opg::ModelType { 99 | nullable: false, 100 | type_description: _opg::ModelTypeDescription::Integer(_opg::ModelSimple { 101 | format: None, 102 | example: Some(#example.to_owned()), 103 | }) 104 | }), 105 | } 106 | } 107 | }) 108 | .map(inline_reference) 109 | .collect::>(); 110 | 111 | quote! { 112 | _opg::Model { 113 | description: #description, 114 | data: _opg::ModelData::OneOf(_opg::ModelOneOf { 115 | one_of: vec![#(#variants),*], 116 | }) 117 | } 118 | } 119 | } else { 120 | let variants = variants 121 | .iter() 122 | .filter(|variant| !variant.attrs.skip_serializing) 123 | .map(|variant| variant.attrs.name.serialized()) 124 | .collect::>(); 125 | 126 | let example = option_string(variants.first().map(|x| x.as_str())); 127 | 128 | quote! { 129 | _opg::Model { 130 | description: #description, 131 | data: _opg::ModelData::Single(_opg::ModelType { 132 | nullable: false, 133 | type_description: _opg::ModelTypeDescription::String(_opg::ModelString { 134 | variants: Some(vec![#(#variants.to_owned()),*]), 135 | data: _opg::ModelSimple { 136 | format: None, 137 | example: #example, 138 | } 139 | }) 140 | }) 141 | } 142 | } 143 | }; 144 | 145 | implement_type(&container.ident, body, container.attrs.inline) 146 | } 147 | 148 | fn serialize_untagged_enum( 149 | container: &Container, 150 | variants: &[Variant], 151 | ) -> proc_macro2::TokenStream { 152 | let description = option_string(container.attrs.description.as_deref()); 153 | 154 | let one_of = variants 155 | .iter() 156 | .filter(|variant| !variant.attrs.skip_serializing) 157 | .map(|variant| match &variant.style { 158 | StructStyle::NewType => { 159 | let field = &variant.fields[0]; 160 | let context_params = ContextParams::from(&field.attrs).or(&variant.attrs); 161 | 162 | field_model_reference(context_params, field, variant.attrs.inline) 163 | } 164 | StructStyle::Struct => inline_reference(object_model( 165 | false, 166 | &variant.attrs.description, 167 | &variant.fields, 168 | |field| variant.attrs.inline || field.attrs.inline, 169 | )), 170 | _ => unreachable!(), 171 | }) 172 | .collect::>(); 173 | 174 | let body = quote! { 175 | _opg::Model { 176 | description: #description, 177 | data: _opg::ModelData::OneOf(_opg::ModelOneOf { 178 | one_of: vec![#(#one_of),*], 179 | }) 180 | } 181 | }; 182 | 183 | implement_type(&container.ident, body, container.attrs.inline) 184 | } 185 | 186 | fn serialize_adjacent_tagged_enum( 187 | container: &Container, 188 | variants: &[Variant], 189 | tag: &str, 190 | content: &str, 191 | ) -> proc_macro2::TokenStream { 192 | let description = option_string(container.attrs.description.as_deref()); 193 | let nullable = container.attrs.nullable; 194 | 195 | let (variants, one_of) = variants 196 | .iter() 197 | .filter(|variant| !variant.attrs.skip_serializing) 198 | .fold( 199 | (Vec::new(), Vec::new()), 200 | |(mut variants, mut one_of), variant| { 201 | let variant_name = variant.attrs.name.serialized(); 202 | 203 | let type_description = match &variant.style { 204 | StructStyle::NewType => { 205 | let field = &variant.fields[0]; 206 | let context_params = ContextParams::from(&field.attrs).or(&variant.attrs); 207 | 208 | field_model_reference(context_params, field, variant.attrs.inline) 209 | } 210 | StructStyle::Tuple => inline_reference(tuple_model( 211 | false, 212 | &variant.attrs.description, 213 | &variant.fields, 214 | |field| variant.attrs.inline || field.attrs.inline, 215 | )), 216 | StructStyle::Struct => inline_reference(tuple_model( 217 | false, 218 | &variant.attrs.description, 219 | &variant.fields, 220 | |field| field.attrs.inline || variant.attrs.inline, 221 | )), 222 | _ => unreachable!(), 223 | }; 224 | 225 | variants.push(variant_name); 226 | one_of.push(type_description); 227 | (variants, one_of) 228 | }, 229 | ); 230 | 231 | let type_example = option_string(variants.first().map(|x| x.as_str())); 232 | let type_name_stringified = container.ident.to_string(); 233 | 234 | let struct_type_description = quote! { 235 | { 236 | let mut properties = std::collections::BTreeMap::new(); 237 | let mut required = Vec::new(); 238 | 239 | properties.insert(#tag.to_owned(), _opg::ModelReference::Inline( 240 | _opg::Model { 241 | description: Some(format!("{} type variant", #type_name_stringified)), 242 | data: _opg::ModelData::Single(_opg::ModelType { 243 | nullable: false, 244 | type_description: _opg::ModelTypeDescription::String(_opg::ModelString { 245 | variants: Some(vec![#(#variants.to_owned()),*]), 246 | data: _opg::ModelSimple { 247 | format: None, 248 | example: #type_example, 249 | } 250 | }) 251 | }) 252 | } 253 | )); 254 | required.push(#tag.to_owned()); 255 | 256 | properties.insert(#content.to_owned(), _opg::ModelReference::Inline( 257 | _opg::Model { 258 | description: #description, 259 | data: _opg::ModelData::OneOf(_opg::ModelOneOf { 260 | one_of: vec![#(#one_of),*], 261 | }) 262 | } 263 | )); 264 | required.push(#content.to_owned()); 265 | 266 | _opg::ModelTypeDescription::Object( 267 | _opg::ModelObject { 268 | properties, 269 | required, 270 | ..Default::default() 271 | } 272 | ) 273 | } 274 | }; 275 | 276 | let body = quote! { 277 | _opg::Model { 278 | description: #description, 279 | data: _opg::ModelData::Single(_opg::ModelType { 280 | nullable: #nullable, 281 | type_description: #struct_type_description 282 | }) 283 | } 284 | }; 285 | 286 | implement_type(&container.ident, body, container.attrs.inline) 287 | } 288 | 289 | fn serialize_external_tagged_enum( 290 | container: &Container, 291 | variants: &[Variant], 292 | ) -> proc_macro2::TokenStream { 293 | let description = option_string(container.attrs.description.as_deref()); 294 | let nullable = container.attrs.nullable; 295 | 296 | let (_, one_of) = variants 297 | .iter() 298 | .filter(|variant| !variant.attrs.skip_serializing) 299 | .fold( 300 | (Vec::new(), Vec::new()), 301 | |(mut variants, mut one_of), variant| { 302 | let variant_name = variant.attrs.name.serialized(); 303 | 304 | let type_description = match &variant.style { 305 | StructStyle::Unit => { 306 | let description = option_string(variant.attrs.description.as_deref()); 307 | 308 | quote! { 309 | _opg::ModelReference::Inline( 310 | _opg::Model { 311 | description: #description, 312 | data: _opg::ModelData::Single(_opg::ModelType { 313 | nullable: false, 314 | type_description: _opg::ModelTypeDescription::String(_opg::ModelString { 315 | variants: Some(vec![#variant_name.to_owned()]), 316 | data: _opg::ModelSimple { 317 | format: None, 318 | example: Some(#variant_name.to_owned()), 319 | } 320 | }) 321 | }) 322 | } 323 | ) 324 | } 325 | } 326 | StructStyle::NewType => { 327 | let field = &variant.fields[0]; 328 | let context_params = ContextParams::from(&field.attrs).or(&variant.attrs); 329 | 330 | field_model_reference( 331 | context_params, 332 | field, 333 | variant.attrs.inline, 334 | ) 335 | }, 336 | StructStyle::Tuple => { 337 | inline_reference(tuple_model(false, 338 | &variant.attrs.description, &variant.fields, |field| { 339 | variant.attrs.inline || field.attrs.inline 340 | })) 341 | } 342 | StructStyle::Struct => inline_reference(object_model(false, 343 | &variant.attrs.description, 344 | &variant.fields, 345 | |field| { 346 | variant.attrs.inline || field.attrs.inline 347 | }, 348 | )), 349 | }; 350 | 351 | variants.push(variant_name); 352 | one_of.push(type_description); 353 | (variants, one_of) 354 | }, 355 | ); 356 | 357 | let body = quote! { 358 | _opg::Model { 359 | description: #description, 360 | data: _opg::ModelData::Single(_opg::ModelType { 361 | nullable: #nullable, 362 | type_description: _opg::ModelTypeDescription::Object( 363 | _opg::ModelObject { 364 | additional_properties: Some(Box::new(_opg::ModelReference::Inline( 365 | _opg::Model { 366 | description: #description, 367 | data: _opg::ModelData::OneOf(_opg::ModelOneOf { 368 | one_of: vec![#(#one_of),*], 369 | }) 370 | } 371 | ))), 372 | ..Default::default() 373 | } 374 | ) 375 | }) 376 | } 377 | }; 378 | 379 | implement_type(&container.ident, body, container.attrs.inline) 380 | } 381 | 382 | fn serialize_internal_tagged_enum( 383 | container: &Container, 384 | variants: &[Variant], 385 | tag: &str, 386 | ) -> proc_macro2::TokenStream { 387 | let description = option_string(container.attrs.description.as_deref()); 388 | let nullable = container.attrs.nullable; 389 | 390 | let type_name_stringified = container.ident.to_string(); 391 | 392 | let one_of = variants 393 | .iter() 394 | .filter(|variant| !variant.attrs.skip_serializing) 395 | .map(|variant| { 396 | let variant_name = variant.attrs.name.serialized(); 397 | 398 | let model = match &variant.style { 399 | StructStyle::Unit => { 400 | object_model(false, &variant.attrs.description, &[], |_| false) 401 | } 402 | StructStyle::NewType => { 403 | let field = &variant.fields[0]; 404 | let type_name = &field.original.ty; 405 | let context_params = ContextParams::from(&field.attrs).or(&variant.attrs).tokenize(); 406 | 407 | quote! { 408 | <#type_name as _opg::OpgModel>::get_schema_with_params(cx, &#context_params) 409 | } 410 | } 411 | StructStyle::Struct => { 412 | object_model(false, &variant.attrs.description, &variant.fields, |field| { 413 | variant.attrs.inline || field.attrs.inline 414 | }) 415 | } 416 | _ => unreachable!(), 417 | }; 418 | 419 | quote! { 420 | { 421 | let mut model = #model; 422 | 423 | let additional_object = { 424 | let mut properties = std::collections::BTreeMap::new(); 425 | 426 | properties.insert(#tag.to_owned(), _opg::ModelReference::Inline( 427 | _opg::Model { 428 | description: Some(format!("{} type variant", #type_name_stringified)), 429 | data: _opg::ModelData::Single(_opg::ModelType { 430 | nullable: false, 431 | type_description: _opg::ModelTypeDescription::String(_opg::ModelString { 432 | variants: Some(vec![#variant_name.to_owned()]), 433 | data: _opg::ModelSimple { 434 | format: None, 435 | example: Some(#variant_name.to_owned()), 436 | } 437 | }) 438 | }) 439 | } 440 | )); 441 | 442 | _opg::ModelTypeDescription::Object(_opg::ModelObject { 443 | properties, 444 | required: vec![#tag.to_owned()], 445 | ..Default::default() 446 | }) 447 | }; 448 | 449 | let _ = model.try_merge(_opg::Model { 450 | description: None, 451 | data: _opg::ModelData::Single(_opg::ModelType { 452 | nullable: #nullable, 453 | type_description: additional_object 454 | }) 455 | }); 456 | 457 | _opg::ModelReference::Inline(model) 458 | } 459 | } 460 | }) 461 | .collect::>(); 462 | 463 | let body = quote! { 464 | _opg::Model { 465 | description: #description, 466 | data: _opg::ModelData::OneOf(_opg::ModelOneOf { 467 | one_of: vec![#(#one_of),*], 468 | }) 469 | } 470 | }; 471 | 472 | implement_type(&container.ident, body, container.attrs.inline) 473 | } 474 | 475 | fn serialize_struct(container: &Container, fields: &[Field]) -> proc_macro2::TokenStream { 476 | let description = option_string(container.attrs.description.as_deref()); 477 | let nullable = container.attrs.nullable; 478 | 479 | let object_type_description = object_type_description(fields, |field| field.attrs.inline); 480 | 481 | let body = quote! { 482 | _opg::Model { 483 | description: #description, 484 | data: _opg::ModelData::Single(_opg::ModelType { 485 | nullable: #nullable, 486 | type_description: #object_type_description 487 | }) 488 | } 489 | }; 490 | 491 | implement_type(&container.ident, body, container.attrs.inline) 492 | } 493 | 494 | fn serialize_tuple_struct(container: &Container, fields: &[Field]) -> proc_macro2::TokenStream { 495 | let description = option_string(container.attrs.description.as_deref()); 496 | let nullable = container.attrs.nullable; 497 | 498 | let tuple_type_description = tuple_type_description(fields, |field| field.attrs.inline); 499 | 500 | let body = quote! { 501 | _opg::Model { 502 | description: #description, 503 | data: _opg::ModelData::Single(_opg::ModelType { 504 | nullable: #nullable, 505 | type_description: #tuple_type_description 506 | }) 507 | } 508 | }; 509 | 510 | implement_type(&container.ident, body, container.attrs.inline) 511 | } 512 | 513 | fn serialize_newtype_struct(container: &Container, field: &Field) -> proc_macro2::TokenStream { 514 | let type_name = &field.original.ty; 515 | 516 | let context_params = ContextParams::from(&field.attrs).or(&container.attrs); 517 | 518 | let body = match container.attrs.explicit_model_type { 519 | Some(explicit_model_type) if explicit_model_type != ExplicitModelType::Any => { 520 | newtype_model( 521 | container.attrs.nullable, 522 | context_params, 523 | explicit_model_type, 524 | ) 525 | } 526 | Some(_) => unreachable!(), 527 | None => { 528 | let context_params = context_params.tokenize(); 529 | 530 | quote! { 531 | <#type_name as _opg::OpgModel>::get_schema_with_params(cx, &#context_params) 532 | } 533 | } 534 | }; 535 | 536 | implement_type(&container.ident, body, container.attrs.inline) 537 | } 538 | 539 | fn tuple_model

( 540 | nullable: bool, 541 | description: &Option, 542 | fields: &[Field], 543 | inline_predicate: P, 544 | ) -> proc_macro2::TokenStream 545 | where 546 | P: Fn(&Field) -> bool, 547 | { 548 | let description = option_string(description.as_deref()); 549 | let tuple_type_description = tuple_type_description(fields, inline_predicate); 550 | 551 | quote! { 552 | _opg::Model { 553 | description: #description, 554 | data: _opg::ModelData::Single(_opg::ModelType { 555 | nullable: #nullable, 556 | type_description: #tuple_type_description 557 | }) 558 | } 559 | } 560 | } 561 | 562 | fn object_model

( 563 | nullable: bool, 564 | description: &Option, 565 | fields: &[Field], 566 | inline_predicate: P, 567 | ) -> proc_macro2::TokenStream 568 | where 569 | P: Fn(&Field) -> bool, 570 | { 571 | let description = option_string(description.as_deref()); 572 | let object_type_description = object_type_description(fields, inline_predicate); 573 | 574 | quote! { 575 | _opg::Model { 576 | description: #description, 577 | data: _opg::ModelData::Single(_opg::ModelType { 578 | nullable: #nullable, 579 | type_description: #object_type_description 580 | }) 581 | } 582 | } 583 | } 584 | 585 | fn inline_reference(model: proc_macro2::TokenStream) -> proc_macro2::TokenStream { 586 | quote! { 587 | _opg::ModelReference::Inline(#model) 588 | } 589 | } 590 | 591 | fn tuple_type_description

(fields: &[Field], inline_predicate: P) -> proc_macro2::TokenStream 592 | where 593 | P: Fn(&Field) -> bool, 594 | { 595 | let data = fields 596 | .iter() 597 | .map(|field| { 598 | field_model_reference( 599 | ContextParams::from(&field.attrs), 600 | field, 601 | inline_predicate(field), 602 | ) 603 | }) 604 | .collect::>(); 605 | 606 | let one_of = quote! { 607 | _opg::Model { 608 | description: None, 609 | data: _opg::ModelData::OneOf(_opg::ModelOneOf { 610 | one_of: vec![#(#data),*], 611 | }) 612 | } 613 | }; 614 | 615 | quote! { 616 | _opg::ModelTypeDescription::Array( 617 | _opg::ModelArray { 618 | items: Box::new(_opg::ModelReference::Inline(#one_of)), 619 | } 620 | ) 621 | } 622 | } 623 | 624 | fn object_type_description

(fields: &[Field], inline_predicate: P) -> proc_macro2::TokenStream 625 | where 626 | P: Fn(&Field) -> bool, 627 | { 628 | let data = fields 629 | .iter() 630 | .filter(|field| !field.attrs.skip_serializing) 631 | .map(|field| { 632 | let field_model_reference = field_model_reference( 633 | ContextParams::from(&field.attrs), 634 | field, 635 | inline_predicate(field), 636 | ); 637 | 638 | let property_name = syn::LitStr::new(&field.attrs.name.serialized(), Span::call_site()); 639 | 640 | let push_required = if !field.attrs.optional { 641 | quote!( required.push(#property_name.to_owned()) ) 642 | } else { 643 | quote!() 644 | }; 645 | 646 | quote! { 647 | properties.insert(#property_name.to_owned(), #field_model_reference); 648 | #push_required; 649 | } 650 | }) 651 | .collect::>(); 652 | 653 | quote! { 654 | { 655 | let mut properties = std::collections::BTreeMap::new(); 656 | let mut required = Vec::new(); 657 | 658 | #(#data)* 659 | 660 | _opg::ModelTypeDescription::Object( 661 | _opg::ModelObject { 662 | properties, 663 | required, 664 | ..Default::default() 665 | } 666 | ) 667 | } 668 | } 669 | } 670 | 671 | fn field_model_reference<'a>( 672 | context_params: ContextParams<'a>, 673 | field: &'a Field, 674 | inline: bool, 675 | ) -> proc_macro2::TokenStream { 676 | let type_name = &field.original.ty; 677 | 678 | match field.attrs.explicit_model_type { 679 | Some(explicit_model_type) if explicit_model_type != ExplicitModelType::Any => { 680 | let model = newtype_model(field.attrs.nullable, context_params, explicit_model_type); 681 | 682 | quote! { 683 | _opg::ModelReference::Inline(#model) 684 | } 685 | } 686 | Some(_) => { 687 | quote! { 688 | _opg::ModelReference::Any 689 | } 690 | } 691 | _ => { 692 | let context_params = context_params.tokenize(); 693 | 694 | quote! { 695 | cx.mention_schema::<#type_name>(#inline, &#context_params) 696 | } 697 | } 698 | } 699 | } 700 | 701 | fn newtype_model( 702 | nullable: bool, 703 | context_params: ContextParams, 704 | explicit_model_type: ExplicitModelType, 705 | ) -> proc_macro2::TokenStream { 706 | let (description, format, example) = context_params.split(); 707 | 708 | let data = match explicit_model_type { 709 | ExplicitModelType::String => quote! { 710 | _opg::ModelTypeDescription::String(_opg::ModelString { 711 | variants: None, 712 | data: _opg::ModelSimple { 713 | format: #format, 714 | example: #example, 715 | } 716 | }) 717 | }, 718 | ExplicitModelType::Integer => quote! { 719 | _opg::ModelTypeDescription::Integer(_opg::ModelSimple { 720 | format: #format, 721 | example: #example, 722 | }) 723 | }, 724 | ExplicitModelType::Number => quote! { 725 | _opg::ModelTypeDescription::Number(_opg::ModelSimple { 726 | format: #format, 727 | example: #example, 728 | }) 729 | }, 730 | ExplicitModelType::Boolean => quote! { 731 | _opg::ModelTypeDescription::Boolean 732 | }, 733 | ExplicitModelType::Any => unreachable!(), 734 | }; 735 | 736 | quote! { 737 | _opg::Model { 738 | description: #description, 739 | data: _opg::ModelData::Single(_opg::ModelType { 740 | nullable: #nullable, 741 | type_description: #data 742 | }), 743 | } 744 | } 745 | } 746 | 747 | fn option_to_string_expr(data: Option<&syn::Expr>) -> proc_macro2::TokenStream { 748 | match data { 749 | Some(data) => { 750 | quote! { Some((#data).to_string()) } 751 | } 752 | None => quote! { None }, 753 | } 754 | } 755 | 756 | fn option_string(data: Option<&str>) -> proc_macro2::TokenStream { 757 | match data { 758 | Some(data) => { 759 | quote! { Some(#data.to_owned()) } 760 | } 761 | None => quote! { None }, 762 | } 763 | } 764 | 765 | fn option_bool(data: Option) -> proc_macro2::TokenStream { 766 | match data { 767 | Some(data) => { 768 | quote! { Some(#data) } 769 | } 770 | None => quote! { None }, 771 | } 772 | } 773 | 774 | fn implement_type( 775 | type_name: &syn::Ident, 776 | body: proc_macro2::TokenStream, 777 | inline: bool, 778 | ) -> proc_macro2::TokenStream { 779 | let inline = if inline { 780 | quote! { 781 | #[inline] 782 | fn type_name() -> Option> { 783 | None 784 | } 785 | 786 | #[inline] 787 | fn select_reference(cx: &mut _opg::Components, _: bool, params: &_opg::ContextParams) -> _opg::ModelReference { 788 | _opg::ModelReference::Inline(Self::get_schema(cx).apply_params(params)) 789 | } 790 | } 791 | } else { 792 | quote! { 793 | #[inline] 794 | fn type_name() -> Option> { 795 | Some(stringify!(#type_name).into()) 796 | } 797 | } 798 | }; 799 | 800 | quote! { 801 | fn get_schema(cx: &mut _opg::Components) -> _opg::Model { 802 | #body 803 | } 804 | 805 | #inline 806 | } 807 | } 808 | 809 | #[derive(Default, Copy, Clone)] 810 | struct ContextParams<'a> { 811 | description: Option<&'a str>, 812 | nullable: Option, 813 | format: Option<&'a str>, 814 | example: Option<&'a syn::Expr>, 815 | } 816 | 817 | impl<'a> From<&'a attr::Container> for ContextParams<'a> { 818 | fn from(attrs: &'a attr::Container) -> Self { 819 | Self::new() 820 | .description(attrs.description.as_deref()) 821 | .nullable(if attrs.nullable { Some(true) } else { None }) 822 | .format(attrs.format.as_deref()) 823 | .example(attrs.example.as_ref()) 824 | } 825 | } 826 | 827 | impl<'a> From<&'a attr::Variant> for ContextParams<'a> { 828 | fn from(attrs: &'a attr::Variant) -> Self { 829 | Self::new() 830 | .description(attrs.description.as_deref()) 831 | .format(attrs.format.as_deref()) 832 | .example(attrs.example.as_ref()) 833 | } 834 | } 835 | 836 | impl<'a> From<&'a attr::Field> for ContextParams<'a> { 837 | fn from(attrs: &'a attr::Field) -> Self { 838 | Self::new() 839 | .description(attrs.description.as_deref()) 840 | .nullable(if attrs.nullable { Some(true) } else { None }) 841 | .format(attrs.format.as_deref()) 842 | .example(attrs.example.as_ref()) 843 | } 844 | } 845 | 846 | impl<'a> ContextParams<'a> { 847 | fn new() -> Self { 848 | Default::default() 849 | } 850 | 851 | fn description(mut self, description: Option<&'a str>) -> Self { 852 | self.description = description; 853 | self 854 | } 855 | 856 | fn nullable(mut self, nullable: Option) -> Self { 857 | self.nullable = nullable; 858 | self 859 | } 860 | 861 | fn format(mut self, format: Option<&'a str>) -> Self { 862 | self.format = format; 863 | self 864 | } 865 | 866 | fn example(mut self, example: Option<&'a syn::Expr>) -> Self { 867 | self.example = example; 868 | self 869 | } 870 | 871 | fn or(mut self, other: T) -> Self 872 | where 873 | T: Into>, 874 | { 875 | let other = other.into(); 876 | self.description = self.description.or(other.description); 877 | self.format = self.format.or(other.format); 878 | self.example = self.example.or(other.example); 879 | self 880 | } 881 | 882 | fn split( 883 | self, 884 | ) -> ( 885 | proc_macro2::TokenStream, 886 | proc_macro2::TokenStream, 887 | proc_macro2::TokenStream, 888 | ) { 889 | ( 890 | option_string(self.description), 891 | option_string(self.format), 892 | option_to_string_expr(self.example), 893 | ) 894 | } 895 | 896 | fn tokenize(self) -> proc_macro2::TokenStream { 897 | let description = option_string(self.description); 898 | let nullable = option_bool(self.nullable); 899 | let format = option_string(self.format); 900 | let example = option_to_string_expr(self.example); 901 | 902 | quote! { 903 | _opg::ContextParams { 904 | description: #description, 905 | nullable: #nullable, 906 | variants: None, 907 | format: #format, 908 | example: #example, 909 | } 910 | } 911 | } 912 | } 913 | -------------------------------------------------------------------------------- /opg/src/macros.rs: -------------------------------------------------------------------------------- 1 | pub use http; 2 | 3 | pub trait FromStrangeTuple { 4 | fn extract(self) -> Option; 5 | } 6 | 7 | impl FromStrangeTuple for () { 8 | fn extract(self) -> Option { 9 | None 10 | } 11 | } 12 | 13 | impl FromStrangeTuple for (T,) { 14 | fn extract(self) -> Option { 15 | Some(self.0) 16 | } 17 | } 18 | 19 | #[macro_export] 20 | macro_rules! describe_type ( 21 | (raw_model => $model:ident) => { 22 | $model 23 | }; 24 | 25 | (raw_type => { 26 | $(description: $description:literal)? 27 | ident: $type:ident 28 | }) => { 29 | $crate::Model { 30 | description: $crate::macros::FromStrangeTuple::extract(($($description.to_string(),)?)), 31 | data: $type, 32 | } 33 | }; 34 | 35 | (string => { 36 | $(description: $description:literal)? 37 | $(format: $format:literal)? 38 | $(example: $example:literal)? 39 | $(variants: [$($variants:literal),*])? 40 | }) => { 41 | $crate::Model { 42 | description: $crate::macros::FromStrangeTuple::extract(($($description.to_string(),)?)), 43 | data: $crate::ModelData::Single($crate::ModelType { 44 | nullable: false, 45 | type_description: $crate::ModelTypeDescription::String($crate::ModelString { 46 | variants: $crate::macros::FromStrangeTuple::extract(($(vec![$($variants.to_string()),*],)?)), 47 | data: $crate::ModelSimple { 48 | format: $crate::macros::FromStrangeTuple::extract(($($format.to_string(),)?)), 49 | example: $crate::macros::FromStrangeTuple::extract(($($example.to_string(),)?)), 50 | } 51 | }) 52 | }) 53 | } 54 | }; 55 | 56 | (number => { 57 | $(description: $description:literal)? 58 | $(format: $format:literal)? 59 | $(example: $example:literal)? 60 | }) => { 61 | $crate::Model { 62 | description: $crate::macros::FromStrangeTuple::extract(($($description.to_string(),)?)), 63 | data: $crate::ModelData::Single($crate::ModelType { 64 | nullable: false, 65 | type_description: $crate::ModelTypeDescription::Number($crate::ModelSimple { 66 | format: $crate::macros::FromStrangeTuple::extract(($($format.to_string(),)?)), 67 | example: $crate::macros::FromStrangeTuple::extract(($($example.to_string(),)?)), 68 | }) 69 | }) 70 | } 71 | }; 72 | 73 | (integer => { 74 | $(description: $description:literal)? 75 | $(format: $format:literal)? 76 | $(example: $example:literal)? 77 | }) => { 78 | $crate::Model { 79 | description: $crate::macros::FromStrangeTuple::extract(($($description.to_string(),)?)), 80 | data: $crate::ModelData::Single($crate::ModelType { 81 | nullable: false, 82 | type_description: $crate::ModelTypeDescription::Integer($crate::ModelSimple { 83 | format: $crate::macros::FromStrangeTuple::extract(($($format.to_string(),)?)), 84 | example: $crate::macros::FromStrangeTuple::extract(($($example.to_string(),)?)), 85 | }) 86 | }) 87 | } 88 | }; 89 | 90 | (boolean => { 91 | $(description: $description:literal)? 92 | }) => { 93 | $crate::Model { 94 | description: $crate::macros::FromStrangeTuple::extract(($($description.to_string(),)?)), 95 | data: $crate::ModelData::Single($crate::ModelType { 96 | nullable: false, 97 | type_description: $crate::ModelTypeDescription::Boolean 98 | }) 99 | } 100 | }; 101 | 102 | (array => { 103 | $(description: $description:literal)? 104 | items: ($($property_tail:tt)*) 105 | }) => { 106 | $crate::Model { 107 | description: $crate::macros::FromStrangeTuple::extract(($($description.to_string(),)?)), 108 | data: $crate::ModelData::Single($crate::ModelType { 109 | nullable: false, 110 | type_description: $crate::ModelTypeDescription::Array($crate::ModelArray { 111 | items: Box::new($crate::describe_type!(@object_property_value $($property_tail)*)) 112 | }) 113 | }) 114 | } 115 | }; 116 | 117 | (object => { 118 | $(description: $description:literal)? 119 | properties: { 120 | $($property_name:ident$([$required:tt])?: ($($property_tail:tt)*))* 121 | } 122 | }) => {{ 123 | let mut properties = std::collections::BTreeMap::new(); 124 | #[allow(unused_mut)] 125 | let mut required = Vec::new(); 126 | 127 | $($crate::describe_type!(@object_property [properties, required] $property_name$([$required])?: ($($property_tail)*)));*; 128 | 129 | $crate::Model { 130 | description: $crate::macros::FromStrangeTuple::extract(($($description.to_string(),)?)), 131 | data: $crate::ModelData::Single($crate::ModelType { 132 | nullable: false, 133 | type_description: $crate::ModelTypeDescription::Object($crate::ModelObject { 134 | properties, 135 | required, 136 | ..Default::default() 137 | }) 138 | }) 139 | } 140 | }}; 141 | 142 | (@object_property [$properties:ident, $required:ident] $property_name:ident: ($($property_tail:tt)*)) => { 143 | $properties.insert(stringify!($property_name).to_string(), $crate::describe_type!(@object_property_value $($property_tail)*)); 144 | }; 145 | 146 | 147 | (@object_property [$properties:ident, $required:ident] $property_name:ident[required]: ($($property_tail:tt)*)) => { 148 | $crate::describe_type!(@object_property [$properties, $required] $property_name: ($($property_tail)*)); 149 | $required.push(stringify!($property_name).to_owned()); 150 | }; 151 | 152 | (@object_property_value link => $ref:literal) => { 153 | $crate::ModelReference::Link($ref.to_owned()) 154 | }; 155 | 156 | (@object_property_value link => $ref:ident) => { 157 | $crate::ModelReference::Link($ref.to_owned()) 158 | }; 159 | 160 | (@object_property_value $type:ident => $($tail:tt)*) => { 161 | $crate::ModelReference::Inline($crate::describe_type!($type => $($tail)*)) 162 | } 163 | ); 164 | 165 | #[macro_export] 166 | macro_rules! impl_opg_model ( 167 | (generic_simple(nullable$(, ?$sized:ident)?): $($type:tt)+) => { 168 | impl $crate::OpgModel for $($type)+ 169 | where 170 | T: $crate::OpgModel$(+ ?$sized)?, 171 | { 172 | fn get_schema(cx: &mut $crate::Components) -> $crate::Model { 173 | ::get_schema_with_params(cx, &$crate::ContextParams { 174 | nullable: Some(true), 175 | ..Default::default() 176 | }) 177 | } 178 | 179 | #[inline] 180 | fn type_name() -> Option> { 181 | ::type_name() 182 | } 183 | } 184 | }; 185 | 186 | (generic_simple$((?$sized:ident))?: $($type:tt)+) => { 187 | impl $crate::OpgModel for $($type)+ 188 | where 189 | T: $crate::OpgModel$(+ ?$sized)?, 190 | { 191 | fn get_schema(cx: &mut $crate::Components) -> $crate::Model { 192 | ::get_schema(cx) 193 | } 194 | 195 | #[inline] 196 | fn type_name() -> Option> { 197 | ::type_name() 198 | } 199 | } 200 | }; 201 | 202 | (generic_tuple: ($($type:ident),+)) => { 203 | impl<$($type),+> $crate::OpgModel for ($($type),+) 204 | where 205 | $($type : $crate::OpgModel),* 206 | { 207 | fn get_schema(cx: &mut $crate::Components) -> $crate::Model { 208 | let item_model = $crate::Model { 209 | description: None, 210 | data: $crate::ModelData::OneOf($crate::ModelOneOf { 211 | one_of: vec![ 212 | $(cx.mention_schema::<$type>(false, &Default::default())),* 213 | ], 214 | }), 215 | }; 216 | 217 | $crate::describe_type!(array => { 218 | items: (raw_model => item_model) 219 | }) 220 | } 221 | 222 | #[inline] 223 | fn type_name() -> Option> { 224 | None 225 | } 226 | 227 | #[inline] 228 | fn select_reference(cx: &mut $crate::Components, _: bool, params: &$crate::ContextParams) -> $crate::ModelReference { 229 | $crate::ModelReference::Inline(Self::get_schema(cx).apply_params(params)) 230 | } 231 | } 232 | }; 233 | 234 | (generic_array: $($type:tt)+) => { 235 | #[allow(clippy::zero_prefixed_literal)] 236 | impl $crate::OpgModel for $($type)+ 237 | where 238 | T: $crate::OpgModel, 239 | { 240 | fn get_schema(cx: &mut $crate::Components) -> $crate::Model { 241 | Model { 242 | description: None, 243 | data: $crate::ModelData::Single($crate::ModelType { 244 | nullable: false, 245 | type_description: $crate::ModelTypeDescription::Array($crate::ModelArray { 246 | items: Box::new(cx.mention_schema::(false, &Default::default())), 247 | }) 248 | }), 249 | } 250 | } 251 | 252 | #[inline] 253 | fn type_name() -> Option> { 254 | None 255 | } 256 | 257 | #[inline] 258 | fn select_reference(cx: &mut $crate::Components, _: bool, params: &$crate::ContextParams) -> $crate::ModelReference { 259 | $crate::ModelReference::Inline(Self::get_schema(cx).apply_params(params)) 260 | } 261 | } 262 | }; 263 | 264 | (generic_dictionary: $($type:tt)+) => { 265 | impl $crate::OpgModel for $($type)+ 266 | where 267 | T: $crate::OpgModel, 268 | K: serde::ser::Serialize, 269 | { 270 | fn get_schema(cx: &mut $crate::Components) -> $crate::Model { 271 | Model { 272 | description: None, 273 | data: $crate::ModelData::Single($crate::ModelType { 274 | nullable: false, 275 | type_description: $crate::ModelTypeDescription::Object($crate::ModelObject { 276 | additional_properties: Some(Box::new(cx.mention_schema::(false, &Default::default()))), 277 | ..Default::default() 278 | }) 279 | }), 280 | } 281 | } 282 | 283 | #[inline] 284 | fn type_name() -> Option> { 285 | None 286 | } 287 | 288 | #[inline] 289 | fn select_reference(cx: &mut $crate::Components, _: bool, params: &$crate::ContextParams) -> $crate::ModelReference { 290 | $crate::ModelReference::Inline(Self::get_schema(cx).apply_params(params)) 291 | } 292 | } 293 | }; 294 | 295 | ($serialized_type:ident$(($format:literal))?: $($type:tt)+ ) => { 296 | impl $crate::OpgModel for $($type)+ { 297 | fn get_schema(cx: &mut $crate::Components) -> Model { 298 | $crate::describe_type!($serialized_type => { 299 | $(format: $format)? 300 | }) 301 | } 302 | 303 | #[inline] 304 | fn type_name() -> Option> { 305 | ::type_name() 306 | } 307 | } 308 | }; 309 | 310 | ($serialized_type:ident(always_inline$(, $format:literal)?): $($type:tt)+) => { 311 | impl $crate::OpgModel for $($type)+ { 312 | fn get_schema(_: &mut $crate::Components) -> Model { 313 | $crate::describe_type!($serialized_type => { 314 | $(format: $format)? 315 | }) 316 | } 317 | 318 | #[inline] 319 | fn type_name() -> Option> { 320 | None 321 | } 322 | 323 | #[inline] 324 | fn select_reference(cx: &mut $crate::Components, _: bool, params: &$crate::ContextParams) -> $crate::ModelReference { 325 | $crate::ModelReference::Inline(Self::get_schema(cx).apply_params(params)) 326 | } 327 | } 328 | }; 329 | ); 330 | 331 | #[macro_export] 332 | macro_rules! describe_api { 333 | ($($property:ident: {$($property_value:tt)*}),*$(,)?) => {{ 334 | let mut result = $crate::models::Opg::default(); 335 | $($crate::describe_api!(@opg_property result $property $($property_value)*));+; 336 | result 337 | }}; 338 | 339 | 340 | (@opg_property $result:ident info $($property:ident: $property_value:expr),*$(,)?) => {{ 341 | $(let $property = $crate::describe_api!(@opg_info_property $property $property_value));*; 342 | $result.info = $crate::models::Info { 343 | $($property,)* 344 | ..Default::default() 345 | }; 346 | }}; 347 | (@opg_info_property title $value:expr) => { ($value).to_string() }; 348 | (@opg_info_property version $value:expr) => { ($value).to_string() }; 349 | (@opg_info_property description $value:expr) => { Some(($value).to_string()) }; 350 | 351 | 352 | (@opg_property $result:ident tags $($tag:ident$(($description:expr))?),*$(,)?) => {{ 353 | $($result.tags.insert(stringify!($tag).to_owned(), $crate::models::Tag { 354 | description: $crate::macros::FromStrangeTuple::extract(($(($description).to_string(),)?)), 355 | }));*; 356 | }}; 357 | 358 | 359 | (@opg_property $result:ident servers $($url:tt$(($description:tt))?),*$(,)?) => {{ 360 | $($result.servers.push($crate::models::Server { 361 | url: $crate::describe_api!(@opg_property_server_literal $url), 362 | description: $crate::macros::FromStrangeTuple::extract(($($crate::describe_api!(@opg_property_server_literal $description),)?)), 363 | }));*; 364 | }}; 365 | (@opg_property_server_literal $l:literal) => { $l.to_owned() }; 366 | (@opg_property_server_literal $l:ident) => { $l.into() }; 367 | 368 | 369 | (@opg_property $result:ident security_schemes $($schemes:tt)*) => { 370 | $crate::describe_api!(@opg_security_scheme $result $($schemes)*,) 371 | }; 372 | (@opg_security_scheme $result:ident $scheme:ident, $($other:tt)*) => { 373 | $result.components.security_schemes.insert(stringity!($scheme).to_owned(), $scheme); 374 | $crate::describe_api!(@opg_security_scheme $result $($other)*) 375 | }; 376 | (@opg_security_scheme $result:ident (http $name:literal): {$($properties:tt)+}, $($other:tt)*) => { 377 | { 378 | let scheme = $crate::models::ParameterNotSpecified; 379 | let mut bearer_format: Option = None; 380 | let mut description: Option = None; 381 | 382 | $crate::describe_api!(@opg_security_scheme_http scheme bearer_format description $($properties)*,); 383 | 384 | let http_security_scheme = match scheme { 385 | $crate::HttpSecuritySchemeKind::Basic => $crate::models::HttpSecurityScheme::Basic { 386 | description, 387 | }, 388 | $crate::HttpSecuritySchemeKind::Bearer => $crate::models::HttpSecurityScheme::Bearer { 389 | format: bearer_format, 390 | description, 391 | }, 392 | }; 393 | 394 | $result.components.security_schemes.insert($name.to_owned(), $crate::models::SecurityScheme::Http(http_security_scheme)); 395 | }; 396 | $crate::describe_api!(@opg_security_scheme $result $($other)*) 397 | }; 398 | (@opg_security_scheme $result:ident (apiKey $name:expr): {$($properties:tt)+}, $($other:tt)*) => { 399 | { 400 | let mut parameter_in = $crate::models::ParameterIn::Header; 401 | let name = $crate::models::ParameterNotSpecified; 402 | let mut description: Option = None; 403 | 404 | $crate::describe_api!(@opg_security_scheme_api_key parameter_in name description $($properties)*,); 405 | 406 | let scheme = $crate::models::ApiKeySecurityScheme { 407 | parameter_in, 408 | name, 409 | description, 410 | }; 411 | 412 | $result.components.security_schemes.insert(($name).to_string(), $crate::models::SecurityScheme::ApiKey(scheme)); 413 | }; 414 | $crate::describe_api!(@opg_security_scheme $result $($other)*) 415 | }; 416 | (@opg_security_scheme $result:ident $(,)?) => {}; 417 | 418 | 419 | (@opg_security_scheme_http $scheme:ident $bearer_format:ident $description:ident scheme: $value:ident, $($other:tt)*) => { 420 | let $scheme = $crate::models::HttpSecuritySchemeKind::$value; 421 | $crate::describe_api!(@opg_security_scheme_http $scheme $bearer_format $description $($other)*) 422 | }; 423 | (@opg_security_scheme_http $scheme:ident $bearer_format:ident $description:ident bearer_format: $value:expr, $($other:tt)*) => { 424 | $bearer_format = Some(($value).to_string()); 425 | $crate::describe_api!(@opg_security_scheme_http $scheme $bearer_format $description $($other)*) 426 | }; 427 | (@opg_security_scheme_http $scheme:ident $bearer_format:ident $description:ident description: $value:expr, $($other:tt)*) => { 428 | $description = Some(($value).to_string()); 429 | $crate::describe_api!(@opg_security_scheme_http $scheme $bearer_format $description $($other)*) 430 | }; 431 | (@opg_security_scheme_http $scheme:ident $bearer_format:ident $description:ident $(,)?) => {}; 432 | 433 | 434 | (@opg_security_scheme_api_key $parameter_in:ident $name:ident $description:ident parameter_in: $value:ident, $($other:tt)*) => { 435 | $parameter_in = $crate::models::ParameterIn::$value; 436 | $crate::describe_api!(@opg_security_scheme_api_key $parameter_in $name $description $($other)*) 437 | }; 438 | (@opg_security_scheme_api_key $parameter_in:ident $name:ident $description:ident name: $value:expr, $($other:tt)*) => { 439 | let $name = ($value).to_string(); 440 | $crate::describe_api!(@opg_security_scheme_api_key $parameter_in $name $description $($other)*) 441 | }; 442 | (@opg_security_scheme_api_key $parameter_in:ident $name:ident $description:ident description: $value:expr, $($other:tt)*) => { 443 | $description = Some(($value).to_string()); 444 | $crate::describe_api!(@opg_security_scheme_api_key $parameter_in $name $description $($other)*) 445 | }; 446 | (@opg_security_scheme_api_key $parameter_in:ident $name:ident $description:ident $(,)?) => {}; 447 | 448 | 449 | (@opg_property $result:ident paths $(($($path_segment:tt)+): { 450 | $($properties:tt)* 451 | }),*$(,)?) => {{ 452 | $({ 453 | let mut path = Vec::new(); 454 | let mut context = $crate::models::PathValue::default(); 455 | 456 | $crate::describe_api!(@opg_property_url path $result context { $($path_segment)* }); 457 | $crate::describe_api!(@opg_path_value_properties $result context $($properties)*,); 458 | 459 | $result.paths.push(($crate::models::Path(path), context)); 460 | };)* 461 | }}; 462 | 463 | (@opg_property_url $path:ident $result:ident $context:ident { / $($rest:tt)* } $($current:tt)+) => { 464 | $crate::describe_api!(@opg_path_url $path $result $context $($current)*); 465 | $crate::describe_api!(@opg_property_url $path $result $context { $($rest)* } ) 466 | }; 467 | (@opg_property_url $path:ident $result:ident $context:ident {} $($current:tt)+) => { 468 | $crate::describe_api!(@opg_path_url $path $result $context $($current)*) 469 | }; 470 | (@opg_property_url $path:ident $result:ident $context:ident { $next:tt $($rest:tt)* } $($current:tt)*) => { 471 | $crate::describe_api!(@opg_property_url $path $result $context { $($rest)* } $($current)* $next ) 472 | }; 473 | 474 | 475 | (@opg_path_value_properties $result:ident $context:ident $field:ident: $value:literal, $($other:tt)*) => { 476 | $context.$field = Some($value.to_owned()); 477 | $crate::describe_api!(@opg_path_value_properties $result $context $($other)*) 478 | }; 479 | (@opg_path_value_properties $result:ident $context:ident parameters: { $($parameters:tt)* }, $($other:tt)*) => { 480 | $crate::describe_api!(@opg_path_value_parameters $result $context $($parameters)*,); 481 | $crate::describe_api!(@opg_path_value_properties $result $context $($other)*) 482 | }; 483 | (@opg_path_value_properties $result:ident $context:ident $method:ident: { $($properties:tt)* }, $($other:tt)*) => { 484 | let mut operation = $crate::models::Operation::default(); 485 | $crate::describe_api!(@opg_path_value_operation_properties $result operation $($properties)*,); 486 | $context.operations.insert($crate::models::HttpMethod::$method, operation); 487 | 488 | $crate::describe_api!(@opg_path_value_properties $result $context $($other)*) 489 | }; 490 | (@opg_path_value_properties $result:ident $context:ident $(,)?) => {}; 491 | 492 | 493 | (@opg_path_value_operation_properties $result:ident $context:ident summary: $value:expr, $($other:tt)*) => { 494 | $context.with_summary($value); 495 | $crate::describe_api!(@opg_path_value_operation_properties $result $context $($other)*) 496 | }; 497 | (@opg_path_value_operation_properties $result:ident $context:ident operationId: $value:expr, $($other:tt)*) => { 498 | $context.with_operation_id($value); 499 | $crate::describe_api!(@opg_path_value_operation_properties $result $context $($other)*) 500 | }; 501 | (@opg_path_value_operation_properties $result:ident $context:ident description: $value:expr, $($other:tt)*) => { 502 | $context.with_description($value); 503 | $crate::describe_api!(@opg_path_value_operation_properties $result $context $($other)*) 504 | }; 505 | (@opg_path_value_operation_properties $result:ident $context:ident deprecated: $value:expr, $($other:tt)*) => { 506 | $context.mark_deprecated($value); 507 | $crate::describe_api!(@opg_path_value_operation_properties $result $context $($other)*) 508 | }; 509 | (@opg_path_value_operation_properties $result:ident $context:ident tags: {$($tag:ident),*$(,)?}, $($other:tt)*) => { 510 | $($context.tags.push(stringify!($tag).to_owned()));*; 511 | $crate::describe_api!(@opg_path_value_operation_properties $result $context $($other)*) 512 | }; 513 | (@opg_path_value_operation_properties $result:ident $context:ident parameters: { $($parameters:tt)* }, $($other:tt)*) => { 514 | $crate::describe_api!(@opg_path_value_parameters $result $context $($parameters)*,); 515 | $crate::describe_api!(@opg_path_value_operation_properties $result $context $($other)*) 516 | }; 517 | (@opg_path_value_operation_properties $result:ident $context:ident callbacks: { $($callbacks:tt)* }, $($other:tt)*) => { 518 | $crate::describe_api!(@opg_path_value_callbacks $result $context $($callbacks)*,); 519 | $crate::describe_api!(@opg_path_value_operation_properties $result $context $($other)*) 520 | }; 521 | (@opg_path_value_operation_properties $result:ident $context:ident security: { $($security:tt)* }, $($other:tt)*) => { 522 | $crate::describe_api!(@opg_path_value_security $result $context $($security)*,); 523 | $crate::describe_api!(@opg_path_value_operation_properties $result $context $($other)*) 524 | }; 525 | (@opg_path_value_operation_properties $result:ident $context:ident body: { $($body:tt)* }, $($other:tt)*) => { 526 | let mut description = None; 527 | let mut required = true; 528 | let schema = $crate::models::ParameterNotSpecified; 529 | $crate::describe_api!(@opg_path_value_body_properties $result description required schema $($body)*,); 530 | $context.with_request_body($crate::models::RequestBody { 531 | description: description.or(Some(String::new())), 532 | required, 533 | schema, // schema must be specified 534 | }); 535 | $crate::describe_api!(@opg_path_value_operation_properties $result $context $($other)*) 536 | }; 537 | (@opg_path_value_operation_properties $result:ident $context:ident body: $type:path, $($other:tt)*) => { 538 | $context.with_request_body($crate::models::RequestBody { 539 | description: Some(String::new()), 540 | required: true, 541 | schema: $result.components.mention_schema::<$type>(false, &Default::default()), 542 | }); 543 | $crate::describe_api!(@opg_path_value_operation_properties $result $context $($other)*) 544 | }; 545 | (@opg_path_value_operation_properties $result:ident $context:ident $response:literal$(($description:literal))?: None, $($other:tt)*) => { 546 | $crate::describe_api!(@opg_path_value_operation_properties $result $context 547 | $response$(($description))?: { None }, 548 | $($other)*) 549 | }; 550 | (@opg_path_value_operation_properties $result:ident $context:ident $response:literal$(($description:literal))?: (), $($other:tt)*) => { 551 | $crate::describe_api!(@opg_path_value_operation_properties $result $context 552 | $response$(($description))?: { Some($result.components.mention_schema::<()>(false, &Default::default())) }, 553 | $($other)*) 554 | }; 555 | (@opg_path_value_operation_properties $result:ident $context:ident $response:literal$(($description:literal))?: $type:path, $($other:tt)*) => { 556 | $crate::describe_api!(@opg_path_value_operation_properties $result $context 557 | $response$(($description))?: { Some($result.components.mention_schema::<$type>(false, &Default::default())) }, 558 | $($other)*) 559 | }; 560 | (@opg_path_value_operation_properties $result:ident $context:ident $response:literal$(($description:literal))?: {$($schema:tt)+}, $($other:tt)*) => { 561 | $context.responses.insert($response, $crate::models::Response { 562 | description: $crate::macros::FromStrangeTuple::extract(($($description.to_owned(),)?)).unwrap_or_else(|| 563 | $crate::macros::http::StatusCode::from_u16($response) 564 | .ok() 565 | .and_then(|status| status.canonical_reason()) 566 | .map(ToString::to_string) 567 | .unwrap_or_else(String::new) 568 | ), 569 | schema: $($schema)+ 570 | }); 571 | $crate::describe_api!(@opg_path_value_operation_properties $result $context $($other)*) 572 | }; 573 | (@opg_path_value_operation_properties $result:ident $context:ident $(,)?) => {}; 574 | 575 | 576 | (@opg_path_value_security $result:ident $context:ident $($security:tt$([$($role:literal),*])?)&&+, $($other:tt)*) => { 577 | { 578 | let mut security = std::collections::BTreeMap::new(); 579 | $($crate::describe_api!(@opg_path_value_security_item $result security $security$([$($role),*])?));*; 580 | $context.security.push(security); 581 | } 582 | $crate::describe_api!(@opg_path_value_security $result $context $($other)*) 583 | }; 584 | (@opg_path_value_security $result:ident $context:ident $(,)*) => {}; 585 | 586 | 587 | (@opg_path_value_security_item $result:ident $context:ident $security:literal$([$($role:literal),*])?) => { 588 | $context.insert($security.to_owned(), vec![$($($role),*)?]) 589 | }; 590 | (@opg_path_value_security_item $result:ident $context:ident $security:ident$([$($role:literal),*])?) => { 591 | $context.insert($result.components.mention_security_scheme(stringify!($security).to_owned(), &$security), vec![$($($role),*)?]) 592 | }; 593 | 594 | 595 | (@opg_path_value_body_properties $result:ident $description:ident $required:ident $schema:ident schema: $type:path, $($other:tt)*) => { 596 | let $schema = $result.components.mention_schema::<$type>(false, &Default::default()); 597 | $crate::describe_api!(@opg_path_value_body_properties $result $description $required $schema $($other)*) 598 | }; 599 | (@opg_path_value_body_properties $result:ident $description:ident $required:ident $schema:ident description: $value:literal, $($other:tt)*) => { 600 | $description = Some($value.to_owned()); 601 | $crate::describe_api!(@opg_path_value_body_properties $result $description $required $schema $($other)*) 602 | }; 603 | (@opg_path_value_body_properties $result:ident $description:ident $required:ident $schema:ident required: $value:literal, $($other:tt)*) => { 604 | $required = $value; 605 | $crate::describe_api!(@opg_path_value_body_properties $result $description $required $schema $($other)*) 606 | }; 607 | (@opg_path_value_body_properties $result:ident $description:ident $required:ident $schema:ident $(,)?) => {}; 608 | 609 | (@opg_path_value_callbacks $result:ident $context:ident $callback:literal: { $($operations:tt)* }, $($other:tt)*) => { 610 | let mut callback_object = CallbackObject::default(); 611 | $crate::describe_api!(@opg_path_value_callbacks_paths $result callback_object $($other)*,); 612 | $context.callbacks.insert($callback.to_owned(), callback_object); 613 | 614 | $crate::describe_api!(@opg_path_value_callbacks $result $context $($other)*) 615 | }; 616 | (@opg_path_value_callbacks $result:ident $context:ident $callback:ident: { $($paths:tt)* }, $($other:tt)*) => { 617 | let mut callback_object = CallbackObject::default(); 618 | $crate::describe_api!(@opg_path_value_callbacks_paths $result callback_object $($paths)*,); 619 | $context.callbacks.insert(stringify!($callback).to_owned(), callback_object); 620 | 621 | $crate::describe_api!(@opg_path_value_callbacks $result $context $($other)*) 622 | }; 623 | (@opg_path_value_callbacks $result:ident $context:ident $(,)?) => {}; 624 | 625 | (@opg_path_value_callbacks_paths $result:ident $context:ident $(($($path_segment:tt)+): { 626 | $($properties:tt)* 627 | }),*$(,)?) => { 628 | $({ 629 | let mut path = Vec::new(); 630 | let mut context = $crate::models::PathValue::default(); 631 | 632 | $crate::describe_api!(@opg_property_url path $result context { $($path_segment)* }); 633 | $crate::describe_api!(@opg_path_value_properties $result context $($properties)*,); 634 | 635 | $context.paths.push(($crate::models::Path(path), context)); 636 | };)* 637 | }; 638 | 639 | (@opg_path_value_parameters $result:ident $context:ident (header $name:expr): { $($properties:tt)* }, $($other:tt)*) => { 640 | { 641 | let mut parameter = $crate::models::OperationParameter { 642 | description: None, 643 | parameter_in: $crate::models::ParameterIn::Header, 644 | required: true, 645 | deprecated: false, 646 | schema: Some($result.components.mention_schema::(false, &Default::default())), 647 | }; 648 | $crate::describe_api!(@opg_path_value_parameter_properties $result parameter $($properties)*,); 649 | $context.parameters.insert(($name).to_string(), parameter); 650 | } 651 | $crate::describe_api!(@opg_path_value_parameters $result $context $($other)*) 652 | }; 653 | (@opg_path_value_parameters $result:ident $context:ident (header $name:expr), $($other:tt)*) => { 654 | { 655 | let mut parameter = $crate::models::OperationParameter { 656 | description: None, 657 | parameter_in: $crate::models::ParameterIn::Header, 658 | required: true, 659 | deprecated: false, 660 | schema: Some($result.components.mention_schema::(false, &Default::default())), 661 | }; 662 | $context.parameters.insert(($name).to_string(), parameter); 663 | } 664 | $crate::describe_api!(@opg_path_value_parameters $result $context $($other)*) 665 | }; 666 | (@opg_path_value_parameters $result:ident $context:ident (query $name:ident: $type:path): { $($properties:tt)* }, $($other:tt)*) => { 667 | { 668 | let mut parameter = $crate::models::OperationParameter { 669 | description: None, 670 | parameter_in: $crate::models::ParameterIn::Query, 671 | required: false, 672 | deprecated: false, 673 | schema: Some($result.components.mention_schema::<$type>(false, &Default::default())), 674 | }; 675 | $crate::describe_api!(@opg_path_value_parameter_properties $result parameter $($properties)*,); 676 | $context.parameters.insert(stringify!($name).to_owned(), parameter); 677 | } 678 | $crate::describe_api!(@opg_path_value_parameters $result $context $($other)*) 679 | }; 680 | (@opg_path_value_parameters $result:ident $context:ident (query $name:ident: $type:path), $($other:tt)*) => { 681 | { 682 | let mut parameter = $crate::models::OperationParameter { 683 | description: None, 684 | parameter_in: $crate::models::ParameterIn::Query, 685 | required: false, 686 | deprecated: false, 687 | schema: Some($result.components.mention_schema::<$type>(false, &Default::default())), 688 | }; 689 | $context.parameters.insert(stringify!($name).to_owned(), parameter); 690 | } 691 | $crate::describe_api!(@opg_path_value_parameters $result $context $($other)*) 692 | }; 693 | (@opg_path_value_parameters $result:ident $context:ident $(,)?) => {}; 694 | 695 | 696 | (@opg_path_value_parameter_properties $result:ident $context:ident description: $value:expr, $($other:tt)*) => { 697 | $context.description = Some(($value).to_string()); 698 | $crate::describe_api!(@opg_path_value_parameter_properties $result $context $($other)*) 699 | }; 700 | (@opg_path_value_parameter_properties $result:ident $context:ident required: $value:expr, $($other:tt)*) => { 701 | $context.required = $value; 702 | $crate::describe_api!(@opg_path_value_parameter_properties $result $context $($other)*) 703 | }; 704 | (@opg_path_value_parameter_properties $result:ident $context:ident deprecated: $value:expr, $($other:tt)*) => { 705 | $context.deprecated = $value; 706 | $crate::describe_api!(@opg_path_value_parameter_properties $result $context $($other)*) 707 | }; 708 | (@opg_path_value_parameter_properties $result:ident $context:ident schema: $type:path, $($other:tt)*) => { 709 | $context.schema = Some($result.components.mention_schema::<$type>(false, &Default::default())); 710 | $crate::describe_api!(@opg_path_value_parameter_properties $result $context $($other)*) 711 | }; 712 | (@opg_path_value_parameter_properties $result:ident $context:ident $(,)?) => {}; 713 | 714 | 715 | (@opg_path_url $path:ident $result:ident $context:ident $current:literal) => { 716 | $path.push($crate::models::PathElement::Path($current.to_owned())); 717 | }; 718 | (@opg_path_url $path:ident $result:ident $context:ident $current:path) => { 719 | $path.push({ 720 | let name = { 721 | let full_name = stringify!($current); 722 | let name = &full_name[full_name.rfind(':').map(|i| i + 1).unwrap_or_default()..]; 723 | name[..1].to_ascii_lowercase() + &name[1..] 724 | }; 725 | $crate::describe_api!(@opg_path_insert_url_param $result $context name $current) 726 | }); 727 | }; 728 | (@opg_path_url $path:ident $result:ident $context:ident {$name:ident: $parameter:path}) => { 729 | $path.push({ 730 | let name = stringify!($name).to_owned(); 731 | $crate::describe_api!(@opg_path_insert_url_param $result $context name $parameter) 732 | }); 733 | }; 734 | (@opg_path_url $path:ident $result:ident $context:ident $current:literal) => { 735 | $path.push($crate::describe_api!(@opg_path_url_element $result $context $current)); 736 | }; 737 | 738 | (@opg_path_insert_url_param $result:ident $context:ident $name:ident $parameter:path) => {{ 739 | $context.parameters.insert($name.clone(), $crate::models::OperationParameter { 740 | description: None, 741 | parameter_in: $crate::models::ParameterIn::Path, 742 | required: true, 743 | deprecated: false, 744 | schema: Some($result.components.mention_schema::<$parameter>(false, &Default::default())) 745 | }); 746 | $crate::models::PathElement::Parameter($name) 747 | }} 748 | } 749 | --------------------------------------------------------------------------------