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