diff --git a/Cargo.lock b/Cargo.lock index 78eb9b3694f..840073c1e63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -764,6 +764,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + [[package]] name = "ecma402_traits" version = "4.0.0" @@ -2552,6 +2558,30 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schemars" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 1.0.109", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2636,6 +2666,17 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "serde_derive_internals" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "serde_json" version = "1.0.107" @@ -3523,6 +3564,7 @@ dependencies = [ "rand", "rand_distr", "rand_pcg", + "schemars", "serde", "serde_json", "twox-hash", diff --git a/utils/zerovec/Cargo.toml b/utils/zerovec/Cargo.toml index b9e7a76569c..65b9fe3a791 100644 --- a/utils/zerovec/Cargo.toml +++ b/utils/zerovec/Cargo.toml @@ -24,6 +24,7 @@ all-features = true [dependencies] zerofrom = { workspace = true } +serde_json = "1.0" zerovec-derive = { workspace = true, optional = true} @@ -37,6 +38,7 @@ serde = { version = "1.0", default-features = false, features = ["alloc"], optio # and all 0.7 versions, but not further. yoke = { version = ">=0.6.0, <0.8.0", path = "../yoke", optional = true } twox-hash = { version = "1.6", default-features = false, optional = true } +schemars = "0.8.16" [dev-dependencies] bincode = "1.3" @@ -48,7 +50,6 @@ rand = "0.8" rand_distr = "0.4" rand_pcg = "0.3" serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" yoke = { path = "../../utils/yoke", features = ["derive"] } zerofrom = { path = "../../utils/zerofrom", features = ["derive"] } diff --git a/utils/zerovec/src/error.rs b/utils/zerovec/src/error.rs index 85de3ecc8dc..7ff9dfcab75 100644 --- a/utils/zerovec/src/error.rs +++ b/utils/zerovec/src/error.rs @@ -5,6 +5,19 @@ use core::any; use core::fmt; +use crate::__zerovec_internal_reexport::boxed::Box; +use alloc::borrow::ToOwned; +use alloc::format; +use alloc::string::String; +use alloc::vec; +use schemars::gen::SchemaGenerator; +use schemars::schema::InstanceType; +use schemars::schema::Schema; +use schemars::schema::SchemaObject; +use schemars::JsonSchema; + +use serde_json::Value; + /// A generic error type to be used for decoding slices of ULE types #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[non_exhaustive] @@ -33,6 +46,27 @@ impl fmt::Display for ZeroVecError { } } } +impl JsonSchema for ZeroVecError { + fn schema_name() -> String { + format!("ZeroVecError") + } + + fn json_schema(_gen: &mut SchemaGenerator) -> Schema { + Schema::Object(SchemaObject { + instance_type: Some(InstanceType::String.into()), + enum_values: Some(vec![ + Value::String("InvalidLength".to_owned()), + Value::String("ParseError".to_owned()), + Value::String("VarZeroVecFormatError".to_owned()), + ]), + metadata: Some(Box::new(schemars::schema::Metadata { + description: Some("ZeroVecError is an enum representing errors that can occur during the decoding of slices of ULE".into()), + ..Default::default() + })), + ..Default::default() + }) + } +} impl ZeroVecError { /// Construct a parse error for the given type diff --git a/utils/zerovec/src/lib.rs b/utils/zerovec/src/lib.rs index 8bb5b17e030..0991bd995ac 100644 --- a/utils/zerovec/src/lib.rs +++ b/utils/zerovec/src/lib.rs @@ -523,6 +523,53 @@ pub use zerovec_derive::make_varule; mod tests { use super::*; use core::mem::size_of; + use schemars::{gen::SchemaGenerator, JsonSchema}; + use serde::{Deserialize, Serialize}; + use serde_json::Value; + + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[derive(JsonSchema)] + pub struct DataStruct<'data> { + #[cfg_attr(feature = "serde", serde(borrow))] + nums: ZeroVec<'data, u32>, + + #[cfg_attr(feature = "serde", serde(borrow))] + chars: ZeroVec<'data, char>, + + #[cfg_attr(feature = "serde", serde(borrow))] + strs: VarZeroVec<'data, str>, + + #[cfg_attr(feature = "serde", serde(borrow))] + nested_numbers: VarZeroVec<'data, ZeroSlice>, + } + + #[test] + fn check_schema() { + let gen = SchemaGenerator::default(); + let schema = gen.into_root_schema_for::(); + let schema_json = + serde_json::to_string_pretty(&schema).expect("Failed to serialize schema"); + println!("{}", schema_json); + let parsed_schema: Value = + serde_json::from_str(&schema_json).expect("Failed to parse schema JSON"); + + // Check for the existence of "ZeroVec" and "ZeroVec" in `definitions` + let definitions = parsed_schema + .get("definitions") + .expect("No definitions found in schema"); + assert!( + definitions.get("ZeroVec").is_some(), + "Definition for ZeroVec not found" + ); + assert!( + definitions.get("ZeroVec").is_some(), + "Definition for ZeroVec not found" + ); + assert!( + definitions.get("VarZeroVec").is_some(), + "Definition for VarZeroVec not found" + ); + } /// Checks that the size of the type is one of the given sizes. /// The size might differ across Rust versions or channels. diff --git a/utils/zerovec/src/varzerovec/vec.rs b/utils/zerovec/src/varzerovec/vec.rs index 0a1e6bfb92b..4bd564873ca 100644 --- a/utils/zerovec/src/varzerovec/vec.rs +++ b/utils/zerovec/src/varzerovec/vec.rs @@ -4,10 +4,17 @@ use crate::ule::*; +use alloc::borrow::Cow; +use alloc::boxed::Box; +use alloc::format; +use alloc::string::String; use alloc::vec::Vec; use core::cmp::{Ord, Ordering, PartialOrd}; use core::fmt; use core::ops::Deref; +use schemars::gen::SchemaGenerator; +use schemars::schema::{ArrayValidation, InstanceType, Schema, SchemaObject}; +use schemars::JsonSchema; use super::*; @@ -232,6 +239,34 @@ impl Deref for VarZeroVec<'_, T, F> { } } +impl<'a, T, F> JsonSchema for VarZeroVec<'a, T, F> +where + T: VarULE + ?Sized + JsonSchema, + F: VarZeroVecFormat, +{ + fn schema_name() -> String { + format!("VarZeroVec<{}>", T::schema_name()) + } + + fn schema_id() -> Cow<'static, str> { + Cow::Owned(format!("zerovec::VarZeroVec<{}>", T::schema_id())) + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + let items_schema = gen.subschema_for::(); + + SchemaObject { + instance_type: Some(InstanceType::Array.into()), + array: Some(Box::new(ArrayValidation { + items: Some(items_schema.into()), + ..Default::default() + })), + ..Default::default() + } + .into() + } +} + impl<'a, T: VarULE + ?Sized, F: VarZeroVecFormat> VarZeroVec<'a, T, F> { /// Creates a new, empty `VarZeroVec`. /// diff --git a/utils/zerovec/src/zerovec/mod.rs b/utils/zerovec/src/zerovec/mod.rs index 50cbb3dd527..2a1c13b886c 100644 --- a/utils/zerovec/src/zerovec/mod.rs +++ b/utils/zerovec/src/zerovec/mod.rs @@ -10,10 +10,15 @@ mod serde; mod slice; +use schemars::gen::SchemaGenerator; +use schemars::schema::{ArrayValidation, InstanceType, Schema}; pub use slice::ZeroSlice; use crate::ule::*; use alloc::borrow::Cow; +use alloc::boxed::Box; +use alloc::format; +use alloc::string::String; use alloc::vec::Vec; use core::cmp::{Ord, Ordering, PartialOrd}; use core::fmt; @@ -23,6 +28,7 @@ use core::mem; use core::num::NonZeroUsize; use core::ops::Deref; use core::ptr::{self, NonNull}; +use schemars::{schema::SchemaObject, JsonSchema}; /// A zero-copy, byte-aligned vector for fixed-width types. /// @@ -114,6 +120,32 @@ impl<'a, T: AsULE> Deref for ZeroVec<'a, T> { } } +impl<'a, T> JsonSchema for ZeroVec<'a, T> +where + T: AsULE + JsonSchema, +{ + fn schema_name() -> String { + format!("ZeroVec<{}>", T::schema_name()) + } + + fn schema_id() -> Cow<'static, str> { + Cow::Owned(format!("zerovec::ZeroVec<{}>", T::schema_id())) + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + let element_schema = gen.subschema_for::(); + SchemaObject { + instance_type: Some(InstanceType::Array.into()), + array: Some(Box::new(ArrayValidation { + items: Some(element_schema.into()), + ..Default::default() + })), + ..Default::default() + } + .into() + } +} + // Represents an unsafe potentially-owned vector/slice type, without a lifetime // working around dropck limitations. // diff --git a/utils/zerovec/src/zerovec/slice.rs b/utils/zerovec/src/zerovec/slice.rs index 12d88deff8e..6d4d480db66 100644 --- a/utils/zerovec/src/zerovec/slice.rs +++ b/utils/zerovec/src/zerovec/slice.rs @@ -3,6 +3,8 @@ // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). use super::*; +use crate::alloc::string::ToString; +use ::serde::Serialize; use alloc::boxed::Box; use core::cmp::Ordering; use core::ops::Range; @@ -543,6 +545,41 @@ impl Ord for ZeroSlice { } } +impl JsonSchema for ZeroSlice +where + T: AsULE + JsonSchema + Serialize, // Ensure T is serializable for accurate schema representation +{ + fn schema_name() -> String { + format!("ZeroSlice<{}>", T::schema_name()) + } + + fn schema_id() -> Cow<'static, str> { + Cow::Owned(format!("zerovec::ZeroSlice<{}>", T::schema_id())) + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + // Instead of generating a subschema for T, we generate a schema representing the byte array + let byte_schema = gen.subschema_for::>(); + + SchemaObject { + instance_type: Some(InstanceType::Object.into()), + object: Some(Box::new({ + let mut object_validation = schemars::schema::ObjectValidation::default(); + object_validation + .properties + .insert("data".to_string(), byte_schema); + object_validation + .properties + .insert("error".to_string(), gen.subschema_for::()); + object_validation.required.insert("data".to_string()); + object_validation + })), + ..Default::default() + } + .into() + } +} + impl AsRef> for Vec { fn as_ref(&self) -> &ZeroSlice { ZeroSlice::::from_ule_slice(self)