Skip to content

Commit

Permalink
Merge branch 'GREsau:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
s-fabian authored Oct 13, 2023
2 parents 274663b + 9415fcb commit 195a475
Show file tree
Hide file tree
Showing 36 changed files with 539 additions and 54 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"rust-analyzer.check.command": "clippy"
"rust-analyzer.check.command": "clippy",
"rust-analyzer.showUnlinkedFileNotification": false
}
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# Changelog

## [0.8.15] - 2023-09-17

### Added:

- Implement `JsonSchema` for `BigDecimal` from `bigdecimal` 0.4 (https://github.com/GREsau/schemars/pull/237)

## [0.8.14] - 2023-09-17

### Added:

- Add `#[schemars(inner(...)]` attribute to specify schema for array items (https://github.com/GREsau/schemars/pull/234)

### Changed:

- New optional associated function on `JsonSchema` trait: `schema_id()`, which is similar to `schema_name()`, but does not have to be human-readable, and defaults to the type name including module path. This allows schemars to differentiate between types with the same name in different modules/crates (https://github.com/GREsau/schemars/issues/62 / https://github.com/GREsau/schemars/pull/247)

### Fixed:

- Schemas for `rust_decimal::Decimal` and `bigdecimal::BigDecimal` now match how those types are serialized by default, i.e. as numeric strings (https://github.com/GREsau/schemars/pull/248)

## [0.8.13] - 2023-08-28

### Added:
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,8 @@ Schemars can implement `JsonSchema` on types from several popular crates, enable
- `bytes` - [bytes](https://crates.io/crates/bytes) (^1.0)
- `enumset` - [enumset](https://crates.io/crates/enumset) (^1.0)
- `rust_decimal` - [rust_decimal](https://crates.io/crates/rust_decimal) (^1.0)
- `bigdecimal` - [bigdecimal](https://crates.io/crates/bigdecimal) (^0.3)
- `bigdecimal03` - [bigdecimal](https://crates.io/crates/bigdecimal) (^0.3)
- `bigdecimal04` - [bigdecimal](https://crates.io/crates/bigdecimal) (^0.4)
- `smol_str` - [smol_str](https://crates.io/crates/smol_str) (^0.1.17)
- `semver` - [semver](https://crates.io/crates/semver) (^1.0.9)

Expand Down
45 changes: 39 additions & 6 deletions docs/2-implementing.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,54 @@ permalink: /implementing/
[Deriving `JsonSchema`]({{ site.baseurl }}{% link 1-deriving.md %}) is usually the easiest way to enable JSON schema generation for your types. But if you need more customisation, you can also implement `JsonSchema` manually. This trait has two associated functions which must be implemented, and one which can optionally be implemented:

## schema_name

```rust
fn schema_name() -> String;
```

This function returns the name of the type's schema, which frequently is just the name of the type itself. The schema name is used as the title for root schemas, and the key within the root's `definitions` property for subschemas.
This function returns the human-readable friendly name of the type's schema, which frequently is just the name of the type itself. The schema name is used as the title for root schemas, and the key within the root's `definitions` property for subschemas.

NB in a future version of schemars, it's likely that this function will be changed to return a `Cow<'static, str>`.

## schema_id

```rust
fn schema_id() -> Cow<'static, str>;
```

This function returns a unique identifier of the type's schema - if two types return the same `schema_id`, then Schemars will consider them identical types. Because of this, if a type takes any generic type parameters, then its ID should depend on the type arguments. For example, the implementation of this function for `Vec<T> where T: JsonSchema` is:

If two types return the same `schema_name`, then Schemars will consider them identical types. Because of this, if a type takes any generic type parameters, then its schema name should depend on the type arguments. For example, the imlementation of this function for `Vec<T> where T: JsonSchema` is:
```rust
fn schema_name() -> String {
format!("Array_of_{}", T::schema_name())
fn schema_id() -> Cow<'static, str> {
Cow::Owned(
format!("[{}]", T::schema_id()))
}
```

`BTreeSet<T>`, `LinkedList<T>`, and similar collection types also use that implementation, since they produce identical JSON schemas so they can be considered the same type.
`&mut Vec<&T>`, `LinkedList<T>`, `Mutex<LinkedList<Arc<T>>>`, and similar collection types also use that implementation, since they produce identical JSON schemas so they can be considered the same type.

For a type with no generic type arguments, a reasonable implementation of this function would be to return the type name including module path (in case there is a type with the same name in another module/crate), e.g.:

```rust
impl JsonSchema for NonGenericType {
fn schema_name() -> String {
// Exclude the module path to make the name in generated schemas clearer.
"NonGenericType".to_owned()
}

fn schema_id() -> Cow<'static, str> {
// Include the module, in case a type with the same name is in another module/crate
Cow::Borrowed(concat!(module_path!(), "::NonGenericType"))
}

fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
todo!()
}
}
```

## json_schema

```rust
fn json_schema(gen: &mut gen::SchemaGenerator) -> Schema;
```
Expand All @@ -35,6 +67,7 @@ This function creates the JSON schema itself. The `gen` argument can be used to
`json_schema` should not return a `$ref` schema.

## is_referenceable (optional)

```rust
fn is_referenceable() -> bool;
```
Expand All @@ -43,4 +76,4 @@ If this function returns `true`, then Schemars can re-use the generate schema wh

Generally, this should return `false` for types with simple schemas (such as primitives). For more complex types, it should return `true`. For recursive types, this **must** return `true` to prevent infinite cycles when generating schemas.

The default implementation of this function returns `true` to reduce the chance of someone inadvertently causing infinite cycles with recursive types.
The default implementation of this function returns `true` to reduce the chance of someone inadvertently causing infinite cycles with recursive types.
3 changes: 2 additions & 1 deletion docs/4-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ Schemars can implement `JsonSchema` on types from several popular crates, enable
- `bytes` - [bytes](https://crates.io/crates/bytes) (^1.0)
- `enumset` - [enumset](https://crates.io/crates/enumset) (^1.0)
- `rust_decimal` - [rust_decimal](https://crates.io/crates/rust_decimal) (^1.0)
- `bigdecimal` - [bigdecimal](https://crates.io/crates/bigdecimal) (^0.3)
- `bigdecimal03` - [bigdecimal](https://crates.io/crates/bigdecimal) (^0.3)
- `bigdecimal04` - [bigdecimal](https://crates.io/crates/bigdecimal) (^0.4)
- `smol_str` - [smol_str](https://crates.io/crates/smol_str) (^0.1.17)
- `semver` - [semver](https://crates.io/crates/semver) (^1.0.9)

Expand Down
13 changes: 10 additions & 3 deletions schemars/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "schemars"
description = "Generate JSON Schemas from Rust code"
homepage = "https://graham.cool/schemars/"
repository = "https://github.com/GREsau/schemars"
version = "0.8.13"
version = "0.8.15"
authors = ["Graham Esau <gesau@hotmail.co.uk>"]
edition = "2021"
license = "MIT"
Expand All @@ -14,7 +14,7 @@ build = "build.rs"
rust-version = "1.60"

[dependencies]
schemars_derive = { version = "=0.8.13", optional = true, path = "../schemars_derive" }
schemars_derive = { version = "=0.8.15", optional = true, path = "../schemars_derive" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.25"
dyn-clone = "1.0"
Expand All @@ -31,7 +31,8 @@ arrayvec07 = { version = "0.7", default-features = false, optional = true, packa
url = { version = "2.0", default-features = false, optional = true }
bytes = { version = "1.0", optional = true }
rust_decimal = { version = "1", default-features = false, optional = true }
bigdecimal = { version = "0.3", default-features = false, optional = true }
bigdecimal03 = { version = "0.3", default-features = false, optional = true, package = "bigdecimal" }
bigdecimal04 = { version = "0.4", default-features = false, optional = true, package = "bigdecimal" }
enumset = { version = "1.0", optional = true }
smol_str = { version = "0.1.17", optional = true }
semver = { version = "1.0.9", features = ["serde"], optional = true }
Expand Down Expand Up @@ -61,6 +62,8 @@ arrayvec = ["arrayvec05"]
indexmap1 = ["indexmap"]

raw_value = ["serde_json/raw_value"]
# `bigdecimal` feature without version suffix is included only for back-compat - will be removed in a later version
bigdecimal = ["bigdecimal03"]

ui_test = []

Expand Down Expand Up @@ -120,5 +123,9 @@ required-features = ["smol_str"]
name = "semver"
required-features = ["semver"]

[[test]]
name = "decimal"
required-features = ["rust_decimal", "bigdecimal03", "bigdecimal04"]

[package.metadata.docs.rs]
all-features = true
75 changes: 53 additions & 22 deletions schemars/src/gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ use crate::schema::*;
use crate::{visit::*, JsonSchema, Map};
use dyn_clone::DynClone;
use serde::Serialize;
use std::borrow::Cow;
use std::collections::HashMap;
use std::{any::Any, collections::HashSet, fmt::Debug};

/// Settings to customize how Schemas are generated.
Expand Down Expand Up @@ -149,15 +151,19 @@ impl SchemaSettings {
pub struct SchemaGenerator {
settings: SchemaSettings,
definitions: Map<String, Schema>,
pending_schema_names: HashSet<String>,
pending_schema_ids: HashSet<Cow<'static, str>>,
schema_id_to_name: HashMap<Cow<'static, str>, String>,
used_schema_names: HashSet<String>,
}

impl Clone for SchemaGenerator {
fn clone(&self) -> Self {
Self {
settings: self.settings.clone(),
definitions: self.definitions.clone(),
pending_schema_names: HashSet::new(),
pending_schema_ids: HashSet::new(),
schema_id_to_name: HashMap::new(),
used_schema_names: HashSet::new(),
}
}
}
Expand Down Expand Up @@ -213,27 +219,54 @@ impl SchemaGenerator {
/// If `T`'s schema depends on any [referenceable](JsonSchema::is_referenceable) schemas, then this method will
/// add them to the `SchemaGenerator`'s schema definitions.
pub fn subschema_for<T: ?Sized + JsonSchema>(&mut self) -> Schema {
let name = T::schema_name();
let id = T::schema_id();
let return_ref = T::is_referenceable()
&& (!self.settings.inline_subschemas || self.pending_schema_names.contains(&name));
&& (!self.settings.inline_subschemas || self.pending_schema_ids.contains(&id));

if return_ref {
let reference = format!("{}{}", self.settings().definitions_path, name);
let name = match self.schema_id_to_name.get(&id).cloned() {
Some(n) => n,
None => {
let base_name = T::schema_name();
let mut name = String::new();

if self.used_schema_names.contains(&base_name) {
for i in 2.. {
name = format!("{}{}", base_name, i);
if !self.used_schema_names.contains(&name) {
break;
}
}
} else {
name = base_name;
}

self.used_schema_names.insert(name.clone());
self.schema_id_to_name.insert(id.clone(), name.clone());
name
}
};

let reference = format!("{}{}", self.settings.definitions_path, name);
if !self.definitions.contains_key(&name) {
self.insert_new_subschema_for::<T>(name);
self.insert_new_subschema_for::<T>(name, id);
}
Schema::new_ref(reference)
} else {
self.json_schema_internal::<T>(&name)
self.json_schema_internal::<T>(id)
}
}

fn insert_new_subschema_for<T: ?Sized + JsonSchema>(&mut self, name: String) {
fn insert_new_subschema_for<T: ?Sized + JsonSchema>(
&mut self,
name: String,
id: Cow<'static, str>,
) {
let dummy = Schema::Bool(false);
// insert into definitions BEFORE calling json_schema to avoid infinite recursion
self.definitions.insert(name.clone(), dummy);

let schema = self.json_schema_internal::<T>(&name);
let schema = self.json_schema_internal::<T>(id);

self.definitions.insert(name, schema);
}
Expand Down Expand Up @@ -274,9 +307,8 @@ impl SchemaGenerator {
/// add them to the `SchemaGenerator`'s schema definitions and include them in the returned `SchemaObject`'s
/// [`definitions`](../schema/struct.Metadata.html#structfield.definitions)
pub fn root_schema_for<T: ?Sized + JsonSchema>(&mut self) -> RootSchema {
let name = T::schema_name();
let mut schema = self.json_schema_internal::<T>(&name).into_object();
schema.metadata().title.get_or_insert(name);
let mut schema = self.json_schema_internal::<T>(T::schema_id()).into_object();
schema.metadata().title.get_or_insert_with(T::schema_name);
let mut root = RootSchema {
meta_schema: self.settings.meta_schema.clone(),
definitions: self.definitions.clone(),
Expand All @@ -295,9 +327,8 @@ impl SchemaGenerator {
/// If `T`'s schema depends on any [referenceable](JsonSchema::is_referenceable) schemas, then this method will
/// include them in the returned `SchemaObject`'s [`definitions`](../schema/struct.Metadata.html#structfield.definitions)
pub fn into_root_schema_for<T: ?Sized + JsonSchema>(mut self) -> RootSchema {
let name = T::schema_name();
let mut schema = self.json_schema_internal::<T>(&name).into_object();
schema.metadata().title.get_or_insert(name);
let mut schema = self.json_schema_internal::<T>(T::schema_id()).into_object();
schema.metadata().title.get_or_insert_with(T::schema_name);
let mut root = RootSchema {
meta_schema: self.settings.meta_schema,
definitions: self.definitions,
Expand Down Expand Up @@ -418,29 +449,29 @@ impl SchemaGenerator {
}
}

fn json_schema_internal<T: ?Sized + JsonSchema>(&mut self, name: &str) -> Schema {
fn json_schema_internal<T: ?Sized + JsonSchema>(&mut self, id: Cow<'static, str>) -> Schema {
struct PendingSchemaState<'a> {
gen: &'a mut SchemaGenerator,
name: &'a str,
id: Cow<'static, str>,
did_add: bool,
}

impl<'a> PendingSchemaState<'a> {
fn new(gen: &'a mut SchemaGenerator, name: &'a str) -> Self {
let did_add = gen.pending_schema_names.insert(name.to_owned());
Self { gen, name, did_add }
fn new(gen: &'a mut SchemaGenerator, id: Cow<'static, str>) -> Self {
let did_add = gen.pending_schema_ids.insert(id.clone());
Self { gen, id, did_add }
}
}

impl Drop for PendingSchemaState<'_> {
fn drop(&mut self) {
if self.did_add {
self.gen.pending_schema_names.remove(self.name);
self.gen.pending_schema_ids.remove(&self.id);
}
}
}

let pss = PendingSchemaState::new(self, name);
let pss = PendingSchemaState::new(self, id);
T::json_schema(pss.gen)
}
}
Expand Down
10 changes: 10 additions & 0 deletions schemars/src/json_schema_impls/array.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use std::borrow::Cow;

// Does not require T: JsonSchema.
impl<T> JsonSchema for [T; 0] {
Expand All @@ -10,6 +11,10 @@ impl<T> JsonSchema for [T; 0] {
"EmptyArray".to_owned()
}

fn schema_id() -> Cow<'static, str> {
Cow::Borrowed("[]")
}

fn json_schema(_: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::Array.into()),
Expand All @@ -33,6 +38,11 @@ macro_rules! array_impls {
format!("Array_size_{}_of_{}", $len, T::schema_name())
}

fn schema_id() -> Cow<'static, str> {
Cow::Owned(
format!("[{}; {}]", $len, T::schema_id()))
}

fn json_schema(gen: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::Array.into()),
Expand Down
9 changes: 9 additions & 0 deletions schemars/src/json_schema_impls/chrono.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::schema::*;
use crate::JsonSchema;
use chrono::prelude::*;
use serde_json::json;
use std::borrow::Cow;

impl JsonSchema for Weekday {
no_ref_schema!();
Expand All @@ -11,6 +12,10 @@ impl JsonSchema for Weekday {
"Weekday".to_owned()
}

fn schema_id() -> Cow<'static, str> {
Cow::Borrowed("chrono::Weekday")
}

fn json_schema(_: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::String.into()),
Expand Down Expand Up @@ -41,6 +46,10 @@ macro_rules! formatted_string_impl {
stringify!($ty).to_owned()
}

fn schema_id() -> Cow<'static, str> {
Cow::Borrowed(stringify!(chrono::$ty))
}

fn json_schema(_: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::String.into()),
Expand Down
Loading

0 comments on commit 195a475

Please sign in to comment.