Skip to main content

noyalib/value/
tag.rs

1//! YAML tag types (`Tag`, `TaggedValue`) and tag utilities.
2
3// SPDX-License-Identifier: MIT OR Apache-2.0
4// Copyright (c) 2026 Noyalib. All rights reserved.
5
6use super::Value;
7use crate::prelude::*;
8use core::cmp::Ordering;
9use core::hash::{Hash, Hasher};
10
11// ============================================================================
12// Tag utilities
13// ============================================================================
14
15/// Strips a leading `!` from a string, if present.
16///
17/// # Examples
18///
19/// ```rust
20/// use noyalib::nobang;
21///
22/// assert_eq!(nobang("!foo"), "foo");
23/// assert_eq!(nobang("foo"), "foo");
24/// assert_eq!(nobang("!!int"), "!int");
25/// ```
26#[must_use]
27pub fn nobang(s: &str) -> &str {
28    s.strip_prefix('!').unwrap_or(s)
29}
30
31/// Result of checking whether a value looks like a YAML tag.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum MaybeTag<T> {
34    /// The value is a tag (starts with `!`).
35    Tag(String),
36    /// The value is not a tag.
37    NotTag(T),
38}
39
40/// Checks whether a value's display representation looks like a YAML tag.
41///
42/// A value is considered a tag if its string representation starts with `!`.
43///
44/// # Examples
45///
46/// ```rust
47/// use noyalib::{check_for_tag, MaybeTag};
48///
49/// let result = check_for_tag(&"!mytag");
50/// assert!(matches!(result, MaybeTag::Tag(_)));
51///
52/// let result = check_for_tag(&"plain");
53/// assert!(matches!(result, MaybeTag::NotTag(_)));
54/// ```
55pub fn check_for_tag<T: fmt::Display>(value: &T) -> MaybeTag<String> {
56    let s = value.to_string();
57    if s.starts_with('!') {
58        MaybeTag::Tag(s)
59    } else {
60        MaybeTag::NotTag(s)
61    }
62}
63
64/// A YAML tag.
65///
66/// Tags are used in YAML to denote the type of a value.
67/// For example, `!custom_type value` has the tag `!custom_type`.
68///
69/// Tag comparison ignores a leading `!` prefix, so `Tag::new("!foo") ==
70/// Tag::new("foo")`.
71///
72/// # Examples
73///
74/// ```rust
75/// use noyalib::Tag;
76///
77/// let tag = Tag::new("!custom");
78/// assert_eq!(tag.as_str(), "!custom");
79/// assert_eq!(Tag::new("!foo"), Tag::new("foo"));
80/// ```
81#[derive(Clone)]
82pub struct Tag(String, bool);
83
84// The `bool` marks an emitter directive: a formatting hint (comment,
85// flow wrapper, block-scalar style, anchor) that only this crate's
86// serializer creates, from the `fmt` and anchor wrapper types. It is set
87// by `Tag::directive` alone, so no tag string from a document, from
88// `Tag::new` or from another serde format can ever steer the emitter;
89// such a tag is always written back as the ordinary tag it is.
90
91impl fmt::Debug for Tag {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        let mut t = f.debug_tuple("Tag");
94        let _ = t.field(&self.0);
95        if self.1 {
96            let _ = t.field(&"directive");
97        }
98        t.finish()
99    }
100}
101
102impl Tag {
103    /// Creates a new tag from a string.
104    ///
105    /// # Examples
106    ///
107    /// ```
108    /// use noyalib::Tag;
109    /// let t = Tag::new("!Custom");
110    /// assert_eq!(t.as_str(), "!Custom");
111    /// ```
112    #[must_use]
113    pub fn new(tag: impl Into<String>) -> Self {
114        Self(tag.into(), false)
115    }
116
117    /// An emitter directive named by one of the `fmt` magic names. Only
118    /// the crate's own serializer calls this.
119    pub(crate) fn directive(name: &'static str) -> Self {
120        Self(name.to_owned(), true)
121    }
122
123    /// Whether this tag is an emitter directive rather than a YAML tag.
124    pub(crate) fn is_directive(&self) -> bool {
125        self.1
126    }
127
128    /// The magic name of a directive, `None` for an ordinary tag.
129    fn directive_name(&self) -> Option<&'static str> {
130        if !self.1 {
131            return None;
132        }
133        crate::fmt::DIRECTIVES
134            .iter()
135            .copied()
136            .find(|name| *name == self.0)
137    }
138
139    /// Returns the tag as a string slice.
140    ///
141    /// The leading `!` (or `!!`) is included; use [`Tag::nobang`]
142    /// for the unprefixed form.
143    ///
144    /// # Examples
145    ///
146    /// ```
147    /// use noyalib::Tag;
148    /// assert_eq!(Tag::new("!Custom").as_str(), "!Custom");
149    /// assert_eq!(Tag::new("!!str").as_str(), "!!str");
150    /// ```
151    #[must_use]
152    pub fn as_str(&self) -> &str {
153        &self.0
154    }
155
156    /// Consumes the tag and returns the inner string.
157    ///
158    /// # Examples
159    ///
160    /// ```
161    /// use noyalib::Tag;
162    /// let s: String = Tag::new("!Custom").into_string();
163    /// assert_eq!(s, "!Custom");
164    /// ```
165    #[must_use]
166    pub fn into_string(self) -> String {
167        self.0
168    }
169
170    /// Returns the tag string with a single leading `!` stripped.
171    ///
172    /// Strips at most one `!` — the YAML 1.2 *primary* tag
173    /// handle. The secondary `!!` handle keeps one `!` after the
174    /// strip (`!!str` → `!str`); use `Tag::as_str().trim_start_matches('!')`
175    /// if you want every `!` removed.
176    ///
177    /// # Examples
178    ///
179    /// ```
180    /// use noyalib::Tag;
181    /// assert_eq!(Tag::new("!Custom").nobang(), "Custom");
182    /// assert_eq!(Tag::new("!!str").nobang(), "!str");
183    /// assert_eq!(Tag::new("plain").nobang(), "plain");
184    /// ```
185    #[must_use]
186    pub fn nobang(&self) -> &str {
187        nobang(&self.0)
188    }
189}
190
191impl fmt::Display for Tag {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        write!(f, "{}", self.0)
194    }
195}
196
197impl From<&str> for Tag {
198    fn from(s: &str) -> Self {
199        Self::new(s)
200    }
201}
202
203impl From<String> for Tag {
204    fn from(s: String) -> Self {
205        Self::new(s)
206    }
207}
208
209impl AsRef<str> for Tag {
210    fn as_ref(&self) -> &str {
211        &self.0
212    }
213}
214
215impl PartialEq for Tag {
216    fn eq(&self, other: &Self) -> bool {
217        self.1 == other.1 && nobang(&self.0) == nobang(&other.0)
218    }
219}
220
221impl Eq for Tag {}
222
223impl Hash for Tag {
224    fn hash<H: Hasher>(&self, state: &mut H) {
225        nobang(&self.0).hash(state);
226        self.1.hash(state);
227    }
228}
229
230impl PartialOrd for Tag {
231    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
232        Some(self.cmp(other))
233    }
234}
235
236impl Ord for Tag {
237    fn cmp(&self, other: &Self) -> Ordering {
238        nobang(&self.0)
239            .cmp(nobang(&other.0))
240            .then(self.1.cmp(&other.1))
241    }
242}
243
244impl TryFrom<&[u8]> for Tag {
245    type Error = core::str::Utf8Error;
246
247    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
248        core::str::from_utf8(bytes).map(Self::new)
249    }
250}
251
252/// A tagged YAML value.
253///
254/// Represents a value with an explicit YAML tag, such as `!custom_type value`.
255/// Tags are used to specify the type or interpretation of a value.
256///
257/// # Examples
258///
259/// ```rust
260/// use noyalib::{Tag, TaggedValue, Value};
261///
262/// let tagged = TaggedValue::new(
263///     Tag::new("!timestamp"),
264///     Value::String("2024-01-01".to_string()),
265/// );
266/// assert_eq!(tagged.tag().as_str(), "!timestamp");
267/// ```
268#[derive(Debug, Clone, PartialEq, Eq)]
269pub struct TaggedValue {
270    /// The tag.
271    tag: Tag,
272    /// The value. `pub(crate)` so the parent module's `Value` impls can
273    /// move the inner value out (e.g. `Value::untag`).
274    pub(crate) value: Box<Value>,
275}
276
277impl TaggedValue {
278    /// Creates a new tagged value.
279    ///
280    /// # Examples
281    ///
282    /// ```
283    /// use noyalib::{Tag, TaggedValue, Value};
284    /// let tv = TaggedValue::new(Tag::new("!Custom"), Value::from("hello"));
285    /// assert_eq!(tv.tag().as_str(), "!Custom");
286    /// assert_eq!(tv.value().as_str(), Some("hello"));
287    /// ```
288    #[must_use]
289    pub fn new(tag: Tag, value: Value) -> Self {
290        Self {
291            tag,
292            value: Box::new(value),
293        }
294    }
295
296    /// Returns a reference to the tag.
297    ///
298    /// # Examples
299    ///
300    /// ```
301    /// use noyalib::{Tag, TaggedValue, Value};
302    /// let tv = TaggedValue::new(Tag::new("!Color"), Value::from("#ff8800"));
303    /// assert_eq!(tv.tag().as_str(), "!Color");
304    /// ```
305    #[must_use]
306    pub fn tag(&self) -> &Tag {
307        &self.tag
308    }
309
310    /// Returns a reference to the inner value.
311    ///
312    /// # Examples
313    ///
314    /// ```
315    /// use noyalib::{Tag, TaggedValue, Value};
316    /// let tv = TaggedValue::new(Tag::new("!Color"), Value::from("#ff8800"));
317    /// assert_eq!(tv.value().as_str(), Some("#ff8800"));
318    /// ```
319    #[must_use]
320    pub fn value(&self) -> &Value {
321        &self.value
322    }
323
324    /// Returns a mutable reference to the inner value.
325    ///
326    /// # Examples
327    ///
328    /// ```
329    /// use noyalib::{Tag, TaggedValue, Value};
330    /// let mut tv = TaggedValue::new(Tag::new("!Color"), Value::from("#000"));
331    /// *tv.value_mut() = Value::from("#ff8800");
332    /// assert_eq!(tv.value().as_str(), Some("#ff8800"));
333    /// ```
334    #[must_use]
335    pub fn value_mut(&mut self) -> &mut Value {
336        &mut self.value
337    }
338
339    /// Consumes the tagged value and returns the tag and value
340    /// as separate owned components.
341    ///
342    /// # Examples
343    ///
344    /// ```
345    /// use noyalib::{Tag, TaggedValue, Value};
346    /// let tv = TaggedValue::new(Tag::new("!Custom"), Value::from(42_i64));
347    /// let (tag, value) = tv.into_parts();
348    /// assert_eq!(tag.as_str(), "!Custom");
349    /// assert_eq!(value.as_i64(), Some(42));
350    /// ```
351    #[must_use]
352    pub fn into_parts(self) -> (Tag, Value) {
353        (self.tag, *self.value)
354    }
355}
356
357impl fmt::Display for TaggedValue {
358    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359        write!(f, "{} {}", self.tag, self.value)
360    }
361}
362
363impl serde_core::Serialize for TaggedValue {
364    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
365    where
366        S: serde_core::Serializer,
367    {
368        // The wire form is a single-entry map keyed by the tag string,
369        // which is what a serializer with no YAML-tag concept
370        // (`serde_json` and friends) needs (#350). It travels inside a
371        // newtype struct named `MAGIC_TAGGED`: a generic serializer
372        // passes a newtype through untouched, and this crate's own
373        // serializer recognises the name, not the shape, when it
374        // rebuilds `Value::Tagged` -- so a genuine mapping whose only
375        // key starts with `!` is never mistaken for a tag (#377).
376        // A directive goes back out under its own magic name, so a
377        // `Value` built by `to_value` keeps steering the emitter when it
378        // is serialised again, and is never confused with a tag.
379        if let Some(name) = self.tag.directive_name() {
380            return serializer.serialize_newtype_struct(name, self.value());
381        }
382        serializer.serialize_newtype_struct(crate::fmt::MAGIC_TAGGED, &TaggedWire(self))
383    }
384}
385
386/// The single-entry-map wire form of a [`TaggedValue`]; see the
387/// `Serialize` impl above.
388struct TaggedWire<'a>(&'a TaggedValue);
389
390impl serde_core::Serialize for TaggedWire<'_> {
391    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
392    where
393        S: serde_core::Serializer,
394    {
395        use serde_core::ser::SerializeMap as _;
396        let mut map = serializer.serialize_map(Some(1))?;
397        map.serialize_entry(self.0.tag.as_str(), self.0.value())?;
398        map.end()
399    }
400}
401
402impl<'de> serde_core::Deserialize<'de> for TaggedValue {
403    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
404    where
405        D: serde_core::Deserializer<'de>,
406    {
407        struct TaggedValueVisitor;
408
409        impl<'de> serde_core::de::Visitor<'de> for TaggedValueVisitor {
410            type Value = TaggedValue;
411
412            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
413                formatter.write_str("a single-entry map representing a tagged value")
414            }
415
416            fn visit_map<A>(self, mut map: A) -> Result<TaggedValue, A::Error>
417            where
418                A: serde_core::de::MapAccess<'de>,
419            {
420                let (tag, value): (String, Value) = map
421                    .next_entry()?
422                    .ok_or_else(|| serde_core::de::Error::custom("expected a single-entry map"))?;
423                Ok(TaggedValue::new(Tag::new(tag), value))
424            }
425        }
426
427        deserializer.deserialize_map(TaggedValueVisitor)
428    }
429}
430
431impl<'de> serde_core::Deserializer<'de> for &'de TaggedValue {
432    type Error = crate::Error;
433
434    fn deserialize_any<V>(self, visitor: V) -> crate::Result<V::Value>
435    where
436        V: serde_core::de::Visitor<'de>,
437    {
438        visitor.visit_map(TaggedValueMapAccess {
439            tag: Some(self.tag.as_str()),
440            value: Some(self.value()),
441        })
442    }
443
444    fn deserialize_enum<V>(
445        self,
446        _name: &'static str,
447        _variants: &'static [&'static str],
448        visitor: V,
449    ) -> crate::Result<V::Value>
450    where
451        V: serde_core::de::Visitor<'de>,
452    {
453        visitor.visit_enum(TaggedValueEnumAccess { tagged: self })
454    }
455
456    serde_core::forward_to_deserialize_any! {
457        bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str string bytes
458        byte_buf option unit unit_struct newtype_struct seq tuple
459        tuple_struct map struct identifier ignored_any
460    }
461}
462
463struct TaggedValueMapAccess<'de> {
464    tag: Option<&'de str>,
465    value: Option<&'de Value>,
466}
467
468impl<'de> serde_core::de::MapAccess<'de> for TaggedValueMapAccess<'de> {
469    type Error = crate::Error;
470
471    fn next_key_seed<K>(&mut self, seed: K) -> crate::Result<Option<K::Value>>
472    where
473        K: serde_core::de::DeserializeSeed<'de>,
474    {
475        match self.tag.take() {
476            Some(tag) => seed
477                .deserialize(serde_core::de::value::BorrowedStrDeserializer::new(tag))
478                .map(Some),
479            None => Ok(None),
480        }
481    }
482
483    fn next_value_seed<V>(&mut self, seed: V) -> crate::Result<V::Value>
484    where
485        V: serde_core::de::DeserializeSeed<'de>,
486    {
487        match self.value.take() {
488            Some(value) => seed.deserialize(value),
489            None => Err(serde_core::de::Error::custom("value is missing")),
490        }
491    }
492}
493
494struct TaggedValueEnumAccess<'de> {
495    tagged: &'de TaggedValue,
496}
497
498impl<'de> serde_core::de::EnumAccess<'de> for TaggedValueEnumAccess<'de> {
499    type Error = crate::Error;
500    type Variant = TaggedValueVariantAccess<'de>;
501
502    fn variant_seed<V>(self, seed: V) -> crate::Result<(V::Value, Self::Variant)>
503    where
504        V: serde_core::de::DeserializeSeed<'de>,
505    {
506        let deserializer = serde_core::de::value::BorrowedStrDeserializer::<crate::Error>::new(
507            self.tagged.tag.nobang(),
508        );
509        let variant = seed.deserialize(deserializer)?;
510        Ok((
511            variant,
512            TaggedValueVariantAccess {
513                value: self.tagged.value(),
514            },
515        ))
516    }
517}
518
519struct TaggedValueVariantAccess<'de> {
520    value: &'de Value,
521}
522
523impl<'de> serde_core::de::VariantAccess<'de> for TaggedValueVariantAccess<'de> {
524    type Error = crate::Error;
525
526    fn unit_variant(self) -> crate::Result<()> {
527        Ok(())
528    }
529
530    fn newtype_variant_seed<T>(self, seed: T) -> crate::Result<T::Value>
531    where
532        T: serde_core::de::DeserializeSeed<'de>,
533    {
534        seed.deserialize(self.value)
535    }
536
537    fn tuple_variant<V>(self, _len: usize, visitor: V) -> crate::Result<V::Value>
538    where
539        V: serde_core::de::Visitor<'de>,
540    {
541        serde_core::Deserializer::deserialize_seq(self.value, visitor)
542    }
543
544    fn struct_variant<V>(
545        self,
546        _fields: &'static [&'static str],
547        visitor: V,
548    ) -> crate::Result<V::Value>
549    where
550        V: serde_core::de::Visitor<'de>,
551    {
552        serde_core::Deserializer::deserialize_map(self.value, visitor)
553    }
554}