use std::sync::{Arc, LazyLock};
use indexmap::IndexMap;
use serde_json::Value;
use crate::generated::catalog as g;
use crate::types::{CatalogSource, ComponentDef, DefSchema, ExtensionDef, MacroDef};
pub const CORE_CATALOG_ID: &str = g::CATALOG_ID;
pub const CORE_LITE_CATALOG_ID: &str = g::LITE_CATALOG_ID;
pub const A2UI_BASIC_CATALOG_ID: &str = g::BASIC_CATALOG_ID;
pub const A2UI_VERSION: &str = g::BASIC_VERSION;
pub const UNKNOWN_COMPONENT: &str = g::UNKNOWN_COMPONENT;
pub const SUPPORTED_CATALOG_IDS: &[&str] = &[CORE_CATALOG_ID, CORE_LITE_CATALOG_ID, A2UI_BASIC_CATALOG_ID];
pub static CORE: LazyLock<CatalogSource> =
LazyLock::new(|| serde_json::from_str(g::CORE_CATALOG_JSON).expect("core.catalog.json"));
#[cfg(feature = "descriptions")]
pub fn descriptions() -> &'static Value {
static DESCRIPTIONS: LazyLock<Value> =
LazyLock::new(|| serde_json::from_str(crate::generated::descriptions::CORE_CATALOG_DESCRIPTIONS_JSON).expect("core.descriptions.json"));
&DESCRIPTIONS
}
#[derive(serde::Deserialize)]
struct MacrosFile {
macros: IndexMap<String, MacroDef>,
}
pub static CORE_MACROS: LazyLock<IndexMap<String, MacroDef>> =
LazyLock::new(|| serde_json::from_str::<MacrosFile>(g::MACROS_JSON).expect("macros.json").macros);
pub static TOKEN_GROUPS: LazyLock<IndexMap<String, Vec<String>>> = LazyLock::new(|| {
let file: IndexMap<String, Value> = serde_json::from_str(g::TOKENS_JSON).expect("tokens.json");
let mut out = IndexMap::new();
for (group, value) in file {
if group.starts_with('$') {
continue;
}
match value {
Value::Array(names) => {
out.insert(group, names.iter().filter_map(|v| v.as_str().map(str::to_string)).collect());
}
Value::Object(subs) => {
let order: Vec<&str> = g::TOKEN_GROUPS.iter().filter_map(|k| k.strip_prefix(&format!("{group}."))).collect();
let mut subs: Vec<(String, Value)> = subs.into_iter().collect();
subs.sort_by_key(|(sub, _)| order.iter().position(|o| *o == sub).unwrap_or(usize::MAX));
for (sub, names) in subs {
let names = names.as_array().map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect()).unwrap_or_default();
out.insert(format!("{group}.{sub}"), names);
}
}
_ => {}
}
}
out
});
pub fn parse_token_ref_str(value: &str) -> Option<(String, String)> {
let body = value.strip_prefix('$')?;
let (group, name) = body.rsplit_once('.')?;
let ident = |s: &str, first_lower: bool| {
let mut chars = s.chars();
let first = chars.next()?;
let ok_first = if first_lower { first.is_ascii_lowercase() } else { first.is_ascii_alphanumeric() };
if !ok_first || !chars.all(|c| c.is_ascii_alphanumeric()) {
return None;
}
Some(())
};
for segment in group.split('.') {
ident(segment, true)?;
}
ident(name, false)?;
Some((group.to_string(), name.to_string()))
}
pub fn parse_token_ref(value: &Value) -> Option<(String, String)> {
value.as_str().and_then(parse_token_ref_str)
}
pub fn is_known_token(value: &Value) -> bool {
match parse_token_ref(value) {
Some((group, name)) => TOKEN_GROUPS.get(&group).is_some_and(|names| names.contains(&name)),
None => false,
}
}
#[derive(Debug, Clone)]
pub struct CatalogView {
pub components: IndexMap<String, ComponentDef>,
pub enums: IndexMap<String, Vec<String>>,
pub defs: IndexMap<String, DefSchema>,
pub macros: IndexMap<String, MacroDef>,
pub extension_ids: Vec<String>,
pub owner: IndexMap<String, String>,
}
static CORE_VIEW: LazyLock<Arc<CatalogView>> = LazyLock::new(|| Arc::new(CatalogView::build(&[])));
impl CatalogView {
fn build(extensions: &[ExtensionDef]) -> CatalogView {
let core = &*CORE;
let mut view = CatalogView {
components: core.components.clone(),
enums: core.enums.clone(),
defs: core.defs.clone(),
macros: CORE_MACROS.clone(),
extension_ids: Vec::new(),
owner: IndexMap::new(),
};
for ext in extensions {
for (k, v) in &ext.components {
view.components.insert(k.clone(), v.clone());
view.owner.insert(k.clone(), ext.id.clone());
}
for (k, v) in ext.enums.iter().flatten() {
view.enums.insert(k.clone(), v.clone());
}
for (k, v) in ext.defs.iter().flatten() {
view.defs.insert(k.clone(), v.clone());
}
for (k, v) in ext.macros.iter().flatten() {
view.macros.insert(k.clone(), v.clone());
}
view.extension_ids.push(ext.id.clone());
}
view
}
pub fn core() -> Arc<CatalogView> {
CORE_VIEW.clone()
}
pub fn with(extensions: &[ExtensionDef]) -> Arc<CatalogView> {
if extensions.is_empty() {
Self::core()
} else {
Arc::new(Self::build(extensions))
}
}
pub fn component(&self, name: &str) -> Option<&ComponentDef> {
self.components.get(name)
}
pub fn knows_catalog(&self, catalog_id: &str) -> bool {
catalog_id == CORE_CATALOG_ID
|| catalog_id == CORE_LITE_CATALOG_ID
|| catalog_id == A2UI_BASIC_CATALOG_ID
|| self.extension_ids.iter().any(|id| id == catalog_id)
}
pub fn extension_of(&self, component: &str) -> Option<&str> {
if CORE.components.contains_key(component) {
return None;
}
self.owner.get(component).map(String::as_str)
}
}
pub fn component_names(lite: bool) -> Vec<String> {
CORE.components
.iter()
.filter(|(_, def)| def.is_offered() && (!lite || def.lite))
.map(|(name, _)| name.clone())
.collect()
}
pub fn component_def(name: &str) -> Option<&'static ComponentDef> {
CORE.components.get(name)
}