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 pub authors: Vec<HistoryAuthor>,
215 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub stage: Option<HistoryStage>,
219 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub merged: Option<HistoryMerged>,
224}
225
226#[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#[derive(Debug, Serialize, Deserialize, JsonSchema)]
239#[serde(deny_unknown_fields)]
240pub struct HistoryAt {
241 pub branch: String,
242 pub version: Version,
243}
244
245#[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#[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#[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#[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 pub request_id: String,
343 pub branch: String,
345 pub expected: Version,
347 pub name: String,
349 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 pub request_id: String,
390 pub branch: String,
392 pub expected: Version,
394 pub from: String,
396 pub reviewed: Version,
398}
399
400#[derive(Debug, Serialize, Deserialize, JsonSchema)]
401#[serde(deny_unknown_fields)]
402pub struct MergeBranchOutput {
403 pub branch: String,
405 pub version: Version,
407 pub request_id: String,
409}
410
411#[derive(Debug, Serialize, Deserialize, JsonSchema)]
412#[serde(deny_unknown_fields)]
413pub struct CreateBranchOutput {
414 pub branch: String,
416 pub version: Version,
418 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}