Allow arbitrary expressions in doc/title/description attributes (#327)
This commit is contained in:
parent
5547e77bcd
commit
df06fc5f66
17 changed files with 206 additions and 159 deletions
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
|
@ -5,18 +5,15 @@ on: [push, pull_request, workflow_dispatch]
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
|
||||||
# work-around https://github.com/rust-lang/cargo/issues/10303
|
|
||||||
CARGO_NET_GIT_FETCH_WITH_CLI: ${{ matrix.rust == '1.60.0' }}
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
rust:
|
rust:
|
||||||
- 1.60.0
|
- 1.65.0
|
||||||
- stable
|
- stable
|
||||||
- beta
|
- beta
|
||||||
- nightly
|
- nightly
|
||||||
include:
|
include:
|
||||||
- rust: 1.60.0
|
- rust: 1.65.0
|
||||||
test_features: ""
|
test_features: ""
|
||||||
allow_failure: false
|
allow_failure: false
|
||||||
- rust: stable
|
- rust: stable
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
[](https://github.com/GREsau/schemars/actions)
|
[](https://github.com/GREsau/schemars/actions)
|
||||||
[](https://crates.io/crates/schemars)
|
[](https://crates.io/crates/schemars)
|
||||||
[](https://docs.rs/schemars/1.0.0--latest)
|
[](https://docs.rs/schemars/1.0.0--latest)
|
||||||
[](https://blog.rust-lang.org/2022/04/07/Rust-1.60.0.html)
|
[](https://blog.rust-lang.org/2022/11/03/Rust-1.65.0.html)
|
||||||
|
|
||||||
Generate JSON Schema documents from Rust code
|
Generate JSON Schema documents from Rust code
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
keywords = ["rust", "json-schema", "serde"]
|
keywords = ["rust", "json-schema", "serde"]
|
||||||
categories = ["encoding", "no-std"]
|
categories = ["encoding", "no-std"]
|
||||||
rust-version = "1.60"
|
rust-version = "1.65"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
schemars_derive = { version = "=1.0.0-alpha.10", optional = true, path = "../schemars_derive" }
|
schemars_derive = { version = "=1.0.0-alpha.10", optional = true, path = "../schemars_derive" }
|
||||||
|
|
|
@ -4,6 +4,10 @@ use crate::{JsonSchema, Schema, SchemaGenerator};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::{json, map::Entry, Map, Value};
|
use serde_json::{json, map::Entry, Map, Value};
|
||||||
|
|
||||||
|
mod rustdoc;
|
||||||
|
|
||||||
|
pub use rustdoc::get_title_and_description;
|
||||||
|
|
||||||
// Helper for generating schemas for flattened `Option` fields.
|
// Helper for generating schemas for flattened `Option` fields.
|
||||||
pub fn json_schema_for_flatten<T: ?Sized + JsonSchema>(
|
pub fn json_schema_for_flatten<T: ?Sized + JsonSchema>(
|
||||||
generator: &mut SchemaGenerator,
|
generator: &mut SchemaGenerator,
|
||||||
|
@ -161,6 +165,17 @@ pub fn insert_metadata_property(schema: &mut Schema, key: &str, value: impl Into
|
||||||
schema.ensure_object().insert(key.to_owned(), value.into());
|
schema.ensure_object().insert(key.to_owned(), value.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insert_metadata_property_if_nonempty(
|
||||||
|
schema: &mut Schema,
|
||||||
|
key: &str,
|
||||||
|
value: impl Into<String>,
|
||||||
|
) {
|
||||||
|
let value: String = value.into();
|
||||||
|
if !value.is_empty() {
|
||||||
|
insert_metadata_property(schema, key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn insert_validation_property(
|
pub fn insert_validation_property(
|
||||||
schema: &mut Schema,
|
schema: &mut Schema,
|
||||||
required_type: &str,
|
required_type: &str,
|
94
schemars/src/_private/rustdoc.rs
Normal file
94
schemars/src/_private/rustdoc.rs
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
pub const fn get_title_and_description(doc: &str) -> (&str, &str) {
|
||||||
|
let doc_bytes = trim_ascii(doc.as_bytes());
|
||||||
|
|
||||||
|
if !doc_bytes.is_empty() && doc_bytes[0] == b'#' {
|
||||||
|
let title_end_index = match strchr(doc_bytes, b'\n') {
|
||||||
|
Some(i) => i,
|
||||||
|
None => doc_bytes.len(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let title = trim_ascii(trim_start(subslice(doc_bytes, 0, title_end_index), b'#'));
|
||||||
|
let description = trim_ascii(subslice(doc_bytes, title_end_index, doc_bytes.len()));
|
||||||
|
|
||||||
|
(to_utf8(title), to_utf8(description))
|
||||||
|
} else {
|
||||||
|
("", to_utf8(doc_bytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn strchr(bytes: &[u8], chr: u8) -> Option<usize> {
|
||||||
|
let len = bytes.len();
|
||||||
|
let mut i = 0;
|
||||||
|
while i < len {
|
||||||
|
if bytes[i] == chr {
|
||||||
|
return Some(i);
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn subslice(mut bytes: &[u8], mut start: usize, end: usize) -> &[u8] {
|
||||||
|
let mut trim_end_count = bytes.len() - end;
|
||||||
|
if trim_end_count > 0 {
|
||||||
|
while let [rest @ .., _last] = bytes {
|
||||||
|
bytes = rest;
|
||||||
|
|
||||||
|
trim_end_count -= 1;
|
||||||
|
if trim_end_count == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if start > 0 {
|
||||||
|
while let [_first, rest @ ..] = bytes {
|
||||||
|
bytes = rest;
|
||||||
|
|
||||||
|
start -= 1;
|
||||||
|
if start == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn to_utf8(bytes: &[u8]) -> &str {
|
||||||
|
match core::str::from_utf8(bytes) {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => panic!("Invalid UTF-8"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn trim_start(mut bytes: &[u8], chr: u8) -> &[u8] {
|
||||||
|
while let [first, rest @ ..] = bytes {
|
||||||
|
if *first == chr {
|
||||||
|
bytes = rest;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn trim_ascii(mut bytes: &[u8]) -> &[u8] {
|
||||||
|
while let [first, rest @ ..] = bytes {
|
||||||
|
if first.is_ascii_whitespace() {
|
||||||
|
bytes = rest;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while let [rest @ .., last] = bytes {
|
||||||
|
if last.is_ascii_whitespace() {
|
||||||
|
bytes = rest;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes
|
||||||
|
}
|
|
@ -458,9 +458,8 @@ impl SchemaGenerator {
|
||||||
}
|
}
|
||||||
|
|
||||||
let pointer = self.definitions_path_stripped();
|
let pointer = self.definitions_path_stripped();
|
||||||
let target = match json_pointer_mut(schema_object, pointer, true) {
|
let Some(target) = json_pointer_mut(schema_object, pointer, true) else {
|
||||||
Some(d) => d,
|
return;
|
||||||
None => return,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
target.append(&mut definitions);
|
target.append(&mut definitions);
|
||||||
|
|
|
@ -315,11 +315,9 @@ impl Transform for RemoveRefSiblings {
|
||||||
fn transform(&mut self, schema: &mut Schema) {
|
fn transform(&mut self, schema: &mut Schema) {
|
||||||
transform_subschemas(self, schema);
|
transform_subschemas(self, schema);
|
||||||
|
|
||||||
if let Some(obj) = schema.as_object_mut() {
|
if let Some(obj) = schema.as_object_mut().filter(|o| o.len() > 1) {
|
||||||
if obj.len() > 1 {
|
|
||||||
if let Some(ref_value) = obj.remove("$ref") {
|
if let Some(ref_value) = obj.remove("$ref") {
|
||||||
if let Value::Array(all_of) =
|
if let Value::Array(all_of) = obj.entry("allOf").or_insert(Value::Array(Vec::new()))
|
||||||
obj.entry("allOf").or_insert(Value::Array(Vec::new()))
|
|
||||||
{
|
{
|
||||||
all_of.push(json!({
|
all_of.push(json!({
|
||||||
"$ref": ref_value
|
"$ref": ref_value
|
||||||
|
@ -328,7 +326,6 @@ impl Transform for RemoveRefSiblings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes the `examples` schema property and (if present) set its first value as the `example` property.
|
/// Removes the `examples` schema property and (if present) set its first value as the `example` property.
|
||||||
|
@ -403,15 +400,14 @@ impl Transform for ReplaceUnevaluatedProperties {
|
||||||
fn transform(&mut self, schema: &mut Schema) {
|
fn transform(&mut self, schema: &mut Schema) {
|
||||||
transform_subschemas(self, schema);
|
transform_subschemas(self, schema);
|
||||||
|
|
||||||
if let Some(obj) = schema.as_object_mut() {
|
let Some(obj) = schema.as_object_mut() else {
|
||||||
if let Some(up) = obj.remove("unevaluatedProperties") {
|
return;
|
||||||
|
};
|
||||||
|
let Some(up) = obj.remove("unevaluatedProperties") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
obj.insert("additionalProperties".to_owned(), up);
|
obj.insert("additionalProperties".to_owned(), up);
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut gather_property_names = GatherPropertyNames::default();
|
let mut gather_property_names = GatherPropertyNames::default();
|
||||||
gather_property_names.transform(schema);
|
gather_property_names.transform(schema);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
mod util;
|
mod util;
|
||||||
use schemars::{generate::SchemaSettings, JsonSchema};
|
use schemars::JsonSchema;
|
||||||
use util::*;
|
use util::*;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
@ -15,6 +15,9 @@ struct MyStruct {
|
||||||
my_undocumented_bool: bool,
|
my_undocumented_bool: bool,
|
||||||
/// A unit struct instance
|
/// A unit struct instance
|
||||||
my_unit: MyUnitStruct,
|
my_unit: MyUnitStruct,
|
||||||
|
#[doc = concat!("# Documented ", "bool")]
|
||||||
|
#[doc = concat!("This bool is documented")]
|
||||||
|
my_documented_bool: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # A Unit
|
/// # A Unit
|
||||||
|
@ -57,12 +60,6 @@ fn doc_comments_struct() -> TestResult {
|
||||||
test_default_generated_schema::<MyStruct>("doc_comments_struct")
|
test_default_generated_schema::<MyStruct>("doc_comments_struct")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn doc_comments_struct_ref_siblings() -> TestResult {
|
|
||||||
let settings = SchemaSettings::draft2019_09();
|
|
||||||
test_generated_schema::<MyStruct>("doc_comments_struct_ref_siblings", settings)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn doc_comments_enum() -> TestResult {
|
fn doc_comments_enum() -> TestResult {
|
||||||
test_default_generated_schema::<MyEnum>("doc_comments_enum")
|
test_default_generated_schema::<MyEnum>("doc_comments_enum")
|
||||||
|
@ -81,6 +78,8 @@ struct OverrideDocs {
|
||||||
/// Also overridden
|
/// Also overridden
|
||||||
#[schemars(title = "", description = "")]
|
#[schemars(title = "", description = "")]
|
||||||
my_undocumented_bool: bool,
|
my_undocumented_bool: bool,
|
||||||
|
#[schemars(title = concat!("Documented ", "bool"), description = "Capitalized".to_uppercase())]
|
||||||
|
my_documented_bool: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -12,10 +12,16 @@
|
||||||
},
|
},
|
||||||
"my_undocumented_bool": {
|
"my_undocumented_bool": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"my_documented_bool": {
|
||||||
|
"title": "Documented bool",
|
||||||
|
"description": "CAPITALIZED",
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"my_int",
|
"my_int",
|
||||||
"my_undocumented_bool"
|
"my_undocumented_bool",
|
||||||
|
"my_documented_bool"
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -15,12 +15,18 @@
|
||||||
"my_unit": {
|
"my_unit": {
|
||||||
"description": "A unit struct instance",
|
"description": "A unit struct instance",
|
||||||
"$ref": "#/$defs/MyUnitStruct"
|
"$ref": "#/$defs/MyUnitStruct"
|
||||||
|
},
|
||||||
|
"my_documented_bool": {
|
||||||
|
"title": "Documented bool",
|
||||||
|
"description": "This bool is documented",
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"my_int",
|
"my_int",
|
||||||
"my_undocumented_bool",
|
"my_undocumented_bool",
|
||||||
"my_unit"
|
"my_unit",
|
||||||
|
"my_documented_bool"
|
||||||
],
|
],
|
||||||
"$defs": {
|
"$defs": {
|
||||||
"MyUnitStruct": {
|
"MyUnitStruct": {
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
|
||||||
"title": "This is the struct's title",
|
|
||||||
"description": "This is the struct's description.",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"my_int": {
|
|
||||||
"title": "An integer",
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
},
|
|
||||||
"my_undocumented_bool": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"my_unit": {
|
|
||||||
"description": "A unit struct instance",
|
|
||||||
"$ref": "#/$defs/MyUnitStruct"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"my_int",
|
|
||||||
"my_undocumented_bool",
|
|
||||||
"my_unit"
|
|
||||||
],
|
|
||||||
"$defs": {
|
|
||||||
"MyUnitStruct": {
|
|
||||||
"title": "A Unit",
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -10,7 +10,7 @@ license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
keywords = ["rust", "json-schema", "serde"]
|
keywords = ["rust", "json-schema", "serde"]
|
||||||
categories = ["encoding", "no-std"]
|
categories = ["encoding", "no-std"]
|
||||||
rust-version = "1.60"
|
rust-version = "1.65"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|
|
@ -26,7 +26,6 @@ impl<'a> FromSerde for Container<'a> {
|
||||||
serde_attrs: serde.attrs,
|
serde_attrs: serde.attrs,
|
||||||
data: Data::from_serde(errors, serde.data)?,
|
data: Data::from_serde(errors, serde.data)?,
|
||||||
generics: serde.generics.clone(),
|
generics: serde.generics.clone(),
|
||||||
original: serde.original,
|
|
||||||
// FIXME this allows with/schema_with attribute on containers
|
// FIXME this allows with/schema_with attribute on containers
|
||||||
attrs: Attrs::new(&serde.original.attrs, errors),
|
attrs: Attrs::new(&serde.original.attrs, errors),
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,7 +10,6 @@ pub struct Container<'a> {
|
||||||
pub serde_attrs: serde_derive_internals::attr::Container,
|
pub serde_attrs: serde_derive_internals::attr::Container,
|
||||||
pub data: Data<'a>,
|
pub data: Data<'a>,
|
||||||
pub generics: syn::Generics,
|
pub generics: syn::Generics,
|
||||||
pub original: &'a syn::DeriveInput,
|
|
||||||
pub attrs: Attrs,
|
pub attrs: Attrs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,54 +1,25 @@
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::ToTokens;
|
||||||
use syn::Attribute;
|
use syn::Attribute;
|
||||||
|
|
||||||
pub fn get_title_and_desc_from_doc(attrs: &[Attribute]) -> (Option<String>, Option<String>) {
|
pub fn get_doc(attrs: &[Attribute]) -> Option<syn::Expr> {
|
||||||
let doc = match get_doc(attrs) {
|
let joiner = quote! {, "\n",};
|
||||||
None => return (None, None),
|
let mut macro_args: TokenStream = TokenStream::new();
|
||||||
Some(doc) => doc,
|
|
||||||
};
|
|
||||||
|
|
||||||
if doc.starts_with('#') {
|
for nv in attrs
|
||||||
let mut split = doc.splitn(2, '\n');
|
|
||||||
let title = split
|
|
||||||
.next()
|
|
||||||
.unwrap()
|
|
||||||
.trim_start_matches('#')
|
|
||||||
.trim()
|
|
||||||
.to_owned();
|
|
||||||
let maybe_desc = split.next().map(|s| s.trim().to_owned());
|
|
||||||
(none_if_empty(title), maybe_desc)
|
|
||||||
} else {
|
|
||||||
(None, Some(doc))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_doc(attrs: &[Attribute]) -> Option<String> {
|
|
||||||
let lines = attrs
|
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|attr| {
|
.filter(|a| a.path().is_ident("doc"))
|
||||||
if !attr.path().is_ident("doc") {
|
.filter_map(|a| a.meta.require_name_value().ok())
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let meta = attr.meta.require_name_value().ok()?;
|
|
||||||
if let syn::Expr::Lit(syn::ExprLit {
|
|
||||||
lit: syn::Lit::Str(lit_str),
|
|
||||||
..
|
|
||||||
}) = &meta.value
|
|
||||||
{
|
{
|
||||||
return Some(lit_str.value());
|
if !macro_args.is_empty() {
|
||||||
|
macro_args.extend(joiner.clone());
|
||||||
|
}
|
||||||
|
macro_args.extend(nv.value.to_token_stream());
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
if macro_args.is_empty() {
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
none_if_empty(lines.join("\n").trim().to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn none_if_empty(s: String) -> Option<String> {
|
|
||||||
if s.is_empty() {
|
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(s)
|
Some(parse_quote!(::core::concat!(#macro_args)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ use proc_macro2::{Group, Span, TokenStream, TokenTree};
|
||||||
use quote::ToTokens;
|
use quote::ToTokens;
|
||||||
use serde_derive_internals::Ctxt;
|
use serde_derive_internals::Ctxt;
|
||||||
use syn::parse::{self, Parse};
|
use syn::parse::{self, Parse};
|
||||||
use syn::{LitStr, Meta, MetaNameValue};
|
use syn::{Expr, LitStr, Meta, MetaNameValue};
|
||||||
|
|
||||||
// FIXME using the same struct for containers+variants+fields means that
|
// FIXME using the same struct for containers+variants+fields means that
|
||||||
// with/schema_with are accepted (but ignored) on containers, and
|
// with/schema_with are accepted (but ignored) on containers, and
|
||||||
|
@ -19,15 +19,16 @@ use syn::{LitStr, Meta, MetaNameValue};
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Attrs {
|
pub struct Attrs {
|
||||||
pub with: Option<WithAttr>,
|
pub with: Option<WithAttr>,
|
||||||
pub title: Option<String>,
|
pub title: Option<Expr>,
|
||||||
pub description: Option<String>,
|
pub description: Option<Expr>,
|
||||||
|
pub doc: Option<Expr>,
|
||||||
pub deprecated: bool,
|
pub deprecated: bool,
|
||||||
pub examples: Vec<syn::Path>,
|
pub examples: Vec<syn::Path>,
|
||||||
pub repr: Option<syn::Type>,
|
pub repr: Option<syn::Type>,
|
||||||
pub crate_name: Option<syn::Path>,
|
pub crate_name: Option<syn::Path>,
|
||||||
pub is_renamed: bool,
|
pub is_renamed: bool,
|
||||||
pub extensions: Vec<(String, TokenStream)>,
|
pub extensions: Vec<(String, TokenStream)>,
|
||||||
pub transforms: Vec<syn::Expr>,
|
pub transforms: Vec<Expr>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -48,26 +49,15 @@ impl Attrs {
|
||||||
.find(|a| a.path().is_ident("repr"))
|
.find(|a| a.path().is_ident("repr"))
|
||||||
.and_then(|a| a.parse_args().ok());
|
.and_then(|a| a.parse_args().ok());
|
||||||
|
|
||||||
let (doc_title, doc_description) = doc::get_title_and_desc_from_doc(attrs);
|
result.doc = doc::get_doc(attrs);
|
||||||
result.title = result.title.or(doc_title);
|
|
||||||
result.description = result.description.or(doc_description);
|
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_metadata(&self) -> SchemaMetadata<'_> {
|
pub fn as_metadata(&self) -> SchemaMetadata<'_> {
|
||||||
#[allow(clippy::ptr_arg)]
|
|
||||||
fn none_if_empty(s: &String) -> Option<&str> {
|
|
||||||
if s.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SchemaMetadata {
|
SchemaMetadata {
|
||||||
title: self.title.as_ref().and_then(none_if_empty),
|
doc: self.doc.as_ref(),
|
||||||
description: self.description.as_ref().and_then(none_if_empty),
|
title: self.title.as_ref(),
|
||||||
|
description: self.description.as_ref(),
|
||||||
deprecated: self.deprecated,
|
deprecated: self.deprecated,
|
||||||
examples: &self.examples,
|
examples: &self.examples,
|
||||||
extensions: &self.extensions,
|
extensions: &self.extensions,
|
||||||
|
@ -128,25 +118,15 @@ impl Attrs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Meta::NameValue(m) if m.path.is_ident("title") => {
|
Meta::NameValue(m) if m.path.is_ident("title") => match self.title {
|
||||||
if let Ok(title) = expr_as_lit_str(errors, attr_type, "title", &m.value) {
|
|
||||||
match self.title {
|
|
||||||
Some(_) => duplicate_error(m),
|
Some(_) => duplicate_error(m),
|
||||||
None => self.title = Some(title.value()),
|
None => self.title = Some(m.value.clone()),
|
||||||
}
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Meta::NameValue(m) if m.path.is_ident("description") => {
|
Meta::NameValue(m) if m.path.is_ident("description") => match self.description {
|
||||||
if let Ok(description) =
|
|
||||||
expr_as_lit_str(errors, attr_type, "description", &m.value)
|
|
||||||
{
|
|
||||||
match self.description {
|
|
||||||
Some(_) => duplicate_error(m),
|
Some(_) => duplicate_error(m),
|
||||||
None => self.description = Some(description.value()),
|
None => self.description = Some(m.value.clone()),
|
||||||
}
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Meta::NameValue(m) if m.path.is_ident("example") => {
|
Meta::NameValue(m) if m.path.is_ident("example") => {
|
||||||
if let Ok(fun) = parse_lit_into_path(errors, attr_type, "example", &m.value) {
|
if let Ok(fun) = parse_lit_into_path(errors, attr_type, "example", &m.value) {
|
||||||
|
@ -239,6 +219,7 @@ impl Attrs {
|
||||||
with: None,
|
with: None,
|
||||||
title: None,
|
title: None,
|
||||||
description: None,
|
description: None,
|
||||||
|
doc: None,
|
||||||
deprecated: false,
|
deprecated: false,
|
||||||
examples,
|
examples,
|
||||||
repr: None,
|
repr: None,
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use syn::spanned::Spanned;
|
use syn::{spanned::Spanned, Expr};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SchemaMetadata<'a> {
|
pub struct SchemaMetadata<'a> {
|
||||||
pub title: Option<&'a str>,
|
pub title: Option<&'a Expr>,
|
||||||
pub description: Option<&'a str>,
|
pub description: Option<&'a Expr>,
|
||||||
|
pub doc: Option<&'a Expr>,
|
||||||
pub deprecated: bool,
|
pub deprecated: bool,
|
||||||
pub read_only: bool,
|
pub read_only: bool,
|
||||||
pub write_only: bool,
|
pub write_only: bool,
|
||||||
pub examples: &'a [syn::Path],
|
pub examples: &'a [syn::Path],
|
||||||
pub default: Option<TokenStream>,
|
pub default: Option<TokenStream>,
|
||||||
pub extensions: &'a [(String, TokenStream)],
|
pub extensions: &'a [(String, TokenStream)],
|
||||||
pub transforms: &'a [syn::Expr],
|
pub transforms: &'a [Expr],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> SchemaMetadata<'a> {
|
impl<'a> SchemaMetadata<'a> {
|
||||||
|
@ -35,14 +36,29 @@ impl<'a> SchemaMetadata<'a> {
|
||||||
fn make_setters(&self) -> Vec<TokenStream> {
|
fn make_setters(&self) -> Vec<TokenStream> {
|
||||||
let mut setters = Vec::<TokenStream>::new();
|
let mut setters = Vec::<TokenStream>::new();
|
||||||
|
|
||||||
|
if let Some(doc) = &self.doc {
|
||||||
|
if self.title.is_none() || self.description.is_none() {
|
||||||
|
setters.push(quote!{
|
||||||
|
const title_and_description: (&str, &str) = schemars::_private::get_title_and_description(#doc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(title) = &self.title {
|
if let Some(title) = &self.title {
|
||||||
setters.push(quote! {
|
setters.push(quote! {
|
||||||
schemars::_private::insert_metadata_property(&mut schema, "title", #title);
|
schemars::_private::insert_metadata_property_if_nonempty(&mut schema, "title", #title);
|
||||||
|
});
|
||||||
|
} else if self.doc.is_some() {
|
||||||
|
setters.push(quote! {
|
||||||
|
schemars::_private::insert_metadata_property_if_nonempty(&mut schema, "title", title_and_description.0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if let Some(description) = &self.description {
|
if let Some(description) = &self.description {
|
||||||
setters.push(quote! {
|
setters.push(quote! {
|
||||||
schemars::_private::insert_metadata_property(&mut schema, "description", #description);
|
schemars::_private::insert_metadata_property_if_nonempty(&mut schema, "description", #description);
|
||||||
|
});
|
||||||
|
} else if self.doc.is_some() {
|
||||||
|
setters.push(quote! {
|
||||||
|
schemars::_private::insert_metadata_property_if_nonempty(&mut schema, "description", title_and_description.1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue