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    /// Who made each request written at this version, as the store recorded
213    /// it (ACCESS-003 R4); empty for a version no served request wrote.
214    pub authors: Vec<HistoryAuthor>,
215}
216
217/// One request written at a version and the caller the store recorded as
218/// making it: the shape `history.list` answers.
219#[derive(Debug, Serialize, Deserialize, JsonSchema)]
220#[serde(deny_unknown_fields)]
221pub struct HistoryAuthor {
222    pub request_id: String,
223    pub caller: HistoryCaller,
224}
225
226/// The author the store established, never one a request named.
227///
228/// On a store with no Lys the issuer is `local` and the subject the login
229/// name of the store's owner; otherwise they are the pass's issuer and
230/// holder, with `responsible`, the person an agent answers to, when an agent
231/// made the request.
232#[derive(Debug, Serialize, Deserialize, JsonSchema)]
233#[serde(deny_unknown_fields)]
234pub struct HistoryCaller {
235    pub issuer: String,
236    pub subject: String,
237    #[serde(default, skip_serializing_if = "Option::is_none")]
238    pub responsible: Option<String>,
239}
240
241#[derive(Debug, Serialize, Deserialize, JsonSchema)]
242#[serde(deny_unknown_fields)]
243pub struct ReadHistoryInput {
244    pub selection: Selection,
245    pub page: PageLimits,
246    pub after: Option<String>,
247    pub record: Option<RecordRef>,
248}
249
250#[derive(Debug, Serialize, Deserialize, JsonSchema)]
251#[serde(deny_unknown_fields)]
252pub struct ReadHistoryOutput {
253    pub selection: Snapshot,
254    pub history: Page<HistoryEntry>,
255    pub record: Option<RecordValue>,
256}
257
258macro_rules! writing_input {
259    ($name:ident { $($(#[$attribute:meta])* $field:ident: $ty:ty),* $(,)? }) => {
260        #[derive(Debug, Serialize, Deserialize, JsonSchema)]
261        #[serde(deny_unknown_fields)]
262        pub struct $name {
263            pub request_id: String,
264            pub branch: String,
265            pub expected: Version,
266            $( $(#[$attribute])* pub $field: $ty, )*
267        }
268    };
269}
270
271writing_input!(DefineCollectionInput {
272    collection: String,
273    schema: CollectionShape,
274});
275writing_input!(PutRecordsInput {
276    collection: String,
277    #[serde(deserialize_with = "nonempty_records")]
278    #[schemars(length(min = 1))]
279    records: Vec<RecordWrite>,
280});
281writing_input!(DeleteRecordInput { record: RecordRef });
282writing_input!(LinkRecordsInput {
283    relation: String,
284    from: RecordRef,
285    to: RecordRef,
286    properties: BTreeMap<String, Box<RawValue>>,
287});
288writing_input!(UnlinkRecordsInput {
289    relation: String,
290    from: RecordRef,
291    to: RecordRef,
292});
293#[derive(Debug, Serialize, Deserialize, JsonSchema)]
294#[serde(deny_unknown_fields)]
295pub struct CreateBranchInput {
296    /// Mutation identifier echoed by the receipt for reconciliation.
297    pub request_id: String,
298    /// Source branch whose current head guards the fork.
299    pub branch: String,
300    /// Expected current head of the source branch, checked before the fork.
301    pub expected: Version,
302    /// Name of the new destination branch.
303    pub name: String,
304    /// Retained source version from which the destination forks.
305    pub from: Version,
306}
307
308fn nonempty_records<'de, D: Deserializer<'de>>(
309    deserializer: D,
310) -> Result<Vec<RecordWrite>, D::Error> {
311    let records = Vec::<RecordWrite>::deserialize(deserializer)?;
312    if records.is_empty() {
313        return Err(D::Error::custom("records must contain at least one record"));
314    }
315    Ok(records)
316}
317
318macro_rules! writing_output {
319    ($($name:ident),* $(,)?) => {
320        $(
321            #[derive(Debug, Serialize, Deserialize, JsonSchema)]
322            #[serde(deny_unknown_fields)]
323            pub struct $name {
324                pub branch: String,
325                pub version: Version,
326                pub request_id: String,
327            }
328        )*
329    };
330}
331
332writing_output!(
333    DefineCollectionOutput,
334    PutRecordsOutput,
335    DeleteRecordOutput,
336    LinkRecordsOutput,
337    UnlinkRecordsOutput,
338);
339
340#[derive(Debug, Serialize, Deserialize, JsonSchema)]
341#[serde(deny_unknown_fields)]
342pub struct MergeBranchInput {
343    /// Mutation identifier echoed by the receipt for reconciliation.
344    pub request_id: String,
345    /// Branch merged into, whose current head guards the merge.
346    pub branch: String,
347    /// Expected current head of `branch`, checked before the merge.
348    pub expected: Version,
349    /// Reviewed branch merged in; it is left as it is.
350    pub from: String,
351    /// Version of `from` that `review_branch` read; any other is refused.
352    pub reviewed: Version,
353}
354
355#[derive(Debug, Serialize, Deserialize, JsonSchema)]
356#[serde(deny_unknown_fields)]
357pub struct MergeBranchOutput {
358    /// Branch merged into.
359    pub branch: String,
360    /// Version of `branch` that carries the merge and its receipt.
361    pub version: Version,
362    /// Mutation identifier echoed from the request for reconciliation.
363    pub request_id: String,
364}
365
366#[derive(Debug, Serialize, Deserialize, JsonSchema)]
367#[serde(deny_unknown_fields)]
368pub struct CreateBranchOutput {
369    /// New destination branch, matching the requested name.
370    pub branch: String,
371    /// Version of the newly created destination branch.
372    pub version: Version,
373    /// Mutation identifier echoed from the request for reconciliation.
374    pub request_id: String,
375}
376
377#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)]
378#[serde(deny_unknown_fields)]
379pub struct ReviewCursors {
380    pub diff: Option<String>,
381    pub counts: Option<String>,
382    pub conflicts: Option<String>,
383    pub dangling: Option<String>,
384}
385
386#[derive(Debug, Serialize, Deserialize, JsonSchema)]
387#[serde(deny_unknown_fields)]
388pub struct ReviewBranchInput {
389    pub selection: Selection,
390    pub page: PageLimits,
391    pub after: ReviewCursors,
392}
393
394#[derive(Debug, Serialize, Deserialize, JsonSchema)]
395#[serde(rename_all = "snake_case")]
396pub enum DifferenceKind {
397    Added,
398    Changed,
399    Removed,
400}
401
402#[derive(Debug, Serialize, Deserialize, JsonSchema)]
403#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
404pub enum ReviewOwner {
405    Collection {
406        collection: String,
407    },
408    Document {
409        record: RecordRef,
410    },
411    Relation {
412        relation: String,
413    },
414    Edge {
415        relation: String,
416        from: RecordRef,
417        to: RecordRef,
418    },
419    Vector {
420        record: RecordRef,
421        field: String,
422    },
423    VectorField {
424        collection: String,
425        field: String,
426    },
427}
428
429#[derive(Debug, Serialize, JsonSchema)]
430#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
431pub enum ReviewValue {
432    Absent,
433    Value { value: Box<RawValue> },
434}
435
436impl<'de> Deserialize<'de> for ReviewValue {
437    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
438        #[derive(Deserialize)]
439        #[serde(rename_all = "snake_case")]
440        enum Kind {
441            Absent,
442            Value,
443        }
444
445        #[derive(Deserialize)]
446        #[serde(deny_unknown_fields)]
447        struct Encoded {
448            kind: Kind,
449            #[serde(default, deserialize_with = "present_review_value")]
450            value: Option<Box<RawValue>>,
451        }
452
453        let encoded = Encoded::deserialize(deserializer)?;
454        match (encoded.kind, encoded.value) {
455            (Kind::Absent, None) => Ok(Self::Absent),
456            (Kind::Value, Some(value)) => Ok(Self::Value { value }),
457            (Kind::Absent, Some(_)) => {
458                Err(D::Error::custom("an absent review value has a payload"))
459            }
460            (Kind::Value, None) => Err(D::Error::custom("a present review value has no payload")),
461        }
462    }
463}
464
465fn present_review_value<'de, D: Deserializer<'de>>(
466    deserializer: D,
467) -> Result<Option<Box<RawValue>>, D::Error> {
468    Box::<RawValue>::deserialize(deserializer).map(Some)
469}
470
471#[derive(Debug, Serialize, Deserialize, JsonSchema)]
472#[serde(deny_unknown_fields)]
473pub struct ReviewLine {
474    pub kind: DifferenceKind,
475    pub owner: ReviewOwner,
476    pub before: ReviewValue,
477    pub after: ReviewValue,
478}
479
480#[derive(Debug, Serialize, Deserialize, JsonSchema)]
481#[serde(deny_unknown_fields)]
482pub struct ReviewCounts {
483    pub changes: u64,
484    pub conflicts: u64,
485}
486
487#[derive(Debug, Serialize, Deserialize, JsonSchema)]
488#[serde(deny_unknown_fields)]
489pub struct ReviewCountPage {
490    pub partial: ReviewCounts,
491    pub next_after: Option<String>,
492}
493
494#[derive(Debug, Serialize, Deserialize, JsonSchema)]
495#[serde(rename_all = "snake_case")]
496pub enum ConflictState {
497    SourceOnly,
498    SameOnBoth,
499    Conflict,
500}
501
502#[derive(Debug, Serialize, Deserialize, JsonSchema)]
503#[serde(deny_unknown_fields)]
504pub struct ConflictLine {
505    pub owner: ReviewOwner,
506    pub ancestor: ReviewValue,
507    pub target: ReviewValue,
508    pub source: ReviewValue,
509    pub state: ConflictState,
510    #[serde(skip_serializing_if = "Option::is_none")]
511    pub choice_token: Option<String>,
512}
513
514#[derive(Debug, Serialize, Deserialize, JsonSchema)]
515#[serde(rename_all = "snake_case")]
516pub enum MissingEnd {
517    From,
518    To,
519}
520
521#[derive(Debug, Serialize, Deserialize, JsonSchema)]
522#[serde(rename_all = "snake_case")]
523pub enum RemovedBy {
524    Source,
525    Target,
526}
527
528#[derive(Debug, Serialize, Deserialize, JsonSchema)]
529#[serde(rename_all = "snake_case")]
530pub enum DanglingState {
531    Dangling,
532    DependsOnChoice,
533}
534
535#[derive(Debug, Serialize, Deserialize, JsonSchema)]
536#[serde(deny_unknown_fields)]
537pub struct DanglingLine {
538    pub choice_token: String,
539    pub relation: String,
540    pub from: RecordRef,
541    pub to: RecordRef,
542    pub missing_end: MissingEnd,
543    pub removed_by: RemovedBy,
544    pub state: DanglingState,
545}
546
547#[derive(Debug, Serialize, Deserialize, JsonSchema)]
548#[serde(deny_unknown_fields)]
549pub struct ReviewBranchOutput {
550    pub selection: Snapshot,
551    pub from: Snapshot,
552    pub target: Snapshot,
553    pub diff: Page<ReviewLine>,
554    pub counts: ReviewCountPage,
555    pub conflicts: Page<ConflictLine>,
556    pub dangling: Page<DanglingLine>,
557    pub base_moved: Option<u64>,
558}
559
560#[cfg(test)]
561pub(super) mod tests {
562    use std::error::Error;
563
564    use serde::de::DeserializeOwned;
565    use serde_json::{Value, json};
566
567    use super::{
568        CreateBranchInput, DefineCollectionInput, DeleteRecordInput, DescribeStoreInput,
569        LinkRecordsInput, MergeBranchInput, PageLimits, PutRecordsInput, ReadHistoryInput,
570        RelationCountReason, RelationInfo, ReviewBranchInput, RunQueryInput, RunQueryOutput,
571        UnlinkRecordsInput, Version,
572    };
573
574    type TestResult = Result<(), Box<dyn Error>>;
575
576    const SELECTED: &str = r#"{"branch":"feature","version":{"generation":"1","sequence":"2"}}"#;
577    const WRITE: &str =
578        r#""request_id":"r1","branch":"feature","expected":{"generation":"1","sequence":"2"}"#;
579
580    pub(in crate::mcp) fn inputs() -> Vec<(&'static str, String)> {
581        vec![
582            (
583                "describe_store",
584                format!(
585                    r#"{{"selection":{SELECTED},"page":{{"rows":10,"bytes":4096}},"after":{{}}}}"#
586                ),
587            ),
588            (
589                "run_query",
590                format!(
591                    r#"{{"selection":{SELECTED},"query":"records | '.'","page":{{"rows":10,"bytes":4096}}}}"#
592                ),
593            ),
594            (
595                "read_history",
596                format!(r#"{{"selection":{SELECTED},"page":{{"rows":10,"bytes":4096}}}}"#),
597            ),
598            (
599                "define_collection",
600                format!(r#"{{{WRITE},"collection":"records","schema":{{"type":"object"}}}}"#),
601            ),
602            (
603                "put_records",
604                format!(
605                    r#"{{{WRITE},"collection":"records","records":[{{"document_id":"a","document":{{"n":1e2}}}}]}}"#
606                ),
607            ),
608            (
609                "delete_record",
610                format!(r#"{{{WRITE},"record":{{"collection":"records","document_id":"a"}}}}"#),
611            ),
612            (
613                "link_records",
614                format!(
615                    r#"{{{WRITE},"relation":"links","from":{{"collection":"records","document_id":"a"}},"to":{{"collection":"records","document_id":"b"}},"properties":{{}}}}"#
616                ),
617            ),
618            (
619                "unlink_records",
620                format!(
621                    r#"{{{WRITE},"relation":"links","from":{{"collection":"records","document_id":"a"}},"to":{{"collection":"records","document_id":"b"}}}}"#
622                ),
623            ),
624            (
625                "create_branch",
626                format!(r#"{{{WRITE},"name":"new","from":{{"generation":"1","sequence":"2"}}}}"#),
627            ),
628            (
629                "review_branch",
630                format!(
631                    r#"{{"selection":{SELECTED},"page":{{"rows":10,"bytes":4096}},"after":{{}}}}"#
632                ),
633            ),
634            (
635                "merge_branch",
636                format!(
637                    r#"{{{WRITE},"from":"import","reviewed":{{"generation":"3","sequence":"4"}}}}"#
638                ),
639            ),
640        ]
641    }
642
643    fn refuses_unknown<T: DeserializeOwned>(input: &str) -> TestResult {
644        let _: T = serde_json::from_str(input)?;
645        let mut value: Value = serde_json::from_str(input)?;
646        value
647            .as_object_mut()
648            .ok_or("input is not an object")?
649            .insert("caller".to_owned(), json!({"subject":"forged"}));
650        let error = serde_json::from_str::<T>(&serde_json::to_string(&value)?)
651            .err()
652            .ok_or("unknown caller member accepted")?;
653        assert!(error.to_string().contains("unknown field"), "{error}");
654        Ok(())
655    }
656
657    #[test]
658    fn every_tool_input_refuses_unknown_members() -> TestResult {
659        let fixtures = inputs();
660        refuses_unknown::<DescribeStoreInput>(&fixtures[0].1)?;
661        refuses_unknown::<RunQueryInput>(&fixtures[1].1)?;
662        refuses_unknown::<ReadHistoryInput>(&fixtures[2].1)?;
663        refuses_unknown::<DefineCollectionInput>(&fixtures[3].1)?;
664        refuses_unknown::<PutRecordsInput>(&fixtures[4].1)?;
665        refuses_unknown::<DeleteRecordInput>(&fixtures[5].1)?;
666        refuses_unknown::<LinkRecordsInput>(&fixtures[6].1)?;
667        refuses_unknown::<UnlinkRecordsInput>(&fixtures[7].1)?;
668        refuses_unknown::<CreateBranchInput>(&fixtures[8].1)?;
669        refuses_unknown::<ReviewBranchInput>(&fixtures[9].1)?;
670        refuses_unknown::<MergeBranchInput>(&fixtures[10].1)
671    }
672
673    #[test]
674    fn relation_metadata_preserves_a_known_relationship_count() -> TestResult {
675        let text = r#"{"relation":"links","from_collection":"records","to_collection":"records","cardinality":"many_to_many","relationships":0}"#;
676        let relation: RelationInfo = serde_json::from_str(text)?;
677        assert_eq!(relation.relationships, Some(0));
678        assert_eq!(relation.relationships_reason, None);
679        let encoded = serde_json::to_value(&relation)?;
680        assert_eq!(encoded["relationships"], 0);
681        assert!(encoded.get("relationships_reason").is_none());
682        Ok(())
683    }
684
685    #[test]
686    fn relation_metadata_preserves_each_named_unknown_count_reason() -> TestResult {
687        for (name, expected) in [
688            ("withheld", RelationCountReason::Withheld),
689            ("counts_not_ready", RelationCountReason::CountsNotReady),
690        ] {
691            let text = format!(
692                r#"{{"relation":"links","from_collection":"records","to_collection":"records","cardinality":"many_to_many","relationships":null,"relationships_reason":"{name}"}}"#
693            );
694            let relation: RelationInfo = serde_json::from_str(&text)?;
695            assert_eq!(relation.relationships, None);
696            assert_eq!(relation.relationships_reason, Some(expected));
697            let encoded = serde_json::to_value(&relation)?;
698            assert!(encoded["relationships"].is_null());
699            assert_eq!(encoded["relationships_reason"], name);
700        }
701        Ok(())
702    }
703
704    #[test]
705    fn version_uses_decimal_strings_without_rounding() -> TestResult {
706        let text = r#"{"generation":"18446744073709551615","sequence":"9007199254740993"}"#;
707        let version: Version = serde_json::from_str(text)?;
708        assert_eq!(version.0.generation, u64::MAX);
709        assert_eq!(version.0.sequence, 9_007_199_254_740_993);
710        assert_eq!(serde_json::to_string(&version)?, text);
711        for invalid in [
712            r#"{"generation":1,"sequence":"2"}"#,
713            r#"{"generation":"1","sequence":2}"#,
714            r#"{"generation":"18446744073709551616","sequence":"2"}"#,
715            r#"{"generation":"1","sequence":"1e2"}"#,
716            r#"{"generation":"1","sequence":"-1"}"#,
717            r#"{"generation":"1","sequence":"2","extra":true}"#,
718        ] {
719            assert!(
720                serde_json::from_str::<Version>(invalid).is_err(),
721                "{invalid}"
722            );
723        }
724        Ok(())
725    }
726
727    #[test]
728    fn a_record_retains_its_number_spelling_through_input_and_output() -> TestResult {
729        for body in [
730            r#"{"n":1e2}"#,
731            r#"{ "n" : 1E+02, "zero": -0, "fraction": 1.2300 }"#,
732            r#"{"big":18446744073709551616001,"nested":[1e-09,0.00]}"#,
733        ] {
734            let text = format!(
735                r#"{{{WRITE},"collection":"records","records":[{{"document_id":"a","document":{body}}}]}}"#
736            );
737            let input: PutRecordsInput = serde_json::from_str(&text)?;
738            assert_eq!(input.records[0].document.get(), body);
739            let encoded = serde_json::to_string(&input)?;
740            let again: PutRecordsInput = serde_json::from_str(&encoded)?;
741            assert_eq!(again.records[0].document.get(), body);
742            let output_text = format!(
743                r#"{{"selection":{SELECTED},"rows":{{"entries":[{{"value":{body},"origin":null}}],"next_after":null}}}}"#
744            );
745            let output: RunQueryOutput = serde_json::from_str(&output_text)?;
746            let encoded = serde_json::to_string(&output)?;
747            let again: RunQueryOutput = serde_json::from_str(&encoded)?;
748            assert_eq!(again.rows.entries[0].value.get(), body);
749            let value_text = format!(r#"{{"kind":"value","value":{body}}}"#);
750            let value: super::ReviewValue = serde_json::from_str(&value_text)?;
751            let encoded = serde_json::to_string(&value)?;
752            let again: super::ReviewValue = serde_json::from_str(&encoded)?;
753            let super::ReviewValue::Value { value } = again else {
754                return Err("present review value decoded as absent".into());
755            };
756            assert_eq!(value.get(), body);
757        }
758        for text in [
759            r#"{"kind":"absent"}"#,
760            r#"{"kind":"value","value":null}"#,
761            r#"{"kind":"value","value":""}"#,
762        ] {
763            let value: super::ReviewValue = serde_json::from_str(text)?;
764            assert_eq!(serde_json::to_string(&value)?, text);
765        }
766        for invalid in [
767            r#"{"kind":"absent","value":null}"#,
768            r#"{"kind":"value"}"#,
769            r#"{"kind":"absent","extra":true}"#,
770            r#"{"kind":"other"}"#,
771        ] {
772            assert!(serde_json::from_str::<super::ReviewValue>(invalid).is_err());
773        }
774        Ok(())
775    }
776
777    #[test]
778    fn empty_imports_and_zero_page_limits_are_refused() {
779        let empty = format!(r#"{{{WRITE},"collection":"records","records":[]}}"#);
780        assert!(serde_json::from_str::<PutRecordsInput>(&empty).is_err());
781        for page in [r#"{"rows":0,"bytes":4096}"#, r#"{"rows":10,"bytes":0}"#] {
782            assert!(serde_json::from_str::<PageLimits>(page).is_err());
783        }
784    }
785
786    #[test]
787    fn a_write_never_defaults_its_branch() -> TestResult {
788        let fixture = inputs();
789        for (name, input) in &fixture[3..9] {
790            let mut value: Value = serde_json::from_str(input)?;
791            value
792                .as_object_mut()
793                .ok_or("input is not an object")?
794                .remove("branch");
795            assert!(value.get("branch").is_none(), "{name}");
796            let encoded = serde_json::to_string(&value)?;
797            let refused = match *name {
798                "define_collection" => {
799                    serde_json::from_str::<DefineCollectionInput>(&encoded).is_err()
800                }
801                "put_records" => serde_json::from_str::<PutRecordsInput>(&encoded).is_err(),
802                "delete_record" => serde_json::from_str::<DeleteRecordInput>(&encoded).is_err(),
803                "link_records" => serde_json::from_str::<LinkRecordsInput>(&encoded).is_err(),
804                "unlink_records" => serde_json::from_str::<UnlinkRecordsInput>(&encoded).is_err(),
805                "create_branch" => serde_json::from_str::<CreateBranchInput>(&encoded).is_err(),
806                _ => return Err("unexpected writing tool fixture".into()),
807            };
808            assert!(refused, "{name}");
809        }
810        Ok(())
811    }
812    #[test]
813    fn a_collection_object_keeps_its_original_text() -> TestResult {
814        use super::CollectionShape;
815
816        let text = r#"{ "type":"object", "minimum":1e2, "maximum":18446744073709551616001 }"#;
817        let shape: CollectionShape = serde_json::from_str(text)?;
818        assert_eq!(serde_json::to_string(&shape)?, text);
819        for invalid in ["[]", r#""object""#, "123"] {
820            assert!(serde_json::from_str::<CollectionShape>(invalid).is_err());
821        }
822        Ok(())
823    }
824}