Skip to main content

light_fw_cli_core/
registry.rs

1use std::collections::HashMap;
2
3use serde::Deserialize;
4
5use crate::manifest::{ComponentKind, Provides};
6
7/// Встроенная таблица известных компонентов.
8///
9/// Файл `registry.toml` компилируется в бинарник через `include_str!`, поэтому
10/// CLI работает офлайн и не требует сетевого доступа.
11const REGISTRY_TOML: &str = include_str!("../registry.toml");
12
13/// Описание одного компонента из реестра.
14#[derive(Debug, Clone)]
15pub struct ComponentInfo {
16    /// Имя крейта-компонента (например, `light-fw-element-form`).
17    pub name: String,
18    /// Тип компонента.
19    pub kind: ComponentKind,
20    /// Человекочитаемое имя.
21    pub display_name: String,
22    /// Последняя известная версия.
23    pub latest_version: semver::Version,
24    /// Требование к версии ядра.
25    pub core_requirement: semver::VersionReq,
26    /// Фичи мета-крейта `light`, которые включает компонент.
27    pub light_features: Vec<String>,
28    /// Что компонент предоставляет (теги, атрибуты, директивы).
29    pub provides: Provides,
30}
31
32impl ComponentInfo {
33    /// Строка для рекомендации CLI, например `light-fw-element-form ^0.3`.
34    pub fn requirement(&self) -> String {
35        format!("{} {}", self.name, self.core_requirement)
36    }
37}
38
39/// Реестр компонентов с индексами по элементам, атрибутам и директивам.
40pub struct ComponentRegistry {
41    components: Vec<ComponentInfo>,
42    by_name: HashMap<String, usize>,
43    by_element: HashMap<String, usize>,
44    by_attribute: HashMap<String, usize>,
45    by_directive: HashMap<String, usize>,
46}
47
48#[derive(Debug, Deserialize)]
49struct RegistryFile {
50    #[serde(default)]
51    components: Vec<RegistryEntry>,
52}
53
54#[derive(Debug, Deserialize)]
55struct RegistryEntry {
56    name: String,
57    kind: ComponentKind,
58    #[serde(rename = "display-name")]
59    display_name: String,
60    version: String,
61    core: String,
62    #[serde(default, rename = "light-features")]
63    light_features: Vec<String>,
64    #[serde(default)]
65    elements: Vec<String>,
66    #[serde(default)]
67    attributes: Vec<String>,
68    #[serde(default)]
69    directives: Vec<String>,
70}
71
72impl ComponentRegistry {
73    /// Загружает встроенный реестр.
74    ///
75    /// Встроенный `registry.toml` корректен по построению; если бы он не
76    /// разбирался, это ошибка сборки, поэтому здесь допустимо ожидать
77    /// успешного разбора.
78    pub fn builtin() -> Self {
79        Self::from_toml(REGISTRY_TOML).expect("встроенный registry.toml должен разбираться")
80    }
81
82    /// Создаёт реестр из TOML-описания.
83    pub fn from_toml(text: &str) -> crate::Result<Self> {
84        let file: RegistryFile =
85            toml_edit::de::from_str(text).map_err(|e| crate::Error::Manifest(e.to_string()))?;
86        let mut registry = ComponentRegistry {
87            components: Vec::new(),
88            by_name: HashMap::new(),
89            by_element: HashMap::new(),
90            by_attribute: HashMap::new(),
91            by_directive: HashMap::new(),
92        };
93        for entry in file.components {
94            let latest_version = semver::Version::parse(&entry.version)
95                .map_err(|e| crate::Error::Manifest(format!("invalid version: {e}")))?;
96            let core_requirement = semver::VersionReq::parse(&entry.core)
97                .map_err(|e| crate::Error::Manifest(format!("invalid core range: {e}")))?;
98            let provides = Provides {
99                elements: entry.elements,
100                attributes: entry.attributes,
101                directives: entry.directives,
102            };
103            registry.insert(ComponentInfo {
104                name: entry.name,
105                kind: entry.kind,
106                display_name: entry.display_name,
107                latest_version,
108                core_requirement,
109                light_features: entry.light_features,
110                provides,
111            });
112        }
113        Ok(registry)
114    }
115
116    fn insert(&mut self, info: ComponentInfo) {
117        let idx = self.components.len();
118        for tag in &info.provides.elements {
119            self.by_element.insert(tag.clone(), idx);
120        }
121        for attr in &info.provides.attributes {
122            self.by_attribute.insert(attr.clone(), idx);
123        }
124        for dir in &info.provides.directives {
125            self.by_directive.insert(dir.clone(), idx);
126        }
127        self.by_name.insert(info.name.clone(), idx);
128        self.components.push(info);
129    }
130
131    /// Все компоненты, известные реестру.
132    pub fn all(&self) -> &[ComponentInfo] {
133        &self.components
134    }
135
136    /// Компонент по имени крейта.
137    pub fn by_name(&self, name: &str) -> Option<&ComponentInfo> {
138        self.by_name.get(name).map(|&i| &self.components[i])
139    }
140
141    /// Компонент, предоставляющий тег.
142    pub fn component_for_element(&self, tag: &str) -> Option<&ComponentInfo> {
143        self.by_element.get(tag).map(|&i| &self.components[i])
144    }
145
146    /// Компонент, предоставляющий атрибут.
147    pub fn component_for_attribute(&self, attr: &str) -> Option<&ComponentInfo> {
148        self.by_attribute.get(attr).map(|&i| &self.components[i])
149    }
150
151    /// Компонент, предоставляющий директиву.
152    pub fn component_for_directive(&self, dir: &str) -> Option<&ComponentInfo> {
153        self.by_directive.get(dir).map(|&i| &self.components[i])
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn builtin_registry_is_valid() {
163        let r = ComponentRegistry::builtin();
164        assert!(r.all().len() >= 2);
165        assert!(r.by_name("light-fw-element-form").is_some());
166    }
167
168    #[test]
169    fn resolves_elements() {
170        let r = ComponentRegistry::builtin();
171        let form = r.component_for_element("checkbox").expect("checkbox");
172        assert_eq!(form.name, "light-fw-element-form");
173        assert!(
174            r.component_for_element("text").is_none(),
175            "текстовые поля входят в ядро и компонента не требуют"
176        );
177    }
178
179    #[test]
180    fn resolves_attributes_and_directives() {
181        let r = ComponentRegistry::builtin();
182        assert_eq!(
183            r.component_for_attribute("required")
184                .map(|c| c.name.as_str()),
185            Some("light-fw-behavior-validation")
186        );
187        assert_eq!(
188            r.component_for_directive("@validate")
189                .map(|c| c.name.as_str()),
190            Some("light-fw-behavior-validation")
191        );
192    }
193
194    #[test]
195    fn unknown_component_is_none() {
196        let r = ComponentRegistry::builtin();
197        assert!(r.component_for_element("definitely-not-real").is_none());
198    }
199}