es_fluent_shared/resource/
catalog.rs1use std::collections::BTreeMap;
2
3use crate::fluent::{FluentDomain, FluentEntryId, FluentIdentifierError};
4
5pub const FALLBACK_CATALOG_FILE_NAME: &str = "es-fluent-fallback.catalog";
7
8#[doc(hidden)]
10pub const FALLBACK_CATALOG_ENV: &str = "ES_FLUENT_FALLBACK_CATALOG";
11
12#[doc(hidden)]
14pub const INVENTORY_RUNNER_ENV: &str = "ES_FLUENT_INVENTORY_RUNNER";
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub enum FluentCatalogEntryKind {
19 Message,
21 MessageWithoutValue,
23 Term,
25}
26
27impl FluentCatalogEntryKind {
28 pub const fn resolves_message(self) -> bool {
30 matches!(self, Self::Message)
31 }
32}
33
34#[derive(Clone, Debug, Eq, PartialEq)]
36pub struct FluentCatalogEntry {
37 pub id: FluentEntryId,
39 pub kind: FluentCatalogEntryKind,
41}
42
43pub fn classify_fluent_entry(
45 entry: &fluent_syntax::ast::Entry<String>,
46) -> Result<Option<FluentCatalogEntry>, FluentIdentifierError> {
47 let (name, kind) = match entry {
48 fluent_syntax::ast::Entry::Message(message) => (
49 message.id.name.as_str(),
50 if message.value.is_some() {
51 FluentCatalogEntryKind::Message
52 } else {
53 FluentCatalogEntryKind::MessageWithoutValue
54 },
55 ),
56 fluent_syntax::ast::Entry::Term(term) => {
57 (term.id.name.as_str(), FluentCatalogEntryKind::Term)
58 },
59 _ => return Ok(None),
60 };
61
62 FluentEntryId::try_new(name.to_string()).map(|id| Some(FluentCatalogEntry { id, kind }))
63}
64
65#[derive(Debug, thiserror::Error)]
67pub enum FallbackCatalogError {
68 #[error("invalid Fluent syntax: {details}")]
70 Syntax { details: String },
71 #[error("invalid Fluent entry identifier: {0}")]
73 Identifier(#[from] FluentIdentifierError),
74 #[error("duplicate Fluent entry '{id}' in domain '{domain}'")]
76 Duplicate { domain: String, id: String },
77}
78
79#[derive(Clone, Debug, Default, Eq, PartialEq)]
81pub struct FallbackCatalog {
82 entries: BTreeMap<(FluentDomain, FluentEntryId), FluentCatalogEntryKind>,
83}
84
85impl FallbackCatalog {
86 pub fn insert_source(
88 &mut self,
89 domain: &FluentDomain,
90 source: String,
91 ) -> Result<(), FallbackCatalogError> {
92 let resource = fluent_syntax::parser::parse(source).map_err(|(_, errors)| {
93 FallbackCatalogError::Syntax {
94 details: format!("{errors:?}"),
95 }
96 })?;
97 self.insert_resource(domain, &resource)
98 }
99
100 pub fn insert_resource(
102 &mut self,
103 domain: &FluentDomain,
104 resource: &fluent_syntax::ast::Resource<String>,
105 ) -> Result<(), FallbackCatalogError> {
106 for entry in &resource.body {
107 let Some(entry) = classify_fluent_entry(entry)? else {
108 continue;
109 };
110 let key = (domain.clone(), entry.id.clone());
111 if self.entries.insert(key, entry.kind).is_some() {
112 return Err(FallbackCatalogError::Duplicate {
113 domain: domain.as_str().to_string(),
114 id: entry.id.as_str().to_string(),
115 });
116 }
117 }
118 Ok(())
119 }
120
121 pub fn encode(&self) -> Vec<u8> {
123 let mut encoded = Vec::new();
124 for ((domain, id), kind) in &self.entries {
125 if !kind.resolves_message() {
126 continue;
127 }
128 encoded.extend_from_slice(domain.as_str().as_bytes());
129 encoded.push(b'\t');
130 encoded.extend_from_slice(id.as_str().as_bytes());
131 encoded.push(b'\n');
132 }
133 encoded
134 }
135}
136
137pub const fn fallback_catalog_contains(catalog: &[u8], domain: &str, id: &str) -> bool {
139 let domain = domain.as_bytes();
140 let id = id.as_bytes();
141 let mut offset = 0;
142
143 while offset < catalog.len() {
144 let mut cursor = offset;
145 let mut domain_index = 0;
146 while domain_index < domain.len()
147 && cursor < catalog.len()
148 && catalog[cursor] == domain[domain_index]
149 {
150 cursor += 1;
151 domain_index += 1;
152 }
153 if domain_index == domain.len() && cursor < catalog.len() && catalog[cursor] == b'\t' {
154 cursor += 1;
155 let mut id_index = 0;
156 while id_index < id.len() && cursor < catalog.len() && catalog[cursor] == id[id_index] {
157 cursor += 1;
158 id_index += 1;
159 }
160 if id_index == id.len() && (cursor == catalog.len() || catalog[cursor] == b'\n') {
161 return true;
162 }
163 }
164
165 while offset < catalog.len() && catalog[offset] != b'\n' {
166 offset += 1;
167 }
168 offset += 1;
169 }
170
171 false
172}