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}
216
217#[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#[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 pub request_id: String,
298 pub branch: String,
300 pub expected: Version,
302 pub name: String,
304 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 pub request_id: String,
345 pub branch: String,
347 pub expected: Version,
349 pub from: String,
351 pub reviewed: Version,
353}
354
355#[derive(Debug, Serialize, Deserialize, JsonSchema)]
356#[serde(deny_unknown_fields)]
357pub struct MergeBranchOutput {
358 pub branch: String,
360 pub version: Version,
362 pub request_id: String,
364}
365
366#[derive(Debug, Serialize, Deserialize, JsonSchema)]
367#[serde(deny_unknown_fields)]
368pub struct CreateBranchOutput {
369 pub branch: String,
371 pub version: Version,
373 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}