ag_ui/patch.rs
1//! JSON Patch operations, as defined by [RFC 6902].
2//!
3//! `STATE_DELTA` and `ACTIVITY_DELTA` carry a patch document: an array of these
4//! operations, applied in order to the previous snapshot.
5//!
6//! [RFC 6902]: https://datatracker.ietf.org/doc/html/rfc6902
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11/// A JSON Patch document: an ordered list of operations.
12pub type JsonPatch = Vec<PatchOperation>;
13
14/// A single [RFC 6902] operation.
15///
16/// The serde representation is the RFC wire format exactly: an object with an
17/// `op` discriminator, a `path` JSON Pointer, and — depending on the operation
18/// — a `value` or a `from` pointer.
19///
20/// # Why `value` has a default
21///
22/// Upstream types both patch fields as `z.array(z.any())` / `List[Any]` and
23/// validates nothing, so a producer that drops `value` still parses there. The
24/// case is not hypothetical: `JSON.stringify({op: "add", path: "/x", value:
25/// undefined})` yields `{"op":"add","path":"/x"}`, which is what a JavaScript
26/// producer emits whenever the new state holds `undefined` at that key. Making
27/// `value` required would turn that into a deserialization failure for the whole
28/// `STATE_DELTA` event — and in an SSE stream a failed event is usually a failed
29/// run. An omitted `value` therefore reads as JSON `null`, which is how the
30/// JavaScript patch libraries apply it, and re-serializes explicitly as `null`.
31///
32/// Everything else about the operation stays strictly typed: an unrecognized
33/// `op` is still rejected, because the six RFC operations are the whole
34/// vocabulary and a seventh is a producer bug worth surfacing rather than
35/// carrying silently to an applier that cannot execute it.
36///
37/// ```
38/// # use ag_ui::PatchOperation;
39/// let op = PatchOperation::Replace {
40/// path: "/counter".into(),
41/// value: serde_json::json!(2),
42/// };
43/// assert_eq!(
44/// serde_json::to_string(&op).unwrap(),
45/// r#"{"op":"replace","path":"/counter","value":2}"#
46/// );
47/// ```
48///
49/// [RFC 6902]: https://datatracker.ietf.org/doc/html/rfc6902
50#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
51#[serde(tag = "op", rename_all = "lowercase")]
52#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
53#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
54pub enum PatchOperation {
55 /// Inserts `value` at `path`, shifting array elements right when `path`
56 /// ends in an array index or `-`.
57 Add {
58 /// JSON Pointer to the location to add.
59 path: String,
60 /// The value to insert. An omitted `value` reads as `null`; see the
61 /// type-level docs.
62 #[serde(default)]
63 value: Value,
64 },
65
66 /// Removes the value at `path`.
67 Remove {
68 /// JSON Pointer to the location to remove.
69 path: String,
70 },
71
72 /// Replaces the value at `path`, which must already exist.
73 Replace {
74 /// JSON Pointer to the location to overwrite.
75 path: String,
76 /// The replacement value. An omitted `value` reads as `null`.
77 #[serde(default)]
78 value: Value,
79 },
80
81 /// Moves the value at `from` to `path`.
82 Move {
83 /// JSON Pointer to the source location.
84 from: String,
85 /// JSON Pointer to the destination.
86 path: String,
87 },
88
89 /// Copies the value at `from` to `path`.
90 Copy {
91 /// JSON Pointer to the source location.
92 from: String,
93 /// JSON Pointer to the destination.
94 path: String,
95 },
96
97 /// Asserts that the value at `path` equals `value`; a failed test aborts
98 /// the whole patch.
99 Test {
100 /// JSON Pointer to the location to test.
101 path: String,
102 /// The value the location is expected to hold. An omitted `value` reads
103 /// as `null`.
104 #[serde(default)]
105 value: Value,
106 },
107}
108
109impl PatchOperation {
110 /// Builds an [`Add`](PatchOperation::Add) operation.
111 pub fn add(path: impl Into<String>, value: impl Into<Value>) -> Self {
112 Self::Add {
113 path: path.into(),
114 value: value.into(),
115 }
116 }
117
118 /// Builds a [`Remove`](PatchOperation::Remove) operation.
119 pub fn remove(path: impl Into<String>) -> Self {
120 Self::Remove { path: path.into() }
121 }
122
123 /// Builds a [`Replace`](PatchOperation::Replace) operation.
124 pub fn replace(path: impl Into<String>, value: impl Into<Value>) -> Self {
125 Self::Replace {
126 path: path.into(),
127 value: value.into(),
128 }
129 }
130
131 /// Builds a [`Move`](PatchOperation::Move) operation.
132 pub fn mv(from: impl Into<String>, path: impl Into<String>) -> Self {
133 Self::Move {
134 from: from.into(),
135 path: path.into(),
136 }
137 }
138
139 /// Builds a [`Copy`](PatchOperation::Copy) operation.
140 pub fn copy(from: impl Into<String>, path: impl Into<String>) -> Self {
141 Self::Copy {
142 from: from.into(),
143 path: path.into(),
144 }
145 }
146
147 /// Builds a [`Test`](PatchOperation::Test) operation.
148 pub fn test(path: impl Into<String>, value: impl Into<Value>) -> Self {
149 Self::Test {
150 path: path.into(),
151 value: value.into(),
152 }
153 }
154
155 /// The `op` string as it appears on the wire.
156 pub const fn op(&self) -> &'static str {
157 match self {
158 Self::Add { .. } => "add",
159 Self::Remove { .. } => "remove",
160 Self::Replace { .. } => "replace",
161 Self::Move { .. } => "move",
162 Self::Copy { .. } => "copy",
163 Self::Test { .. } => "test",
164 }
165 }
166
167 /// The JSON Pointer this operation targets.
168 pub fn path(&self) -> &str {
169 match self {
170 Self::Add { path, .. }
171 | Self::Remove { path }
172 | Self::Replace { path, .. }
173 | Self::Move { path, .. }
174 | Self::Copy { path, .. }
175 | Self::Test { path, .. } => path,
176 }
177 }
178
179 /// The source pointer of a `move` or `copy`, or `None` for other operations.
180 pub fn from(&self) -> Option<&str> {
181 match self {
182 Self::Move { from, .. } | Self::Copy { from, .. } => Some(from),
183 _ => None,
184 }
185 }
186
187 /// The payload of an `add`, `replace` or `test`, or `None` for other
188 /// operations.
189 pub const fn value(&self) -> Option<&Value> {
190 match self {
191 Self::Add { value, .. } | Self::Replace { value, .. } | Self::Test { value, .. } => {
192 Some(value)
193 }
194 _ => None,
195 }
196 }
197}