Skip to main content

ferrocat_po/api/
compile_types.rs

1use std::collections::BTreeMap;
2
3use super::{
4    ApiError, CatalogMessageKey, CatalogSemantics, NormalizedParsedCatalog,
5    compile::{
6        compiled_catalog_translation_kind_for_message, compiled_key_for,
7        describe_compiled_id_catalogs,
8    },
9};
10
11/// Translation value stored in a compiled runtime catalog.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum CompiledTranslation {
14    /// Singular runtime value.
15    Singular(String),
16    /// Structured plural runtime value.
17    Plural(BTreeMap<String, String>),
18}
19
20/// Built-in key strategy used when compiling runtime catalogs.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub enum CompiledKeyStrategy {
23    /// `ferrocat` v1 key format: SHA-256 over a versioned, length-delimited
24    /// `msgctxt`/`msgid` payload, truncated to 64 bits and encoded as unpadded
25    /// `Base64URL`.
26    #[default]
27    FerrocatV1,
28}
29
30/// Options controlling runtime catalog compilation.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct CompileCatalogOptions<'a> {
33    /// Built-in strategy used to derive stable runtime keys.
34    pub key_strategy: CompiledKeyStrategy,
35    /// Whether empty source-locale values should be filled from the source text.
36    pub source_fallback: bool,
37    /// Source locale used when `source_fallback` is enabled.
38    pub source_locale: Option<&'a str>,
39    /// High-level semantics used by the input catalog set.
40    pub semantics: CatalogSemantics,
41}
42
43impl Default for CompileCatalogOptions<'_> {
44    fn default() -> Self {
45        Self {
46            key_strategy: CompiledKeyStrategy::FerrocatV1,
47            source_fallback: false,
48            source_locale: None,
49            semantics: CatalogSemantics::IcuNative,
50        }
51    }
52}
53
54/// Options controlling high-level compiled catalog artifact generation.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct CompileCatalogArtifactOptions<'a> {
57    /// Locale for which the runtime artifact should be produced.
58    pub requested_locale: &'a str,
59    /// Source locale used for explicit source fallback behavior.
60    pub source_locale: &'a str,
61    /// Ordered fallback locales consulted after the requested locale.
62    pub fallback_chain: &'a [String],
63    /// Built-in strategy used to derive stable runtime keys.
64    pub key_strategy: CompiledKeyStrategy,
65    /// Whether source text should be used when no non-source translation exists.
66    pub source_fallback: bool,
67    /// Whether invalid final ICU messages should fail compilation instead of producing diagnostics.
68    pub strict_icu: bool,
69    /// High-level semantics used by the input catalog set.
70    pub semantics: CatalogSemantics,
71}
72
73impl Default for CompileCatalogArtifactOptions<'_> {
74    fn default() -> Self {
75        Self {
76            requested_locale: "",
77            source_locale: "",
78            fallback_chain: &[],
79            key_strategy: CompiledKeyStrategy::FerrocatV1,
80            source_fallback: false,
81            strict_icu: false,
82            semantics: CatalogSemantics::IcuNative,
83        }
84    }
85}
86
87/// Options controlling selected-subset compiled catalog artifact generation.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct CompileSelectedCatalogArtifactOptions<'a> {
90    /// Locale for which the runtime artifact should be produced.
91    pub requested_locale: &'a str,
92    /// Source locale used for explicit source fallback behavior.
93    pub source_locale: &'a str,
94    /// Ordered fallback locales consulted after the requested locale.
95    pub fallback_chain: &'a [String],
96    /// Built-in strategy used to derive stable runtime keys.
97    pub key_strategy: CompiledKeyStrategy,
98    /// Whether source text should be used when no non-source translation exists.
99    pub source_fallback: bool,
100    /// Whether invalid final ICU messages should fail compilation instead of producing diagnostics.
101    pub strict_icu: bool,
102    /// High-level semantics used by the input catalog set.
103    pub semantics: CatalogSemantics,
104    /// Requested compiled runtime IDs to include in the artifact.
105    pub compiled_ids: &'a [String],
106}
107
108impl Default for CompileSelectedCatalogArtifactOptions<'_> {
109    fn default() -> Self {
110        Self {
111            requested_locale: "",
112            source_locale: "",
113            fallback_chain: &[],
114            key_strategy: CompiledKeyStrategy::FerrocatV1,
115            source_fallback: false,
116            strict_icu: false,
117            semantics: CatalogSemantics::IcuNative,
118            compiled_ids: &[],
119        }
120    }
121}
122
123impl CompileSelectedCatalogArtifactOptions<'_> {
124    pub(super) fn artifact_options(&self) -> CompileCatalogArtifactOptions<'_> {
125        CompileCatalogArtifactOptions {
126            requested_locale: self.requested_locale,
127            source_locale: self.source_locale,
128            fallback_chain: self.fallback_chain,
129            key_strategy: self.key_strategy,
130            source_fallback: self.source_fallback,
131            strict_icu: self.strict_icu,
132            semantics: self.semantics,
133        }
134    }
135}
136
137/// High-level translation kind associated with a compiled runtime ID.
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum CompiledCatalogTranslationKind {
140    /// Translation is a single string value.
141    Singular,
142    /// Translation is a plural/category map.
143    Plural,
144}
145
146/// A compiled runtime message keyed by a derived lookup key.
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct CompiledMessage {
149    /// Stable runtime key derived from the source identity.
150    pub key: String,
151    /// Original gettext identity preserved for diagnostics and tooling.
152    pub source_key: CatalogMessageKey,
153    /// Materialized translation payload for runtime lookup.
154    pub translation: CompiledTranslation,
155}
156
157/// Runtime-oriented lookup structure compiled from a normalized catalog.
158#[derive(Debug, Clone, PartialEq, Eq, Default)]
159pub struct CompiledCatalog {
160    pub(super) entries: BTreeMap<String, CompiledMessage>,
161}
162
163impl CompiledCatalog {
164    /// Returns the compiled message for `key`, if present.
165    #[must_use]
166    pub fn get(&self, key: &str) -> Option<&CompiledMessage> {
167        self.entries.get(key)
168    }
169
170    /// Returns the number of compiled entries.
171    #[must_use]
172    pub fn len(&self) -> usize {
173        self.entries.len()
174    }
175
176    /// Returns `true` when the compiled catalog has no entries.
177    #[must_use]
178    pub fn is_empty(&self) -> bool {
179        self.entries.is_empty()
180    }
181
182    /// Iterates over compiled entries in key order.
183    pub fn iter(&self) -> impl Iterator<Item = (&str, &CompiledMessage)> + '_ {
184        self.entries
185            .iter()
186            .map(|(key, message)| (key.as_str(), message))
187    }
188}
189
190/// Stable compiled runtime ID index built from one or more normalized catalogs.
191#[derive(Debug, Clone, PartialEq, Eq, Default)]
192pub struct CompiledCatalogIdIndex {
193    pub(super) ids: BTreeMap<String, CatalogMessageKey>,
194}
195
196/// Metadata describing one compiled runtime ID for a specific catalog set.
197#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct CompiledCatalogIdDescription {
199    /// Stable runtime ID derived from the source identity.
200    pub compiled_id: String,
201    /// Original gettext identity preserved for diagnostics and tooling.
202    pub source_key: CatalogMessageKey,
203    /// Locales from the provided catalog set that contain this non-obsolete message.
204    pub available_locales: Vec<String>,
205    /// Whether the message is singular or plural in the provided catalog set.
206    pub translation_kind: CompiledCatalogTranslationKind,
207}
208
209/// Report returned by [`CompiledCatalogIdIndex::describe_compiled_ids`].
210#[derive(Debug, Clone, PartialEq, Eq, Default)]
211pub struct DescribeCompiledIdsReport {
212    /// Metadata for requested IDs that were known to the index and present in the provided catalogs.
213    pub described: Vec<CompiledCatalogIdDescription>,
214    /// Requested compiled IDs that were not known to the index at all.
215    pub unknown_compiled_ids: Vec<String>,
216    /// Requested compiled IDs that were known to the index but not present in the provided catalogs.
217    pub unavailable_compiled_ids: Vec<CompiledCatalogUnavailableId>,
218}
219
220/// Known compiled runtime ID that was not present in the provided catalog set.
221#[derive(Debug, Clone, PartialEq, Eq)]
222pub struct CompiledCatalogUnavailableId {
223    /// Stable runtime ID derived from the source identity.
224    pub compiled_id: String,
225    /// Original gettext identity preserved for diagnostics and tooling.
226    pub source_key: CatalogMessageKey,
227}
228
229impl CompiledCatalogIdIndex {
230    /// Builds a deterministic compiled-ID index for the union of non-obsolete messages.
231    ///
232    /// # Errors
233    ///
234    /// Returns [`ApiError::Conflict`] when two different source identities compile to the same ID.
235    pub fn new(
236        catalogs: &[&NormalizedParsedCatalog],
237        key_strategy: CompiledKeyStrategy,
238    ) -> Result<Self, ApiError> {
239        Self::new_with_key_generator(catalogs, key_strategy, compiled_key_for)
240    }
241
242    pub(super) fn new_with_key_generator<F>(
243        catalogs: &[&NormalizedParsedCatalog],
244        key_strategy: CompiledKeyStrategy,
245        mut key_generator: F,
246    ) -> Result<Self, ApiError>
247    where
248        F: FnMut(CompiledKeyStrategy, &CatalogMessageKey) -> String,
249    {
250        let mut ids = BTreeMap::<String, CatalogMessageKey>::new();
251
252        for catalog in catalogs {
253            for (source_key, message) in catalog.iter() {
254                if message.obsolete {
255                    continue;
256                }
257                let compiled_id = key_generator(key_strategy, source_key);
258                if let Some(existing) = ids.get(&compiled_id) {
259                    if existing != source_key {
260                        return Err(ApiError::Conflict(format!(
261                            "compiled catalog key collision for {:?} / {:?} and {:?} / {:?} using key {}",
262                            existing.msgctxt,
263                            existing.msgid,
264                            source_key.msgctxt,
265                            source_key.msgid,
266                            compiled_id
267                        )));
268                    }
269                    continue;
270                }
271                ids.insert(compiled_id, source_key.clone());
272            }
273        }
274
275        Ok(Self { ids })
276    }
277
278    /// Returns the source key for `compiled_id`, if present.
279    #[must_use]
280    pub fn get(&self, compiled_id: &str) -> Option<&CatalogMessageKey> {
281        self.ids.get(compiled_id)
282    }
283
284    /// Returns `true` when the index contains `compiled_id`.
285    #[must_use]
286    pub fn contains_id(&self, compiled_id: &str) -> bool {
287        self.ids.contains_key(compiled_id)
288    }
289
290    /// Returns the number of indexed compiled IDs.
291    #[must_use]
292    pub fn len(&self) -> usize {
293        self.ids.len()
294    }
295
296    /// Returns `true` when the index contains no compiled IDs.
297    #[must_use]
298    pub fn is_empty(&self) -> bool {
299        self.ids.is_empty()
300    }
301
302    /// Iterates over compiled IDs in sorted order.
303    pub fn iter(&self) -> impl Iterator<Item = (&str, &CatalogMessageKey)> + '_ {
304        self.ids
305            .iter()
306            .map(|(compiled_id, source_key)| (compiled_id.as_str(), source_key))
307    }
308
309    /// Returns the underlying ordered compiled-ID map by reference.
310    #[must_use]
311    pub fn as_btreemap(&self) -> &BTreeMap<String, CatalogMessageKey> {
312        &self.ids
313    }
314
315    /// Consumes the index and returns the underlying ordered compiled-ID map.
316    #[must_use]
317    pub fn into_btreemap(self) -> BTreeMap<String, CatalogMessageKey> {
318        self.ids
319    }
320
321    /// Describes selected compiled IDs against a provided catalog set.
322    ///
323    /// # Errors
324    ///
325    /// Returns [`ApiError::InvalidArguments`] when a provided catalog does not declare
326    /// a locale, or [`ApiError::Conflict`] when the same compiled ID maps to different
327    /// translation kinds across the provided catalogs.
328    pub fn describe_compiled_ids(
329        &self,
330        catalogs: &[&NormalizedParsedCatalog],
331        compiled_ids: &[String],
332    ) -> Result<DescribeCompiledIdsReport, ApiError> {
333        let locales = describe_compiled_id_catalogs(catalogs)?;
334        let mut report = DescribeCompiledIdsReport::default();
335
336        for compiled_id in std::collections::BTreeSet::from_iter(compiled_ids.iter().cloned()) {
337            let Some(source_key) = self.get(&compiled_id).cloned() else {
338                report.unknown_compiled_ids.push(compiled_id);
339                continue;
340            };
341
342            let mut available_locales = Vec::new();
343            let mut translation_kind = None;
344
345            for (locale, catalog) in &locales {
346                let Some(message) = catalog.get(&source_key) else {
347                    continue;
348                };
349                if message.obsolete {
350                    continue;
351                }
352                let next_kind = compiled_catalog_translation_kind_for_message(
353                    catalog.parsed_catalog().semantics,
354                    message,
355                );
356                if let Some(existing_kind) = translation_kind {
357                    if existing_kind != next_kind {
358                        return Err(ApiError::Conflict(format!(
359                            "compiled ID {:?} resolves to inconsistent translation shapes across the provided catalogs",
360                            compiled_id
361                        )));
362                    }
363                } else {
364                    translation_kind = Some(next_kind);
365                }
366                available_locales.push(locale.clone());
367            }
368
369            if let Some(translation_kind) = translation_kind {
370                report.described.push(CompiledCatalogIdDescription {
371                    compiled_id,
372                    source_key,
373                    available_locales,
374                    translation_kind,
375                });
376            } else {
377                report
378                    .unavailable_compiled_ids
379                    .push(CompiledCatalogUnavailableId {
380                        compiled_id,
381                        source_key,
382                    });
383            }
384        }
385
386        Ok(report)
387    }
388}
389
390/// Host-neutral compiled runtime artifact for one requested locale.
391#[derive(Debug, Clone, PartialEq, Eq, Default)]
392pub struct CompiledCatalogArtifact {
393    /// Final runtime message map keyed by the derived lookup key.
394    pub messages: BTreeMap<String, String>,
395    /// Messages that were missing from the requested locale and had to fall back.
396    pub missing: Vec<CompiledCatalogMissingMessage>,
397    /// Diagnostics collected while validating final runtime messages.
398    pub diagnostics: Vec<CompiledCatalogDiagnostic>,
399}
400
401/// Missing-message record emitted by [`super::compile_catalog_artifact`].
402#[derive(Debug, Clone, PartialEq, Eq)]
403pub struct CompiledCatalogMissingMessage {
404    /// Stable runtime key derived from the source identity.
405    pub key: String,
406    /// Original gettext identity preserved for diagnostics and tooling.
407    pub source_key: CatalogMessageKey,
408    /// Requested locale for this artifact compilation.
409    pub requested_locale: String,
410    /// Locale that ultimately provided the runtime value, if any.
411    pub resolved_locale: Option<String>,
412}
413
414/// Diagnostic emitted by [`super::compile_catalog_artifact`].
415#[derive(Debug, Clone, PartialEq, Eq)]
416pub struct CompiledCatalogDiagnostic {
417    /// Severity for the collected diagnostic.
418    pub severity: super::DiagnosticSeverity,
419    /// Stable machine-readable diagnostic code.
420    pub code: String,
421    /// Human-readable explanation of the problem.
422    pub message: String,
423    /// Stable runtime key derived from the source identity.
424    pub key: String,
425    /// Source `msgid` associated with the diagnostic.
426    pub msgid: String,
427    /// Source `msgctxt` associated with the diagnostic.
428    pub msgctxt: Option<String>,
429    /// Locale whose final runtime message produced the diagnostic.
430    pub locale: String,
431}