Skip to main content

devset_core/part/
mod.rs

1//! Parts of a file: the keys, or the marked block, one profile owns in a file the target otherwise
2//! owns.
3//!
4//! A part is read out of its file as *content*, bytes the rest of devset treats as a file's: for
5//! keys, one line per owned leaf with its value as canonical JSON, so layout is never drift; for a
6//! block, its lines as written.
7
8mod block;
9mod json;
10mod keys;
11mod toml;
12mod yaml;
13
14use alloc::collections::{BTreeMap, BTreeSet};
15use core::ops::Range;
16use core::str;
17
18use derive_more::Display;
19use schemars::JsonSchema;
20use serde::{Deserialize, Serialize};
21use serde_json::Value;
22
23pub use self::block::Comment;
24use self::block::Markers;
25pub(crate) use self::keys::{Key, Leaves, Written, decode, display, encode, merge, overlap};
26use self::keys::{lookup, walk};
27use crate::digest::BOM;
28use crate::errors::{ParseError, ProfileError, Result};
29use crate::format::Format;
30use crate::path::RelPath;
31
32/// Where a managed thing lives: a whole file, or one profile's part of it.
33#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
34pub(crate) struct Slot {
35    /// The file.
36    pub path: RelPath,
37    /// The profile whose part it is; `None` for the whole file.
38    pub part: Option<String>,
39}
40
41impl Slot {
42    /// The whole of `path`.
43    pub(crate) const fn whole(path: RelPath) -> Self {
44        Self { path, part: None }
45    }
46}
47
48/// How much of a file a profile owns.
49#[derive(
50    Clone,
51    Copy,
52    Debug,
53    Default,
54    PartialEq,
55    Eq,
56    PartialOrd,
57    Ord,
58    Hash,
59    Serialize,
60    Deserialize,
61    JsonSchema,
62    Display,
63)]
64#[serde(rename_all = "lowercase")]
65#[display(rename_all = "lowercase")]
66pub enum Scope {
67    /// The whole file.
68    #[default]
69    File,
70    /// Every leaf the payload, a partial document, defines; the file's other keys are the target's.
71    Keys,
72    /// One comment-marked block, named after the profile; the file's other lines are the target's.
73    Block,
74}
75
76/// A structured format whose keys a profile can own.
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub(crate) enum Syntax {
79    /// TOML, through `toml_edit`.
80    Toml,
81    /// JSON and JSONC, through `jsonc-parser`.
82    Json,
83    /// YAML, through `yaml-edit`; experimental.
84    Yaml,
85}
86
87/// Why a document could not be read or written, and where.
88type Failure = (Option<Range<usize>>, String);
89
90/// A document's reading or writing: done, or the failure.
91type Parsed<T> = Result<T, Failure>;
92
93/// One key to set to a value, or to remove.
94type Edit<'a> = (&'a Key, Option<&'a Value>);
95
96impl Syntax {
97    /// The syntax of `format`, if it is a structured one.
98    const fn of(format: Format) -> Option<Self> {
99        match format {
100            Format::Toml => Some(Self::Toml),
101            Format::Json => Some(Self::Json),
102            Format::Yaml => Some(Self::Yaml),
103            Format::None => None,
104        }
105    }
106
107    /// The document's values, which must be a table; an empty document is an empty one.
108    fn semantic(self, text: &str) -> Parsed<Value> {
109        match self {
110            Self::Toml => toml::semantic(text),
111            Self::Json => json::semantic(text),
112            Self::Yaml => yaml::semantic(text),
113        }
114    }
115
116    /// The leaves a partial document defines, in its order.
117    fn leaves(self, text: &str) -> Parsed<Written> {
118        match self {
119            // TOML's syntax says more than its values: which arrays are arrays of tables.
120            Self::Toml => toml::leaves(text),
121            Self::Json | Self::Yaml => {
122                let mut leaves = Vec::new();
123                walk(&self.semantic(text)?, &mut Vec::new(), &mut leaves);
124                Ok(Written { leaves, source: text.to_owned(), ..Written::default() })
125            },
126        }
127    }
128
129    /// A document's values at `keys`; a key it lacks is left out.
130    fn values(self, text: &str, keys: &BTreeSet<Key>) -> Parsed<Leaves> {
131        let values = self.semantic(text)?;
132        Ok(keys
133            .iter()
134            .filter_map(|key| Some((key.clone(), lookup(&values, key)?.clone())))
135            .collect())
136    }
137
138    /// A document with each edit made, in order, as `payload` writes each: a value the payload
139    /// gives keeps the payload's layout, and TOML its arrays of tables, comments and all.
140    fn apply(self, text: &str, edits: &[Edit<'_>], payload: &Written) -> Parsed<String> {
141        match self {
142            Self::Toml => toml::apply(text, edits, payload),
143            Self::Json => json::apply(text, edits, payload),
144            Self::Yaml => yaml::apply(text, edits, payload),
145        }
146    }
147}
148
149/// How one profile's part of a file is found, read and written.
150#[derive(Clone, Debug, PartialEq, Eq)]
151pub(crate) enum Shape {
152    /// The leaves of a partial document.
153    Keys(Syntax),
154    /// The lines between two markers.
155    Block(Markers),
156}
157
158impl Shape {
159    /// The shape of `owner`'s part of `path` under `scope`, read as `format`; `None` for the
160    /// whole file.
161    ///
162    /// # Errors
163    /// - [`ProfileError::NoKeys`], `format` is not TOML, JSON or YAML.
164    /// - [`ProfileError::NoComment`], devset knows no comment syntax for `path` and none is given.
165    pub(crate) fn of(
166        scope: Scope, path: &RelPath, format: Format, comment: Option<Comment>, owner: &str,
167    ) -> Result<Option<Self>> {
168        match scope {
169            Scope::File => Ok(None),
170            Scope::Keys => Syntax::of(format)
171                .map(|syntax| Some(Self::Keys(syntax)))
172                .ok_or_else(|| ProfileError::NoKeys { path: path.clone() }.into()),
173            Scope::Block => {
174                let comment = comment
175                    .or_else(|| block::comment(path))
176                    .ok_or_else(|| ProfileError::NoComment { path: path.clone() })?;
177                Ok(Some(Self::Block(Markers::new(comment, owner))))
178            },
179        }
180    }
181
182    /// What `payload` gives the part: its content, a partial document's leaves or a block's
183    /// lines; and for keys, the leaves as the payload writes them.
184    pub(crate) fn read(&self, payload: &[u8], label: &str) -> Result<(Vec<u8>, Written)> {
185        match self {
186            Self::Keys(syntax) => {
187                let (text, _) = text(payload, label)?;
188                let written = syntax.leaves(text).map_err(|f| failed(label, text, f))?;
189                let leaves: Leaves = written.leaves.iter().cloned().collect();
190                Ok((encode(&leaves), written))
191            },
192            Self::Block(_) => Ok((payload.to_vec(), Written::default())),
193        }
194    }
195
196    /// The keys `content` owns; none for a block.
197    pub(crate) fn keys(&self, content: &[u8]) -> BTreeSet<Key> {
198        match self {
199            Self::Keys(_) => decode(content).map_or_default(|leaves| leaves.into_keys().collect()),
200            Self::Block(_) => BTreeSet::new(),
201        }
202    }
203
204    /// What `file` holds of the part over `keys`, as content; `None` when none of it is there.
205    pub(crate) fn view(
206        &self, file: &[u8], keys: &BTreeSet<Key>, label: &str,
207    ) -> Result<Option<Vec<u8>>> {
208        let (text, _) = text(file, label)?;
209        match self {
210            Self::Keys(syntax) => {
211                let found = syntax.values(text, keys).map_err(|f| failed(label, text, f))?;
212                // A part of no keys is all there whenever its file is.
213                Ok((!found.is_empty() || keys.is_empty()).then(|| encode(&found)))
214            },
215            Self::Block(markers) => {
216                let lines = markers
217                    .find(text)
218                    .map_err(|(span, why)| failed(label, text, (Some(span), why)))?;
219                Ok(lines.and_then(|lines| text.get(lines)).map(|lines| lines.as_bytes().to_vec()))
220            },
221        }
222    }
223
224    /// `file` with the part holding `content` over `keys`; a key `content` lacks is removed.
225    ///
226    /// Only what changes is rewritten, so the rest keeps its layout; a new key goes where
227    /// `payload`, the profile's leaves as written, puts it, and a value the profile wrote is
228    /// written as it wrote it.
229    pub(crate) fn splice(
230        &self, file: &[u8], content: &[u8], keys: &BTreeSet<Key>, payload: &Written, label: &str,
231    ) -> Result<Vec<u8>> {
232        let (text, bom) = text(file, label)?;
233        let spliced = match self {
234            Self::Keys(syntax) => {
235                let target = decode(content).unwrap_or_default();
236                let current = syntax.values(text, keys).map_err(|f| failed(label, text, f))?;
237                let written: BTreeMap<&Key, &Value> =
238                    payload.leaves.iter().map(|(k, v)| (k, v)).collect();
239                let order = payload
240                    .leaves
241                    .iter()
242                    .map(|(key, _)| key)
243                    .filter(|key| keys.contains(*key))
244                    .chain(keys.iter().filter(|key| !written.contains_key(key)));
245                let edits: Vec<Edit<'_>> = order
246                    .filter(|key| current.get(*key) != target.get(*key))
247                    .map(|key| {
248                        let value = target.get(key);
249                        let styled = written.get(key).copied().filter(|w| Some(*w) == value);
250                        (key, styled.or(value))
251                    })
252                    .collect();
253                if edits.is_empty() {
254                    text.to_owned()
255                } else {
256                    syntax.apply(text, &edits, payload).map_err(|f| failed(label, text, f))?
257                }
258            },
259            Self::Block(markers) => {
260                let (content, _) = self::text(content, label)?;
261                markers
262                    .splice(text, content)
263                    .map_err(|(span, why)| failed(label, text, (Some(span), why)))?
264            },
265        };
266        let mut out = if bom { BOM.to_vec() } else { Vec::new() };
267        out.extend_from_slice(spliced.as_bytes());
268        Ok(out)
269    }
270
271    /// `file` without the part: its keys over `keys`, or its block, markers included.
272    pub(crate) fn remove(&self, file: &[u8], keys: &BTreeSet<Key>, label: &str) -> Result<Vec<u8>> {
273        match self {
274            Self::Keys(_) => {
275                self.splice(file, &encode(&Leaves::new()), keys, &Written::default(), label)
276            },
277            Self::Block(markers) => {
278                let (text, bom) = text(file, label)?;
279                let removed = markers
280                    .remove(text)
281                    .map_err(|(span, why)| failed(label, text, (Some(span), why)))?;
282                let mut out = if bom && !removed.is_empty() { BOM.to_vec() } else { Vec::new() };
283                out.extend_from_slice(removed.as_bytes());
284                Ok(out)
285            },
286        }
287    }
288}
289
290/// `bytes` as text, a leading BOM set aside, and whether there was one.
291fn text<'a>(bytes: &'a [u8], label: &str) -> Result<(&'a str, bool)> {
292    let bom = bytes.starts_with(BOM);
293    let bytes = bytes.strip_prefix(BOM).unwrap_or(bytes);
294    let text = str::from_utf8(bytes).map_err(|e| ParseError {
295        file: label.to_owned(),
296        text: String::new(),
297        span: None,
298        message: e.to_string(),
299    })?;
300    Ok((text, bom))
301}
302
303/// `failure` in `text` of `label`, as a parse error that points at it.
304fn failed(label: &str, text: &str, (span, message): Failure) -> crate::Error {
305    ParseError { file: label.to_owned(), text: text.to_owned(), span, message }.into()
306}
307
308#[cfg(test)]
309mod tests {
310    use alloc::collections::BTreeSet;
311
312    use super::{Scope, Shape, Syntax};
313    use crate::format::Format;
314    use crate::path::RelPath;
315
316    /// The keys shape of `path`.
317    fn keys(path: &str) -> Shape {
318        let path = RelPath::new(path).expect("a valid path");
319        let format = Format::of(&path);
320        Shape::of(Scope::Keys, &path, format, None, "p")
321            .expect("a structured file")
322            .expect("a part")
323    }
324
325    /// Splices `payload`'s keys into `file` as `shape` does, returning the file.
326    fn splice(shape: &Shape, file: &str, payload: &str) -> String {
327        let (content, written) = shape.read(payload.as_bytes(), "payload").expect("a payload");
328        let owned: BTreeSet<_> = shape.keys(&content);
329        let out =
330            shape.splice(file.as_bytes(), &content, &owned, &written, "file").expect("splices");
331        String::from_utf8(out).expect("UTF-8 out")
332    }
333
334    #[test]
335    fn toml_keys_join_the_target_tables() {
336        let shape = keys("Cargo.toml");
337        let file = "[workspace]\nmembers = [\"lib/*\"]\n\n[workspace.lints.clippy]\n# why\nunwrap_used = \"warn\" # ours\nlocal = \"allow\"\n";
338        let payload = "[workspace.lints.clippy]\nunwrap_used = \"deny\"\npedantic = { level = \"deny\", priority = -1 }\n";
339        let out = splice(&shape, file, payload);
340        assert!(out.contains("members = [\"lib/*\"]"), "the target's own keys stay: {out}");
341        assert!(out.contains("local = \"allow\""), "and its keys beside the shared ones");
342        assert!(
343            out.contains("unwrap_used = \"deny\" # ours"),
344            "a replaced value keeps its comment"
345        );
346        assert!(out.contains("# why"), "and the comment above it: {out}");
347        assert!(
348            out.contains("pedantic = { level = \"deny\", priority = -1 }"),
349            "a new leaf is added"
350        );
351    }
352
353    #[test]
354    fn toml_new_keys_keep_the_payload_layout() {
355        let shape = keys("deny.toml");
356        let payload = "[bans]\nmultiple-versions = \"deny\"\nwildcards         = \"deny\"\ndeny = [\n    { name = \"a\" },\n    { name = \"b\" },\n]\n";
357        let out = splice(
358            &shape,
359            "[bans]\nwildcards = \"allow\"\n\n[graph]\nall-features = true\n",
360            payload,
361        );
362        assert!(
363            out.contains("multiple-versions = \"deny\"\n"),
364            "a new key is aligned as the payload aligns it: {out}"
365        );
366        assert!(
367            out.contains("wildcards = \"deny\"\n"),
368            "a key the file holds keeps the file's layout: {out}"
369        );
370        assert!(
371            out.contains("deny = [\n    { name = \"a\" },\n    { name = \"b\" },\n]"),
372            "and a new value is laid out as the payload lays it out: {out}"
373        );
374        let out = splice(&shape, "[graph]\nall-features = true\n", payload);
375        assert!(
376            out.contains("[bans]\nmultiple-versions = \"deny\"\nwildcards         = \"deny\"\n"),
377            "keys of a new table are aligned as the payload aligns them: {out}"
378        );
379    }
380
381    #[test]
382    fn toml_keys_take_the_alignment_of_the_table_they_join() {
383        let shape = keys("Cargo.toml");
384        let out = splice(
385            &shape,
386            "[package]\nname    = \"x\"\nversion = \"1\"\n\n[lints]\nworkspace = true\n",
387            "[package]\ndescription = \"d\"\n",
388        );
389        assert!(
390            out.starts_with(
391                "[package]\nname        = \"x\"\nversion     = \"1\"\ndescription = \"d\"\n"
392            ),
393            "a key joining an aligned group realigns it, as taplo would: {out}"
394        );
395        let out = splice(&shape, "[a]\nx = 1\nlong = 2\n", "[a]\nmedium = 3\nz      = 4\n");
396        assert_eq!(
397            out, "[a]\nx = 1\nlong = 2\nmedium = 3\nz = 4\n",
398            "a file that spaces its keys by one keeps doing so, whatever the payload does"
399        );
400        let out = splice(&shape, "[a]\nx = 1\n", "[a]\nlonger = 2\nz      = 3\n");
401        assert_eq!(
402            out, "[a]\nx      = 1\nlonger = 2\nz      = 3\n",
403            "a file no group of which says takes the payload's alignment"
404        );
405    }
406
407    #[test]
408    fn toml_keys_realign_the_comments_of_their_group() {
409        let shape = keys("Cargo.toml");
410        let out = splice(
411            &shape,
412            "[a]\nx        = 1     # one\nlong_key = \"abc\" # two\n",
413            "[a]\nlongest_key = 3\n",
414        );
415        assert_eq!(
416            out, "[a]\nx           = 1     # one\nlong_key    = \"abc\" # two\nlongest_key = 3\n",
417            "comments line up after the longest line: {out}"
418        );
419        let file = "[a]\nk = [\n    \"a\",\n    \"b\",\n]\nlonger = 2\n\n[b]\nx    = 1\nlong = 2\n";
420        let out = splice(&shape, file, "[a]\nz = 3\n");
421        assert!(
422            out.starts_with("[a]\nk = [\n    \"a\",\n    \"b\",\n]\nlonger = 2\nz = 3\n"),
423            "a group with a value over several lines is aligned by no one: {out}"
424        );
425    }
426
427    #[test]
428    fn toml_tables_are_created_and_pruned() {
429        let shape = keys("Cargo.toml");
430        let out = splice(
431            &shape,
432            "[package]\nname = \"x\"\n",
433            "[workspace.lints.rust]\nunsafe_code = \"deny\"\n",
434        );
435        assert!(out.contains("[workspace.lints.rust]\nunsafe_code = \"deny\""), "created: {out}");
436        let (content, written) = shape
437            .read(
438                b"[workspace.lints.rust]
439unsafe_code = \"deny\"
440",
441                "p",
442            )
443            .expect("valid");
444        let owned = shape.keys(&content);
445        let emptied = shape.splice(out.as_bytes(), b"", &owned, &written, "file").expect("splices");
446        let emptied = String::from_utf8(emptied).expect("UTF-8");
447        assert_eq!(emptied.trim(), "[package]\nname = \"x\"", "a table the part leaves empty goes");
448    }
449
450    #[test]
451    fn toml_arrays_of_objects_are_arrays_of_tables() {
452        let shape = keys(".config/mise/mise.lock");
453        let file = "[[tools.dprint]]\nversion = \"0.57.4\"\n\n[tools.dprint.\"platforms.linux-x64\"]\nchecksum = \"sha256:a\"\n";
454        let payload = "[[tools.taplo]]\nversion = \"0.10.0\"\nbackend = \"aqua:tamasfe/taplo\"\n\n[tools.taplo.\"platforms.linux-x64\"]\nurl = \"https://example.com/taplo.gz\"\n";
455        let out = splice(&shape, file, payload);
456        assert!(out.starts_with(file), "the entry already there is untouched: {out}");
457        assert!(out.contains("[[tools.taplo]]\nversion = \"0.10.0\""), "an array of tables: {out}");
458        assert!(
459            out.contains("[tools.taplo.\"platforms.linux-x64\"]\nurl = "),
460            "with its sub-tables as tables: {out}"
461        );
462    }
463
464    #[test]
465    fn toml_arrays_of_tables_keep_the_payload_layout() {
466        let shape = keys("taplo.toml");
467        let rules = "# Ours, sorted.\n[[rule]]\ninclude = [\"**/Cargo.toml\"]\nkeys    = [\"dependencies\"]\n\n[rule.formatting]\nreorder_keys = true\n\n# Theirs.\n[[rule]]\ninclude = [\"x.toml\"]\n";
468        let payload = format!("[formatting]\nalign_entries = true\n\n{rules}");
469        let file = "exclude = [\"target/**\"]\n\n[formatting]\nalign_entries = false\n\n[[rule]]\ninclude = [\"**/Cargo.toml\"]\nkeys = [\"dependencies\"]\n\n[rule.formatting]\nreorder_keys = true\n";
470        let out = splice(&shape, file, &payload);
471        assert!(
472            out.ends_with(&format!("align_entries = true\n\n{rules}")),
473            "an array of tables the file holds is rewritten as the payload writes it: {out}"
474        );
475        assert!(out.starts_with("exclude = [\"target/**\"]\n"), "and the file's own keys stay");
476        let out = splice(&shape, "[formatting]\nalign_entries = true\n", &payload);
477        assert!(out.ends_with(rules), "as is one the file lacks: {out}");
478    }
479
480    #[test]
481    fn toml_arrays_keep_the_form_the_payload_writes() {
482        let shape = keys("deny.toml");
483        let out =
484            splice(&shape, "[bans]\nwildcards = \"deny\"\n", "[bans]\ndeny = [{ name = \"a\" }]\n");
485        assert!(
486            out.contains("deny = [{ name = \"a\" }]"),
487            "written inline, as the payload is: {out}"
488        );
489    }
490
491    #[test]
492    fn json_keys_keep_comments_and_commas() {
493        let shape = keys(".vscode/settings.json");
494        assert_eq!(shape, Shape::Keys(Syntax::Json), "JSON by extension");
495        let file = "{\n  // ours\n  \"rust-analyzer.cargo.target\": \"aarch64\",\n  \"editor.formatOnSave\": false,\n}\n";
496        let out = splice(
497            &shape,
498            file,
499            "{ \"editor.formatOnSave\": true, \"[toml]\": { \"editor.tabSize\": 2 } }",
500        );
501        assert!(out.contains("// ours"), "comments survive: {out}");
502        assert!(
503            out.contains("\"rust-analyzer.cargo.target\": \"aarch64\""),
504            "the target's keys stay"
505        );
506        assert!(out.contains("\"editor.formatOnSave\": true"), "an owned key is set");
507        assert!(out.contains("\"editor.tabSize\": 2"), "a nested key is added");
508    }
509
510    #[test]
511    fn json_new_keys_keep_the_payload_layout() {
512        let shape = keys("dprint.json");
513        let payload = "{\n  \"includes\": [\"**/*.md\"],\n  \"markdown\": { \"lineWidth\": 80, \"textWrap\": \"always\" }\n}\n";
514        let out = splice(
515            &shape,
516            "{\n  \"lineWidth\": 100,\n  \"includes\": [\"**/*.json\"]\n}\n",
517            payload,
518        );
519        assert!(
520            out.contains("\n  \"markdown\": { \"lineWidth\": 80, \"textWrap\": \"always\" }\n"),
521            "a key the file lacks is written whole, as the payload writes it: {out}"
522        );
523        assert!(
524            out.contains("\n  \"includes\": [\"**/*.md\"],\n"),
525            "and a value the file holds is replaced as the payload writes it: {out}"
526        );
527        let out = splice(&shape, "{\n  \"markdown\": { \"ours\": true }\n}\n", payload);
528        assert!(
529            out.contains("\"ours\": true") && out.contains("\"textWrap\": \"always\""),
530            "an object the file holds keeps its own keys beside the payload's: {out}"
531        );
532        let plugins = "{\n    \"plugins\": [\n        \"a\",\n        \"b\"\n    ]\n}\n";
533        let out = splice(&shape, "{\n  \"lineWidth\": 100\n}\n", plugins);
534        assert!(
535            out.contains("\n  \"plugins\": [\n      \"a\",\n      \"b\"\n  ]\n"),
536            "a value on several lines moves from the payload's indent to the file's: {out}"
537        );
538    }
539
540    #[test]
541    fn yaml_keys_keep_comments() {
542        let shape = keys(".github/dependabot.yml");
543        let file =
544            "# ours\nversion: 2\nupdates:\n  - package-ecosystem: cargo\n    directory: \"/\"\n";
545        let out =
546            splice(&shape, file, "version: 2\nregistries:\n  crates:\n    type: cargo-registry\n");
547        assert!(out.contains("# ours"), "comments survive: {out}");
548        assert!(out.contains("package-ecosystem: cargo"), "the target's keys stay");
549        assert!(out.contains("type: cargo-registry"), "a nested key is added");
550    }
551
552    #[test]
553    fn yaml_new_keys_keep_the_payload_layout() {
554        let shape = keys(".yamllint.yaml");
555        let payload = "extends: default\nignore-from-file: [.gitignore]\nrules:\n  truthy:\n    allowed-values: [\"true\", \"false\"]\n";
556        let out = splice(&shape, "extends: default\n", payload);
557        assert!(
558            out.contains("ignore-from-file: [.gitignore]\n"),
559            "a new sequence is written as the payload writes it: {out}"
560        );
561        assert!(
562            out.contains("rules:\n  truthy:\n    allowed-values: [\"true\", \"false\"]\n"),
563            "and a new value under mappings made for it, quotes and all: {out}"
564        );
565        let shape = keys(".github/dependabot.yml");
566        let updates = "updates:\n  - package-ecosystem: cargo\n    directory: \"/\"\n";
567        let out = splice(&shape, "version: 2\n", updates);
568        assert!(out.ends_with(updates), "a sequence of mappings keeps its keys' order: {out}");
569    }
570
571    #[test]
572    fn views_compare_values_not_layout() {
573        let shape = keys("a.toml");
574        let owned = shape.keys(&shape.read(b"a = 1\nb = \"x\"\n", "p").expect("valid").0);
575        let one = shape.view(b"a = 1\nb = \"x\"\n", &owned, "f").expect("reads");
576        let two = shape.view(b"b    =   'x'   # hi\na=1\nother = 2\n", &owned, "f").expect("reads");
577        assert_eq!(one, two, "reformatting and other keys are not part of the view");
578        assert_eq!(shape.view(b"other = 1\n", &owned, "f").expect("reads"), None, "absent part");
579    }
580}