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}