use std::collections::BTreeMap;
use std::num::{NonZeroU32, NonZeroU64};
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize, de::Error};
use serde_json::value::RawValue;
use crate::serve::protocol;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct Version(#[schemars(with = "VersionShape")] pub protocol::Version);
#[derive(Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
struct VersionShape {
generation: String,
sequence: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Selection {
pub branch: String,
pub version: Option<Version>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Snapshot {
pub branch: String,
pub version: Version,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PageLimits {
#[schemars(range(min = 1, max = u32::MAX))]
pub rows: NonZeroU32,
pub bytes: NonZeroU64,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Page<T> {
pub entries: Vec<T>,
pub next_after: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct RecordRef {
pub collection: String,
pub document_id: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct RecordWrite {
pub document_id: String,
pub document: Box<RawValue>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct RecordValue {
pub record: RecordRef,
pub document: Box<RawValue>,
}
#[derive(Debug, Serialize, JsonSchema)]
#[serde(untagged)]
pub enum CollectionShape {
Remove,
Boolean(bool),
Rules(#[schemars(with = "BTreeMap<String, Box<RawValue>>")] Box<RawValue>),
}
impl<'de> Deserialize<'de> for CollectionShape {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = Box::<RawValue>::deserialize(deserializer)?;
match raw.get() {
"null" => Ok(Self::Remove),
"true" => Ok(Self::Boolean(true)),
"false" => Ok(Self::Boolean(false)),
text if text.starts_with('{') => Ok(Self::Rules(raw)),
_ => Err(D::Error::custom(
"schema must be null, a boolean, or an object",
)),
}
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CollectionInfo {
pub name: String,
pub schema: CollectionShape,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Cardinality {
OneToOne,
OneToMany,
ManyToMany,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct RelationInfo {
pub relation: String,
pub from_collection: String,
pub to_collection: String,
pub cardinality: Cardinality,
pub relationships: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub relationships_reason: Option<RelationCountReason>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum RelationCountReason {
Withheld,
CountsNotReady,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct VectorField {
pub collection: String,
pub field: String,
pub dimensions: NonZeroU64,
pub count: Option<u64>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Counts {
pub collections: u64,
pub documents: u64,
pub edges: u64,
pub vectors: u64,
}
#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct DescribeCursors {
pub branches: Option<String>,
pub collections: Option<String>,
pub relations: Option<String>,
pub vector_fields: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct DescribeStoreInput {
pub selection: Selection,
pub page: PageLimits,
pub after: DescribeCursors,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct DescribeStoreOutput {
pub selection: Snapshot,
pub branches: Page<Snapshot>,
pub collections: Page<CollectionInfo>,
pub relations: Page<RelationInfo>,
pub vector_fields: Page<VectorField>,
pub counts: Counts,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct RunQueryInput {
pub selection: Selection,
pub query: String,
#[serde(default)]
pub parameters: BTreeMap<String, Box<RawValue>>,
pub page: PageLimits,
pub after: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct QueryRow {
pub value: Box<RawValue>,
pub origin: Option<RecordRef>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct RunQueryOutput {
pub selection: Snapshot,
pub rows: Page<QueryRow>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct HistoryRoot {
pub shard: usize,
pub root: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct HistoryEntry {
pub branch: String,
pub version: Version,
pub roots: Vec<HistoryRoot>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ReadHistoryInput {
pub selection: Selection,
pub page: PageLimits,
pub after: Option<String>,
pub record: Option<RecordRef>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ReadHistoryOutput {
pub selection: Snapshot,
pub history: Page<HistoryEntry>,
pub record: Option<RecordValue>,
}
macro_rules! writing_input {
($name:ident { $($(#[$attribute:meta])* $field:ident: $ty:ty),* $(,)? }) => {
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct $name {
pub request_id: String,
pub branch: String,
pub expected: Version,
$( $(#[$attribute])* pub $field: $ty, )*
}
};
}
writing_input!(DefineCollectionInput {
collection: String,
schema: CollectionShape,
});
writing_input!(PutRecordsInput {
collection: String,
#[serde(deserialize_with = "nonempty_records")]
#[schemars(length(min = 1))]
records: Vec<RecordWrite>,
});
writing_input!(DeleteRecordInput { record: RecordRef });
writing_input!(LinkRecordsInput {
relation: String,
from: RecordRef,
to: RecordRef,
properties: BTreeMap<String, Box<RawValue>>,
});
writing_input!(UnlinkRecordsInput {
relation: String,
from: RecordRef,
to: RecordRef,
});
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CreateBranchInput {
pub request_id: String,
pub branch: String,
pub expected: Version,
pub name: String,
pub from: Version,
}
fn nonempty_records<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<RecordWrite>, D::Error> {
let records = Vec::<RecordWrite>::deserialize(deserializer)?;
if records.is_empty() {
return Err(D::Error::custom("records must contain at least one record"));
}
Ok(records)
}
macro_rules! writing_output {
($($name:ident),* $(,)?) => {
$(
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct $name {
pub branch: String,
pub version: Version,
pub request_id: String,
}
)*
};
}
writing_output!(
DefineCollectionOutput,
PutRecordsOutput,
DeleteRecordOutput,
LinkRecordsOutput,
UnlinkRecordsOutput,
);
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MergeBranchInput {
pub request_id: String,
pub branch: String,
pub expected: Version,
pub from: String,
pub reviewed: Version,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MergeBranchOutput {
pub branch: String,
pub version: Version,
pub request_id: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CreateBranchOutput {
pub branch: String,
pub version: Version,
pub request_id: String,
}
#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ReviewCursors {
pub diff: Option<String>,
pub counts: Option<String>,
pub conflicts: Option<String>,
pub dangling: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ReviewBranchInput {
pub selection: Selection,
pub page: PageLimits,
pub after: ReviewCursors,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DifferenceKind {
Added,
Changed,
Removed,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum ReviewOwner {
Collection {
collection: String,
},
Document {
record: RecordRef,
},
Relation {
relation: String,
},
Edge {
relation: String,
from: RecordRef,
to: RecordRef,
},
Vector {
record: RecordRef,
field: String,
},
VectorField {
collection: String,
field: String,
},
}
#[derive(Debug, Serialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum ReviewValue {
Absent,
Value { value: Box<RawValue> },
}
impl<'de> Deserialize<'de> for ReviewValue {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
enum Kind {
Absent,
Value,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Encoded {
kind: Kind,
#[serde(default, deserialize_with = "present_review_value")]
value: Option<Box<RawValue>>,
}
let encoded = Encoded::deserialize(deserializer)?;
match (encoded.kind, encoded.value) {
(Kind::Absent, None) => Ok(Self::Absent),
(Kind::Value, Some(value)) => Ok(Self::Value { value }),
(Kind::Absent, Some(_)) => {
Err(D::Error::custom("an absent review value has a payload"))
}
(Kind::Value, None) => Err(D::Error::custom("a present review value has no payload")),
}
}
}
fn present_review_value<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<Box<RawValue>>, D::Error> {
Box::<RawValue>::deserialize(deserializer).map(Some)
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ReviewLine {
pub kind: DifferenceKind,
pub owner: ReviewOwner,
pub before: ReviewValue,
pub after: ReviewValue,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ReviewCounts {
pub changes: u64,
pub conflicts: u64,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ReviewCountPage {
pub partial: ReviewCounts,
pub next_after: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ConflictState {
SourceOnly,
SameOnBoth,
Conflict,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ConflictLine {
pub owner: ReviewOwner,
pub ancestor: ReviewValue,
pub target: ReviewValue,
pub source: ReviewValue,
pub state: ConflictState,
#[serde(skip_serializing_if = "Option::is_none")]
pub choice_token: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum MissingEnd {
From,
To,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum RemovedBy {
Source,
Target,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DanglingState {
Dangling,
DependsOnChoice,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct DanglingLine {
pub choice_token: String,
pub relation: String,
pub from: RecordRef,
pub to: RecordRef,
pub missing_end: MissingEnd,
pub removed_by: RemovedBy,
pub state: DanglingState,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ReviewBranchOutput {
pub selection: Snapshot,
pub from: Snapshot,
pub target: Snapshot,
pub diff: Page<ReviewLine>,
pub counts: ReviewCountPage,
pub conflicts: Page<ConflictLine>,
pub dangling: Page<DanglingLine>,
pub base_moved: Option<u64>,
}
#[cfg(test)]
pub(super) mod tests {
use std::error::Error;
use serde::de::DeserializeOwned;
use serde_json::{Value, json};
use super::{
CreateBranchInput, DefineCollectionInput, DeleteRecordInput, DescribeStoreInput,
LinkRecordsInput, MergeBranchInput, PageLimits, PutRecordsInput, ReadHistoryInput,
RelationCountReason, RelationInfo, ReviewBranchInput, RunQueryInput, RunQueryOutput,
UnlinkRecordsInput, Version,
};
type TestResult = Result<(), Box<dyn Error>>;
const SELECTED: &str = r#"{"branch":"feature","version":{"generation":"1","sequence":"2"}}"#;
const WRITE: &str =
r#""request_id":"r1","branch":"feature","expected":{"generation":"1","sequence":"2"}"#;
pub(in crate::mcp) fn inputs() -> Vec<(&'static str, String)> {
vec![
(
"describe_store",
format!(
r#"{{"selection":{SELECTED},"page":{{"rows":10,"bytes":4096}},"after":{{}}}}"#
),
),
(
"run_query",
format!(
r#"{{"selection":{SELECTED},"query":"records | '.'","page":{{"rows":10,"bytes":4096}}}}"#
),
),
(
"read_history",
format!(r#"{{"selection":{SELECTED},"page":{{"rows":10,"bytes":4096}}}}"#),
),
(
"define_collection",
format!(r#"{{{WRITE},"collection":"records","schema":{{"type":"object"}}}}"#),
),
(
"put_records",
format!(
r#"{{{WRITE},"collection":"records","records":[{{"document_id":"a","document":{{"n":1e2}}}}]}}"#
),
),
(
"delete_record",
format!(r#"{{{WRITE},"record":{{"collection":"records","document_id":"a"}}}}"#),
),
(
"link_records",
format!(
r#"{{{WRITE},"relation":"links","from":{{"collection":"records","document_id":"a"}},"to":{{"collection":"records","document_id":"b"}},"properties":{{}}}}"#
),
),
(
"unlink_records",
format!(
r#"{{{WRITE},"relation":"links","from":{{"collection":"records","document_id":"a"}},"to":{{"collection":"records","document_id":"b"}}}}"#
),
),
(
"create_branch",
format!(r#"{{{WRITE},"name":"new","from":{{"generation":"1","sequence":"2"}}}}"#),
),
(
"review_branch",
format!(
r#"{{"selection":{SELECTED},"page":{{"rows":10,"bytes":4096}},"after":{{}}}}"#
),
),
(
"merge_branch",
format!(
r#"{{{WRITE},"from":"import","reviewed":{{"generation":"3","sequence":"4"}}}}"#
),
),
]
}
fn refuses_unknown<T: DeserializeOwned>(input: &str) -> TestResult {
let _: T = serde_json::from_str(input)?;
let mut value: Value = serde_json::from_str(input)?;
value
.as_object_mut()
.ok_or("input is not an object")?
.insert("caller".to_owned(), json!({"subject":"forged"}));
let error = serde_json::from_str::<T>(&serde_json::to_string(&value)?)
.err()
.ok_or("unknown caller member accepted")?;
assert!(error.to_string().contains("unknown field"), "{error}");
Ok(())
}
#[test]
fn every_tool_input_refuses_unknown_members() -> TestResult {
let fixtures = inputs();
refuses_unknown::<DescribeStoreInput>(&fixtures[0].1)?;
refuses_unknown::<RunQueryInput>(&fixtures[1].1)?;
refuses_unknown::<ReadHistoryInput>(&fixtures[2].1)?;
refuses_unknown::<DefineCollectionInput>(&fixtures[3].1)?;
refuses_unknown::<PutRecordsInput>(&fixtures[4].1)?;
refuses_unknown::<DeleteRecordInput>(&fixtures[5].1)?;
refuses_unknown::<LinkRecordsInput>(&fixtures[6].1)?;
refuses_unknown::<UnlinkRecordsInput>(&fixtures[7].1)?;
refuses_unknown::<CreateBranchInput>(&fixtures[8].1)?;
refuses_unknown::<ReviewBranchInput>(&fixtures[9].1)?;
refuses_unknown::<MergeBranchInput>(&fixtures[10].1)
}
#[test]
fn relation_metadata_preserves_a_known_relationship_count() -> TestResult {
let text = r#"{"relation":"links","from_collection":"records","to_collection":"records","cardinality":"many_to_many","relationships":0}"#;
let relation: RelationInfo = serde_json::from_str(text)?;
assert_eq!(relation.relationships, Some(0));
assert_eq!(relation.relationships_reason, None);
let encoded = serde_json::to_value(&relation)?;
assert_eq!(encoded["relationships"], 0);
assert!(encoded.get("relationships_reason").is_none());
Ok(())
}
#[test]
fn relation_metadata_preserves_each_named_unknown_count_reason() -> TestResult {
for (name, expected) in [
("withheld", RelationCountReason::Withheld),
("counts_not_ready", RelationCountReason::CountsNotReady),
] {
let text = format!(
r#"{{"relation":"links","from_collection":"records","to_collection":"records","cardinality":"many_to_many","relationships":null,"relationships_reason":"{name}"}}"#
);
let relation: RelationInfo = serde_json::from_str(&text)?;
assert_eq!(relation.relationships, None);
assert_eq!(relation.relationships_reason, Some(expected));
let encoded = serde_json::to_value(&relation)?;
assert!(encoded["relationships"].is_null());
assert_eq!(encoded["relationships_reason"], name);
}
Ok(())
}
#[test]
fn version_uses_decimal_strings_without_rounding() -> TestResult {
let text = r#"{"generation":"18446744073709551615","sequence":"9007199254740993"}"#;
let version: Version = serde_json::from_str(text)?;
assert_eq!(version.0.generation, u64::MAX);
assert_eq!(version.0.sequence, 9_007_199_254_740_993);
assert_eq!(serde_json::to_string(&version)?, text);
for invalid in [
r#"{"generation":1,"sequence":"2"}"#,
r#"{"generation":"1","sequence":2}"#,
r#"{"generation":"18446744073709551616","sequence":"2"}"#,
r#"{"generation":"1","sequence":"1e2"}"#,
r#"{"generation":"1","sequence":"-1"}"#,
r#"{"generation":"1","sequence":"2","extra":true}"#,
] {
assert!(
serde_json::from_str::<Version>(invalid).is_err(),
"{invalid}"
);
}
Ok(())
}
#[test]
fn a_record_retains_its_number_spelling_through_input_and_output() -> TestResult {
for body in [
r#"{"n":1e2}"#,
r#"{ "n" : 1E+02, "zero": -0, "fraction": 1.2300 }"#,
r#"{"big":18446744073709551616001,"nested":[1e-09,0.00]}"#,
] {
let text = format!(
r#"{{{WRITE},"collection":"records","records":[{{"document_id":"a","document":{body}}}]}}"#
);
let input: PutRecordsInput = serde_json::from_str(&text)?;
assert_eq!(input.records[0].document.get(), body);
let encoded = serde_json::to_string(&input)?;
let again: PutRecordsInput = serde_json::from_str(&encoded)?;
assert_eq!(again.records[0].document.get(), body);
let output_text = format!(
r#"{{"selection":{SELECTED},"rows":{{"entries":[{{"value":{body},"origin":null}}],"next_after":null}}}}"#
);
let output: RunQueryOutput = serde_json::from_str(&output_text)?;
let encoded = serde_json::to_string(&output)?;
let again: RunQueryOutput = serde_json::from_str(&encoded)?;
assert_eq!(again.rows.entries[0].value.get(), body);
let value_text = format!(r#"{{"kind":"value","value":{body}}}"#);
let value: super::ReviewValue = serde_json::from_str(&value_text)?;
let encoded = serde_json::to_string(&value)?;
let again: super::ReviewValue = serde_json::from_str(&encoded)?;
let super::ReviewValue::Value { value } = again else {
return Err("present review value decoded as absent".into());
};
assert_eq!(value.get(), body);
}
for text in [
r#"{"kind":"absent"}"#,
r#"{"kind":"value","value":null}"#,
r#"{"kind":"value","value":""}"#,
] {
let value: super::ReviewValue = serde_json::from_str(text)?;
assert_eq!(serde_json::to_string(&value)?, text);
}
for invalid in [
r#"{"kind":"absent","value":null}"#,
r#"{"kind":"value"}"#,
r#"{"kind":"absent","extra":true}"#,
r#"{"kind":"other"}"#,
] {
assert!(serde_json::from_str::<super::ReviewValue>(invalid).is_err());
}
Ok(())
}
#[test]
fn empty_imports_and_zero_page_limits_are_refused() {
let empty = format!(r#"{{{WRITE},"collection":"records","records":[]}}"#);
assert!(serde_json::from_str::<PutRecordsInput>(&empty).is_err());
for page in [r#"{"rows":0,"bytes":4096}"#, r#"{"rows":10,"bytes":0}"#] {
assert!(serde_json::from_str::<PageLimits>(page).is_err());
}
}
#[test]
fn a_write_never_defaults_its_branch() -> TestResult {
let fixture = inputs();
for (name, input) in &fixture[3..9] {
let mut value: Value = serde_json::from_str(input)?;
value
.as_object_mut()
.ok_or("input is not an object")?
.remove("branch");
assert!(value.get("branch").is_none(), "{name}");
let encoded = serde_json::to_string(&value)?;
let refused = match *name {
"define_collection" => {
serde_json::from_str::<DefineCollectionInput>(&encoded).is_err()
}
"put_records" => serde_json::from_str::<PutRecordsInput>(&encoded).is_err(),
"delete_record" => serde_json::from_str::<DeleteRecordInput>(&encoded).is_err(),
"link_records" => serde_json::from_str::<LinkRecordsInput>(&encoded).is_err(),
"unlink_records" => serde_json::from_str::<UnlinkRecordsInput>(&encoded).is_err(),
"create_branch" => serde_json::from_str::<CreateBranchInput>(&encoded).is_err(),
_ => return Err("unexpected writing tool fixture".into()),
};
assert!(refused, "{name}");
}
Ok(())
}
#[test]
fn a_collection_object_keeps_its_original_text() -> TestResult {
use super::CollectionShape;
let text = r#"{ "type":"object", "minimum":1e2, "maximum":18446744073709551616001 }"#;
let shape: CollectionShape = serde_json::from_str(text)?;
assert_eq!(serde_json::to_string(&shape)?, text);
for invalid in ["[]", r#""object""#, "123"] {
assert!(serde_json::from_str::<CollectionShape>(invalid).is_err());
}
Ok(())
}
}