use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use super::client_data::ClientData;
use super::contract_type::ContractType;
use super::prop_schema::PropSchema;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PropsSchema {
fields: BTreeMap<String, PropSchema>,
}
impl PropsSchema {
#[must_use]
pub fn new() -> Self {
Self {
fields: BTreeMap::new(),
}
}
#[must_use]
pub fn required(mut self, name: impl Into<String>, ty: ContractType) -> Self {
self.fields.insert(name.into(), PropSchema::required(ty));
self
}
#[must_use]
pub fn optional(mut self, name: impl Into<String>, ty: ContractType) -> Self {
self.fields.insert(name.into(), PropSchema::optional(ty));
self
}
#[must_use]
pub fn nested<T: ClientData>(mut self, name: impl Into<String>) -> Self {
self.fields.insert(
name.into(),
PropSchema::required(ContractType::object(T::exposure_schema())),
);
self
}
#[must_use]
pub fn nested_optional<T: ClientData>(mut self, name: impl Into<String>) -> Self {
self.fields.insert(
name.into(),
PropSchema::optional(ContractType::nullable(ContractType::object(
T::exposure_schema(),
))),
);
self
}
#[must_use]
pub fn nested_array<T: ClientData>(mut self, name: impl Into<String>) -> Self {
self.fields.insert(
name.into(),
PropSchema::required(ContractType::array(ContractType::object(
T::exposure_schema(),
))),
);
self
}
#[must_use]
pub fn fields(&self) -> &BTreeMap<String, PropSchema> {
&self.fields
}
pub(super) fn into_fields(self) -> BTreeMap<String, PropSchema> {
self.fields
}
}
impl Default for PropsSchema {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(serde::Serialize)]
struct Tag;
impl ClientData for Tag {
fn exposure_schema() -> PropsSchema {
PropsSchema::new().required("label", ContractType::string())
}
}
#[test]
fn required_and_optional_props_are_recorded() {
let schema = PropsSchema::new()
.required("title", ContractType::string())
.optional("description", ContractType::string());
assert!(schema.fields()["title"].is_required());
assert!(!schema.fields()["description"].is_required());
}
#[test]
fn field_order_is_deterministic_regardless_of_declaration_order() {
let one = PropsSchema::new()
.required("b", ContractType::number())
.required("a", ContractType::number());
let two = PropsSchema::new()
.required("a", ContractType::number())
.required("b", ContractType::number());
assert_eq!(one, two);
assert_eq!(one.fields().keys().collect::<Vec<_>>(), vec!["a", "b"]);
}
#[test]
fn nested_embeds_the_client_data_schema() {
let schema = PropsSchema::new().nested::<Tag>("tag");
let ContractType::Object { fields } = schema.fields()["tag"].ty() else {
panic!("expected a nested object");
};
assert!(fields.contains_key("label"));
}
#[test]
fn nested_array_wraps_the_nested_object_in_an_array() {
let schema = PropsSchema::new().nested_array::<Tag>("tags");
assert!(matches!(
schema.fields()["tags"].ty(),
ContractType::Array { .. }
));
}
#[test]
fn nested_optional_is_optional_and_nullable() {
let schema = PropsSchema::new().nested_optional::<Tag>("tag");
assert!(!schema.fields()["tag"].is_required());
assert!(matches!(
schema.fields()["tag"].ty(),
ContractType::Nullable { .. }
));
}
}