use std::collections::BTreeMap;
use crate::fluent::{FluentDomain, FluentEntryId, FluentIdentifierError};
pub const FALLBACK_CATALOG_FILE_NAME: &str = "es-fluent-fallback.catalog";
#[doc(hidden)]
pub const FALLBACK_CATALOG_ENV: &str = "ES_FLUENT_FALLBACK_CATALOG";
#[doc(hidden)]
pub const INVENTORY_RUNNER_ENV: &str = "ES_FLUENT_INVENTORY_RUNNER";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FluentCatalogEntryKind {
Message,
MessageWithoutValue,
Term,
}
impl FluentCatalogEntryKind {
pub const fn resolves_message(self) -> bool {
matches!(self, Self::Message)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FluentCatalogEntry {
pub id: FluentEntryId,
pub kind: FluentCatalogEntryKind,
}
pub fn classify_fluent_entry(
entry: &fluent_syntax::ast::Entry<String>,
) -> Result<Option<FluentCatalogEntry>, FluentIdentifierError> {
let (name, kind) = match entry {
fluent_syntax::ast::Entry::Message(message) => (
message.id.name.as_str(),
if message.value.is_some() {
FluentCatalogEntryKind::Message
} else {
FluentCatalogEntryKind::MessageWithoutValue
},
),
fluent_syntax::ast::Entry::Term(term) => {
(term.id.name.as_str(), FluentCatalogEntryKind::Term)
},
_ => return Ok(None),
};
FluentEntryId::try_new(name.to_string()).map(|id| Some(FluentCatalogEntry { id, kind }))
}
#[derive(Debug, thiserror::Error)]
pub enum FallbackCatalogError {
#[error("invalid Fluent syntax: {details}")]
Syntax { details: String },
#[error("invalid Fluent entry identifier: {0}")]
Identifier(#[from] FluentIdentifierError),
#[error("duplicate Fluent entry '{id}' in domain '{domain}'")]
Duplicate { domain: String, id: String },
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct FallbackCatalog {
entries: BTreeMap<(FluentDomain, FluentEntryId), FluentCatalogEntryKind>,
}
impl FallbackCatalog {
pub fn insert_source(
&mut self,
domain: &FluentDomain,
source: String,
) -> Result<(), FallbackCatalogError> {
let resource = fluent_syntax::parser::parse(source).map_err(|(_, errors)| {
FallbackCatalogError::Syntax {
details: format!("{errors:?}"),
}
})?;
self.insert_resource(domain, &resource)
}
pub fn insert_resource(
&mut self,
domain: &FluentDomain,
resource: &fluent_syntax::ast::Resource<String>,
) -> Result<(), FallbackCatalogError> {
for entry in &resource.body {
let Some(entry) = classify_fluent_entry(entry)? else {
continue;
};
let key = (domain.clone(), entry.id.clone());
if self.entries.insert(key, entry.kind).is_some() {
return Err(FallbackCatalogError::Duplicate {
domain: domain.as_str().to_string(),
id: entry.id.as_str().to_string(),
});
}
}
Ok(())
}
pub fn encode(&self) -> Vec<u8> {
let mut encoded = Vec::new();
for ((domain, id), kind) in &self.entries {
if !kind.resolves_message() {
continue;
}
encoded.extend_from_slice(domain.as_str().as_bytes());
encoded.push(b'\t');
encoded.extend_from_slice(id.as_str().as_bytes());
encoded.push(b'\n');
}
encoded
}
}
pub const fn fallback_catalog_contains(catalog: &[u8], domain: &str, id: &str) -> bool {
let domain = domain.as_bytes();
let id = id.as_bytes();
let mut offset = 0;
while offset < catalog.len() {
let mut cursor = offset;
let mut domain_index = 0;
while domain_index < domain.len()
&& cursor < catalog.len()
&& catalog[cursor] == domain[domain_index]
{
cursor += 1;
domain_index += 1;
}
if domain_index == domain.len() && cursor < catalog.len() && catalog[cursor] == b'\t' {
cursor += 1;
let mut id_index = 0;
while id_index < id.len() && cursor < catalog.len() && catalog[cursor] == id[id_index] {
cursor += 1;
id_index += 1;
}
if id_index == id.len() && (cursor == catalog.len() || catalog[cursor] == b'\n') {
return true;
}
}
while offset < catalog.len() && catalog[offset] != b'\n' {
offset += 1;
}
offset += 1;
}
false
}