yaml-edit 0.3.5

A lossless parser and editor for YAML files
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
//! Conversion of syntax nodes to [`YamlValue`]s, following aliases and
//! applying merge keys.

use crate::anchor_resolution::{
    is_merge_key, merge_sources, resolve_alias_node, AliasError, AnchorRegistry, MergedMapping,
    ResolveError,
};
use crate::as_yaml::{NodeIdentity, YamlNode};
use crate::lex::SyntaxKind;
use crate::value::YamlValue;
use crate::yaml::SyntaxNode;
use rowan::ast::AstNode;
use std::collections::hash_map::{Entry, HashMap};

/// The identity of `key` as a mapping key, following aliases through
/// `registry`; see [`NodeIdentity`]. Without a registry, an alias is a
/// [`ResolveError::UnresolvedAlias`].
fn key_identity(
    key: &YamlNode,
    registry: Option<&AnchorRegistry>,
) -> Result<NodeIdentity, ResolveError> {
    crate::as_yaml::key_identity(key, registry).map_err(|e| match (registry, e) {
        (None, AliasError::Undefined { alias }) => ResolveError::UnresolvedAlias { alias },
        (_, e) => e.into(),
    })
}

/// The node `alias` refers to through `registry`, as the anchored node
/// and its content, and the registry itself.
///
/// An alias to one of the nodes in `expanding` is recursive.
fn alias_target<'r>(
    alias: &crate::yaml::Alias,
    registry: Option<&'r AnchorRegistry>,
    expanding: &[SyntaxNode],
) -> Result<(&'r AnchorRegistry, SyntaxNode, YamlNode), ResolveError> {
    let name = alias.name();
    let Some(registry) = registry else {
        return Err(ResolveError::UnresolvedAlias { alias: name });
    };
    let target = registry
        .resolve_alias_at(&name, alias.syntax())
        .ok_or_else(|| AliasError::Undefined {
            alias: name.clone(),
        })?;
    if expanding.contains(target) {
        return Err(AliasError::Recursive { alias: name }.into());
    }
    let node = YamlNode::from_syntax_peeled(target.clone())
        .ok_or(AliasError::Undefined { alias: name })?;
    Ok((registry, target.clone(), node))
}

/// Apply merge keys to create a new merged mapping (returns BTreeMap for YamlValue::Mapping)
///
/// Direct keys win over merged ones, and merge sources are taken in the
/// precedence order [`merge_sources`] returns them. Keys are compared by
/// [`key_identity`], so a direct `16` shadows a merged `0x10`; within one
/// mapping, base or merged, an equal key is a
/// [`ResolveError::DuplicateKey`]. Each key is stored under its
/// [`NodeIdentity::key_string`], and distinct keys with the same string
/// form, such as `1` and `'1'`, are a [`ResolveError::KeyCollision`].
/// Without a registry, `<<` is an ordinary key and an alias key is a
/// [`ResolveError::UnresolvedAlias`].
fn apply_merge_keys(
    mapping: &crate::yaml::Mapping,
    registry: Option<&AnchorRegistry>,
    expanding: &mut Vec<SyntaxNode>,
) -> Result<std::collections::BTreeMap<String, YamlValue>, ResolveError> {
    let mut merged_pairs = std::collections::BTreeMap::new();
    let mut seen = SeenKeys::default();
    let sources = match registry {
        Some(registry) => merge_sources(mapping, registry)?,
        None => Vec::new(),
    };
    for source in std::iter::once(mapping).chain(&sources) {
        // The identities of the keys of this mapping, and the keys as
        // written.
        let mut own_keys: HashMap<NodeIdentity, YamlNode> = HashMap::new();
        for (key, value) in source.iter() {
            check_single_node_in_parent(key.syntax())?;
            if registry.is_some() && is_merge_key(&key) {
                continue;
            }
            let identity = key_identity(&key, registry)?;
            match own_keys.entry(identity.clone()) {
                Entry::Occupied(first) => {
                    return Err(ResolveError::DuplicateKey {
                        first: first.get().to_string(),
                        second: key.to_string(),
                    });
                }
                Entry::Vacant(slot) => {
                    slot.insert(key.clone());
                }
            }
            // An earlier mapping, the base or a merged one, shadows this key.
            if seen.by_identity.contains_key(&identity) {
                continue;
            }
            let key_str = seen.record(identity, key)?;
            if let Some(yaml_value) = resolved_value(value, registry, expanding)? {
                merged_pairs.insert(key_str, yaml_value);
            }
        }
    }
    Ok(merged_pairs)
}

/// Fail if `slot`, a node that holds a single node such as a VALUE, holds
/// more than one, so converting it would drop the rest.
fn check_single_node(slot: &SyntaxNode) -> Result<(), ResolveError> {
    let extra = slot
        .children()
        .filter(|c| {
            matches!(
                c.kind(),
                SyntaxKind::SCALAR
                    | SyntaxKind::MAPPING
                    | SyntaxKind::SEQUENCE
                    | SyntaxKind::TAGGED_NODE
                    | SyntaxKind::ALIAS
            )
        })
        .nth(1);
    match extra {
        Some(extra) => Err(ResolveError::ExtraNode {
            node: extra.text().to_string(),
        }),
        None => Ok(()),
    }
}

/// Like [`check_single_node`], for the slot `node` sits in.
fn check_single_node_in_parent(node: &SyntaxNode) -> Result<(), ResolveError> {
    match node.parent() {
        Some(parent)
            if matches!(
                parent.kind(),
                SyntaxKind::DOCUMENT
                    | SyntaxKind::KEY
                    | SyntaxKind::VALUE
                    | SyntaxKind::SEQUENCE_ENTRY
                    | SyntaxKind::TAGGED_NODE
            ) =>
        {
            check_single_node(&parent)
        }
        _ => Ok(()),
    }
}

/// Turn `node` into a [`YamlValue`], following aliases and applying merge
/// keys at every level.
///
/// Without a registry, as for [`YamlValue::cast`], an alias is a
/// [`ResolveError::UnresolvedAlias`] and merge keys are not applied.
///
/// `expanding` holds the anchored nodes currently being expanded, so an alias
/// that refers back to one of them is reported as recursive.
pub(crate) fn resolved_value(
    node: YamlNode,
    registry: Option<&AnchorRegistry>,
    expanding: &mut Vec<SyntaxNode>,
) -> Result<Option<YamlValue>, ResolveError> {
    check_single_node_in_parent(node.syntax())?;
    match node {
        YamlNode::Alias(alias) => {
            let (registry, target, target_node) = alias_target(&alias, registry, expanding)?;
            expanding.push(target);
            let value = resolved_value(target_node, Some(registry), expanding);
            expanding.pop();
            value
        }
        YamlNode::Mapping(mapping) => Ok(Some(YamlValue::Mapping(apply_merge_keys(
            &mapping, registry, expanding,
        )?))),
        YamlNode::Sequence(seq) => {
            let mut items = Vec::new();
            for item in seq.values() {
                items.extend(resolved_value(item, registry, expanding)?);
            }
            Ok(Some(YamlValue::Sequence(items)))
        }
        YamlNode::TaggedNode(tagged) => resolved_tagged(&tagged, registry, expanding),
        YamlNode::Scalar(scalar) => Ok(Some(YamlValue::Scalar(
            crate::scalar::ScalarValue::from_scalar(&scalar),
        ))),
    }
}

/// Resolve a tagged node like [`resolved_value`]: `!!set`, `!!omap` and
/// `!!pairs` get their own [`YamlValue`] variants, other tagged
/// collections are [`YamlValue::Tagged`] and a tagged scalar keeps its tag
/// on the [`ScalarValue`](crate::ScalarValue).
///
/// A tag on an alias, which YAML does not allow but the parser accepts,
/// applies to the node the alias refers to, as if the alias were a copy of
/// it.
fn resolved_tagged(
    tagged: &crate::yaml::TaggedNode,
    registry: Option<&AnchorRegistry>,
    expanding: &mut Vec<SyntaxNode>,
) -> Result<Option<YamlValue>, ResolveError> {
    check_single_node(tagged.syntax())?;
    let content = tagged.syntax().children().find_map(YamlNode::from_syntax);
    let (Some(tag), Some(content)) = (Tag::of(tagged), content) else {
        return Ok(crate::scalar::ScalarValue::from_tagged(tagged).map(YamlValue::Scalar));
    };
    match content {
        YamlNode::Alias(alias) => resolved_tagged_alias(&tag, &alias, registry, expanding),
        content => resolved_with_tag(&tag, content, registry, expanding),
    }
}

/// A tag as written, which values keep, and as the `%TAG` directives where
/// it was written resolve it, which decides what it means.
struct Tag {
    written: String,
    resolved: String,
}

impl Tag {
    fn of(tagged: &crate::yaml::TaggedNode) -> Option<Self> {
        Some(Tag {
            written: tagged.tag()?,
            resolved: tagged.resolved_tag()?,
        })
    }
}

/// Resolve `alias`, which has tag `tag`, to the node it refers to under
/// that tag.
fn resolved_tagged_alias(
    tag: &Tag,
    alias: &crate::yaml::Alias,
    registry: Option<&AnchorRegistry>,
    expanding: &mut Vec<SyntaxNode>,
) -> Result<Option<YamlValue>, ResolveError> {
    let (registry, target, target_node) = alias_target(alias, registry, expanding)?;
    expanding.push(target);
    let value = match target_node {
        YamlNode::Alias(inner) => resolved_tagged_alias(tag, &inner, Some(registry), expanding),
        // The tag of the alias takes the place of that of the node.
        YamlNode::TaggedNode(tagged) => {
            match tagged.syntax().children().find_map(YamlNode::from_syntax) {
                Some(YamlNode::Alias(inner)) => {
                    resolved_tagged_alias(tag, &inner, Some(registry), expanding)
                }
                Some(content) => resolved_with_tag(tag, content, Some(registry), expanding),
                None => Ok(crate::scalar::ScalarValue::from_tagged(&tagged).map(|s| {
                    YamlValue::Scalar(s.with_tag_resolved_as(tag.written.clone(), &tag.resolved))
                })),
            }
        }
        content => resolved_with_tag(tag, content, Some(registry), expanding),
    };
    expanding.pop();
    value
}

/// Resolve `content`, an untagged scalar, mapping or sequence, as if it
/// had tag `tag`.
fn resolved_with_tag(
    tag: &Tag,
    content: YamlNode,
    registry: Option<&AnchorRegistry>,
    expanding: &mut Vec<SyntaxNode>,
) -> Result<Option<YamlValue>, ResolveError> {
    match (tag.resolved.as_str(), content) {
        (crate::tag::SET, YamlNode::Mapping(mapping)) => {
            resolved_set(&mapping, registry, expanding).map(|set| Some(YamlValue::Set(set)))
        }
        (crate::tag::SET, content) => Err(ResolveError::InvalidSetContent {
            content: content.syntax().text().to_string(),
        }),
        (crate::tag::OMAP, YamlNode::Sequence(seq)) => Ok(Some(YamlValue::OrderedMapping(
            resolved_pairs(tag, &seq, registry, expanding)?,
        ))),
        (crate::tag::PAIRS, YamlNode::Sequence(seq)) => Ok(Some(YamlValue::Pairs(resolved_pairs(
            tag, &seq, registry, expanding,
        )?))),
        (crate::tag::OMAP | crate::tag::PAIRS, content) => Err(ResolveError::InvalidPairsContent {
            tag: tag.written.clone(),
            content: content.syntax().text().to_string(),
        }),
        (_, YamlNode::Scalar(scalar)) => Ok(Some(YamlValue::Scalar(
            crate::scalar::ScalarValue::from_scalar(&scalar)
                .with_tag_resolved_as(tag.written.clone(), &tag.resolved),
        ))),
        (_, content) => Ok(resolved_value(content, registry, expanding)?
            .map(|value| YamlValue::Tagged(tag.written.clone(), Box::new(value)))),
    }
}

/// The members of a `!!set` whose content is `mapping`.
fn resolved_set(
    mapping: &crate::yaml::Mapping,
    registry: Option<&AnchorRegistry>,
    expanding: &mut Vec<SyntaxNode>,
) -> Result<std::collections::BTreeSet<String>, ResolveError> {
    for entry in mapping.entries() {
        let Some(value) = entry.value_node() else {
            continue;
        };
        let is_null = matches!(
            resolved_value(value.clone(), registry, expanding)?,
            Some(YamlValue::Scalar(s)) if s.scalar_type() == crate::scalar::ScalarType::Null
        );
        if !is_null {
            return Err(ResolveError::NonNullSetValue {
                member: entry
                    .key_node()
                    .map_or_else(String::new, |key| key.to_string()),
                value: value.to_string(),
            });
        }
    }
    let mut members = std::collections::BTreeSet::new();
    let mut seen = SeenKeys::default();
    for member in mapping.keys() {
        let Some(YamlValue::Scalar(_)) = resolved_value(member.clone(), registry, expanding)?
        else {
            return Err(ResolveError::InvalidSetMember {
                member: member.to_string(),
            });
        };
        let identity = key_identity(&member, registry)?;
        members.insert(seen.record(identity, member)?);
    }
    Ok(members)
}

/// The entries of a `!!omap` or `!!pairs` sequence tagged `tag`, in order.
///
/// Each item has to be a mapping, untagged or tagged `!!map`, with a single
/// entry.
fn resolved_pairs(
    tag: &Tag,
    seq: &crate::yaml::Sequence,
    registry: Option<&AnchorRegistry>,
    expanding: &mut Vec<SyntaxNode>,
) -> Result<Vec<(String, YamlValue)>, ResolveError> {
    let mut pairs = Vec::new();
    let mut seen = SeenKeys::default();
    for item in seq.values() {
        // A value keeps its tag as written, so a `!!map` item is told by
        // what the tag on its node resolves to.
        let item_is_map = matches!(
            &item,
            YamlNode::TaggedNode(t) if t.resolved_tag().as_deref() == Some(crate::tag::MAP)
        );
        let entries = match resolved_value(item.clone(), registry, expanding)? {
            Some(YamlValue::Mapping(entries)) => Some(entries),
            Some(YamlValue::Tagged(t, inner))
                if item_is_map || crate::tag::normalize(&t) == crate::tag::MAP =>
            {
                match *inner {
                    YamlValue::Mapping(entries) => Some(entries),
                    _ => None,
                }
            }
            _ => None,
        }
        .filter(|entries| entries.len() == 1)
        .ok_or_else(|| ResolveError::InvalidPairsItem {
            tag: tag.written.clone(),
            item: item.to_string(),
        })?;
        if tag.resolved == crate::tag::OMAP {
            let key = pair_key(&tag.written, &item, registry)?;
            seen.record(key_identity(&key, registry)?, key)?;
        }
        pairs.extend(entries);
    }
    Ok(pairs)
}

/// The key of `item`, a `!!omap` or `!!pairs` item that converts to a
/// mapping with a single entry, as written.
fn pair_key(
    tag: &str,
    item: &YamlNode,
    registry: Option<&AnchorRegistry>,
) -> Result<YamlNode, ResolveError> {
    let invalid = || ResolveError::InvalidPairsItem {
        tag: tag.to_string(),
        item: item.to_string(),
    };
    let resolved = match registry {
        Some(registry) => resolve_alias_node(item.clone(), registry)?,
        None => item.clone(),
    };
    let mapping = match resolved {
        YamlNode::Mapping(mapping) => mapping,
        YamlNode::TaggedNode(tagged) => tagged.as_mapping().ok_or_else(invalid)?,
        _ => return Err(invalid()),
    };
    let key = match registry {
        Some(registry) => MergedMapping::new(mapping, registry)?.keys().next(),
        None => mapping.keys().next(),
    };
    key.ok_or_else(invalid)
}

/// The keys seen so far in one mapping, `!!set` or `!!omap`, as written,
/// by their identity and by their string form.
#[derive(Default)]
struct SeenKeys {
    by_identity: HashMap<NodeIdentity, YamlNode>,
    by_string: HashMap<String, YamlNode>,
}

impl SeenKeys {
    /// Add `key`, as written, with identity `identity`, and return its
    /// string form.
    ///
    /// A key equal to one seen before is a [`ResolveError::DuplicateKey`],
    /// and a distinct key with the same string form a
    /// [`ResolveError::KeyCollision`].
    fn record(&mut self, identity: NodeIdentity, key: YamlNode) -> Result<String, ResolveError> {
        if let Some(first) = self.by_identity.get(&identity) {
            return Err(ResolveError::DuplicateKey {
                first: first.to_string(),
                second: key.to_string(),
            });
        }
        let key_str = identity.key_string();
        if let Some(first) = self.by_string.get(&key_str) {
            return Err(ResolveError::KeyCollision {
                first: first.to_string(),
                second: key.to_string(),
            });
        }
        self.by_identity.insert(identity, key.clone());
        self.by_string.insert(key_str.clone(), key);
        Ok(key_str)
    }
}