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}