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