From 90870e3b6a16bcd41859d3491bb05c16faa4afd4 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 7 Mar 2021 19:05:25 +0100 Subject: [PATCH] basic structure for openapi_type crate --- Cargo.toml | 4 +- openapi_type/Cargo.toml | 20 ++++ openapi_type/src/lib.rs | 76 +++++++++++++++ .../tests/fail/generics_not_openapitype.rs | 12 +++ .../fail/generics_not_openapitype.stderr | 21 +++++ openapi_type/tests/pass/unit_struct.rs | 6 ++ openapi_type/tests/trybuild.rs | 8 ++ openapi_type_derive/Cargo.toml | 22 +++++ openapi_type_derive/src/lib.rs | 93 +++++++++++++++++++ 9 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 openapi_type/Cargo.toml create mode 100644 openapi_type/src/lib.rs create mode 100644 openapi_type/tests/fail/generics_not_openapitype.rs create mode 100644 openapi_type/tests/fail/generics_not_openapitype.stderr create mode 100644 openapi_type/tests/pass/unit_struct.rs create mode 100644 openapi_type/tests/trybuild.rs create mode 100644 openapi_type_derive/Cargo.toml create mode 100644 openapi_type_derive/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 08bf06d..7c0baee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ # -*- eval: (cargo-minor-mode 1) -*- [workspace] -members = [".", "./derive", "./example"] +members = [".", "./derive", "./example", "./openapi_type", "./openapi_type_derive"] [package] name = "gotham_restful" @@ -76,3 +76,5 @@ features = ["full"] [patch.crates-io] gotham_restful = { path = "." } gotham_restful_derive = { path = "./derive" } +openapi_type = { path = "./openapi_type" } +openapi_type_derive = { path = "./openapi_type_derive" } diff --git a/openapi_type/Cargo.toml b/openapi_type/Cargo.toml new file mode 100644 index 0000000..5e2972a --- /dev/null +++ b/openapi_type/Cargo.toml @@ -0,0 +1,20 @@ +# -*- eval: (cargo-minor-mode 1) -*- + +[package] +workspace = ".." +name = "openapi_type" +version = "0.1.0-dev" +authors = ["Dominic Meiser "] +edition = "2018" +description = "OpenAPI type information for Rust structs and enums" +keywords = ["openapi", "type"] +license = "Apache-2.0" +repository = "https://gitlab.com/msrd0/gotham-restful/-/tree/master/openapi_type" + +[dependencies] +indexmap = "1.6" +openapi_type_derive = "0.1.0-dev" +openapiv3 = "=0.3.2" + +[dev-dependencies] +trybuild = "1.0" diff --git a/openapi_type/src/lib.rs b/openapi_type/src/lib.rs new file mode 100644 index 0000000..b5c0341 --- /dev/null +++ b/openapi_type/src/lib.rs @@ -0,0 +1,76 @@ +#![warn(missing_debug_implementations, rust_2018_idioms)] +#![forbid(unsafe_code)] +#![cfg_attr(feature = "cargo-clippy", allow(clippy::tabs_in_doc_comments))] +/*! +TODO +*/ + +pub use indexmap; +pub use openapi_type_derive::OpenapiType; +pub use openapiv3 as openapi; + +use indexmap::IndexMap; +use openapi::{Schema, SchemaData, SchemaKind}; + +// TODO update the documentation +/** +This struct needs to be available for every type that can be part of an OpenAPI Spec. It is +already implemented for primitive types, String, Vec, Option and the like. To have it available +for your type, simply derive from [OpenapiType]. +*/ +#[derive(Debug, Clone, PartialEq)] +pub struct OpenapiSchema { + /// The name of this schema. If it is None, the schema will be inlined. + pub name: Option, + /// Whether this particular schema is nullable. Note that there is no guarantee that this will + /// make it into the final specification, it might just be interpreted as a hint to make it + /// an optional parameter. + pub nullable: bool, + /// The actual OpenAPI schema. + pub schema: SchemaKind, + /// Other schemas that this schema depends on. They will be included in the final OpenAPI Spec + /// along with this schema. + pub dependencies: IndexMap +} + +impl OpenapiSchema { + /// Create a new schema that has no name. + pub fn new(schema: SchemaKind) -> Self { + Self { + name: None, + nullable: false, + schema, + dependencies: IndexMap::new() + } + } + + /// Convert this schema to a [Schema] that can be serialized to the OpenAPI Spec. + pub fn into_schema(self) -> Schema { + Schema { + schema_data: SchemaData { + nullable: self.nullable, + title: self.name, + ..Default::default() + }, + schema_kind: self.schema + } + } +} + +/** +This trait needs to be implemented by every type that is being used in the OpenAPI Spec. It gives +access to the [OpenapiSchema] of this type. It is provided for primitive types, String and the +like. For use on your own types, there is a derive macro: + +``` +# #[macro_use] extern crate openapi_type_derive; +# +#[derive(OpenapiType)] +struct MyResponse { + message: String +} +``` +*/ +pub trait OpenapiType { + fn schema() -> OpenapiSchema; +} diff --git a/openapi_type/tests/fail/generics_not_openapitype.rs b/openapi_type/tests/fail/generics_not_openapitype.rs new file mode 100644 index 0000000..3d2a09d --- /dev/null +++ b/openapi_type/tests/fail/generics_not_openapitype.rs @@ -0,0 +1,12 @@ +use openapi_type::OpenapiType; + +#[derive(OpenapiType)] +struct Foo { + bar: T +} + +struct Bar; + +fn main() { + >::schema(); +} diff --git a/openapi_type/tests/fail/generics_not_openapitype.stderr b/openapi_type/tests/fail/generics_not_openapitype.stderr new file mode 100644 index 0000000..193eadb --- /dev/null +++ b/openapi_type/tests/fail/generics_not_openapitype.stderr @@ -0,0 +1,21 @@ +error[E0599]: no function or associated item named `schema` found for struct `Foo` in the current scope + --> $DIR/generics_not_openapitype.rs:11:14 + | +4 | struct Foo { + | ------------- + | | + | function or associated item `schema` not found for this + | doesn't satisfy `Foo: OpenapiType` +... +8 | struct Bar; + | ----------- doesn't satisfy `Bar: OpenapiType` +... +11 | >::schema(); + | ^^^^^^ function or associated item not found in `Foo` + | + = note: the method `schema` exists but the following trait bounds were not satisfied: + `Bar: OpenapiType` + which is required by `Foo: OpenapiType` + = help: items from traits can only be used if the trait is implemented and in scope + = note: the following trait defines an item `schema`, perhaps you need to implement it: + candidate #1: `OpenapiType` diff --git a/openapi_type/tests/pass/unit_struct.rs b/openapi_type/tests/pass/unit_struct.rs new file mode 100644 index 0000000..79f6443 --- /dev/null +++ b/openapi_type/tests/pass/unit_struct.rs @@ -0,0 +1,6 @@ +use openapi_type_derive::OpenapiType; + +#[derive(OpenapiType)] +struct Foo; + +fn main() {} diff --git a/openapi_type/tests/trybuild.rs b/openapi_type/tests/trybuild.rs new file mode 100644 index 0000000..28574f1 --- /dev/null +++ b/openapi_type/tests/trybuild.rs @@ -0,0 +1,8 @@ +use trybuild::TestCases; + +#[test] +fn trybuild() { + let t = TestCases::new(); + t.pass("tests/pass/*.rs"); + t.compile_fail("tests/fail/*.rs"); +} diff --git a/openapi_type_derive/Cargo.toml b/openapi_type_derive/Cargo.toml new file mode 100644 index 0000000..c1cbedc --- /dev/null +++ b/openapi_type_derive/Cargo.toml @@ -0,0 +1,22 @@ +# -*- eval: (cargo-minor-mode 1) -*- + +[package] +workspace = ".." +name = "openapi_type_derive" +version = "0.1.0-dev" +authors = ["Dominic Meiser "] +edition = "2018" +description = "Implementation detail of the openapi_type crate" +license = "Apache-2.0" +repository = "https://gitlab.com/msrd0/gotham-restful/-/tree/master/openapi_type_derive" + +# tests are done using trybuild exclusively +autotests = false + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = "1.0" diff --git a/openapi_type_derive/src/lib.rs b/openapi_type_derive/src/lib.rs new file mode 100644 index 0000000..bc049b3 --- /dev/null +++ b/openapi_type_derive/src/lib.rs @@ -0,0 +1,93 @@ +#![warn(missing_debug_implementations, rust_2018_idioms)] +#![deny(broken_intra_doc_links)] +#![forbid(unsafe_code)] +//! This crate defines the macros for `#[derive(OpenapiType)]`. + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::{ + parse_macro_input, spanned::Spanned as _, Data, DataEnum, DataStruct, DataUnion, DeriveInput, TraitBound, + TraitBoundModifier, TypeParamBound +}; + +#[proc_macro_derive(OpenapiType)] +pub fn derive_openapi_type(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input); + expand_openapi_type(input).unwrap_or_else(|err| err.to_compile_error()).into() +} + +macro_rules! path { + (:: $($segment:ident)::*) => { + path!(@private Some(Default::default()), $($segment),*) + }; + ($($segment:ident)::*) => { + path!(@private None, $($segment),*) + }; + (@private $leading_colon:expr, $($segment:ident),*) => { + { + #[allow(unused_mut)] + let mut segments: ::syn::punctuated::Punctuated<::syn::PathSegment, _> = Default::default(); + $( + segments.push(::syn::PathSegment { + ident: ::proc_macro2::Ident::new(stringify!($segment), ::proc_macro2::Span::call_site()), + arguments: Default::default() + }); + )* + ::syn::Path { + leading_colon: $leading_colon, + segments + } + } + }; +} + +fn expand_openapi_type(mut input: DeriveInput) -> syn::Result { + let ident = &input.ident; + + // prepare the generics - all impl generics will get `OpenapiType` requirement + let (impl_generics, ty_generics, where_clause) = { + let generics = &mut input.generics; + generics.type_params_mut().for_each(|param| { + param.colon_token.get_or_insert_with(Default::default); + param.bounds.push(TypeParamBound::Trait(TraitBound { + paren_token: None, + modifier: TraitBoundModifier::None, + lifetimes: None, + path: path!(::openapi_type::OpenapiType) + })) + }); + generics.split_for_impl() + }; + + // parse the input data + match &input.data { + Data::Struct(strukt) => parse_struct(strukt)?, + Data::Enum(inum) => parse_enum(inum)?, + Data::Union(union) => parse_union(union)? + }; + + // generate the impl code + Ok(quote! { + impl #impl_generics ::openapi_type::OpenapiType for #ident #ty_generics #where_clause { + fn schema() -> ::openapi_type::OpenapiSchema { + unimplemented!() + } + } + }) +} + +fn parse_struct(_strukt: &DataStruct) -> syn::Result<()> { + Ok(()) +} + +fn parse_enum(_inum: &DataEnum) -> syn::Result<()> { + unimplemented!() +} + +fn parse_union(union: &DataUnion) -> syn::Result<()> { + Err(syn::Error::new( + union.union_token.span(), + "#[derive(OpenapiType)] cannot be used on unions" + )) +}