1use anyhow::{bail, Context, Result};
9
10use crate::aap::{
11 Artifact, EditOp, Envelope, HandleContentItem, Name, OpType, Meta, SynthesizeContentItem,
12 Target, TargetInfo, PROTOCOL_VERSION,
13};
14
15pub trait Resolve {
19 type Content: Clone;
20
21 fn find_by_id(&self, content: &Self::Content, id: &str) -> Result<(usize, usize)>;
22 fn find_by_id_inclusive(&self, content: &Self::Content, id: &str) -> Result<(usize, usize)>;
23 fn find_by_pointer(&self, content: &Self::Content, pointer: &str) -> Result<(usize, usize)>;
24 fn replace(&self, content: &mut Self::Content, start: usize, end: usize, replacement: &str);
25 fn insert(&self, content: &mut Self::Content, pos: usize, text: &str);
26 fn delete(&self, content: &mut Self::Content, start: usize, end: usize);
27 fn to_string(&self, content: &Self::Content) -> String;
28 fn from_string(&self, s: &str) -> Self::Content;
29}
30
31pub struct TextResolver {
35 pub format: String,
36}
37
38impl Resolve for TextResolver {
39 type Content = String;
40
41 fn find_by_id(&self, content: &String, id: &str) -> Result<(usize, usize)> {
42 crate::markers::find_target_range(content, id, &self.format)
43 }
44
45 fn find_by_id_inclusive(&self, content: &String, id: &str) -> Result<(usize, usize)> {
46 crate::markers::find_target_range_inclusive(content, id, &self.format)
47 }
48
49 fn find_by_pointer(&self, content: &String, pointer: &str) -> Result<(usize, usize)> {
50 let value: serde_json::Value = serde_json::from_str(content)
51 .context("pointer targeting requires valid JSON content")?;
52 let serialized = serde_json::to_string_pretty(&value)?;
53 let _ = value
54 .pointer(pointer)
55 .with_context(|| format!("pointer not found: {pointer}"))?;
56 Ok((0, serialized.len()))
57 }
58
59 fn replace(&self, content: &mut String, start: usize, end: usize, replacement: &str) {
60 *content = format!("{}{}{}", &content[..start], replacement, &content[end..]);
61 }
62
63 fn insert(&self, content: &mut String, pos: usize, text: &str) {
64 *content = format!("{}{}{}", &content[..pos], text, &content[pos..]);
65 }
66
67 fn delete(&self, content: &mut String, start: usize, end: usize) {
68 *content = format!("{}{}", &content[..start], &content[end..]);
69 }
70
71 fn to_string(&self, content: &String) -> String {
72 content.clone()
73 }
74
75 fn from_string(&self, s: &str) -> String {
76 s.to_string()
77 }
78}
79
80fn extract_synthesize_item(envelope: &Envelope) -> Result<SynthesizeContentItem> {
83 serde_json::from_value(
84 envelope
85 .content
86 .first()
87 .context("synthesize: empty content array")?
88 .clone(),
89 )
90 .context("synthesize: failed to parse content item")
91}
92
93fn build_handle_envelope(artifact: &Artifact) -> Result<Envelope> {
94 let target_ids = crate::markers::extract_targets(&artifact.body, &artifact.format);
95 let targets = if target_ids.is_empty() {
96 None
97 } else {
98 Some(target_ids.into_iter().map(|id| TargetInfo {
99 id,
100 label: None,
101 accepts: None,
102 }).collect())
103 };
104 let handle = HandleContentItem {
105 id: artifact.id.clone(),
106 version: artifact.version,
107 token_count: Some(artifact.body.len() as u64 / 4), state: None,
109 content: None,
110 targets,
111 };
112 Ok(Envelope {
113 protocol: PROTOCOL_VERSION.to_string(),
114 id: artifact.id.clone(),
115 version: artifact.version,
116 name: Name::Handle,
117 meta: Meta {
118 format: Some(artifact.format.clone()),
119 tokens_used: None,
120 checksum: None,
121 state: None,
122 },
123 content: vec![
124 serde_json::to_value(handle).context("failed to serialize handle")?
125 ],
126 })
127}
128
129pub fn apply(artifact: Option<&Artifact>, envelope: &Envelope) -> Result<(Artifact, Envelope)> {
131 let format = envelope
132 .meta
133 .format
134 .as_deref()
135 .unwrap_or("text/html");
136
137 let resolver = TextResolver {
138 format: format.to_string(),
139 };
140
141 let result_artifact = match envelope.name {
142 Name::Synthesize => {
143 let item = extract_synthesize_item(envelope)?;
144 Artifact {
145 id: envelope.id.clone(),
146 version: envelope.version,
147 format: format.to_string(),
148 body: item.body,
149 }
150 }
151 Name::Edit => {
152 let art = artifact.context("edit requires a base artifact")?;
153 let ops: Vec<EditOp> = envelope
154 .content
155 .iter()
156 .map(|v| serde_json::from_value(v.clone()))
157 .collect::<std::result::Result<Vec<_>, _>>()
158 .context("edit: failed to parse content items")?;
159
160 let has_pointer = ops.iter().any(|op| matches!(op.target, Target::Pointer(_)));
161 let body = if has_pointer {
162 apply_edit_pointers(&art.body, &ops)?
163 } else {
164 apply_edit(&resolver, &art.body, &ops)?
165 };
166
167 Artifact {
168 id: envelope.id.clone(),
169 version: envelope.version,
170 format: format.to_string(),
171 body,
172 }
173 }
174 Name::Handle => {
175 bail!("handle is an output envelope, not an input operation")
176 }
177 };
178
179 let handle = build_handle_envelope(&result_artifact)?;
180 Ok((result_artifact, handle))
181}
182
183pub fn apply_edit<R: Resolve<Content = String>>(
185 resolver: &R,
186 base: &str,
187 operations: &[EditOp],
188) -> Result<String> {
189 let mut content = resolver.from_string(base);
190
191 for (i, op) in operations.iter().enumerate() {
192 let (start, end) = resolve_target(resolver, &content, &op.target)
195 .with_context(|| format!("operation {i}: target not found"))?;
196 match op.op {
197 OpType::Replace => {
198 let replacement = op.content.as_deref().unwrap_or("");
199 resolver.replace(&mut content, start, end, replacement);
200 }
201 OpType::Delete => {
202 resolver.delete(&mut content, start, end);
203 }
204 OpType::InsertBefore => {
205 let text = op.content.as_deref().unwrap_or("");
206 resolver.insert(&mut content, start, text);
207 }
208 OpType::InsertAfter => {
209 let text = op.content.as_deref().unwrap_or("");
210 resolver.insert(&mut content, end, text);
211 }
212 }
213 }
214
215 Ok(resolver.to_string(&content))
216}
217
218fn resolve_target<R: Resolve<Content = String>>(
219 resolver: &R,
220 content: &String,
221 target: &Target,
222) -> Result<(usize, usize)> {
223 match target {
224 Target::Id(id) => resolver.find_by_id(content, id),
225 Target::Pointer(pointer) => resolver.find_by_pointer(content, pointer),
226 }
227}
228
229fn apply_edit_pointers(base: &str, operations: &[EditOp]) -> Result<String> {
231 let mut value: serde_json::Value =
232 serde_json::from_str(base).context("pointer targeting requires valid JSON content")?;
233
234 for (i, op) in operations.iter().enumerate() {
235 let pointer = match &op.target {
236 Target::Pointer(p) => p.as_str(),
237 _ => bail!("operation {i}: expected pointer target"),
238 };
239
240 match op.op {
241 OpType::Replace => {
242 let content = op.content.as_deref().context("replace requires content")?;
243 let new_val: serde_json::Value =
244 serde_json::from_str(content).context("content must be valid JSON")?;
245 let target = value
246 .pointer_mut(pointer)
247 .with_context(|| format!("pointer not found: {pointer}"))?;
248 *target = new_val;
249 }
250 OpType::Delete => {
251 let (parent_ptr, key) = split_pointer(pointer).context("cannot delete root")?;
252 let parent = value
253 .pointer_mut(&parent_ptr)
254 .with_context(|| format!("parent not found: {parent_ptr}"))?;
255 remove_child(parent, &key)?;
256 }
257 OpType::InsertBefore | OpType::InsertAfter => {
258 let content = op.content.as_deref().context("insert requires content")?;
259 let new_val: serde_json::Value =
260 serde_json::from_str(content).context("content must be valid JSON")?;
261 let (parent_ptr, key) = split_pointer(pointer).context("cannot insert at root")?;
262 let parent = value
263 .pointer_mut(&parent_ptr)
264 .with_context(|| format!("parent not found: {parent_ptr}"))?;
265 let arr = parent
266 .as_array_mut()
267 .context("insert requires array parent")?;
268 let index: usize = key.parse().context("insert requires numeric array index")?;
269 let insert_at = if op.op == OpType::InsertAfter { index + 1 } else { index };
270 arr.insert(insert_at, new_val);
271 }
272 }
273 }
274
275 serde_json::to_string_pretty(&value).context("failed to re-serialize JSON")
276}
277
278fn split_pointer(pointer: &str) -> Result<(String, String)> {
279 if pointer.is_empty() || !pointer.starts_with('/') {
280 bail!("invalid JSON Pointer: {pointer:?}");
281 }
282 match pointer.rfind('/') {
283 Some(0) => Ok(("".to_string(), pointer[1..].to_string())),
284 Some(pos) => Ok((pointer[..pos].to_string(), pointer[pos + 1..].to_string())),
285 None => bail!("invalid JSON Pointer: {pointer:?}"),
286 }
287}
288
289fn remove_child(parent: &mut serde_json::Value, key: &str) -> Result<()> {
290 let unescaped = key.replace("~1", "/").replace("~0", "~");
291 if let Some(obj) = parent.as_object_mut() {
292 if obj.remove(&unescaped).is_none() {
293 bail!("key not found: {unescaped}");
294 }
295 } else if let Some(arr) = parent.as_array_mut() {
296 let index: usize = unescaped.parse().with_context(|| format!("expected array index: {unescaped}"))?;
297 if index >= arr.len() { bail!("array index out of bounds: {index}"); }
298 arr.remove(index);
299 } else {
300 bail!("parent is neither object nor array");
301 }
302 Ok(())
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 fn synth_env(id: &str, version: u64, body: &str) -> Envelope {
310 Envelope {
311 protocol: PROTOCOL_VERSION.to_string(),
312 id: id.to_string(),
313 version,
314 name: Name::Synthesize,
315 meta: Meta {
316 format: Some("text/html".to_string()),
317 tokens_used: None, checksum: None, state: None,
318 },
319 content: vec![serde_json::json!({ "body": body })],
320 }
321 }
322
323 fn edit_env(id: &str, version: u64, ops: Vec<EditOp>) -> Envelope {
324 Envelope {
325 protocol: PROTOCOL_VERSION.to_string(),
326 id: id.to_string(),
327 version,
328 name: Name::Edit,
329 meta: Meta {
330 format: Some("text/html".to_string()),
331 tokens_used: None, checksum: None, state: None,
332 },
333 content: ops.iter().map(|o| serde_json::to_value(o).unwrap()).collect(),
334 }
335 }
336
337 fn id_target(id: &str) -> Target { Target::Id(id.to_string()) }
338 fn ptr_target(p: &str) -> Target { Target::Pointer(p.to_string()) }
339
340 #[test]
341 fn test_synthesize() {
342 let env = synth_env("test", 1, "<div>hello</div>");
343 let (art, handle) = apply(None, &env).unwrap();
344 assert_eq!(art.body, "<div>hello</div>");
345 assert_eq!(art.id, "test");
346 assert_eq!(art.version, 1);
347 assert_eq!(handle.name, Name::Handle);
348 }
349
350 #[test]
351 fn test_edit_replace_by_id() {
352 let env = synth_env("t", 1, r#"<aap:target id="rev">$12,340</aap:target>"#);
353 let (art, _) = apply(None, &env).unwrap();
354
355 let edit = edit_env("t", 2, vec![EditOp {
356 op: OpType::Replace,
357 target: id_target("rev"),
358 content: Some("$15,720".to_string()),
359 }]);
360 let (art2, _) = apply(Some(&art), &edit).unwrap();
361 assert!(art2.body.contains("$15,720"));
362 assert!(!art2.body.contains("$12,340"));
363 assert!(art2.body.contains(r#"<aap:target id="rev">"#));
364 }
365
366 #[test]
367 fn test_edit_delete_by_id() {
368 let env = synth_env("t", 1, r#"before<aap:target id="tmp">remove</aap:target>after"#);
372 let (art, _) = apply(None, &env).unwrap();
373
374 let edit = edit_env("t", 2, vec![EditOp {
375 op: OpType::Delete, target: id_target("tmp"), content: None,
376 }]);
377 let (art2, _) = apply(Some(&art), &edit).unwrap();
378 assert_eq!(art2.body, r#"before<aap:target id="tmp"></aap:target>after"#);
379 }
380
381 #[test]
382 fn test_edit_insert_after() {
383 let env = synth_env("t", 1, r#"<aap:target id="list">item1</aap:target>"#);
384 let (art, _) = apply(None, &env).unwrap();
385
386 let edit = edit_env("t", 2, vec![EditOp {
387 op: OpType::InsertAfter, target: id_target("list"),
388 content: Some(", item2".to_string()),
389 }]);
390 let (art2, _) = apply(Some(&art), &edit).unwrap();
391 assert!(art2.body.contains("item1, item2"));
392 }
393
394 #[test]
395 fn test_nested_targets() {
396 let body = r#"<aap:target id="outer"><h2>Stats</h2><aap:target id="val">100</aap:target></aap:target>"#;
397 let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
398
399 let edit = edit_env("t", 2, vec![EditOp {
400 op: OpType::Replace, target: id_target("val"),
401 content: Some("200".to_string()),
402 }]);
403 let (art2, _) = apply(Some(&art), &edit).unwrap();
404 assert!(art2.body.contains("200"));
405 assert!(art2.body.contains("<h2>Stats</h2>"));
406 }
407
408 #[test]
409 fn test_target_serde_roundtrip() {
410 let t = Target::Id("revenue".to_string());
411 let json = serde_json::to_string(&t).unwrap();
412 let parsed: Target = serde_json::from_str(&json).unwrap();
413 assert!(matches!(parsed, Target::Id(ref s) if s == "revenue"));
414
415 let op = EditOp {
416 op: OpType::Replace,
417 target: Target::Id("rev".to_string()),
418 content: Some("new".to_string()),
419 };
420 let json = serde_json::to_string(&op).unwrap();
421 let parsed: EditOp = serde_json::from_str(&json).unwrap();
422 assert!(matches!(parsed.target, Target::Id(ref s) if s == "rev"));
423 }
424
425 #[test]
426 fn test_edit_from_json_string() {
427 let json = r#"{
428 "protocol": "aap/0.1", "id": "x", "version": 2, "name": "edit",
429 "meta": {"format": "text/html"},
430 "content": [{"op": "replace", "target": {"type": "id", "value": "rev"}, "content": "new"}]
431 }"#;
432 let env: Envelope = serde_json::from_str(json).unwrap();
433 let art_body = r#"<aap:target id="rev">old</aap:target>"#;
434 let (art, _) = apply(None, &synth_env("x", 1, art_body)).unwrap();
435 let (art2, _) = apply(Some(&art), &env).unwrap();
436 assert!(art2.body.contains("new"));
437 assert!(!art2.body.contains("old"));
438 }
439
440 #[test]
441 fn test_pointer_replace() {
442 let base = r#"{"name": "Alice", "age": 30}"#;
443 let (art, _) = apply(None, &synth_env("t", 1, base)).unwrap();
444
445 let mut edit = edit_env("t", 2, vec![EditOp {
446 op: OpType::Replace, target: ptr_target("/name"),
447 content: Some(r#""Bob""#.to_string()),
448 }]);
449 edit.meta.format = Some("application/json".to_string());
450 let (art2, _) = apply(Some(&art), &edit).unwrap();
451 let parsed: serde_json::Value = serde_json::from_str(&art2.body).unwrap();
452 assert_eq!(parsed["name"], "Bob");
453 assert_eq!(parsed["age"], 30);
454 }
455
456 #[test]
457 fn test_pointer_delete() {
458 let base = r#"{"name": "Alice", "temp": true}"#;
459 let (art, _) = apply(None, &synth_env("t", 1, base)).unwrap();
460
461 let mut edit = edit_env("t", 2, vec![EditOp {
462 op: OpType::Delete, target: ptr_target("/temp"), content: None,
463 }]);
464 edit.meta.format = Some("application/json".to_string());
465 let (art2, _) = apply(Some(&art), &edit).unwrap();
466 let parsed: serde_json::Value = serde_json::from_str(&art2.body).unwrap();
467 assert!(parsed.get("temp").is_none());
468 }
469
470 #[test]
471 fn test_handle_is_not_input() {
472 let env = Envelope {
473 protocol: PROTOCOL_VERSION.to_string(),
474 id: "t".to_string(), version: 1, name: Name::Handle,
475 meta: Meta {
476 format: None,
477 tokens_used: None, checksum: None, state: None,
478 },
479 content: vec![],
480 };
481 assert!(apply(None, &env).is_err());
482 }
483
484 #[test]
485 fn test_synthesize_returns_targets() {
486 let body = r#"<aap:target id="stats"><aap:target id="rev">$100</aap:target></aap:target>"#;
487 let (_, handle) = apply(None, &synth_env("t", 1, body)).unwrap();
488 let item: crate::aap::HandleContentItem =
489 serde_json::from_value(handle.content[0].clone()).unwrap();
490 let targets = item.targets.unwrap();
491 let ids: Vec<&str> = targets.iter().map(|t| t.id.as_str()).collect();
492 assert_eq!(ids, vec!["stats", "rev"]);
493 }
494
495 #[test]
496 fn test_nested_target_invalidation() {
497 let body = r#"<aap:target id="outer"><aap:target id="inner">v</aap:target></aap:target>"#;
498 let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
499
500 let edit = edit_env("t", 2, vec![EditOp {
502 op: OpType::Replace, target: id_target("outer"),
503 content: Some("no nested targets here".to_string()),
504 }]);
505 let (_, handle) = apply(Some(&art), &edit).unwrap();
506 let item: crate::aap::HandleContentItem =
507 serde_json::from_value(handle.content[0].clone()).unwrap();
508 let targets = item.targets.unwrap();
509 let ids: Vec<&str> = targets.iter().map(|t| t.id.as_str()).collect();
510 assert_eq!(ids, vec!["outer"]);
512 }
513
514 #[test]
515 fn test_no_targets_for_json() {
516 let base = r#"{"key": "value"}"#;
517 let mut env = synth_env("t", 1, base);
518 env.meta.format = Some("application/json".to_string());
519 let (_, handle) = apply(None, &env).unwrap();
520 let item: crate::aap::HandleContentItem =
521 serde_json::from_value(handle.content[0].clone()).unwrap();
522 assert!(item.targets.is_none());
523 }
524
525 #[test]
528 fn test_edit_insert_before() {
529 let env = synth_env("t", 1, r#"<aap:target id="list">item1</aap:target>"#);
530 let (art, _) = apply(None, &env).unwrap();
531
532 let edit = edit_env("t", 2, vec![EditOp {
533 op: OpType::InsertBefore, target: id_target("list"),
534 content: Some("item0, ".to_string()),
535 }]);
536 let (art2, _) = apply(Some(&art), &edit).unwrap();
537 assert!(art2.body.contains("item0, item1"));
538 assert!(art2.body.contains(r#"<aap:target id="list">"#));
539 }
540
541 #[test]
542 fn test_replace_with_empty_string() {
543 let env = synth_env("t", 1, r#"<aap:target id="val">old</aap:target>"#);
544 let (art, _) = apply(None, &env).unwrap();
545
546 let edit = edit_env("t", 2, vec![EditOp {
547 op: OpType::Replace, target: id_target("val"),
548 content: Some("".to_string()),
549 }]);
550 let (art2, _) = apply(Some(&art), &edit).unwrap();
551 assert_eq!(art2.body, r#"<aap:target id="val"></aap:target>"#);
552 }
553
554 #[test]
555 fn test_replace_with_none_content() {
556 let env = synth_env("t", 1, r#"<aap:target id="val">old</aap:target>"#);
557 let (art, _) = apply(None, &env).unwrap();
558
559 let edit = edit_env("t", 2, vec![EditOp {
560 op: OpType::Replace, target: id_target("val"),
561 content: None,
562 }]);
563 let (art2, _) = apply(Some(&art), &edit).unwrap();
564 assert_eq!(art2.body, r#"<aap:target id="val"></aap:target>"#);
565 }
566
567 #[test]
568 fn test_delete_preserves_markers_for_reuse() {
569 let body = r#"<aap:target id="msg">hello</aap:target>"#;
571 let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
572
573 let delete = edit_env("t", 2, vec![EditOp {
574 op: OpType::Delete, target: id_target("msg"), content: None,
575 }]);
576 let (art2, _) = apply(Some(&art), &delete).unwrap();
577 assert!(art2.body.contains(r#"<aap:target id="msg">"#));
578 assert!(art2.body.contains("</aap:target>"));
579 assert!(!art2.body.contains("hello"));
580
581 let replace = edit_env("t", 3, vec![EditOp {
583 op: OpType::Replace, target: id_target("msg"),
584 content: Some("world".to_string()),
585 }]);
586 let (art3, _) = apply(Some(&art2), &replace).unwrap();
587 assert!(art3.body.contains("world"));
588 }
589
590 #[test]
591 fn test_delete_target_still_in_handle() {
592 let body = r#"<aap:target id="msg">hello</aap:target>"#;
594 let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
595
596 let delete = edit_env("t", 2, vec![EditOp {
597 op: OpType::Delete, target: id_target("msg"), content: None,
598 }]);
599 let (_, handle) = apply(Some(&art), &delete).unwrap();
600 let item: crate::aap::HandleContentItem =
601 serde_json::from_value(handle.content[0].clone()).unwrap();
602 let targets = item.targets.unwrap();
603 let ids: Vec<&str> = targets.iter().map(|t| t.id.as_str()).collect();
604 assert_eq!(ids, vec!["msg"]);
605 }
606
607 #[test]
608 fn test_multiple_ops_same_target() {
609 let body = r#"<aap:target id="x">old</aap:target>"#;
611 let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
612
613 let edit = edit_env("t", 2, vec![
614 EditOp { op: OpType::Delete, target: id_target("x"), content: None },
615 EditOp { op: OpType::InsertAfter, target: id_target("x"), content: Some("new".to_string()) },
616 ]);
617 let (art2, _) = apply(Some(&art), &edit).unwrap();
618 assert!(art2.body.contains("new"));
619 assert!(!art2.body.contains("old"));
620 }
621
622 #[test]
623 fn test_multiple_ops_different_targets() {
624 let body = r#"<aap:target id="a">1</aap:target><aap:target id="b">2</aap:target>"#;
625 let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
626
627 let edit = edit_env("t", 2, vec![
628 EditOp { op: OpType::Replace, target: id_target("a"), content: Some("X".to_string()) },
629 EditOp { op: OpType::Replace, target: id_target("b"), content: Some("Y".to_string()) },
630 ]);
631 let (art2, _) = apply(Some(&art), &edit).unwrap();
632 assert!(art2.body.contains("X"));
633 assert!(art2.body.contains("Y"));
634 assert!(!art2.body.contains("1"));
635 assert!(!art2.body.contains("2"));
636 }
637
638 #[test]
639 fn test_nonexistent_target_fails() {
640 let body = r#"<aap:target id="a">val</aap:target>"#;
641 let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
642
643 let edit = edit_env("t", 2, vec![EditOp {
644 op: OpType::Replace, target: id_target("nonexistent"),
645 content: Some("x".to_string()),
646 }]);
647 assert!(apply(Some(&art), &edit).is_err());
648 }
649
650 #[test]
651 fn test_deeply_nested_targets() {
652 let body = r#"<aap:target id="l1"><aap:target id="l2"><aap:target id="l3">deep</aap:target></aap:target></aap:target>"#;
653 let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
654
655 let edit = edit_env("t", 2, vec![EditOp {
657 op: OpType::Replace, target: id_target("l3"),
658 content: Some("shallow".to_string()),
659 }]);
660 let (art2, _) = apply(Some(&art), &edit).unwrap();
661 assert!(art2.body.contains("shallow"));
662 assert!(art2.body.contains(r#"<aap:target id="l1">"#));
664 assert!(art2.body.contains(r#"<aap:target id="l2">"#));
665 }
666
667 #[test]
668 fn test_adjacent_sibling_targets() {
669 let body = r#"<aap:target id="a">1</aap:target><aap:target id="b">2</aap:target><aap:target id="c">3</aap:target>"#;
670 let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
671
672 let edit = edit_env("t", 2, vec![EditOp {
674 op: OpType::Replace, target: id_target("b"),
675 content: Some("X".to_string()),
676 }]);
677 let (art2, _) = apply(Some(&art), &edit).unwrap();
678 assert!(art2.body.contains(r#"<aap:target id="a">1</aap:target>"#));
679 assert!(art2.body.contains(r#"<aap:target id="b">X</aap:target>"#));
680 assert!(art2.body.contains(r#"<aap:target id="c">3</aap:target>"#));
681 }
682
683 #[test]
684 fn test_replace_with_content_containing_new_targets() {
685 let body = r#"<aap:target id="section">old</aap:target>"#;
686 let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
687
688 let new_content = r#"<aap:target id="inner">nested</aap:target>"#;
689 let edit = edit_env("t", 2, vec![EditOp {
690 op: OpType::Replace, target: id_target("section"),
691 content: Some(new_content.to_string()),
692 }]);
693 let (_, handle) = apply(Some(&art), &edit).unwrap();
694 let item: crate::aap::HandleContentItem =
695 serde_json::from_value(handle.content[0].clone()).unwrap();
696 let targets = item.targets.unwrap();
697 let ids: Vec<&str> = targets.iter().map(|t| t.id.as_str()).collect();
698 assert_eq!(ids, vec!["section", "inner"]);
699 }
700
701 #[test]
702 fn test_empty_target_content() {
703 let body = r#"<aap:target id="empty"></aap:target>"#;
704 let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
705
706 let edit = edit_env("t", 2, vec![EditOp {
707 op: OpType::InsertAfter, target: id_target("empty"),
708 content: Some("filled".to_string()),
709 }]);
710 let (art2, _) = apply(Some(&art), &edit).unwrap();
711 assert!(art2.body.contains("filled"));
712 }
713
714 #[test]
715 fn test_edit_without_base_artifact_fails() {
716 let edit = edit_env("t", 2, vec![EditOp {
717 op: OpType::Replace, target: id_target("x"),
718 content: Some("y".to_string()),
719 }]);
720 assert!(apply(None, &edit).is_err());
721 }
722
723 #[test]
724 fn test_synthesize_empty_content_array_fails() {
725 let env = Envelope {
726 protocol: PROTOCOL_VERSION.to_string(),
727 id: "t".to_string(), version: 1, name: Name::Synthesize,
728 meta: Meta { format: Some("text/html".to_string()),
729 tokens_used: None, checksum: None, state: None },
730 content: vec![],
731 };
732 assert!(apply(None, &env).is_err());
733 }
734
735 #[test]
736 fn test_edit_empty_ops_is_noop() {
737 let body = r#"<aap:target id="a">val</aap:target>"#;
738 let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
739
740 let edit = edit_env("t", 2, vec![]);
741 let (art2, _) = apply(Some(&art), &edit).unwrap();
742 assert_eq!(art2.body, body);
743 }
744
745 #[test]
746 fn test_default_format_is_html() {
747 let env = Envelope {
748 protocol: PROTOCOL_VERSION.to_string(),
749 id: "t".to_string(), version: 1, name: Name::Synthesize,
750 meta: Meta { format: None, tokens_used: None, checksum: None, state: None },
751 content: vec![serde_json::json!({ "body": "<div>hi</div>" })],
752 };
753 let (art, _) = apply(None, &env).unwrap();
754 assert_eq!(art.format, "text/html");
755 }
756
757 #[test]
758 fn test_synthesize_overwrites_existing_artifact() {
759 let (art, _) = apply(None, &synth_env("t", 1, "v1")).unwrap();
760 let (art2, _) = apply(Some(&art), &synth_env("t", 2, "v2")).unwrap();
761 assert_eq!(art2.body, "v2");
762 assert_eq!(art2.version, 2);
763 }
764
765 #[test]
766 fn test_all_or_nothing_semantics() {
767 let body = r#"<aap:target id="a">old</aap:target>"#;
769 let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
770
771 let edit = edit_env("t", 2, vec![
772 EditOp { op: OpType::Replace, target: id_target("a"), content: Some("new".to_string()) },
773 EditOp { op: OpType::Replace, target: id_target("missing"), content: Some("x".to_string()) },
774 ]);
775 assert!(apply(Some(&art), &edit).is_err());
776 assert_eq!(art.body, body);
778 }
779
780 #[test]
781 fn test_sequential_ops_with_position_shift() {
782 let body = r#"<aap:target id="list">a</aap:target>"#;
784 let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
785
786 let edit = edit_env("t", 2, vec![
787 EditOp { op: OpType::InsertAfter, target: id_target("list"), content: Some("b".to_string()) },
788 EditOp { op: OpType::InsertAfter, target: id_target("list"), content: Some("c".to_string()) },
789 ]);
790 let (art2, _) = apply(Some(&art), &edit).unwrap();
791 assert!(art2.body.contains("abc"));
792 }
793
794 #[test]
795 fn test_insert_before_and_after_combined() {
796 let body = r#"<aap:target id="mid">M</aap:target>"#;
797 let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
798
799 let edit = edit_env("t", 2, vec![
800 EditOp { op: OpType::InsertBefore, target: id_target("mid"), content: Some("B".to_string()) },
801 EditOp { op: OpType::InsertAfter, target: id_target("mid"), content: Some("A".to_string()) },
802 ]);
803 let (art2, _) = apply(Some(&art), &edit).unwrap();
804 assert!(art2.body.contains("BMA"));
805 }
806
807 #[test]
808 fn test_delete_nested_inner_preserves_outer() {
809 let body = r#"<aap:target id="outer">pre<aap:target id="inner">val</aap:target>post</aap:target>"#;
810 let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
811
812 let edit = edit_env("t", 2, vec![EditOp {
813 op: OpType::Delete, target: id_target("inner"), content: None,
814 }]);
815 let (art2, _) = apply(Some(&art), &edit).unwrap();
816 assert!(art2.body.contains(r#"<aap:target id="inner"></aap:target>"#));
818 assert!(art2.body.contains("pre"));
819 assert!(art2.body.contains("post"));
820 assert!(art2.body.contains(r#"<aap:target id="outer">"#));
821 }
822
823 #[test]
824 fn test_handle_version_matches_envelope() {
825 let env = synth_env("t", 5, "<div>hi</div>");
826 let (art, handle) = apply(None, &env).unwrap();
827 assert_eq!(art.version, 5);
828 assert_eq!(handle.version, 5);
829 }
830
831 #[test]
832 fn test_handle_id_matches_envelope() {
833 let env = synth_env("my-artifact", 1, "body");
834 let (_, handle) = apply(None, &env).unwrap();
835 assert_eq!(handle.id, "my-artifact");
836 }
837
838 #[test]
839 fn test_multiline_content_in_targets() {
840 let body = "<aap:target id=\"code\">line1\nline2\nline3</aap:target>";
841 let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
842
843 let edit = edit_env("t", 2, vec![EditOp {
844 op: OpType::Replace, target: id_target("code"),
845 content: Some("replaced\ncontent".to_string()),
846 }]);
847 let (art2, _) = apply(Some(&art), &edit).unwrap();
848 assert!(art2.body.contains("replaced\ncontent"));
849 assert!(!art2.body.contains("line1"));
850 }
851
852 fn json_edit_env(id: &str, version: u64, ops: Vec<EditOp>) -> Envelope {
855 let mut env = edit_env(id, version, ops);
856 env.meta.format = Some("application/json".to_string());
857 env
858 }
859
860 fn json_synth_env(id: &str, version: u64, body: &str) -> Envelope {
861 let mut env = synth_env(id, version, body);
862 env.meta.format = Some("application/json".to_string());
863 env
864 }
865
866 #[test]
867 fn test_pointer_nested_path() {
868 let base = r#"{"a": {"b": {"c": 1}}}"#;
869 let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
870
871 let edit = json_edit_env("t", 2, vec![EditOp {
872 op: OpType::Replace, target: ptr_target("/a/b/c"),
873 content: Some("42".to_string()),
874 }]);
875 let (art2, _) = apply(Some(&art), &edit).unwrap();
876 let v: serde_json::Value = serde_json::from_str(&art2.body).unwrap();
877 assert_eq!(v["a"]["b"]["c"], 42);
878 }
879
880 #[test]
881 fn test_pointer_replace_array_element() {
882 let base = r#"{"items": [10, 20, 30]}"#;
883 let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
884
885 let edit = json_edit_env("t", 2, vec![EditOp {
886 op: OpType::Replace, target: ptr_target("/items/1"),
887 content: Some("99".to_string()),
888 }]);
889 let (art2, _) = apply(Some(&art), &edit).unwrap();
890 let v: serde_json::Value = serde_json::from_str(&art2.body).unwrap();
891 assert_eq!(v["items"], serde_json::json!([10, 99, 30]));
892 }
893
894 #[test]
895 fn test_pointer_delete_array_element() {
896 let base = r#"{"items": [1, 2, 3]}"#;
897 let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
898
899 let edit = json_edit_env("t", 2, vec![EditOp {
900 op: OpType::Delete, target: ptr_target("/items/1"), content: None,
901 }]);
902 let (art2, _) = apply(Some(&art), &edit).unwrap();
903 let v: serde_json::Value = serde_json::from_str(&art2.body).unwrap();
904 assert_eq!(v["items"], serde_json::json!([1, 3]));
905 }
906
907 #[test]
908 fn test_pointer_insert_before_array() {
909 let base = r#"{"items": [1, 2, 3]}"#;
910 let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
911
912 let edit = json_edit_env("t", 2, vec![EditOp {
913 op: OpType::InsertBefore, target: ptr_target("/items/1"),
914 content: Some("99".to_string()),
915 }]);
916 let (art2, _) = apply(Some(&art), &edit).unwrap();
917 let v: serde_json::Value = serde_json::from_str(&art2.body).unwrap();
918 assert_eq!(v["items"], serde_json::json!([1, 99, 2, 3]));
919 }
920
921 #[test]
922 fn test_pointer_insert_after_array() {
923 let base = r#"{"items": [1, 2, 3]}"#;
924 let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
925
926 let edit = json_edit_env("t", 2, vec![EditOp {
927 op: OpType::InsertAfter, target: ptr_target("/items/1"),
928 content: Some("99".to_string()),
929 }]);
930 let (art2, _) = apply(Some(&art), &edit).unwrap();
931 let v: serde_json::Value = serde_json::from_str(&art2.body).unwrap();
932 assert_eq!(v["items"], serde_json::json!([1, 2, 99, 3]));
933 }
934
935 #[test]
936 fn test_pointer_multiple_ops() {
937 let base = r#"{"name": "Alice", "age": 30, "city": "NYC"}"#;
938 let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
939
940 let edit = json_edit_env("t", 2, vec![
941 EditOp { op: OpType::Replace, target: ptr_target("/name"), content: Some(r#""Bob""#.to_string()) },
942 EditOp { op: OpType::Delete, target: ptr_target("/city"), content: None },
943 ]);
944 let (art2, _) = apply(Some(&art), &edit).unwrap();
945 let v: serde_json::Value = serde_json::from_str(&art2.body).unwrap();
946 assert_eq!(v["name"], "Bob");
947 assert_eq!(v["age"], 30);
948 assert!(v.get("city").is_none());
949 }
950
951 #[test]
952 fn test_pointer_nonexistent_path_fails() {
953 let base = r#"{"a": 1}"#;
954 let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
955
956 let edit = json_edit_env("t", 2, vec![EditOp {
957 op: OpType::Replace, target: ptr_target("/nonexistent"),
958 content: Some("1".to_string()),
959 }]);
960 assert!(apply(Some(&art), &edit).is_err());
961 }
962
963 #[test]
964 fn test_pointer_rfc6901_escaping() {
965 let base = r#"{"a/b": 1, "c~d": 2}"#;
967 let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
968
969 let edit = json_edit_env("t", 2, vec![EditOp {
970 op: OpType::Replace, target: ptr_target("/a~1b"),
971 content: Some("10".to_string()),
972 }]);
973 let (art2, _) = apply(Some(&art), &edit).unwrap();
974 let v: serde_json::Value = serde_json::from_str(&art2.body).unwrap();
975 assert_eq!(v["a/b"], 10);
976
977 let edit2 = json_edit_env("t", 3, vec![EditOp {
978 op: OpType::Replace, target: ptr_target("/c~0d"),
979 content: Some("20".to_string()),
980 }]);
981 let (art3, _) = apply(Some(&art2), &edit2).unwrap();
982 let v2: serde_json::Value = serde_json::from_str(&art3.body).unwrap();
983 assert_eq!(v2["c~d"], 20);
984 }
985
986 #[test]
987 fn test_pointer_insert_on_object_fails() {
988 let base = r#"{"a": {"b": 1}}"#;
990 let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
991
992 let edit = json_edit_env("t", 2, vec![EditOp {
993 op: OpType::InsertBefore, target: ptr_target("/a/b"),
994 content: Some("2".to_string()),
995 }]);
996 assert!(apply(Some(&art), &edit).is_err());
997 }
998
999 #[test]
1000 fn test_pointer_delete_root_fails() {
1001 let base = r#"{"a": 1}"#;
1002 let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
1003
1004 let edit = json_edit_env("t", 2, vec![EditOp {
1005 op: OpType::Delete, target: ptr_target(""), content: None,
1006 }]);
1007 assert!(apply(Some(&art), &edit).is_err());
1008 }
1009
1010 #[test]
1011 fn test_pointer_array_out_of_bounds_fails() {
1012 let base = r#"{"items": [1, 2]}"#;
1013 let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
1014
1015 let edit = json_edit_env("t", 2, vec![EditOp {
1016 op: OpType::Delete, target: ptr_target("/items/5"), content: None,
1017 }]);
1018 assert!(apply(Some(&art), &edit).is_err());
1019 }
1020
1021 #[test]
1022 fn test_pointer_replace_with_complex_value() {
1023 let base = r#"{"config": null}"#;
1024 let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
1025
1026 let edit = json_edit_env("t", 2, vec![EditOp {
1027 op: OpType::Replace, target: ptr_target("/config"),
1028 content: Some(r#"{"host": "localhost", "port": 5432}"#.to_string()),
1029 }]);
1030 let (art2, _) = apply(Some(&art), &edit).unwrap();
1031 let v: serde_json::Value = serde_json::from_str(&art2.body).unwrap();
1032 assert_eq!(v["config"]["host"], "localhost");
1033 assert_eq!(v["config"]["port"], 5432);
1034 }
1035
1036 #[test]
1037 fn test_pointer_replace_invalid_json_content_fails() {
1038 let base = r#"{"a": 1}"#;
1039 let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
1040
1041 let edit = json_edit_env("t", 2, vec![EditOp {
1042 op: OpType::Replace, target: ptr_target("/a"),
1043 content: Some("not valid json".to_string()),
1044 }]);
1045 assert!(apply(Some(&art), &edit).is_err());
1046 }
1047
1048 #[test]
1049 fn test_pointer_on_non_json_content_fails() {
1050 let body = "not json at all";
1052 let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
1053
1054 let edit = json_edit_env("t", 2, vec![EditOp {
1055 op: OpType::Replace, target: ptr_target("/field"),
1056 content: Some("1".to_string()),
1057 }]);
1058 assert!(apply(Some(&art), &edit).is_err());
1059 }
1060
1061 #[test]
1062 fn test_pointer_all_or_nothing() {
1063 let base = r#"{"a": 1, "b": 2}"#;
1064 let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
1065
1066 let edit = json_edit_env("t", 2, vec![
1067 EditOp { op: OpType::Replace, target: ptr_target("/a"), content: Some("10".to_string()) },
1068 EditOp { op: OpType::Replace, target: ptr_target("/missing"), content: Some("1".to_string()) },
1069 ]);
1070 assert!(apply(Some(&art), &edit).is_err());
1071 let v: serde_json::Value = serde_json::from_str(&art.body).unwrap();
1073 assert_eq!(v["a"], 1);
1074 }
1075
1076 #[test]
1079 fn test_python_format_targets() {
1080 let body = r#"<aap:target id="imports">import os</aap:target>"#;
1081 let mut env = synth_env("t", 1, body);
1082 env.meta.format = Some("text/x-python".to_string());
1083 let (art, _) = apply(None, &env).unwrap();
1084 assert_eq!(art.format, "text/x-python");
1085
1086 let mut edit = edit_env("t", 2, vec![EditOp {
1087 op: OpType::Replace, target: id_target("imports"),
1088 content: Some("import sys".to_string()),
1089 }]);
1090 edit.meta.format = Some("text/x-python".to_string());
1091 let (art2, _) = apply(Some(&art), &edit).unwrap();
1092 assert!(art2.body.contains("import sys"));
1093 }
1094
1095 #[test]
1098 fn test_store_edit_without_synthesize_fails() {
1099 let mut store = crate::store::ArtifactStore::new(10);
1100 let edit = edit_env("t", 2, vec![EditOp {
1101 op: OpType::Replace, target: id_target("x"),
1102 content: Some("y".to_string()),
1103 }]);
1104 assert!(store.apply(&edit).is_err());
1105 }
1106
1107 #[test]
1108 fn test_store_multiple_artifacts() {
1109 let mut store = crate::store::ArtifactStore::new(10);
1110 store.apply(&synth_env("a", 1, "artifact-a")).unwrap();
1111 store.apply(&synth_env("b", 1, "artifact-b")).unwrap();
1112 assert_eq!(store.get("a").unwrap().body, "artifact-a");
1113 assert_eq!(store.get("b").unwrap().body, "artifact-b");
1114 }
1115
1116 #[test]
1117 fn test_store_max_history_eviction() {
1118 let mut store = crate::store::ArtifactStore::new(2);
1119 store.apply(&synth_env("t", 1, "v1")).unwrap();
1120 store.apply(&synth_env("t", 2, "v2")).unwrap();
1121 store.apply(&synth_env("t", 3, "v3")).unwrap();
1122 assert!(store.rollback("t", 1).is_err());
1124 let rolled = store.rollback("t", 2).unwrap();
1126 assert_eq!(rolled.body, "v2");
1127 }
1128
1129 #[test]
1130 fn test_store_synthesize_resets_chain() {
1131 let mut store = crate::store::ArtifactStore::new(10);
1133 store.apply(&synth_env("t", 1, "v1")).unwrap();
1134 store.apply(&synth_env("t", 10, "v10")).unwrap();
1136 assert_eq!(store.current_version("t"), Some(10));
1137 }
1138}