Skip to content

Commit

Permalink
Chore enhance generic schema collection (#1116)
Browse files Browse the repository at this point in the history
This commit enhances generic schema collection from generic arguments
for pre defined set of container types. Prior to this commit if the
generic argument was one of known container types like `Vec` or
`LinkedList` then types within that container type was not correctly
composed. This commit addresses this allowing better compose ability.

This commit changes the auto collect of schemas to only collect non
inlined schemas from their usage. This results leaner OpenAPI document
by avoid polluting the OpenAPI document from unrelated schemas (which
are not used by referencing). This behavior can be altered with
`utoipa-config` by setting `SchemaCollect::All` mode on which will
return to old behavior of collecting also inlined schemas.

Closes #1114
  • Loading branch information
juhaku authored Oct 12, 2024
1 parent d2f076a commit 449b164
Show file tree
Hide file tree
Showing 17 changed files with 920 additions and 305 deletions.
1 change: 1 addition & 0 deletions utoipa-config/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

### Changed

* Chore enhance generic schema collection (https://github.com/juhaku/utoipa/pull/1116)
* Add test for aliases on enum variant values
* Fixed broken tests at `utoipa-config`
* Remove commit commit id from changelogs (https://github.com/juhaku/utoipa/pull/1077)
Expand Down
14 changes: 9 additions & 5 deletions utoipa-config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@
[![docs.rs](https://img.shields.io/static/v1?label=docs.rs&message=utoipa-config&color=blue&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K)](https://docs.rs/utoipa-config/latest/)
![rustc](https://img.shields.io/static/v1?label=rustc&message=1.75&color=orange&logo=rust)

This crate provides global configuration capabilities for `utoipa`. Currently only supports providing Rust type aliases.
This crate provides global configuration capabilities for `utoipa`.

## Config options

* Define rust type aliases for `utoipa` with `.alias_for(...)` method.
* Define schema collect mode for `utoipa` with `.schema_collect(...)` method.
* `SchemaCollect:All` will collect all schemas from usages including inlined with `inline(T)`
* `SchemaCollect::NonInlined` will only collect non inlined schemas from usages.

## Install

Expand All @@ -16,9 +23,6 @@ Add dependency declaration to `Cargo.toml`.
utoipa_config = "0.1"
```

> [!NOTE]
> Not released yet.
## Examples

Create `build.rs` file with following content, then in your code you can just use `MyType` as
Expand All @@ -34,7 +38,7 @@ fn main() {
}
```

See full [example for utoipa-config](../examples/config-test-crate/).
See full [example for utoipa-config](../examples/utoipa-config-test/).

## License

Expand Down
19 changes: 14 additions & 5 deletions utoipa-config/config-test-crate/tests/config.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
use std::borrow::Cow;

use utoipa::{OpenApi, ToSchema};
use utoipa_config::Config;
use utoipa_config::{Config, SchemaCollect};

#[test]
fn test_create_config_with_aliases() {
Config::new()
.alias_for("i32", "Option<String>")
.write_to_file();
let config: Config<'_> = Config::new().alias_for("i32", "Option<String>");
let json = serde_json::to_string(&config).expect("config is json serializable");

let config = Config::read_from_file();
let config: Config = serde_json::from_str(&json).expect("config is json deserializable");

assert!(!config.aliases.is_empty());
assert!(config.aliases.contains_key("i32"));
Expand All @@ -19,6 +18,16 @@ fn test_create_config_with_aliases() {
);
}

#[test]
fn test_config_with_collect_all() {
let config: Config<'_> = Config::new().schema_collect(utoipa_config::SchemaCollect::All);
let json = serde_json::to_string(&config).expect("config is json serializable");

let config: Config = serde_json::from_str(&json).expect("config is json deserializable");

assert!(matches!(config.schema_collect, SchemaCollect::All));
}

#[test]
fn test_to_schema_with_aliases() {
#[allow(unused)]
Expand Down
82 changes: 79 additions & 3 deletions utoipa-config/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
#![warn(missing_docs)]
#![warn(rustdoc::broken_intra_doc_links)]
#![cfg_attr(doc_cfg, feature(doc_cfg))]
//! This crate provides global configuration capabilities for [`utoipa`](https://docs.rs/utoipa/latest/utoipa/). Currently only
//! supports providing Rust type aliases.
//! This crate provides global configuration capabilities for [`utoipa`](https://docs.rs/utoipa/latest/utoipa/).
//!
//! ## Config options
//!
//! * Define rust type aliases for `utoipa` with `.alias_for(...)` method.
//! * Define schema collect mode for `utoipa` with `.schema_collect(...)` method.
//! * [`SchemaCollect::All`] will collect all schemas from usages including inlined with `inline(T)`
//! * [`SchemaCollect::NonInlined`] will only collect non inlined schemas from usages.
//!
//! ## Install
//!
Expand All @@ -29,13 +35,14 @@
//! }
//! ```
//!
//! See full [example for utoipa-config](https://github.com/juhaku/utoipa/tree/master/examples/config-test-crate/).
//! See full [example for utoipa-config](https://github.com/juhaku/utoipa/tree/master/examples/utoipa-config-test/).
use std::borrow::Cow;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;

use serde::de::Visitor;
use serde::{Deserialize, Serialize};

/// Global configuration initialized in `build.rs` of user project.
Expand All @@ -50,6 +57,61 @@ pub struct Config<'c> {
/// A map of global aliases `utoipa` will recognize as types.
#[doc(hidden)]
pub aliases: HashMap<Cow<'c, str>, Cow<'c, str>>,
/// Schema collect mode for `utoipa`. By default only non inlined schemas are collected.
pub schema_collect: SchemaCollect,
}

/// Configures schema collect mode. By default only non explicitly inlined schemas are collected.
/// but this behavior can be changed to collect also inlined schemas by setting
/// [`SchemaCollect::All`].
#[derive(Default)]
pub enum SchemaCollect {
/// Makes sure that all schemas from usages are collected including inlined.
All,
/// Collect only non explicitly inlined schemas to the OpenAPI. This will result smaller schema
/// foot print in the OpenAPI if schemas are typically inlined with `inline(T)` on usage.
#[default]
NonInlined,
}

impl Serialize for SchemaCollect {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::All => serializer.serialize_str("all"),
Self::NonInlined => serializer.serialize_str("non_inlined"),
}
}
}

impl<'de> Deserialize<'de> for SchemaCollect {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct SchemaCollectVisitor;
impl<'d> Visitor<'d> for SchemaCollectVisitor {
type Value = SchemaCollect;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("expected str `all` or `non_inlined`")
}

fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if v == "all" {
Ok(SchemaCollect::All)
} else {
Ok(SchemaCollect::NonInlined)
}
}
}

deserializer.deserialize_str(SchemaCollectVisitor)
}
}

impl<'c> Config<'c> {
Expand Down Expand Up @@ -104,6 +166,20 @@ impl<'c> Config<'c> {
self
}

/// Define schema collect mode for `utoipa`.
///
/// Method accepts one argument [`SchemaCollect`] which defines the collect mode to be used by
/// `utiopa`. If none is defined [`SchemaCollect::NonInlined`] schemas will be collected by
/// default.
///
/// This can be changed to [`SchemaCollect::All`] if schemas called with `inline(T)` is wished
/// to be collected to the resulting OpenAPI.
pub fn schema_collect(mut self, schema_collect: SchemaCollect) -> Self {
self.schema_collect = schema_collect;

self
}

fn get_out_dir() -> Option<String> {
match std::env::var("OUT_DIR") {
Ok(out_dir) => Some(out_dir),
Expand Down
1 change: 1 addition & 0 deletions utoipa-gen/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@

### Changed

* Chore enhance generic schema collection (https://github.com/juhaku/utoipa/pull/1116)
* Enhance file uploads (https://github.com/juhaku/utoipa/pull/1113)
* Move `schemas` into `ToSchema` for schemas (https://github.com/juhaku/utoipa/pull/1112)
* Refactor `KnownFormat`
Expand Down
11 changes: 8 additions & 3 deletions utoipa-gen/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,7 @@ pub struct SchemaReference {
pub name: TokenStream,
pub tokens: TokenStream,
pub references: TokenStream,
pub is_inline: bool,
}

impl SchemaReference {
Expand Down Expand Up @@ -1199,6 +1200,7 @@ impl ComponentSchema {
let title_tokens = as_tokens_or_diagnostics!(&title);

if is_inline {
object_schema_reference.is_inline = true;
let items_tokens = if let Some(children) = &type_tree.children {
schema_references.extend(Self::compose_child_references(children)?);

Expand Down Expand Up @@ -1237,7 +1239,7 @@ impl ComponentSchema {
schema.to_tokens(tokens);
} else {
let index = container.generics.get_generic_type_param_index(type_tree);
// only set schema references for concrete non generic types
// only set schema references tokens for concrete non generic types
if index.is_none() {
let reference_tokens = if let Some(children) = &type_tree.children {
let composed_generics =
Expand All @@ -1247,9 +1249,11 @@ impl ComponentSchema {
quote! { <#rewritten_path as utoipa::PartialSchema>::schema() }
};
object_schema_reference.tokens = reference_tokens;
object_schema_reference.references =
quote! { <#rewritten_path as utoipa::ToSchema>::schemas(schemas) };
}
// any case the references call should be passed down in generic and non
// non generic likewise.
object_schema_reference.references =
quote! { <#rewritten_path as utoipa::ToSchema>::schemas(schemas) };
let composed_or_ref = |item_tokens: TokenStream| -> TokenStream {
if let Some(index) = &index {
quote_spanned! {type_path.span()=>
Expand Down Expand Up @@ -1421,6 +1425,7 @@ impl ComponentSchema {
name: quote! { String::from(< #rewritten_path as utoipa::ToSchema >::name().as_ref()) },
tokens: quote! { <#rewritten_path as utoipa::PartialSchema>::schema() },
references: quote !{ <#rewritten_path as utoipa::ToSchema>::schemas(schemas) },
is_inline: false,
}))
)
} else {
Expand Down
38 changes: 34 additions & 4 deletions utoipa-gen/src/component/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,42 +78,71 @@ impl ToTokensDiagnostics for Schema<'_> {
attributes: self.attributes,
};
let variant = SchemaVariant::new(self.data, &root)?;
let schema_references = variant
let (generic_references, schema_references): (Vec<_>, Vec<_>) = variant
.get_schema_references()
.filter(|schema_reference| !schema_reference.is_partial());
.partition(|schema_reference| schema_reference.is_partial());

struct SchemaRef<'a>(&'a TokenStream, &'a TokenStream, &'a TokenStream);
struct SchemaRef<'a>(&'a TokenStream, &'a TokenStream, &'a TokenStream, bool);
impl ToTokens for SchemaRef<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let SchemaRef(name, ref_tokens, ..) = self;
tokens.extend(quote! { (#name, #ref_tokens) });
}
}
let schema_refs = schema_references
.iter()
.map(|schema_reference| {
SchemaRef(
&schema_reference.name,
&schema_reference.tokens,
&schema_reference.references,
schema_reference.is_inline,
)
})
.collect::<Array<SchemaRef>>();

let references = schema_refs.iter().fold(
TokenStream::new(),
|mut tokens, SchemaRef(_, _, references)| {
|mut tokens, SchemaRef(_, _, references, _)| {
tokens.extend(quote!( #references; ));

tokens
},
);
let generic_references = generic_references
.into_iter()
.map(|schema_reference| {
let reference = &schema_reference.references;
quote! {#reference;}
})
.collect::<TokenStream>();

let schema_refs = schema_refs
.iter()
.filter(|SchemaRef(_, _, _, is_inline)| {
#[cfg(feature = "config")]
{
(matches!(
crate::CONFIG.schema_collect,
utoipa_config::SchemaCollect::NonInlined
) && !is_inline)
|| matches!(
crate::CONFIG.schema_collect,
utoipa_config::SchemaCollect::All
)
}
#[cfg(not(feature = "config"))]
!is_inline
})
.collect::<Array<_>>();

let name = if let Some(schema_as) = variant.get_schema_as() {
schema_as.to_schema_formatted_string()
} else {
ident.to_string()
};

// TODO refactor this to avoid clone
if let Some(Bound(bound)) = variant.get_schema_bound() {
where_clause.predicates.extend(bound.clone());
} else {
Expand Down Expand Up @@ -142,6 +171,7 @@ impl ToTokensDiagnostics for Schema<'_> {
fn schemas(schemas: &mut Vec<(String, utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>)>) {
schemas.extend(#schema_refs);
#references;
#generic_references
}
}
});
Expand Down
17 changes: 15 additions & 2 deletions utoipa-gen/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -474,14 +474,27 @@ impl<'p> ToTokensDiagnostics for Path<'p> {

fn to_schema_references(
mut schemas: TokenStream2,
component_schema: ComponentSchema,
(is_inline, component_schema): (bool, ComponentSchema),
) -> TokenStream2 {
for reference in component_schema.schema_references {
let name = &reference.name;
let tokens = &reference.tokens;
let references = &reference.references;

schemas.extend(quote!( schemas.push((#name, #tokens)); ));
#[cfg(feature = "config")]
let should_collect_schema = (matches!(
crate::CONFIG.schema_collect,
utoipa_config::SchemaCollect::NonInlined
) && !is_inline)
|| matches!(
crate::CONFIG.schema_collect,
utoipa_config::SchemaCollect::All
);
#[cfg(not(feature = "config"))]
let should_collect_schema = !is_inline;
if should_collect_schema {
schemas.extend(quote!( schemas.push((#name, #tokens)); ));
}
schemas.extend(quote!( #references; ));
}

Expand Down
10 changes: 10 additions & 0 deletions utoipa-gen/src/path/media_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,16 @@ impl Schema<'_> {
Self::Ext(ext) => ext.get_component_schema(),
}
}

pub fn is_inline(&self) -> bool {
match self {
Self::Default(def) => match def {
DefaultSchema::TypePath(parsed) => parsed.is_inline,
_ => false,
},
Self::Ext(_) => false,
}
}
}

impl ToTokensDiagnostics for Schema<'_> {
Expand Down
Loading

0 comments on commit 449b164

Please sign in to comment.