kdl/
entry.rs

1#[cfg(feature = "span")]
2use miette::SourceSpan;
3use std::{fmt::Display, str::FromStr};
4
5use crate::{v2_parser, KdlError, KdlIdentifier, KdlValue};
6
7/// KDL Entries are the "arguments" to KDL nodes: either a (positional)
8/// [`Argument`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#argument) or
9/// a (key/value)
10/// [`Property`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#property)
11#[derive(Debug, Clone, Eq)]
12pub struct KdlEntry {
13    pub(crate) ty: Option<KdlIdentifier>,
14    pub(crate) value: KdlValue,
15    pub(crate) name: Option<KdlIdentifier>,
16    pub(crate) format: Option<KdlEntryFormat>,
17    #[cfg(feature = "span")]
18    pub(crate) span: SourceSpan,
19}
20
21impl PartialEq for KdlEntry {
22    fn eq(&self, other: &Self) -> bool {
23        self.ty == other.ty
24            && self.value == other.value
25            && self.name == other.name
26            && self.format == other.format
27        // intentionally omitted: self.span == other.span
28    }
29}
30
31impl std::hash::Hash for KdlEntry {
32    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
33        self.ty.hash(state);
34        self.value.hash(state);
35        self.name.hash(state);
36        self.format.hash(state);
37        // intentionally omitted: self.span.hash(state)
38    }
39}
40
41impl KdlEntry {
42    /// Creates a new Argument (positional) KdlEntry.
43    pub fn new(value: impl Into<KdlValue>) -> Self {
44        Self {
45            ty: None,
46            value: value.into(),
47            name: None,
48            format: None,
49            #[cfg(feature = "span")]
50            span: (0..0).into(),
51        }
52    }
53
54    /// Gets a reference to this entry's name, if it's a property entry.
55    pub fn name(&self) -> Option<&KdlIdentifier> {
56        self.name.as_ref()
57    }
58
59    /// Gets a mutable reference to this node's name.
60    pub fn name_mut(&mut self) -> Option<&mut KdlIdentifier> {
61        self.name.as_mut()
62    }
63
64    /// Sets this node's name.
65    pub fn set_name(&mut self, name: Option<impl Into<KdlIdentifier>>) {
66        self.name = name.map(|x| x.into());
67    }
68
69    /// Gets the entry's value.
70    pub fn value(&self) -> &KdlValue {
71        &self.value
72    }
73
74    /// Gets a mutable reference to this entry's value.
75    pub fn value_mut(&mut self) -> &mut KdlValue {
76        &mut self.value
77    }
78
79    /// Sets the entry's value.
80    pub fn set_value(&mut self, value: impl Into<KdlValue>) {
81        self.value = value.into();
82    }
83
84    /// Gets this entry's span.
85    ///
86    /// This value will be properly initialized when created via [`crate::KdlDocument::parse`]
87    /// but may become invalidated if the document is mutated. We do not currently
88    /// guarantee this to yield any particularly consistent results at that point.
89    #[cfg(feature = "span")]
90    pub fn span(&self) -> SourceSpan {
91        self.span
92    }
93
94    /// Sets this entry's span.
95    #[cfg(feature = "span")]
96    pub fn set_span(&mut self, span: impl Into<SourceSpan>) {
97        self.span = span.into();
98    }
99
100    /// Gets the entry's type.
101    pub fn ty(&self) -> Option<&KdlIdentifier> {
102        self.ty.as_ref()
103    }
104
105    /// Gets a mutable reference to this entry's type.
106    pub fn ty_mut(&mut self) -> Option<&mut KdlIdentifier> {
107        self.ty.as_mut()
108    }
109
110    /// Sets the entry's type.
111    pub fn set_ty(&mut self, ty: impl Into<KdlIdentifier>) {
112        self.ty = Some(ty.into());
113    }
114
115    /// Gets the formatting details (including whitespace and comments) for this entry.
116    pub fn format(&self) -> Option<&KdlEntryFormat> {
117        self.format.as_ref()
118    }
119
120    /// Gets a mutable reference to this entry's formatting details.
121    pub fn format_mut(&mut self) -> Option<&mut KdlEntryFormat> {
122        self.format.as_mut()
123    }
124
125    /// Sets the formatting details for this entry.
126    pub fn set_format(&mut self, format: KdlEntryFormat) {
127        self.format = Some(format);
128    }
129
130    /// Creates a new Property (key/value) KdlEntry.
131    pub fn new_prop(key: impl Into<KdlIdentifier>, value: impl Into<KdlValue>) -> Self {
132        Self {
133            ty: None,
134            value: value.into(),
135            name: Some(key.into()),
136            format: None,
137            #[cfg(feature = "span")]
138            span: SourceSpan::from(0..0),
139        }
140    }
141
142    /// Clears leading and trailing text (whitespace, comments), as well as
143    /// resetting this entry's value to its default representation.
144    pub fn clear_format(&mut self) {
145        self.format = None;
146        if let Some(ty) = &mut self.ty {
147            ty.clear_format();
148        }
149        if let Some(name) = &mut self.name {
150            name.clear_format();
151        }
152    }
153
154    /// Length of this entry when rendered as a string.
155    pub fn len(&self) -> usize {
156        format!("{self}").len()
157    }
158
159    /// Returns true if this entry is completely empty (including whitespace).
160    pub fn is_empty(&self) -> bool {
161        self.len() == 0
162    }
163
164    /// Keeps the general entry formatting, though v1 entries will still be
165    /// updated to v2 while preserving as much as possible.
166    pub fn keep_format(&mut self) {
167        if let Some(fmt) = self.format_mut() {
168            fmt.autoformat_keep = true;
169        }
170    }
171
172    /// Auto-formats this entry.
173    pub fn autoformat(&mut self) {
174        // TODO once MSRV allows (1.80.0):
175        //self.format.take_if(|f| !f.autoformat_keep);
176        if !self
177            .format
178            .as_ref()
179            .map(|f| f.autoformat_keep)
180            .unwrap_or(false)
181        {
182            self.format = None;
183        } else {
184            #[cfg(feature = "v1")]
185            self.ensure_v2();
186            self.format = self.format.take().map(|f| KdlEntryFormat {
187                value_repr: f.value_repr,
188                leading: f.leading,
189                ..Default::default()
190            });
191        }
192
193        if let Some(name) = &mut self.name {
194            name.autoformat();
195        }
196    }
197
198    /// Parses a string into a entry.
199    ///
200    /// If the `v1-fallback` feature is enabled, this method will first try to
201    /// parse the string as a KDL v2 entry, and, if that fails, it will try
202    /// to parse again as a KDL v1 entry. If both fail, only the v2 parse
203    /// errors will be returned.
204    pub fn parse(s: &str) -> Result<Self, KdlError> {
205        #[cfg(not(feature = "v1-fallback"))]
206        {
207            v2_parser::try_parse(v2_parser::padded_node_entry, s)
208        }
209        #[cfg(feature = "v1-fallback")]
210        {
211            v2_parser::try_parse(v2_parser::padded_node_entry, s)
212                .or_else(|e| KdlEntry::parse_v1(s).map_err(|_| e))
213        }
214    }
215
216    /// Parses a KDL v1 string into an entry.
217    #[cfg(feature = "v1")]
218    pub fn parse_v1(s: &str) -> Result<Self, KdlError> {
219        let ret: Result<kdlv1::KdlEntry, kdlv1::KdlError> = s.parse();
220        ret.map(|x| x.into()).map_err(|e| e.into())
221    }
222
223    /// Makes sure this entry is in v2 format.
224    pub fn ensure_v2(&mut self) {
225        let value_repr = self.format.as_ref().map(|x| {
226            match &self.value {
227                KdlValue::String(val) => {
228                    // cleanup. I don't _think_ this should have any whitespace,
229                    // but just in case.
230                    let s = x.value_repr.trim();
231                    // convert raw strings to new format
232                    let s = s.strip_prefix('r').unwrap_or(s);
233                    let s = if crate::value::is_plain_ident(val) {
234                        val.into()
235                    } else if s
236                        .find(|c| v2_parser::NEWLINES.iter().any(|nl| nl.contains(c)))
237                        .is_some()
238                    {
239                        // Multiline string. Need triple quotes if they're not there already.
240                        if s.contains("\"\"\"") {
241                            // We're probably good. This could be more precise, but close enough.
242                            s.to_string()
243                        } else {
244                            // `"` -> `"""` but also extra newlines need to be
245                            // added because v2 strips the first and last ones.
246                            let s = s.replacen('\"', "\"\"\"\n", 1);
247                            s.chars()
248                                .rev()
249                                .collect::<String>()
250                                .replacen('\"', "\"\"\"\n", 1)
251                                .chars()
252                                .rev()
253                                .collect::<String>()
254                        }
255                    } else if !s.starts_with('#') {
256                        // `/` is no longer an escaped char in v2.
257                        s.replace("\\/", "/")
258                    } else {
259                        // We're all good! Let's move on.
260                        s.to_string()
261                    };
262                    s
263                }
264                // These have `#` prefixes now. The regular Display impl will
265                // take care of that.
266                KdlValue::Bool(_) | KdlValue::Null => format!("{}", self.value),
267                // These should be fine as-is?
268                KdlValue::Integer(_) | KdlValue::Float(_) => x.value_repr.clone(),
269            }
270        });
271
272        if let Some(value_repr) = value_repr.as_ref() {
273            self.format = Some(
274                self.format
275                    .clone()
276                    .map(|mut x| {
277                        x.value_repr = value_repr.into();
278                        x
279                    })
280                    .unwrap_or_else(|| KdlEntryFormat {
281                        value_repr: value_repr.into(),
282                        leading: " ".into(),
283                        ..Default::default()
284                    }),
285            )
286        }
287    }
288
289    /// Makes sure this entry is in v1 format.
290    #[cfg(feature = "v1")]
291    pub fn ensure_v1(&mut self) {
292        let value_repr = self.format.as_ref().map(|x| {
293            match &self.value {
294                KdlValue::String(val) => {
295                    // cleanup. I don't _think_ this should have any whitespace,
296                    // but just in case.
297                    let s = x.value_repr.trim();
298                    // convert raw strings to v1 format
299                    let s = if s.starts_with('#') {
300                        format!("r{s}")
301                    } else {
302                        s.to_string()
303                    };
304                    let s = if crate::value::is_plain_ident(val)
305                        && !s.starts_with('\"')
306                        && !s.starts_with("r#")
307                    {
308                        format!("\"{val}\"")
309                    } else if s
310                        .find(|c| v2_parser::NEWLINES.iter().any(|nl| nl.contains(c)))
311                        .is_some()
312                    {
313                        // Multiline string. Let's make sure it's v1.
314                        if s.contains("\"\"\"") {
315                            let prefix = s
316                                .chars()
317                                .rev()
318                                .skip_while(|c| c == &'"')
319                                .take_while(|c| {
320                                    v2_parser::NEWLINES.iter().any(|nl| nl.contains(*c))
321                                })
322                                .collect::<String>();
323                            let prefix = prefix.chars().rev().collect::<String>();
324                            // Sigh. Yeah. I didn't promise this would be _efficient_.
325                            let mut s = s;
326                            for nl in v2_parser::NEWLINES {
327                                s = s.replace(&format!("{nl}{prefix}"), nl);
328                            }
329                            // And now we strips the beginning and ending newlines.
330                            // Finally, replace `"""` with `"`.
331                            s
332                        } else {
333                            // It's already a v1 string
334                            s
335                        }
336                    } else if !s.starts_with("r#") {
337                        // `/` is an escaped char in v2
338                        let s = s.replace("\\/", "/"); // Maneuvering. Will fix in a sec.
339                        s.replace('/', "\\/")
340                    } else {
341                        // We're all good! Let's move on.
342                        s.to_string()
343                    };
344                    s
345                }
346                // No more # prefix for these
347                KdlValue::Bool(b) => b.to_string(),
348                KdlValue::Null => "null".to_string(),
349                // These should be fine as-is?
350                KdlValue::Integer(_) | KdlValue::Float(_) => x.value_repr.clone(),
351            }
352        });
353
354        if let Some(value_repr) = value_repr.as_ref() {
355            self.format = Some(
356                self.format
357                    .clone()
358                    .map(|mut x| {
359                        x.value_repr = value_repr.into();
360                        x
361                    })
362                    .unwrap_or_else(|| KdlEntryFormat {
363                        value_repr: value_repr.into(),
364                        leading: " ".into(),
365                        ..Default::default()
366                    }),
367            )
368        } else {
369            let v1_val = match self.value() {
370                KdlValue::String(s) => kdlv1::KdlValue::String(s.clone()),
371                KdlValue::Integer(i) => kdlv1::KdlValue::Base10(*i as i64),
372                KdlValue::Float(f) => kdlv1::KdlValue::Base10Float(*f),
373                KdlValue::Bool(b) => kdlv1::KdlValue::Bool(*b),
374                KdlValue::Null => kdlv1::KdlValue::Null,
375            };
376            self.format = Some(KdlEntryFormat {
377                value_repr: v1_val.to_string(),
378                leading: " ".into(),
379                ..Default::default()
380            })
381        }
382    }
383}
384
385#[cfg(feature = "v1")]
386impl From<kdlv1::KdlEntry> for KdlEntry {
387    fn from(value: kdlv1::KdlEntry) -> Self {
388        Self {
389            ty: value.ty().map(|x| x.clone().into()),
390            value: value.value().clone().into(),
391            name: value.name().map(|x| x.clone().into()),
392            format: Some(KdlEntryFormat {
393                value_repr: value.value_repr().unwrap_or("").into(),
394                leading: value.leading().unwrap_or("").into(),
395                trailing: value.trailing().unwrap_or("").into(),
396                ..Default::default()
397            }),
398            #[cfg(feature = "span")]
399            span: SourceSpan::new(value.span().offset().into(), value.span().len()),
400        }
401    }
402}
403
404impl Display for KdlEntry {
405    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
406        if let Some(KdlEntryFormat { leading, .. }) = &self.format {
407            write!(f, "{leading}")?;
408        }
409        if let Some(name) = &self.name {
410            write!(f, "{name}")?;
411            if let Some(KdlEntryFormat {
412                after_key,
413                after_eq,
414                ..
415            }) = &self.format
416            {
417                write!(f, "{after_key}={after_eq}")?;
418            } else {
419                write!(f, "=")?;
420            }
421        }
422        if let Some(ty) = &self.ty {
423            write!(f, "(")?;
424            if let Some(KdlEntryFormat { before_ty_name, .. }) = &self.format {
425                write!(f, "{before_ty_name}")?;
426            }
427            write!(f, "{ty}")?;
428            if let Some(KdlEntryFormat { after_ty_name, .. }) = &self.format {
429                write!(f, "{after_ty_name}")?;
430            }
431            write!(f, ")")?;
432        }
433        if let Some(KdlEntryFormat {
434            after_ty,
435            value_repr,
436            ..
437        }) = &self.format
438        {
439            write!(f, "{after_ty}{value_repr}")?;
440        } else {
441            write!(f, "{}", self.value)?;
442        }
443        if let Some(KdlEntryFormat { trailing, .. }) = &self.format {
444            write!(f, "{trailing}")?;
445        }
446        Ok(())
447    }
448}
449
450impl<T> From<T> for KdlEntry
451where
452    T: Into<KdlValue>,
453{
454    fn from(value: T) -> Self {
455        Self::new(value)
456    }
457}
458
459impl<K, V> From<(K, V)> for KdlEntry
460where
461    K: Into<KdlIdentifier>,
462    V: Into<KdlValue>,
463{
464    fn from((key, value): (K, V)) -> Self {
465        Self::new_prop(key, value)
466    }
467}
468
469impl FromStr for KdlEntry {
470    type Err = KdlError;
471
472    fn from_str(s: &str) -> Result<Self, Self::Err> {
473        Self::parse(s)
474    }
475}
476
477/// Formatting details for [`KdlEntry`]s.
478#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
479pub struct KdlEntryFormat {
480    /// The actual text representation of the entry's value.
481    pub value_repr: String,
482    /// Whitespace and comments preceding the entry itself.
483    pub leading: String,
484    /// Whitespace and comments following the entry itself.
485    pub trailing: String,
486    /// Whitespace and comments after the entry's type annotation's closing
487    /// `)`, before its value.
488    pub after_ty: String,
489    /// Whitespace and comments between the opening `(` of an entry's type
490    /// annotation and its actual type name.
491    pub before_ty_name: String,
492    /// Whitespace and comments between the actual type name and the closing
493    /// `)` in an entry's type annotation.
494    pub after_ty_name: String,
495    /// Whitespace and comments between an entry's key name and its equals sign.
496    pub after_key: String,
497    /// Whitespace and comments between an entry's equals sign and its value.
498    pub after_eq: String,
499    /// Do not clobber this format during autoformat
500    pub autoformat_keep: bool,
501}
502
503#[cfg(test)]
504mod test {
505    use super::*;
506
507    #[test]
508    fn reset_value_repr() -> miette::Result<()> {
509        let mut left_entry: KdlEntry = "   name=1.03e2".parse()?;
510        let mut right_entry: KdlEntry = "   name=103.0".parse()?;
511        assert_ne!(left_entry, right_entry);
512        left_entry.clear_format();
513        right_entry.clear_format();
514        assert_eq!(left_entry, right_entry);
515        Ok(())
516    }
517
518    #[test]
519    fn new() {
520        let entry = KdlEntry::new(42);
521        assert_eq!(
522            entry,
523            KdlEntry {
524                ty: None,
525                value: KdlValue::Integer(42),
526                name: None,
527                format: None,
528                #[cfg(feature = "span")]
529                span: SourceSpan::from(0..0),
530            }
531        );
532
533        let entry = KdlEntry::new_prop("name", 42);
534        assert_eq!(
535            entry,
536            KdlEntry {
537                ty: None,
538                value: KdlValue::Integer(42),
539                name: Some("name".into()),
540                format: None,
541                #[cfg(feature = "span")]
542                span: SourceSpan::from(0..0),
543            }
544        );
545    }
546
547    #[test]
548    fn parsing() -> miette::Result<()> {
549        let entry: KdlEntry = "foo".parse()?;
550        assert_eq!(
551            entry,
552            KdlEntry {
553                ty: None,
554                value: KdlValue::from("foo"),
555                name: None,
556                format: Some(KdlEntryFormat {
557                    value_repr: "foo".into(),
558                    ..Default::default()
559                }),
560                #[cfg(feature = "span")]
561                span: SourceSpan::from(0..3),
562            }
563        );
564
565        let entry: KdlEntry = "foo=bar".parse()?;
566        assert_eq!(
567            entry,
568            KdlEntry {
569                ty: None,
570                value: KdlValue::from("bar"),
571                name: Some("foo".parse()?),
572                format: Some(KdlEntryFormat {
573                    value_repr: "bar".into(),
574                    ..Default::default()
575                }),
576                #[cfg(feature = "span")]
577                span: SourceSpan::from(0..7),
578            }
579        );
580
581        let entry: KdlEntry = " \\\n (\"m\\\"eh\")0xDEADbeef\t\\\n".parse()?;
582        #[cfg_attr(not(feature = "span"), allow(unused_mut))]
583        {
584            let mut ty: KdlIdentifier = "\"m\\\"eh\"".parse()?;
585            #[cfg(feature = "span")]
586            {
587                ty.span = (5..12).into();
588            }
589            assert_eq!(
590                entry,
591                KdlEntry {
592                    ty: Some(ty),
593                    value: KdlValue::Integer(0xdeadbeef),
594                    name: None,
595                    format: Some(KdlEntryFormat {
596                        leading: " \\\n ".into(),
597                        trailing: "\t\\\n".into(),
598                        value_repr: "0xDEADbeef".into(),
599                        ..Default::default()
600                    }),
601                    #[cfg(feature = "span")]
602                    span: SourceSpan::from(0..26),
603                }
604            );
605        }
606
607        let entry: KdlEntry = " \\\n \"foo\"=(\"m\\\"eh\")0xDEADbeef\t\\\n".parse()?;
608        assert_eq!(
609            entry,
610            KdlEntry {
611                format: Some(KdlEntryFormat {
612                    leading: " \\\n ".into(),
613                    trailing: "\t\\\n".into(),
614                    value_repr: "0xDEADbeef".into(),
615                    before_ty_name: "".into(),
616                    after_ty_name: "".into(),
617                    after_ty: "".into(),
618                    after_key: "".into(),
619                    after_eq: "".into(),
620                    autoformat_keep: false
621                }),
622                ty: Some("\"m\\\"eh\"".parse()?),
623                value: KdlValue::Integer(0xdeadbeef),
624                name: Some("\"foo\"".parse()?),
625                #[cfg(feature = "span")]
626                span: SourceSpan::from(0..0),
627            }
628        );
629
630        Ok(())
631    }
632
633    #[test]
634    fn display() {
635        let entry = KdlEntry::new(KdlValue::Integer(42));
636        assert_eq!(format!("{entry}"), "42");
637
638        let entry = KdlEntry::new_prop("name", KdlValue::Integer(42));
639        assert_eq!(format!("{entry}"), "name=42");
640    }
641
642    #[cfg(feature = "v1")]
643    #[test]
644    fn v1_to_v2_format() -> miette::Result<()> {
645        let mut entry = KdlEntry::parse_v1(r##"r#"hello, world!"#"##)?;
646        entry.keep_format();
647        entry.autoformat();
648        assert_eq!(format!("{}", entry), r##" #"hello, world!"#"##);
649
650        let mut entry = KdlEntry::parse_v1(r#""hello, \" world!""#)?;
651        entry.keep_format();
652        entry.autoformat();
653        assert_eq!(format!("{}", entry), r#" "hello, \" world!""#);
654
655        let mut entry = KdlEntry::parse_v1("\"foo!`~.,<>\"")?;
656        entry.keep_format();
657        entry.autoformat();
658        assert_eq!(format!("{}", entry), " foo!`~.,<>");
659
660        let mut entry = KdlEntry::parse_v1("\"\nhello, world!\"")?;
661        entry.keep_format();
662        entry.autoformat();
663        assert_eq!(format!("{}", entry), " \"\"\"\n\nhello, world!\n\"\"\"");
664
665        let mut entry = KdlEntry::parse_v1("r#\"\nhello, world!\"#")?;
666        entry.keep_format();
667        entry.autoformat();
668        assert_eq!(format!("{}", entry), " #\"\"\"\n\nhello, world!\n\"\"\"#");
669
670        let mut entry = KdlEntry::parse_v1("true")?;
671        entry.keep_format();
672        entry.autoformat();
673        assert_eq!(format!("{}", entry), " #true");
674
675        let mut entry = KdlEntry::parse_v1("false")?;
676        entry.keep_format();
677        entry.autoformat();
678        assert_eq!(format!("{}", entry), " #false");
679
680        let mut entry = KdlEntry::parse_v1("null")?;
681        entry.keep_format();
682        entry.autoformat();
683        assert_eq!(format!("{}", entry), " #null");
684
685        let mut entry = KdlEntry::parse_v1("1_234_567")?;
686        entry.keep_format();
687        entry.autoformat();
688        assert_eq!(format!("{}", entry), " 1_234_567");
689
690        let mut entry = KdlEntry::parse_v1("1_234_567E-10")?;
691        entry.keep_format();
692        entry.autoformat();
693        assert_eq!(format!("{}", entry), " 1_234_567E-10");
694        Ok(())
695    }
696}