pub mod detect;
use std::collections::HashMap;
use serde::{Deserialize, Deserializer, Serialize};
use crate::core::signatures::ExpectedDomain;
pub const CATALOG_DATA: &str = include_str!("data/catalog.json");
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
pub struct Locale(String);
impl Locale {
pub fn new(value: &str) -> Locale {
Locale(value.trim().to_ascii_lowercase())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl<'de> Deserialize<'de> for Locale {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Ok(Self::new(&value))
}
}
impl std::fmt::Display for Locale {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Kind {
Structural = 0,
Action = 1,
Value = 2,
Event = 3,
Operator = 4,
Enum = 5,
Setting = 6,
}
impl Kind {
pub const NUM_KINDS: usize = 7;
pub const fn as_index(self) -> usize {
self as usize
}
pub fn as_str(self) -> &'static str {
match self {
Kind::Structural => "structural",
Kind::Action => "action",
Kind::Value => "value",
Kind::Event => "event",
Kind::Operator => "operator",
Kind::Enum => "enum",
Kind::Setting => "setting",
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ParamCoercions {
#[serde(default)]
pub false_as_number: bool,
#[serde(default)]
pub true_as_number: bool,
#[serde(default)]
pub zero_as_null: bool,
#[serde(default)]
pub null_vector_as_null: bool,
#[serde(default)]
pub empty_array_as_string: bool,
}
#[derive(Debug, Clone)]
pub struct CatalogEntry {
pub id: String,
pub kind: Kind,
pub(crate) params: Vec<String>,
pub(crate) param_names: Option<Vec<String>>,
pub(crate) param_aliases: Vec<HashMap<Locale, Vec<String>>>,
pub(crate) param_domains: Vec<Option<String>>,
pub(crate) param_defaults: Vec<Option<String>>,
pub(crate) param_types: Vec<Option<String>>,
pub(crate) param_coercions: Vec<Option<ParamCoercions>>,
pub(crate) return_type: Option<String>,
pub(crate) variadic: bool,
pub(crate) aliases: HashMap<Locale, Vec<String>>,
}
#[derive(Debug, Clone)]
pub struct LocalizedStringEntry {
pub id: String,
pub(crate) aliases: HashMap<Locale, Vec<String>>,
}
fn spelling<'a>(aliases: &'a HashMap<Locale, Vec<String>>, locale: &Locale) -> Option<&'a str> {
aliases
.get(locale)
.and_then(|spellings| spellings.first())
.map(String::as_str)
}
fn spellings_for<'a>(aliases: &'a HashMap<Locale, Vec<String>>, locale: &Locale) -> &'a [String] {
aliases.get(locale).map(Vec::as_slice).unwrap_or_default()
}
impl LocalizedStringEntry {
pub fn spelling(&self, locale: &Locale) -> Option<&str> {
spelling(&self.aliases, locale)
}
pub fn spellings(&self, locale: &Locale) -> &[String] {
spellings_for(&self.aliases, locale)
}
}
impl CatalogEntry {
pub fn spelling(&self, locale: &Locale) -> Option<&str> {
spelling(&self.aliases, locale)
}
pub fn spellings(&self, locale: &Locale) -> &[String] {
spellings_for(&self.aliases, locale)
}
pub fn resolve_param(&self, locale: &Locale, spelling: &str) -> Option<usize> {
let matches = self
.params
.iter()
.enumerate()
.filter(|(index, canonical)| {
canonical == &spelling
|| self
.param_aliases
.get(*index)
.and_then(|aliases| aliases.get(locale))
.is_some_and(|aliases| aliases.iter().any(|alias| alias == spelling))
})
.map(|(index, _)| index)
.collect::<Vec<_>>();
(matches.len() == 1).then(|| matches[0])
}
pub fn params(&self) -> &[String] {
&self.params
}
pub fn param_count(&self) -> usize {
self.params.len()
}
pub fn param_name(&self, index: usize) -> Option<&str> {
let names = self.param_names.as_deref().unwrap_or(&self.params);
param_at(names, index, self.variadic).map(String::as_str)
}
pub fn required_param_count(&self) -> usize {
(0..self.params.len())
.rev()
.find(|index| {
self.param_defaults
.get(*index)
.and_then(Option::as_ref)
.is_none()
})
.map_or(0, |index| index + 1)
}
pub fn has_param_defaults(&self) -> bool {
self.param_defaults.iter().any(Option::is_some)
}
pub fn param_default(&self, index: usize) -> Option<&str> {
param_at(&self.param_defaults, index, self.variadic).and_then(Option::as_deref)
}
pub fn param_domain(&self, index: usize) -> Option<&str> {
param_at(&self.param_domains, index, self.variadic).and_then(Option::as_deref)
}
pub fn param_type(&self, index: usize) -> Option<&str> {
param_at(&self.param_types, index, self.variadic).and_then(Option::as_deref)
}
pub fn param_coercions(&self, index: usize) -> Option<&ParamCoercions> {
param_at(&self.param_coercions, index, self.variadic).and_then(Option::as_ref)
}
pub fn return_type(&self) -> Option<&str> {
self.return_type.as_deref()
}
pub fn is_variadic(&self) -> bool {
self.variadic
}
}
fn param_at<T>(values: &[T], index: usize, variadic: bool) -> Option<&T> {
values
.get(index)
.or_else(|| variadic.then(|| values.last()).flatten())
}
#[derive(Debug, Clone)]
pub struct EnumMember {
pub member: String,
pub(crate) aliases: HashMap<Locale, Vec<String>>,
}
impl EnumMember {
pub fn spelling(&self, locale: &Locale) -> Option<&str> {
spelling(&self.aliases, locale)
}
pub fn spellings(&self, locale: &Locale) -> &[String] {
spellings_for(&self.aliases, locale)
}
}
#[derive(Debug, Clone)]
pub struct EnumDomain {
pub domain: String,
pub(crate) aliases: HashMap<Locale, Vec<String>>,
pub members: Vec<EnumMember>,
}
impl EnumDomain {
pub fn spelling(&self, locale: &Locale) -> Option<&str> {
spelling(&self.aliases, locale)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[non_exhaustive]
pub struct TargetMeta {
pub game: String,
pub format: String,
pub surface: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Provenance {
pub generator: String,
pub generator_version: String,
pub source: String,
pub license: String,
pub reviewed: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub source_notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[non_exhaustive]
pub struct LocaleCoverage {
pub locale: Locale,
pub mapped: usize,
pub total: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct CatalogIdentity {
pub implementation_version: String,
pub catalog_version: String,
pub catalog_digest: Option<String>,
pub locale_coverage: Vec<LocaleCoverage>,
pub target: TargetMeta,
pub provenance: Provenance,
}
pub(crate) type MemberIndexMap = HashMap<String, HashMap<String, (usize, usize)>>;
#[derive(Debug, Clone)]
pub struct Catalog {
detection_index: Option<detect::AliasIndex>,
pub(crate) schema_version: u32,
pub(crate) locales: Vec<Locale>,
pub(crate) target: TargetMeta,
pub(crate) provenance: Provenance,
pub(crate) catalog_version: String,
pub(crate) catalog_digest: Option<String>,
pub(crate) entries: Vec<CatalogEntry>,
pub(crate) localized_strings: Vec<LocalizedStringEntry>,
pub(crate) enums: Vec<EnumDomain>,
pub(crate) by_id: [HashMap<String, usize>; Kind::NUM_KINDS],
pub(crate) alias_to_entry: HashMap<Locale, [HashMap<String, usize>; Kind::NUM_KINDS]>,
pub(crate) localized_string_by_id: HashMap<String, usize>,
pub(crate) localized_string_alias: HashMap<Locale, HashMap<String, usize>>,
pub(crate) enum_by_domain: HashMap<String, usize>,
pub(crate) enum_alias_to_domain: HashMap<Locale, HashMap<String, String>>,
pub(crate) enum_alias_to_member: HashMap<Locale, MemberIndexMap>,
pub(crate) bare_member_index: HashMap<Locale, HashMap<String, Vec<(String, String)>>>,
}
mod load;
pub use load::{build_canonical, canonicalize, content_digest};
impl Catalog {
pub(crate) fn schema_version(&self) -> u32 {
self.schema_version
}
pub fn locales(&self) -> &[Locale] {
&self.locales
}
pub fn primary_locale(&self) -> &Locale {
&self.locales[0]
}
pub fn supports(&self, locale: &Locale) -> bool {
self.locales.contains(locale)
}
pub fn catalog_version(&self) -> &str {
&self.catalog_version
}
pub fn catalog_digest(&self) -> Option<&str> {
self.catalog_digest.as_deref()
}
pub fn implementation_version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
pub fn identity(&self) -> CatalogIdentity {
CatalogIdentity {
implementation_version: Self::implementation_version().to_string(),
catalog_version: self.catalog_version.clone(),
catalog_digest: self.catalog_digest.clone(),
locale_coverage: self
.locales
.iter()
.map(|locale| self.locale_coverage(locale))
.collect(),
target: self.target.clone(),
provenance: self.provenance.clone(),
}
}
pub fn locale_coverage(&self, locale: &Locale) -> LocaleCoverage {
let member_total: usize = self.enums.iter().map(|domain| domain.members.len()).sum();
let total = self.entries.len() + self.localized_strings.len() + member_total;
let mapped = self
.entries
.iter()
.filter(|entry| entry.aliases.contains_key(locale))
.count()
+ self
.localized_strings
.iter()
.filter(|entry| entry.aliases.contains_key(locale))
.count()
+ self
.enums
.iter()
.flat_map(|domain| &domain.members)
.filter(|member| member.aliases.contains_key(locale))
.count();
LocaleCoverage {
locale: locale.clone(),
mapped,
total,
}
}
pub fn locale_coverage_all(&self) -> Vec<LocaleCoverage> {
self.locales
.iter()
.map(|locale| self.locale_coverage(locale))
.collect()
}
pub fn entry(&self, kind: Kind, id: &str) -> Option<&CatalogEntry> {
self.by_id[kind.as_index()]
.get(id)
.map(|i| &self.entries[*i])
}
pub fn resolve(&self, kind: Kind, locale: &Locale, spelling: &str) -> Option<&CatalogEntry> {
self.alias_to_entry
.get(locale)
.and_then(|by_kind| by_kind[kind.as_index()].get(spelling))
.map(|i| &self.entries[*i])
}
pub fn spelling(&self, kind: Kind, locale: &Locale, id: &str) -> Option<&str> {
self.entry(kind, id)?.spelling(locale)
}
pub fn entries_of(&self, kind: Kind) -> impl Iterator<Item = &CatalogEntry> {
self.entries.iter().filter(move |entry| entry.kind == kind)
}
pub fn resolve_localized_string(
&self,
locale: &Locale,
spelling: &str,
) -> Option<&LocalizedStringEntry> {
self.localized_string_alias
.get(locale)
.and_then(|map| map.get(spelling))
.map(|index| &self.localized_strings[*index])
}
pub fn localized_string_spelling(&self, locale: &Locale, id: &str) -> Option<&str> {
self.localized_string_by_id
.get(id)
.and_then(|i| self.localized_strings.get(*i))
.and_then(|entry| entry.spelling(locale))
}
pub fn localized_strings(&self) -> impl Iterator<Item = &LocalizedStringEntry> {
self.localized_strings.iter()
}
pub fn entry_count(&self) -> usize {
self.entries.len()
}
pub fn enum_domains_count(&self) -> usize {
self.enums.len()
}
pub fn enum_domain(&self, domain: &str) -> Option<&EnumDomain> {
self.enum_by_domain.get(domain).map(|i| &self.enums[*i])
}
pub fn resolve_enum_domain(&self, locale: &Locale, spelling: &str) -> Option<&str> {
self.enum_by_domain
.get_key_value(spelling)
.map(|(domain, _)| domain.as_str())
.or_else(|| {
self.enum_alias_to_domain
.get(locale)
.and_then(|map| map.get(spelling))
.map(String::as_str)
})
}
pub fn enum_domains(&self) -> impl Iterator<Item = &EnumDomain> {
self.enums.iter()
}
pub fn resolve_enum_member(
&self,
domain: &str,
locale: &Locale,
spelling: &str,
) -> Option<(String, String)> {
let &(domain_index, member_index) = self
.enum_alias_to_member
.get(locale)?
.get(domain)?
.get(spelling)?;
Some((
domain.to_string(),
self.enums[domain_index].members[member_index]
.member
.clone(),
))
}
pub fn enum_spelling(&self, domain: &str, locale: &Locale, member: &str) -> Option<&str> {
let domain_index = self.enum_by_domain.get(domain)?;
let domain = &self.enums[*domain_index];
domain
.members
.iter()
.find(|candidate| candidate.member == member)?
.spelling(locale)
}
pub fn localized_enum_spelling(
&self,
domain: &str,
locale: &Locale,
member: &str,
) -> Option<&str> {
self.enum_spelling(domain, locale, member).or_else(|| {
(domain == "Color" && member == "WHITE").then_some(match locale.as_str() {
"de-de" => "Weiß",
"es-es" | "es-mx" => "Blanco",
"fr-fr" => "Blanc",
"it-it" => "Bianco",
"ja-jp" => "白",
"ko-kr" => "흰색",
"pl-pl" => "Biały",
"pt-br" => "Branco",
"ru-ru" => "Белый",
"th-th" => "สีขาว",
"tr-tr" => "Beyaz",
"zh-tw" => "白色",
_ => return None,
})
})
}
pub fn bare_member_matches(&self, locale: &Locale, spelling: &str) -> Vec<(String, String)> {
self.bare_member_index
.get(locale)
.and_then(|map| map.get(spelling))
.cloned()
.unwrap_or_default()
}
}
impl ExpectedDomain for Catalog {
fn expected_domain(&self, catalog_id: &str, arg_index: usize) -> Option<&str> {
for kind in [Kind::Action, Kind::Value] {
if let Some(entry) = self.entry(kind, catalog_id) {
if let Some(domain) = entry.param_domain(arg_index) {
return Some(domain);
}
}
}
None
}
}