Skip to main content

haematite_cli/mcp/
types.rs

1use std::collections::BTreeMap;
2use std::num::{NonZeroU32, NonZeroU64};
3
4use schemars::JsonSchema;
5use serde::{Deserialize, Deserializer, Serialize, de::Error};
6use serde_json::value::RawValue;
7
8use crate::serve::protocol;
9
10#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
11#[serde(transparent)]
12pub struct Version(#[schemars(with = "VersionShape")] pub protocol::Version);
13
14#[derive(Serialize, JsonSchema)]
15#[serde(deny_unknown_fields)]
16struct VersionShape {
17    generation: String,
18    sequence: String,
19}
20
21#[derive(Debug, Serialize, Deserialize, JsonSchema)]
22#[serde(deny_unknown_fields)]
23pub struct Selection {
24    pub branch: String,
25    pub version: Option<Version>,
26}
27
28#[derive(Debug, Serialize, Deserialize, JsonSchema)]
29#[serde(deny_unknown_fields)]
30pub struct Snapshot {
31    pub branch: String,
32    pub version: Version,
33}
34
35#[derive(Debug, Serialize, Deserialize, JsonSchema)]
36#[serde(deny_unknown_fields)]
37pub struct PageLimits {
38    #[schemars(range(min = 1, max = u32::MAX))]
39    pub rows: NonZeroU32,
40    pub bytes: NonZeroU64,
41}
42
43#[derive(Debug, Serialize, Deserialize, JsonSchema)]
44#[serde(deny_unknown_fields)]
45pub struct Page<T> {
46    pub entries: Vec<T>,
47    pub next_after: Option<String>,
48}
49
50#[derive(Debug, Serialize, Deserialize, JsonSchema)]
51#[serde(deny_unknown_fields)]
52pub struct RecordRef {
53    pub collection: String,
54    pub document_id: String,
55}
56
57#[derive(Debug, Serialize, Deserialize, JsonSchema)]
58#[serde(deny_unknown_fields)]
59pub struct RecordWrite {
60    pub document_id: String,
61    pub document: Box<RawValue>,
62}
63
64#[derive(Debug, Serialize, Deserialize, JsonSchema)]
65#[serde(deny_unknown_fields)]
66pub struct RecordValue {
67    pub record: RecordRef,
68    pub document: Box<RawValue>,
69}
70
71#[derive(Debug, Serialize, JsonSchema)]
72#[serde(untagged)]
73pub enum CollectionShape {
74    Remove,
75    Boolean(bool),
76    Rules(#[schemars(with = "BTreeMap<String, Box<RawValue>>")] Box<RawValue>),
77}
78
79impl<'de> Deserialize<'de> for CollectionShape {
80    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
81        let raw = Box::<RawValue>::deserialize(deserializer)?;
82        match raw.get() {
83            "null" => Ok(Self::Remove),
84            "true" => Ok(Self::Boolean(true)),
85            "false" => Ok(Self::Boolean(false)),
86            text if text.starts_with('{') => Ok(Self::Rules(raw)),
87            _ => Err(D::Error::custom(
88                "schema must be null, a boolean, or an object",
89            )),
90        }
91    }
92}
93
94#[derive(Debug, Serialize, Deserialize, JsonSchema)]
95#[serde(deny_unknown_fields)]
96pub struct CollectionInfo {
97    pub name: String,
98    pub schema: CollectionShape,
99}
100
101#[derive(Debug, Serialize, Deserialize, JsonSchema)]
102#[serde(rename_all = "snake_case")]
103pub enum Cardinality {
104    OneToOne,
105    OneToMany,
106    ManyToMany,
107}
108
109#[derive(Debug, Serialize, Deserialize, JsonSchema)]
110#[serde(deny_unknown_fields)]
111pub struct RelationInfo {
112    pub relation: String,
113    pub from_collection: String,
114    pub to_collection: String,
115    pub cardinality: Cardinality,
116    pub relationships: Option<u64>,
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub relationships_reason: Option<RelationCountReason>,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
122#[serde(rename_all = "snake_case")]
123pub enum RelationCountReason {
124    Withheld,
125    CountsNotReady,
126}
127
128#[derive(Debug, Serialize, Deserialize, JsonSchema)]
129#[serde(deny_unknown_fields)]
130pub struct VectorField {
131    pub collection: String,
132    pub field: String,
133    pub dimensions: NonZeroU64,
134    pub count: Option<u64>,
135}
136
137#[derive(Debug, Serialize, Deserialize, JsonSchema)]
138#[serde(deny_unknown_fields)]
139pub struct Counts {
140    pub collections: u64,
141    pub documents: u64,
142    pub edges: u64,
143    pub vectors: u64,
144}
145
146#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)]
147#[serde(deny_unknown_fields)]
148pub struct DescribeCursors {
149    pub branches: Option<String>,
150    pub collections: Option<String>,
151    pub relations: Option<String>,
152    pub vector_fields: Option<String>,
153}
154
155#[derive(Debug, Serialize, Deserialize, JsonSchema)]
156#[serde(deny_unknown_fields)]
157pub struct DescribeStoreInput {
158    pub selection: Selection,
159    pub page: PageLimits,
160    pub after: DescribeCursors,
161}
162
163#[derive(Debug, Serialize, Deserialize, JsonSchema)]
164#[serde(deny_unknown_fields)]
165pub struct DescribeStoreOutput {
166    pub selection: Snapshot,
167    pub branches: Page<Snapshot>,
168    pub collections: Page<CollectionInfo>,
169    pub relations: Page<RelationInfo>,
170    pub vector_fields: Page<VectorField>,
171    pub counts: Counts,
172}
173
174#[derive(Debug, Serialize, Deserialize, JsonSchema)]
175#[serde(deny_unknown_fields)]
176pub struct RunQueryInput {
177    pub selection: Selection,
178    pub query: String,
179    #[serde(default)]
180    pub parameters: BTreeMap<String, Box<RawValue>>,
181    pub page: PageLimits,
182    pub after: Option<String>,
183}
184
185#[derive(Debug, Serialize, Deserialize, JsonSchema)]
186#[serde(deny_unknown_fields)]
187pub struct QueryRow {
188    pub value: Box<RawValue>,
189    pub origin: Option<RecordRef>,
190}
191
192#[derive(Debug, Serialize, Deserialize, JsonSchema)]
193#[serde(deny_unknown_fields)]
194pub struct RunQueryOutput {
195    pub selection: Snapshot,
196    pub rows: Page<QueryRow>,
197}
198
199#[derive(Debug, Serialize, Deserialize, JsonSchema)]
200#[serde(deny_unknown_fields)]
201pub struct HistoryRoot {
202    pub shard: usize,
203    pub root: String,
204}
205
206#[derive(Debug, Serialize, Deserialize, JsonSchema)]
207#[serde(deny_unknown_fields)]
208pub struct HistoryEntry {
209    pub branch: String,
210    pub version: Version,
211    pub roots: Vec<HistoryRoot>,
212}
213
214#[derive(Debug, Serialize, Deserialize, JsonSchema)]
215#[serde(deny_unknown_fields)]
216pub struct ReadHistoryInput {
217    pub selection: Selection,
218    pub page: PageLimits,
219    pub after: Option<String>,
220    pub record: Option<RecordRef>,
221}
222
223#[derive(Debug, Serialize, Deserialize, JsonSchema)]
224#[serde(deny_unknown_fields)]
225pub struct ReadHistoryOutput {
226    pub selection: Snapshot,
227    pub history: Page<HistoryEntry>,
228    pub record: Option<RecordValue>,
229}
230
231macro_rules! writing_input {
232    ($name:ident { $($(#[$attribute:meta])* $field:ident: $ty:ty),* $(,)? }) => {
233        #[derive(Debug, Serialize, Deserialize, JsonSchema)]
234        #[serde(deny_unknown_fields)]
235        pub struct $name {
236            pub request_id: String,
237            pub branch: String,
238            pub expected: Version,
239            $( $(#[$attribute])* pub $field: $ty, )*
240        }
241    };
242}
243
244writing_input!(DefineCollectionInput {
245    collection: String,
246    schema: CollectionShape,
247});
248writing_input!(PutRecordsInput {
249    collection: String,
250    #[serde(deserialize_with = "nonempty_records")]
251    #[schemars(length(min = 1))]
252    records: Vec<RecordWrite>,
253});
254writing_input!(DeleteRecordInput { record: RecordRef });
255writing_input!(LinkRecordsInput {
256    relation: String,
257    from: RecordRef,
258    to: RecordRef,
259    properties: BTreeMap<String, Box<RawValue>>,
260});
261writing_input!(UnlinkRecordsInput {
262    relation: String,
263    from: RecordRef,
264    to: RecordRef,
265});
266#[derive(Debug, Serialize, Deserialize, JsonSchema)]
267#[serde(deny_unknown_fields)]
268pub struct CreateBranchInput {
269    /// Mutation identifier echoed by the receipt for reconciliation.
270    pub request_id: String,
271    /// Source branch whose current head guards the fork.
272    pub branch: String,
273    /// Expected current head of the source branch, checked before the fork.
274    pub expected: Version,
275    /// Name of the new destination branch.
276    pub name: String,
277    /// Retained source version from which the destination forks.
278    pub from: Version,
279}
280
281fn nonempty_records<'de, D: Deserializer<'de>>(
282    deserializer: D,
283) -> Result<Vec<RecordWrite>, D::Error> {
284    let records = Vec::<RecordWrite>::deserialize(deserializer)?;
285    if records.is_empty() {
286        return Err(D::Error::custom("records must contain at least one record"));
287    }
288    Ok(records)
289}
290
291macro_rules! writing_output {
292    ($($name:ident),* $(,)?) => {
293        $(
294            #[derive(Debug, Serialize, Deserialize, JsonSchema)]
295            #[serde(deny_unknown_fields)]
296            pub struct $name {
297                pub branch: String,
298                pub version: Version,
299                pub request_id: String,
300            }
301        )*
302    };
303}
304
305writing_output!(
306    DefineCollectionOutput,
307    PutRecordsOutput,
308    DeleteRecordOutput,
309    LinkRecordsOutput,
310    UnlinkRecordsOutput,
311);
312
313#[derive(Debug, Serialize, Deserialize, JsonSchema)]
314#[serde(deny_unknown_fields)]
315pub struct MergeBranchInput {
316    /// Mutation identifier echoed by the receipt for reconciliation.
317    pub request_id: String,
318    /// Branch merged into, whose current head guards the merge.
319    pub branch: String,
320    /// Expected current head of `branch`, checked before the merge.
321    pub expected: Version,
322    /// Reviewed branch merged in; it is left as it is.
323    pub from: String,
324    /// Version of `from` that `review_branch` read; any other is refused.
325    pub reviewed: Version,
326}
327
328#[derive(Debug, Serialize, Deserialize, JsonSchema)]
329#[serde(deny_unknown_fields)]
330pub struct MergeBranchOutput {
331    /// Branch merged into.
332    pub branch: String,
333    /// Version of `branch` that carries the merge and its receipt.
334    pub version: Version,
335    /// Mutation identifier echoed from the request for reconciliation.
336    pub request_id: String,
337}
338
339#[derive(Debug, Serialize, Deserialize, JsonSchema)]
340#[serde(deny_unknown_fields)]
341pub struct CreateBranchOutput {
342    /// New destination branch, matching the requested name.
343    pub branch: String,
344    /// Version of the newly created destination branch.
345    pub version: Version,
346    /// Mutation identifier echoed from the request for reconciliation.
347    pub request_id: String,
348}
349
350#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)]
351#[serde(deny_unknown_fields)]
352pub struct ReviewCursors {
353    pub diff: Option<String>,
354    pub counts: Option<String>,
355    pub conflicts: Option<String>,
356    pub dangling: Option<String>,
357}
358
359#[derive(Debug, Serialize, Deserialize, JsonSchema)]
360#[serde(deny_unknown_fields)]
361pub struct ReviewBranchInput {
362    pub selection: Selection,
363    pub page: PageLimits,
364    pub after: ReviewCursors,
365}
366
367#[derive(Debug, Serialize, Deserialize, JsonSchema)]
368#[serde(rename_all = "snake_case")]
369pub enum DifferenceKind {
370    Added,
371    Changed,
372    Removed,
373}
374
375#[derive(Debug, Serialize, Deserialize, JsonSchema)]
376#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
377pub enum ReviewOwner {
378    Collection {
379        collection: String,
380    },
381    Document {
382        record: RecordRef,
383    },
384    Relation {
385        relation: String,
386    },
387    Edge {
388        relation: String,
389        from: RecordRef,
390        to: RecordRef,
391    },
392    Vector {
393        record: RecordRef,
394        field: String,
395    },
396    VectorField {
397        collection: String,
398        field: String,
399    },
400}
401
402#[derive(Debug, Serialize, JsonSchema)]
403#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
404pub enum ReviewValue {
405    Absent,
406    Value { value: Box<RawValue> },
407}
408
409impl<'de> Deserialize<'de> for ReviewValue {
410    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
411        #[derive(Deserialize)]
412        #[serde(rename_all = "snake_case")]
413        enum Kind {
414            Absent,
415            Value,
416        }
417
418        #[derive(Deserialize)]
419        #[serde(deny_unknown_fields)]
420        struct Encoded {
421            kind: Kind,
422            #[serde(default, deserialize_with = "present_review_value")]
423            value: Option<Box<RawValue>>,
424        }
425
426        let encoded = Encoded::deserialize(deserializer)?;
427        match (encoded.kind, encoded.value) {
428            (Kind::Absent, None) => Ok(Self::Absent),
429            (Kind::Value, Some(value)) => Ok(Self::Value { value }),
430            (Kind::Absent, Some(_)) => {
431                Err(D::Error::custom("an absent review value has a payload"))
432            }
433            (Kind::Value, None) => Err(D::Error::custom("a present review value has no payload")),
434        }
435    }
436}
437
438fn present_review_value<'de, D: Deserializer<'de>>(
439    deserializer: D,
440) -> Result<Option<Box<RawValue>>, D::Error> {
441    Box::<RawValue>::deserialize(deserializer).map(Some)
442}
443
444#[derive(Debug, Serialize, Deserialize, JsonSchema)]
445#[serde(deny_unknown_fields)]
446pub struct ReviewLine {
447    pub kind: DifferenceKind,
448    pub owner: ReviewOwner,
449    pub before: ReviewValue,
450    pub after: ReviewValue,
451}
452
453#[derive(Debug, Serialize, Deserialize, JsonSchema)]
454#[serde(deny_unknown_fields)]
455pub struct ReviewCounts {
456    pub changes: u64,
457    pub conflicts: u64,
458}
459
460#[derive(Debug, Serialize, Deserialize, JsonSchema)]
461#[serde(deny_unknown_fields)]
462pub struct ReviewCountPage {
463    pub partial: ReviewCounts,
464    pub next_after: Option<String>,
465}
466
467#[derive(Debug, Serialize, Deserialize, JsonSchema)]
468#[serde(rename_all = "snake_case")]
469pub enum ConflictState {
470    SourceOnly,
471    SameOnBoth,
472    Conflict,
473}
474
475#[derive(Debug, Serialize, Deserialize, JsonSchema)]
476#[serde(deny_unknown_fields)]
477pub struct ConflictLine {
478    pub owner: ReviewOwner,
479    pub ancestor: ReviewValue,
480    pub target: ReviewValue,
481    pub source: ReviewValue,
482    pub state: ConflictState,
483    #[serde(skip_serializing_if = "Option::is_none")]
484    pub choice_token: Option<String>,
485}
486
487#[derive(Debug, Serialize, Deserialize, JsonSchema)]
488#[serde(rename_all = "snake_case")]
489pub enum MissingEnd {
490    From,
491    To,
492}
493
494#[derive(Debug, Serialize, Deserialize, JsonSchema)]
495#[serde(rename_all = "snake_case")]
496pub enum RemovedBy {
497    Source,
498    Target,
499}
500
501#[derive(Debug, Serialize, Deserialize, JsonSchema)]
502#[serde(rename_all = "snake_case")]
503pub enum DanglingState {
504    Dangling,
505    DependsOnChoice,
506}
507
508#[derive(Debug, Serialize, Deserialize, JsonSchema)]
509#[serde(deny_unknown_fields)]
510pub struct DanglingLine {
511    pub choice_token: String,
512    pub relation: String,
513    pub from: RecordRef,
514    pub to: RecordRef,
515    pub missing_end: MissingEnd,
516    pub removed_by: RemovedBy,
517    pub state: DanglingState,
518}
519
520#[derive(Debug, Serialize, Deserialize, JsonSchema)]
521#[serde(deny_unknown_fields)]
522pub struct ReviewBranchOutput {
523    pub selection: Snapshot,
524    pub from: Snapshot,
525    pub target: Snapshot,
526    pub diff: Page<ReviewLine>,
527    pub counts: ReviewCountPage,
528    pub conflicts: Page<ConflictLine>,
529    pub dangling: Page<DanglingLine>,
530    pub base_moved: Option<u64>,
531}
532
533#[cfg(test)]
534pub(super) mod tests {
535    use std::error::Error;
536
537    use serde::de::DeserializeOwned;
538    use serde_json::{Value, json};
539
540    use super::{
541        CreateBranchInput, DefineCollectionInput, DeleteRecordInput, DescribeStoreInput,
542        LinkRecordsInput, MergeBranchInput, PageLimits, PutRecordsInput, ReadHistoryInput,
543        RelationCountReason, RelationInfo, ReviewBranchInput, RunQueryInput, RunQueryOutput,
544        UnlinkRecordsInput, Version,
545    };
546
547    type TestResult = Result<(), Box<dyn Error>>;
548
549    const SELECTED: &str = r#"{"branch":"feature","version":{"generation":"1","sequence":"2"}}"#;
550    const WRITE: &str =
551        r#""request_id":"r1","branch":"feature","expected":{"generation":"1","sequence":"2"}"#;
552
553    pub(in crate::mcp) fn inputs() -> Vec<(&'static str, String)> {
554        vec![
555            (
556                "describe_store",
557                format!(
558                    r#"{{"selection":{SELECTED},"page":{{"rows":10,"bytes":4096}},"after":{{}}}}"#
559                ),
560            ),
561            (
562                "run_query",
563                format!(
564                    r#"{{"selection":{SELECTED},"query":"records | '.'","page":{{"rows":10,"bytes":4096}}}}"#
565                ),
566            ),
567            (
568                "read_history",
569                format!(r#"{{"selection":{SELECTED},"page":{{"rows":10,"bytes":4096}}}}"#),
570            ),
571            (
572                "define_collection",
573                format!(r#"{{{WRITE},"collection":"records","schema":{{"type":"object"}}}}"#),
574            ),
575            (
576                "put_records",
577                format!(
578                    r#"{{{WRITE},"collection":"records","records":[{{"document_id":"a","document":{{"n":1e2}}}}]}}"#
579                ),
580            ),
581            (
582                "delete_record",
583                format!(r#"{{{WRITE},"record":{{"collection":"records","document_id":"a"}}}}"#),
584            ),
585            (
586                "link_records",
587                format!(
588                    r#"{{{WRITE},"relation":"links","from":{{"collection":"records","document_id":"a"}},"to":{{"collection":"records","document_id":"b"}},"properties":{{}}}}"#
589                ),
590            ),
591            (
592                "unlink_records",
593                format!(
594                    r#"{{{WRITE},"relation":"links","from":{{"collection":"records","document_id":"a"}},"to":{{"collection":"records","document_id":"b"}}}}"#
595                ),
596            ),
597            (
598                "create_branch",
599                format!(r#"{{{WRITE},"name":"new","from":{{"generation":"1","sequence":"2"}}}}"#),
600            ),
601            (
602                "review_branch",
603                format!(
604                    r#"{{"selection":{SELECTED},"page":{{"rows":10,"bytes":4096}},"after":{{}}}}"#
605                ),
606            ),
607            (
608                "merge_branch",
609                format!(
610                    r#"{{{WRITE},"from":"import","reviewed":{{"generation":"3","sequence":"4"}}}}"#
611                ),
612            ),
613        ]
614    }
615
616    fn refuses_unknown<T: DeserializeOwned>(input: &str) -> TestResult {
617        let _: T = serde_json::from_str(input)?;
618        let mut value: Value = serde_json::from_str(input)?;
619        value
620            .as_object_mut()
621            .ok_or("input is not an object")?
622            .insert("caller".to_owned(), json!({"subject":"forged"}));
623        let error = serde_json::from_str::<T>(&serde_json::to_string(&value)?)
624            .err()
625            .ok_or("unknown caller member accepted")?;
626        assert!(error.to_string().contains("unknown field"), "{error}");
627        Ok(())
628    }
629
630    #[test]
631    fn every_tool_input_refuses_unknown_members() -> TestResult {
632        let fixtures = inputs();
633        refuses_unknown::<DescribeStoreInput>(&fixtures[0].1)?;
634        refuses_unknown::<RunQueryInput>(&fixtures[1].1)?;
635        refuses_unknown::<ReadHistoryInput>(&fixtures[2].1)?;
636        refuses_unknown::<DefineCollectionInput>(&fixtures[3].1)?;
637        refuses_unknown::<PutRecordsInput>(&fixtures[4].1)?;
638        refuses_unknown::<DeleteRecordInput>(&fixtures[5].1)?;
639        refuses_unknown::<LinkRecordsInput>(&fixtures[6].1)?;
640        refuses_unknown::<UnlinkRecordsInput>(&fixtures[7].1)?;
641        refuses_unknown::<CreateBranchInput>(&fixtures[8].1)?;
642        refuses_unknown::<ReviewBranchInput>(&fixtures[9].1)?;
643        refuses_unknown::<MergeBranchInput>(&fixtures[10].1)
644    }
645
646    #[test]
647    fn relation_metadata_preserves_a_known_relationship_count() -> TestResult {
648        let text = r#"{"relation":"links","from_collection":"records","to_collection":"records","cardinality":"many_to_many","relationships":0}"#;
649        let relation: RelationInfo = serde_json::from_str(text)?;
650        assert_eq!(relation.relationships, Some(0));
651        assert_eq!(relation.relationships_reason, None);
652        let encoded = serde_json::to_value(&relation)?;
653        assert_eq!(encoded["relationships"], 0);
654        assert!(encoded.get("relationships_reason").is_none());
655        Ok(())
656    }
657
658    #[test]
659    fn relation_metadata_preserves_each_named_unknown_count_reason() -> TestResult {
660        for (name, expected) in [
661            ("withheld", RelationCountReason::Withheld),
662            ("counts_not_ready", RelationCountReason::CountsNotReady),
663        ] {
664            let text = format!(
665                r#"{{"relation":"links","from_collection":"records","to_collection":"records","cardinality":"many_to_many","relationships":null,"relationships_reason":"{name}"}}"#
666            );
667            let relation: RelationInfo = serde_json::from_str(&text)?;
668            assert_eq!(relation.relationships, None);
669            assert_eq!(relation.relationships_reason, Some(expected));
670            let encoded = serde_json::to_value(&relation)?;
671            assert!(encoded["relationships"].is_null());
672            assert_eq!(encoded["relationships_reason"], name);
673        }
674        Ok(())
675    }
676
677    #[test]
678    fn version_uses_decimal_strings_without_rounding() -> TestResult {
679        let text = r#"{"generation":"18446744073709551615","sequence":"9007199254740993"}"#;
680        let version: Version = serde_json::from_str(text)?;
681        assert_eq!(version.0.generation, u64::MAX);
682        assert_eq!(version.0.sequence, 9_007_199_254_740_993);
683        assert_eq!(serde_json::to_string(&version)?, text);
684        for invalid in [
685            r#"{"generation":1,"sequence":"2"}"#,
686            r#"{"generation":"1","sequence":2}"#,
687            r#"{"generation":"18446744073709551616","sequence":"2"}"#,
688            r#"{"generation":"1","sequence":"1e2"}"#,
689            r#"{"generation":"1","sequence":"-1"}"#,
690            r#"{"generation":"1","sequence":"2","extra":true}"#,
691        ] {
692            assert!(
693                serde_json::from_str::<Version>(invalid).is_err(),
694                "{invalid}"
695            );
696        }
697        Ok(())
698    }
699
700    #[test]
701    fn a_record_retains_its_number_spelling_through_input_and_output() -> TestResult {
702        for body in [
703            r#"{"n":1e2}"#,
704            r#"{ "n" : 1E+02, "zero": -0, "fraction": 1.2300 }"#,
705            r#"{"big":18446744073709551616001,"nested":[1e-09,0.00]}"#,
706        ] {
707            let text = format!(
708                r#"{{{WRITE},"collection":"records","records":[{{"document_id":"a","document":{body}}}]}}"#
709            );
710            let input: PutRecordsInput = serde_json::from_str(&text)?;
711            assert_eq!(input.records[0].document.get(), body);
712            let encoded = serde_json::to_string(&input)?;
713            let again: PutRecordsInput = serde_json::from_str(&encoded)?;
714            assert_eq!(again.records[0].document.get(), body);
715            let output_text = format!(
716                r#"{{"selection":{SELECTED},"rows":{{"entries":[{{"value":{body},"origin":null}}],"next_after":null}}}}"#
717            );
718            let output: RunQueryOutput = serde_json::from_str(&output_text)?;
719            let encoded = serde_json::to_string(&output)?;
720            let again: RunQueryOutput = serde_json::from_str(&encoded)?;
721            assert_eq!(again.rows.entries[0].value.get(), body);
722            let value_text = format!(r#"{{"kind":"value","value":{body}}}"#);
723            let value: super::ReviewValue = serde_json::from_str(&value_text)?;
724            let encoded = serde_json::to_string(&value)?;
725            let again: super::ReviewValue = serde_json::from_str(&encoded)?;
726            let super::ReviewValue::Value { value } = again else {
727                return Err("present review value decoded as absent".into());
728            };
729            assert_eq!(value.get(), body);
730        }
731        for text in [
732            r#"{"kind":"absent"}"#,
733            r#"{"kind":"value","value":null}"#,
734            r#"{"kind":"value","value":""}"#,
735        ] {
736            let value: super::ReviewValue = serde_json::from_str(text)?;
737            assert_eq!(serde_json::to_string(&value)?, text);
738        }
739        for invalid in [
740            r#"{"kind":"absent","value":null}"#,
741            r#"{"kind":"value"}"#,
742            r#"{"kind":"absent","extra":true}"#,
743            r#"{"kind":"other"}"#,
744        ] {
745            assert!(serde_json::from_str::<super::ReviewValue>(invalid).is_err());
746        }
747        Ok(())
748    }
749
750    #[test]
751    fn empty_imports_and_zero_page_limits_are_refused() {
752        let empty = format!(r#"{{{WRITE},"collection":"records","records":[]}}"#);
753        assert!(serde_json::from_str::<PutRecordsInput>(&empty).is_err());
754        for page in [r#"{"rows":0,"bytes":4096}"#, r#"{"rows":10,"bytes":0}"#] {
755            assert!(serde_json::from_str::<PageLimits>(page).is_err());
756        }
757    }
758
759    #[test]
760    fn a_write_never_defaults_its_branch() -> TestResult {
761        let fixture = inputs();
762        for (name, input) in &fixture[3..9] {
763            let mut value: Value = serde_json::from_str(input)?;
764            value
765                .as_object_mut()
766                .ok_or("input is not an object")?
767                .remove("branch");
768            assert!(value.get("branch").is_none(), "{name}");
769            let encoded = serde_json::to_string(&value)?;
770            let refused = match *name {
771                "define_collection" => {
772                    serde_json::from_str::<DefineCollectionInput>(&encoded).is_err()
773                }
774                "put_records" => serde_json::from_str::<PutRecordsInput>(&encoded).is_err(),
775                "delete_record" => serde_json::from_str::<DeleteRecordInput>(&encoded).is_err(),
776                "link_records" => serde_json::from_str::<LinkRecordsInput>(&encoded).is_err(),
777                "unlink_records" => serde_json::from_str::<UnlinkRecordsInput>(&encoded).is_err(),
778                "create_branch" => serde_json::from_str::<CreateBranchInput>(&encoded).is_err(),
779                _ => return Err("unexpected writing tool fixture".into()),
780            };
781            assert!(refused, "{name}");
782        }
783        Ok(())
784    }
785    #[test]
786    fn a_collection_object_keeps_its_original_text() -> TestResult {
787        use super::CollectionShape;
788
789        let text = r#"{ "type":"object", "minimum":1e2, "maximum":18446744073709551616001 }"#;
790        let shape: CollectionShape = serde_json::from_str(text)?;
791        assert_eq!(serde_json::to_string(&shape)?, text);
792        for invalid in ["[]", r#""object""#, "123"] {
793            assert!(serde_json::from_str::<CollectionShape>(invalid).is_err());
794        }
795        Ok(())
796    }
797}