Skip to main content

lex_core/lex/ast/elements/
label.rs

1//! Label element
2//!
3//!     A label is a short identifier used by annotations and other elements. Labels are
4//!     components that carry a bit of information inside an element, only used in metadata.
5//!
6//!     Labels serve similar roles but have relevant differences from:
7//!         - Tags: An annotation can only have one label, while tags are typically multiple.
8//!         - IDs: labels are not unique, even in the same element
9//!
10//!     Labels support dot notation for namespaces:
11//!         Namespaced: lex.internal, plugin.myapp.custom
12//!         Namespaces are user defined, with the exception of the doc and lex namespaces
13//!         which are reserved.
14//!
15//! Syntax
16//!
17//!     <letter> (<letter> | <digit> | "_" | "-" | ".")*
18//!
19//!     Labels are used in data nodes, which have the syntax:
20//!         :: label params?
21//!
22//!     See [Data](super::data::Data) for how labels are used in data nodes.
23//!
24//!     Learn More:
25//!         - Labels spec: specs/v1/elements/label.lex
26
27use super::super::range::{Position, Range};
28use std::fmt;
29
30/// How the user spelled a label site, relative to the resolved canonical.
31///
32/// Forward-looking infrastructure for the label namespace model
33/// described in `comms/specs/general.lex` §4. The eventual contract:
34/// Lex accepts up to three spellings of any `lex.*` label and one
35/// spelling of any community label, and round-trips the user's choice
36/// so `lexd format` does not silently rewrite the source.
37///
38/// **Status in this PR (#584 PR 1/5):** the enum and the `form` field
39/// on [`Label`] exist; the parse-time `NormalizeLabels` stage tags
40/// labels that match its legacy-rewrite table, defaulting to
41/// `Canonical` for every other site. No formatter consults `form` yet
42/// — `lexd format` still emits `label.value` verbatim. PR 2 expands
43/// `NormalizeLabels` into the full resolution rules (universal
44/// prefix-strip, `Community` classification, hard-error for `doc.*`
45/// and unrecognized bare); PR 3 wires `form` through the formatter
46/// so the roundtrip promise lands end-to-end.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
48pub enum LabelForm {
49    /// User wrote the canonical form verbatim (`lex.metadata.author`).
50    /// This is also the default when a Label is constructed
51    /// programmatically without a specific source form (e.g. from the
52    /// wire codec, where the wire always carries the canonical).
53    Canonical,
54    /// User wrote the prefix-stripped form (`metadata.author`).
55    /// Resolves to canonical by prepending `lex.`.
56    Stripped,
57    /// User wrote the one-segment shortcut (`author`). Resolves to
58    /// canonical via the normative shortcut table.
59    Shortcut,
60    /// User wrote a community label (`acme.task`). Carries a single
61    /// accepted spelling and round-trips unchanged. Registry validation
62    /// is deferred to the analysis stage.
63    Community,
64}
65
66impl Default for LabelForm {
67    fn default() -> Self {
68        Self::Canonical
69    }
70}
71
72/// A label represents a named identifier in lex documents
73#[derive(Debug, Clone, PartialEq, Eq, Hash)]
74pub struct Label {
75    /// The resolved canonical spelling. For `lex.*` labels this is the
76    /// full `lex.X.Y.Z` form. For community labels it is the spelling
77    /// the user wrote (community labels carry only one accepted form).
78    pub value: String,
79    pub location: Range,
80    /// Which input form the user wrote. Defaults to
81    /// [`LabelForm::Canonical`] when a Label is built programmatically;
82    /// the parse-time `NormalizeLabels` stage tags this for the labels
83    /// it rewrites. See [`LabelForm`]'s docs for the PR-by-PR status —
84    /// in this PR the field is recorded but no formatter consults it.
85    pub form: LabelForm,
86}
87
88impl Label {
89    fn default_location() -> Range {
90        Range::new(0..0, Position::new(0, 0), Position::new(0, 0))
91    }
92    pub fn new(value: String) -> Self {
93        Self {
94            value,
95            location: Self::default_location(),
96            form: LabelForm::Canonical,
97        }
98    }
99    pub fn from_string(value: &str) -> Self {
100        Self {
101            value: value.to_string(),
102            location: Self::default_location(),
103            form: LabelForm::Canonical,
104        }
105    }
106
107    /// Preferred builder: `at(location)`
108    pub fn at(mut self, location: Range) -> Self {
109        self.location = location;
110        self
111    }
112
113    /// Builder: tag the input form the user wrote.
114    pub fn with_form(mut self, form: LabelForm) -> Self {
115        self.form = form;
116        self
117    }
118}
119
120impl fmt::Display for Label {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        write!(f, "{}", self.value)
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn test_label() {
132        let location = super::super::super::range::Range::new(
133            0..0,
134            super::super::super::range::Position::new(1, 0),
135            super::super::super::range::Position::new(1, 10),
136        );
137        let label = Label::new("test".to_string()).at(location.clone());
138        assert_eq!(label.location, location);
139    }
140
141    #[test]
142    fn label_defaults_form_to_canonical() {
143        assert_eq!(Label::new("x".into()).form, LabelForm::Canonical);
144        assert_eq!(Label::from_string("y").form, LabelForm::Canonical);
145    }
146
147    #[test]
148    fn with_form_tags_label() {
149        let l = Label::from_string("author").with_form(LabelForm::Shortcut);
150        assert_eq!(l.form, LabelForm::Shortcut);
151        assert_eq!(l.value, "author");
152    }
153
154    #[test]
155    fn label_form_default_is_canonical() {
156        assert_eq!(LabelForm::default(), LabelForm::Canonical);
157    }
158}