1use std::sync::{Arc, LazyLock};
6
7use indexmap::IndexMap;
8use serde_json::Value;
9
10use crate::generated::catalog as g;
11use crate::types::{CatalogSource, ComponentDef, DefSchema, ExtensionDef, MacroDef};
12
13pub const CORE_CATALOG_ID: &str = g::CATALOG_ID;
15pub const CORE_LITE_CATALOG_ID: &str = g::LITE_CATALOG_ID;
17pub const A2UI_BASIC_CATALOG_ID: &str = g::BASIC_CATALOG_ID;
19pub const A2UI_VERSION: &str = g::BASIC_VERSION;
21pub const UNKNOWN_COMPONENT: &str = g::UNKNOWN_COMPONENT;
23pub const SUPPORTED_CATALOG_IDS: &[&str] = &[CORE_CATALOG_ID, CORE_LITE_CATALOG_ID, A2UI_BASIC_CATALOG_ID];
25
26pub static CORE: LazyLock<CatalogSource> =
29 LazyLock::new(|| serde_json::from_str(g::CORE_CATALOG_JSON).expect("core.catalog.json"));
30
31#[cfg(feature = "descriptions")]
37pub fn descriptions() -> &'static Value {
38 static DESCRIPTIONS: LazyLock<Value> =
39 LazyLock::new(|| serde_json::from_str(crate::generated::descriptions::CORE_CATALOG_DESCRIPTIONS_JSON).expect("core.descriptions.json"));
40 &DESCRIPTIONS
41}
42
43#[derive(serde::Deserialize)]
44struct MacrosFile {
45 macros: IndexMap<String, MacroDef>,
46}
47
48pub static CORE_MACROS: LazyLock<IndexMap<String, MacroDef>> =
50 LazyLock::new(|| serde_json::from_str::<MacrosFile>(g::MACROS_JSON).expect("macros.json").macros);
51
52pub static TOKEN_GROUPS: LazyLock<IndexMap<String, Vec<String>>> = LazyLock::new(|| {
54 let file: IndexMap<String, Value> = serde_json::from_str(g::TOKENS_JSON).expect("tokens.json");
55 let mut out = IndexMap::new();
56 for (group, value) in file {
57 if group.starts_with('$') {
58 continue;
59 }
60 match value {
61 Value::Array(names) => {
62 out.insert(group, names.iter().filter_map(|v| v.as_str().map(str::to_string)).collect());
63 }
64 Value::Object(subs) => {
65 let order: Vec<&str> = g::TOKEN_GROUPS.iter().filter_map(|k| k.strip_prefix(&format!("{group}."))).collect();
68 let mut subs: Vec<(String, Value)> = subs.into_iter().collect();
69 subs.sort_by_key(|(sub, _)| order.iter().position(|o| *o == sub).unwrap_or(usize::MAX));
70 for (sub, names) in subs {
71 let names = names.as_array().map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect()).unwrap_or_default();
72 out.insert(format!("{group}.{sub}"), names);
73 }
74 }
75 _ => {}
76 }
77 }
78 out
79});
80
81pub fn parse_token_ref_str(value: &str) -> Option<(String, String)> {
84 let body = value.strip_prefix('$')?;
85 let (group, name) = body.rsplit_once('.')?;
86 let ident = |s: &str, first_lower: bool| {
87 let mut chars = s.chars();
88 let first = chars.next()?;
89 let ok_first = if first_lower { first.is_ascii_lowercase() } else { first.is_ascii_alphanumeric() };
90 if !ok_first || !chars.all(|c| c.is_ascii_alphanumeric()) {
91 return None;
92 }
93 Some(())
94 };
95 for segment in group.split('.') {
96 ident(segment, true)?;
97 }
98 ident(name, false)?;
99 Some((group.to_string(), name.to_string()))
100}
101
102pub fn parse_token_ref(value: &Value) -> Option<(String, String)> {
104 value.as_str().and_then(parse_token_ref_str)
105}
106
107pub fn is_known_token(value: &Value) -> bool {
109 match parse_token_ref(value) {
110 Some((group, name)) => TOKEN_GROUPS.get(&group).is_some_and(|names| names.contains(&name)),
111 None => false,
112 }
113}
114
115#[derive(Debug, Clone)]
118pub struct CatalogView {
119 pub components: IndexMap<String, ComponentDef>,
120 pub enums: IndexMap<String, Vec<String>>,
121 pub defs: IndexMap<String, DefSchema>,
122 pub macros: IndexMap<String, MacroDef>,
123 pub extension_ids: Vec<String>,
125 pub owner: IndexMap<String, String>,
127}
128
129static CORE_VIEW: LazyLock<Arc<CatalogView>> = LazyLock::new(|| Arc::new(CatalogView::build(&[])));
130
131impl CatalogView {
132 fn build(extensions: &[ExtensionDef]) -> CatalogView {
133 let core = &*CORE;
134 let mut view = CatalogView {
135 components: core.components.clone(),
136 enums: core.enums.clone(),
137 defs: core.defs.clone(),
138 macros: CORE_MACROS.clone(),
139 extension_ids: Vec::new(),
140 owner: IndexMap::new(),
141 };
142 for ext in extensions {
143 for (k, v) in &ext.components {
144 view.components.insert(k.clone(), v.clone());
145 view.owner.insert(k.clone(), ext.id.clone());
146 }
147 for (k, v) in ext.enums.iter().flatten() {
148 view.enums.insert(k.clone(), v.clone());
149 }
150 for (k, v) in ext.defs.iter().flatten() {
151 view.defs.insert(k.clone(), v.clone());
152 }
153 for (k, v) in ext.macros.iter().flatten() {
154 view.macros.insert(k.clone(), v.clone());
155 }
156 view.extension_ids.push(ext.id.clone());
157 }
158 view
159 }
160
161 pub fn core() -> Arc<CatalogView> {
163 CORE_VIEW.clone()
164 }
165
166 pub fn with(extensions: &[ExtensionDef]) -> Arc<CatalogView> {
168 if extensions.is_empty() {
169 Self::core()
170 } else {
171 Arc::new(Self::build(extensions))
172 }
173 }
174
175 pub fn component(&self, name: &str) -> Option<&ComponentDef> {
176 self.components.get(name)
177 }
178
179 pub fn knows_catalog(&self, catalog_id: &str) -> bool {
181 catalog_id == CORE_CATALOG_ID
182 || catalog_id == CORE_LITE_CATALOG_ID
183 || catalog_id == A2UI_BASIC_CATALOG_ID
184 || self.extension_ids.iter().any(|id| id == catalog_id)
185 }
186
187 pub fn extension_of(&self, component: &str) -> Option<&str> {
189 if CORE.components.contains_key(component) {
190 return None;
191 }
192 self.owner.get(component).map(String::as_str)
193 }
194}
195
196pub fn component_names(lite: bool) -> Vec<String> {
199 CORE.components
200 .iter()
201 .filter(|(_, def)| def.is_offered() && (!lite || def.lite))
202 .map(|(name, _)| name.clone())
203 .collect()
204}
205
206pub fn component_def(name: &str) -> Option<&'static ComponentDef> {
208 CORE.components.get(name)
209}