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