Skip to main content

weavatrix_edit/
envelope.rs

1//! Hand-written wire codecs for the frozen `weavatrix.edit-plan.v1` envelope.
2//!
3//! [`TextEdit`], [`FileEdit`], and [`EditPlan`] previously derived serde with
4//! `#[serde(flatten)]` extension maps. The derive read declared members
5//! directly and buffered only undeclared ones into serde's private `Content`
6//! tree before a second `FlatMapDeserializer` pass; these codecs read every
7//! member exactly once instead. Measured on its own that is close to a wash
8//! (see `docs/decoder-comparison.md`) — the point of writing them by hand is
9//! that `flatten` cannot express "decode this envelope and skip the undeclared
10//! members", which is where the actual cost lives.
11//!
12//! The map visitors are therefore generic over an [`ExtensionPolicy`]: the
13//! capturing decode and the declared-only decode behind
14//! [`DeclaredEditPlan`] share one implementation of field matching, duplicate
15//! detection, and missing-field reporting, and cannot drift apart.
16//!
17//! Every observable behaviour of the derive is preserved: the same accepted
18//! documents, the same declared-field-then-`BTreeMap`-order serialized bytes,
19//! and the same `duplicate field`, `missing field`, and `invalid type` messages
20//! at the same input positions. `tests/envelope_wire.rs` pins this against an
21//! independent restatement of the original derives on two serde drivers.
22
23use core::fmt;
24use core::marker::PhantomData;
25use std::collections::BTreeMap;
26
27use blazingly_json::Value;
28use serde::de::{DeserializeSeed, Deserializer, IgnoredAny, MapAccess, SeqAccess, Visitor};
29use serde::ser::SerializeMap;
30use serde::{Deserialize, Serialize, Serializer, de};
31
32use crate::{
33    model::{Completeness, EditPlan, FileEdit, TextEdit},
34    provenance::Provenance,
35};
36
37/// Declared member names of a [`TextEdit`], in serialization order.
38pub(crate) const TEXT_EDIT_FIELDS: [&str; 7] = [
39    "startLine",
40    "startChar",
41    "endLine",
42    "endChar",
43    "before",
44    "after",
45    "provenance",
46];
47
48/// Declared member names of a [`FileEdit`], in serialization order.
49pub(crate) const FILE_EDIT_FIELDS: [&str; 3] = ["path", "sha256", "edits"];
50
51/// Declared member names of an [`EditPlan`], in serialization order.
52pub(crate) const EDIT_PLAN_FIELDS: [&str; 4] =
53    ["schemaVersion", "operation", "files", "completeness"];
54
55// ---------------------------------------------------------------------------
56// Extension policy
57// ---------------------------------------------------------------------------
58
59/// How one decode pass treats members that are not declared by the envelope.
60///
61/// Both policies match declared members identically, so a declared-only decode
62/// accepts and rejects exactly the documents the capturing decode does.
63trait ExtensionPolicy: Copy {
64    /// Retained key representation for an undeclared member.
65    type Key;
66    /// Accumulator threaded through one object.
67    type Sink;
68
69    fn key(name: &str) -> Self::Key;
70
71    fn sink() -> Self::Sink;
72
73    /// Consumes the pending value of an undeclared member.
74    fn absorb<'de, A>(sink: &mut Self::Sink, key: Self::Key, map: &mut A) -> Result<(), A::Error>
75    where
76        A: MapAccess<'de>;
77
78    fn finish(sink: Self::Sink) -> BTreeMap<String, Value>;
79}
80
81/// Retains every undeclared member as an owned JSON value.
82#[derive(Clone, Copy)]
83struct Capture;
84
85impl ExtensionPolicy for Capture {
86    type Key = String;
87    type Sink = BTreeMap<String, Value>;
88
89    fn key(name: &str) -> String {
90        name.to_owned()
91    }
92
93    fn sink() -> Self::Sink {
94        BTreeMap::new()
95    }
96
97    fn absorb<'de, A>(sink: &mut Self::Sink, key: String, map: &mut A) -> Result<(), A::Error>
98    where
99        A: MapAccess<'de>,
100    {
101        // Last duplicate wins, matching the derive's `FlatMapDeserializer` pass.
102        sink.insert(key, map.next_value()?);
103        Ok(())
104    }
105
106    fn finish(sink: Self::Sink) -> BTreeMap<String, Value> {
107        sink
108    }
109}
110
111/// Skips every undeclared member without allocating a key or a value tree.
112#[derive(Clone, Copy)]
113struct Discard;
114
115impl ExtensionPolicy for Discard {
116    type Key = ();
117    type Sink = ();
118
119    fn key(_name: &str) {}
120
121    fn sink() -> Self::Sink {}
122
123    fn absorb<'de, A>(_sink: &mut (), _key: (), map: &mut A) -> Result<(), A::Error>
124    where
125        A: MapAccess<'de>,
126    {
127        map.next_value::<IgnoredAny>()?;
128        Ok(())
129    }
130
131    fn finish((): Self::Sink) -> BTreeMap<String, Value> {
132        BTreeMap::new()
133    }
134}
135
136// ---------------------------------------------------------------------------
137// Member names
138// ---------------------------------------------------------------------------
139
140enum Field<K> {
141    /// Index into the declaring struct's field-name table.
142    Known(usize),
143    Unknown(K),
144}
145
146/// Resolves a member name to a declared field index.
147///
148/// Each implementation is a `match` over string literals, not a scan of a
149/// runtime slice, so the compiler lowers it to a length switch plus at most a
150/// couple of comparisons -- the same shape the derive generated.
151trait FieldTable {
152    fn lookup(name: &str) -> Option<usize>;
153}
154
155struct TextEditFields;
156
157impl FieldTable for TextEditFields {
158    fn lookup(name: &str) -> Option<usize> {
159        Some(match name {
160            "startLine" => 0,
161            "startChar" => 1,
162            "endLine" => 2,
163            "endChar" => 3,
164            "before" => 4,
165            "after" => 5,
166            "provenance" => 6,
167            _ => return None,
168        })
169    }
170}
171
172struct FileEditFields;
173
174impl FieldTable for FileEditFields {
175    fn lookup(name: &str) -> Option<usize> {
176        Some(match name {
177            "path" => 0,
178            "sha256" => 1,
179            "edits" => 2,
180            _ => return None,
181        })
182    }
183}
184
185struct EditPlanFields;
186
187impl FieldTable for EditPlanFields {
188    fn lookup(name: &str) -> Option<usize> {
189        Some(match name {
190            "schemaVersion" => 0,
191            "operation" => 1,
192            "files" => 2,
193            "completeness" => 3,
194            _ => return None,
195        })
196    }
197}
198
199struct FieldSeed<P, T>(PhantomData<(P, T)>);
200
201impl<P, T> Clone for FieldSeed<P, T> {
202    fn clone(&self) -> Self {
203        *self
204    }
205}
206
207impl<P, T> Copy for FieldSeed<P, T> {}
208
209impl<P, T> FieldSeed<P, T> {
210    const fn new() -> Self {
211        Self(PhantomData)
212    }
213}
214
215impl<'de, P: ExtensionPolicy, T: FieldTable> DeserializeSeed<'de> for FieldSeed<P, T> {
216    type Value = Field<P::Key>;
217
218    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
219    where
220        D: Deserializer<'de>,
221    {
222        deserializer.deserialize_identifier(self)
223    }
224}
225
226impl<P: ExtensionPolicy, T: FieldTable> Visitor<'_> for FieldSeed<P, T> {
227    type Value = Field<P::Key>;
228
229    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
230        formatter.write_str("field identifier")
231    }
232
233    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
234    where
235        E: de::Error,
236    {
237        // `visit_borrowed_str` and `visit_string` forward here by default, so an
238        // escaped spelling of a declared name still binds to that field.
239        Ok(match T::lookup(value) {
240            Some(index) => Field::Known(index),
241            None => Field::Unknown(P::key(value)),
242        })
243    }
244}
245
246// ---------------------------------------------------------------------------
247// Sequences of seeded elements
248// ---------------------------------------------------------------------------
249
250/// Mirrors `Vec<T>`'s own codec, threading an [`ExtensionPolicy`] into elements.
251struct VecSeed<S>(S);
252
253impl<'de, S> DeserializeSeed<'de> for VecSeed<S>
254where
255    S: DeserializeSeed<'de> + Copy,
256{
257    type Value = Vec<S::Value>;
258
259    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
260    where
261        D: Deserializer<'de>,
262    {
263        deserializer.deserialize_seq(self)
264    }
265}
266
267impl<'de, S> Visitor<'de> for VecSeed<S>
268where
269    S: DeserializeSeed<'de> + Copy,
270{
271    type Value = Vec<S::Value>;
272
273    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
274        formatter.write_str("a sequence")
275    }
276
277    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
278    where
279        A: SeqAccess<'de>,
280    {
281        let mut items = Vec::with_capacity(cautious_capacity::<S::Value>(seq.size_hint()));
282        while let Some(item) = seq.next_element_seed(self.0)? {
283            items.push(item);
284        }
285        Ok(items)
286    }
287}
288
289/// Bounds a hinted preallocation the way serde's own sequence codecs do, so a
290/// hostile length hint cannot reserve unbounded memory before any element is
291/// read.
292fn cautious_capacity<T>(hint: Option<usize>) -> usize {
293    const MAX_PREALLOCATED_BYTES: usize = 1024 * 1024;
294    MAX_PREALLOCATED_BYTES
295        .checked_div(size_of::<T>())
296        .map_or(0, |ceiling| hint.unwrap_or(0).min(ceiling))
297}
298
299// ---------------------------------------------------------------------------
300// TextEdit
301// ---------------------------------------------------------------------------
302
303struct TextEditSeed<P>(PhantomData<P>);
304
305impl<P> Clone for TextEditSeed<P> {
306    fn clone(&self) -> Self {
307        *self
308    }
309}
310
311impl<P> Copy for TextEditSeed<P> {}
312
313impl<P> TextEditSeed<P> {
314    const fn new() -> Self {
315        Self(PhantomData)
316    }
317}
318
319impl<'de, P: ExtensionPolicy> DeserializeSeed<'de> for TextEditSeed<P> {
320    type Value = TextEdit;
321
322    fn deserialize<D>(self, deserializer: D) -> Result<TextEdit, D::Error>
323    where
324        D: Deserializer<'de>,
325    {
326        // `deserialize_map`, not `deserialize_struct`: the derive this replaces
327        // also used the map entry point, and a driver that accepts a positional
328        // sequence for a struct must keep rejecting one here.
329        deserializer.deserialize_map(self)
330    }
331}
332
333impl<'de, P: ExtensionPolicy> Visitor<'de> for TextEditSeed<P> {
334    type Value = TextEdit;
335
336    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
337        formatter.write_str("struct TextEdit")
338    }
339
340    fn visit_map<A>(self, mut map: A) -> Result<TextEdit, A::Error>
341    where
342        A: MapAccess<'de>,
343    {
344        let mut start_line: Option<u32> = None;
345        let mut start_char: Option<u32> = None;
346        let mut end_line: Option<u32> = None;
347        let mut end_char: Option<u32> = None;
348        let mut before: Option<String> = None;
349        let mut after: Option<String> = None;
350        let mut provenance: Option<Provenance> = None;
351        let mut sink = P::sink();
352
353        while let Some(field) = map.next_key_seed(FieldSeed::<P, TextEditFields>::new())? {
354            match field {
355                Field::Known(0) => {
356                    if start_line.is_some() {
357                        return Err(de::Error::duplicate_field("startLine"));
358                    }
359                    start_line = Some(map.next_value()?);
360                }
361                Field::Known(1) => {
362                    if start_char.is_some() {
363                        return Err(de::Error::duplicate_field("startChar"));
364                    }
365                    start_char = Some(map.next_value()?);
366                }
367                Field::Known(2) => {
368                    if end_line.is_some() {
369                        return Err(de::Error::duplicate_field("endLine"));
370                    }
371                    end_line = Some(map.next_value()?);
372                }
373                Field::Known(3) => {
374                    if end_char.is_some() {
375                        return Err(de::Error::duplicate_field("endChar"));
376                    }
377                    end_char = Some(map.next_value()?);
378                }
379                Field::Known(4) => {
380                    if before.is_some() {
381                        return Err(de::Error::duplicate_field("before"));
382                    }
383                    before = Some(map.next_value()?);
384                }
385                Field::Known(5) => {
386                    if after.is_some() {
387                        return Err(de::Error::duplicate_field("after"));
388                    }
389                    after = Some(map.next_value()?);
390                }
391                // The final declared index; the table has no further entries.
392                Field::Known(_) => {
393                    if provenance.is_some() {
394                        return Err(de::Error::duplicate_field("provenance"));
395                    }
396                    provenance = Some(map.next_value()?);
397                }
398                Field::Unknown(key) => P::absorb(&mut sink, key, &mut map)?,
399            }
400        }
401
402        Ok(TextEdit {
403            start_line: start_line.ok_or_else(|| de::Error::missing_field("startLine"))?,
404            start_char: start_char.ok_or_else(|| de::Error::missing_field("startChar"))?,
405            end_line: end_line.ok_or_else(|| de::Error::missing_field("endLine"))?,
406            end_char: end_char.ok_or_else(|| de::Error::missing_field("endChar"))?,
407            before: before.ok_or_else(|| de::Error::missing_field("before"))?,
408            after: after.ok_or_else(|| de::Error::missing_field("after"))?,
409            provenance: provenance.ok_or_else(|| de::Error::missing_field("provenance"))?,
410            extensions: P::finish(sink),
411        })
412    }
413}
414
415impl<'de> Deserialize<'de> for TextEdit {
416    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
417    where
418        D: Deserializer<'de>,
419    {
420        TextEditSeed::<Capture>::new().deserialize(deserializer)
421    }
422}
423
424impl Serialize for TextEdit {
425    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
426    where
427        S: Serializer,
428    {
429        // A map of unknown length, exactly as the flattened derive emitted, so
430        // both compact and pretty output stay byte-identical.
431        let mut map = serializer.serialize_map(None)?;
432        map.serialize_entry(TEXT_EDIT_FIELDS[0], &self.start_line)?;
433        map.serialize_entry(TEXT_EDIT_FIELDS[1], &self.start_char)?;
434        map.serialize_entry(TEXT_EDIT_FIELDS[2], &self.end_line)?;
435        map.serialize_entry(TEXT_EDIT_FIELDS[3], &self.end_char)?;
436        map.serialize_entry(TEXT_EDIT_FIELDS[4], &self.before)?;
437        map.serialize_entry(TEXT_EDIT_FIELDS[5], &self.after)?;
438        map.serialize_entry(TEXT_EDIT_FIELDS[6], &self.provenance)?;
439        serialize_extensions(&mut map, &self.extensions)?;
440        map.end()
441    }
442}
443
444// ---------------------------------------------------------------------------
445// FileEdit
446// ---------------------------------------------------------------------------
447
448struct FileEditSeed<P>(PhantomData<P>);
449
450impl<P> Clone for FileEditSeed<P> {
451    fn clone(&self) -> Self {
452        *self
453    }
454}
455
456impl<P> Copy for FileEditSeed<P> {}
457
458impl<P> FileEditSeed<P> {
459    const fn new() -> Self {
460        Self(PhantomData)
461    }
462}
463
464impl<'de, P: ExtensionPolicy> DeserializeSeed<'de> for FileEditSeed<P> {
465    type Value = FileEdit;
466
467    fn deserialize<D>(self, deserializer: D) -> Result<FileEdit, D::Error>
468    where
469        D: Deserializer<'de>,
470    {
471        deserializer.deserialize_map(self)
472    }
473}
474
475impl<'de, P: ExtensionPolicy> Visitor<'de> for FileEditSeed<P> {
476    type Value = FileEdit;
477
478    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
479        formatter.write_str("struct FileEdit")
480    }
481
482    fn visit_map<A>(self, mut map: A) -> Result<FileEdit, A::Error>
483    where
484        A: MapAccess<'de>,
485    {
486        let mut path: Option<String> = None;
487        let mut sha256: Option<String> = None;
488        let mut edits: Option<Vec<TextEdit>> = None;
489        let mut sink = P::sink();
490
491        while let Some(field) = map.next_key_seed(FieldSeed::<P, FileEditFields>::new())? {
492            match field {
493                Field::Known(0) => {
494                    if path.is_some() {
495                        return Err(de::Error::duplicate_field("path"));
496                    }
497                    path = Some(map.next_value()?);
498                }
499                Field::Known(1) => {
500                    if sha256.is_some() {
501                        return Err(de::Error::duplicate_field("sha256"));
502                    }
503                    sha256 = Some(map.next_value()?);
504                }
505                Field::Known(_) => {
506                    if edits.is_some() {
507                        return Err(de::Error::duplicate_field("edits"));
508                    }
509                    edits = Some(map.next_value_seed(VecSeed(TextEditSeed::<P>::new()))?);
510                }
511                Field::Unknown(key) => P::absorb(&mut sink, key, &mut map)?,
512            }
513        }
514
515        Ok(FileEdit {
516            path: path.ok_or_else(|| de::Error::missing_field("path"))?,
517            sha256: sha256.ok_or_else(|| de::Error::missing_field("sha256"))?,
518            edits: edits.ok_or_else(|| de::Error::missing_field("edits"))?,
519            extensions: P::finish(sink),
520        })
521    }
522}
523
524impl<'de> Deserialize<'de> for FileEdit {
525    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
526    where
527        D: Deserializer<'de>,
528    {
529        FileEditSeed::<Capture>::new().deserialize(deserializer)
530    }
531}
532
533impl Serialize for FileEdit {
534    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
535    where
536        S: Serializer,
537    {
538        let mut map = serializer.serialize_map(None)?;
539        map.serialize_entry(FILE_EDIT_FIELDS[0], &self.path)?;
540        map.serialize_entry(FILE_EDIT_FIELDS[1], &self.sha256)?;
541        map.serialize_entry(FILE_EDIT_FIELDS[2], &self.edits)?;
542        serialize_extensions(&mut map, &self.extensions)?;
543        map.end()
544    }
545}
546
547// ---------------------------------------------------------------------------
548// EditPlan
549// ---------------------------------------------------------------------------
550
551struct EditPlanSeed<P>(PhantomData<P>);
552
553impl<P> Clone for EditPlanSeed<P> {
554    fn clone(&self) -> Self {
555        *self
556    }
557}
558
559impl<P> Copy for EditPlanSeed<P> {}
560
561impl<P> EditPlanSeed<P> {
562    const fn new() -> Self {
563        Self(PhantomData)
564    }
565}
566
567impl<'de, P: ExtensionPolicy> DeserializeSeed<'de> for EditPlanSeed<P> {
568    type Value = EditPlan;
569
570    fn deserialize<D>(self, deserializer: D) -> Result<EditPlan, D::Error>
571    where
572        D: Deserializer<'de>,
573    {
574        deserializer.deserialize_map(self)
575    }
576}
577
578impl<'de, P: ExtensionPolicy> Visitor<'de> for EditPlanSeed<P> {
579    type Value = EditPlan;
580
581    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
582        formatter.write_str("struct EditPlan")
583    }
584
585    fn visit_map<A>(self, mut map: A) -> Result<EditPlan, A::Error>
586    where
587        A: MapAccess<'de>,
588    {
589        let mut schema_version: Option<String> = None;
590        let mut operation: Option<String> = None;
591        let mut files: Option<Vec<FileEdit>> = None;
592        // The outer `Option` records presence, so an explicit `null` still
593        // counts as a occupied member and a repeat is a duplicate field.
594        let mut completeness: Option<Option<Completeness>> = None;
595        let mut sink = P::sink();
596
597        while let Some(field) = map.next_key_seed(FieldSeed::<P, EditPlanFields>::new())? {
598            match field {
599                Field::Known(0) => {
600                    if schema_version.is_some() {
601                        return Err(de::Error::duplicate_field("schemaVersion"));
602                    }
603                    schema_version = Some(map.next_value()?);
604                }
605                Field::Known(1) => {
606                    if operation.is_some() {
607                        return Err(de::Error::duplicate_field("operation"));
608                    }
609                    operation = Some(map.next_value()?);
610                }
611                Field::Known(2) => {
612                    if files.is_some() {
613                        return Err(de::Error::duplicate_field("files"));
614                    }
615                    files = Some(map.next_value_seed(VecSeed(FileEditSeed::<P>::new()))?);
616                }
617                Field::Known(_) => {
618                    if completeness.is_some() {
619                        return Err(de::Error::duplicate_field("completeness"));
620                    }
621                    completeness = Some(map.next_value()?);
622                }
623                Field::Unknown(key) => P::absorb(&mut sink, key, &mut map)?,
624            }
625        }
626
627        Ok(EditPlan {
628            schema_version: schema_version
629                .ok_or_else(|| de::Error::missing_field("schemaVersion"))?,
630            operation: operation.ok_or_else(|| de::Error::missing_field("operation"))?,
631            files: files.ok_or_else(|| de::Error::missing_field("files"))?,
632            completeness: completeness.unwrap_or_default(),
633            extensions: P::finish(sink),
634        })
635    }
636}
637
638impl<'de> Deserialize<'de> for EditPlan {
639    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
640    where
641        D: Deserializer<'de>,
642    {
643        EditPlanSeed::<Capture>::new().deserialize(deserializer)
644    }
645}
646
647impl Serialize for EditPlan {
648    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
649    where
650        S: Serializer,
651    {
652        let mut map = serializer.serialize_map(None)?;
653        map.serialize_entry(EDIT_PLAN_FIELDS[0], &self.schema_version)?;
654        map.serialize_entry(EDIT_PLAN_FIELDS[1], &self.operation)?;
655        map.serialize_entry(EDIT_PLAN_FIELDS[2], &self.files)?;
656        if self.completeness.is_some() {
657            map.serialize_entry(EDIT_PLAN_FIELDS[3], &self.completeness)?;
658        }
659        serialize_extensions(&mut map, &self.extensions)?;
660        map.end()
661    }
662}
663
664/// Emits extension members in `BTreeMap` order, after the declared members, so
665/// an extension key that shadows a declared name produces the duplicate JSON
666/// key the flattened derive produced.
667fn serialize_extensions<M>(
668    map: &mut M,
669    extensions: &BTreeMap<String, Value>,
670) -> Result<(), M::Error>
671where
672    M: SerializeMap,
673{
674    for (key, value) in extensions {
675        map.serialize_entry(key, value)?;
676    }
677    Ok(())
678}
679
680// ---------------------------------------------------------------------------
681// Declared-only decode
682// ---------------------------------------------------------------------------
683
684/// An [`EditPlan`] decoded without materializing any extension member.
685///
686/// Decoding an envelope through [`EditPlan`] retains every undeclared member as
687/// an owned JSON value at all three levels. That is required to round-trip a
688/// plan, and it is the dominant cost of decoding a large multi-file plan. A
689/// consumer that only validates or applies a plan never reads those values and
690/// should not pay for them.
691///
692/// This wrapper accepts and rejects exactly the documents [`EditPlan`] accepts
693/// and rejects, with the same error messages, but skips undeclared members
694/// structurally instead of building a key and a value tree for each one. The
695/// [`EditPlan`] it yields has empty `extensions` at every level.
696///
697/// # Extensions are dropped, not hidden
698///
699/// The recovered plan is **not** round-trippable: re-serializing it emits only
700/// declared members. Decode through [`EditPlan`] whenever the extension members
701/// must survive. Validation is unaffected either way — a reserved member name
702/// can never reach an extension map through the wire, because a JSON member
703/// spelled like a declared field always binds to that field.
704///
705/// # Examples
706///
707/// ```
708/// use weavatrix_edit::DeclaredEditPlan;
709///
710/// let json = r#"{
711///     "schemaVersion": "weavatrix.edit-plan.v1",
712///     "operation": "rename_symbol",
713///     "createdAt": "2026-08-01T12:00:00Z",
714///     "files": [{
715///         "path": "src/user.ts",
716///         "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
717///         "language": "typescript",
718///         "edits": [{
719///             "startLine": 10, "startChar": 8, "endLine": 10, "endChar": 15,
720///             "before": "getUser", "after": "getCustomer", "provenance": "EXACT_LSP"
721///         }]
722///     }]
723/// }"#;
724///
725/// let declared: DeclaredEditPlan = blazingly_json::from_str(json)?;
726/// let plan = declared.into_plan();
727/// assert!(plan.validate().is_ok());
728/// assert!(plan.extensions.is_empty());
729/// assert!(plan.files[0].extensions.is_empty());
730/// # Ok::<(), blazingly_json::Error>(())
731/// ```
732#[derive(Clone, Debug, PartialEq)]
733pub struct DeclaredEditPlan(EditPlan);
734
735impl DeclaredEditPlan {
736    /// Borrows the recovered plan.
737    #[must_use]
738    pub const fn plan(&self) -> &EditPlan {
739        &self.0
740    }
741
742    /// Takes the recovered plan, whose extension maps are all empty.
743    #[must_use]
744    pub fn into_plan(self) -> EditPlan {
745        self.0
746    }
747}
748
749impl AsRef<EditPlan> for DeclaredEditPlan {
750    fn as_ref(&self) -> &EditPlan {
751        &self.0
752    }
753}
754
755impl From<DeclaredEditPlan> for EditPlan {
756    fn from(declared: DeclaredEditPlan) -> Self {
757        declared.0
758    }
759}
760
761impl<'de> Deserialize<'de> for DeclaredEditPlan {
762    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
763    where
764        D: Deserializer<'de>,
765    {
766        EditPlanSeed::<Discard>::new()
767            .deserialize(deserializer)
768            .map(Self)
769    }
770}
771
772#[cfg(test)]
773mod tests {
774    use super::{EDIT_PLAN_FIELDS, FILE_EDIT_FIELDS, TEXT_EDIT_FIELDS};
775    use crate::validation::FILE_EDIT_RESERVED_EXTENSION_KEYS;
776
777    #[test]
778    fn reserved_extension_keys_track_the_wire_field_names() {
779        // Validation rejects an extension key that shadows a declared member.
780        // That list must be the wire field list, or a future field rename would
781        // silently open a collision.
782        assert_eq!(FILE_EDIT_RESERVED_EXTENSION_KEYS, FILE_EDIT_FIELDS);
783        assert_eq!(FILE_EDIT_FIELDS.len(), 3);
784        assert_eq!(TEXT_EDIT_FIELDS.len(), 7);
785        assert_eq!(EDIT_PLAN_FIELDS.len(), 4);
786    }
787}