Skip to main content

es_fluent_shared/resource/
plan.rs

1use std::collections::HashSet;
2
3use crate::fluent::FluentDomain;
4use crate::namespace::ResolvedNamespace;
5use crate::registry::StaticFluentDomain;
6
7use super::{ModuleResourceSpec, ResourceKey, ResourcePlanError};
8
9/// Canonical resource plan for a module domain.
10#[derive(Clone, Debug, Eq, PartialEq)]
11pub struct ResourcePlan {
12    specs: Vec<ModuleResourceSpec>,
13}
14
15impl ResourcePlan {
16    /// Builds the global/default canonical resource plan for a validated static domain.
17    pub fn for_static_domain(
18        domain: StaticFluentDomain,
19        namespaces: &[&str],
20    ) -> Result<Self, ResourcePlanError> {
21        let mut resolved_namespaces = Vec::with_capacity(namespaces.len());
22        let mut seen = HashSet::new();
23
24        for namespace in namespaces {
25            let namespace = ResolvedNamespace::new(*namespace).map_err(|details| {
26                ResourcePlanError::InvalidNamespace {
27                    namespace: (*namespace).to_string(),
28                    details,
29                }
30            })?;
31
32            if seen.insert(namespace.clone()) {
33                resolved_namespaces.push(namespace);
34            }
35        }
36
37        Ok(Self::from_static_domain_and_namespaces(
38            domain,
39            &resolved_namespaces,
40            namespaces.is_empty(),
41        ))
42    }
43
44    fn from_static_domain_and_namespaces(
45        domain: StaticFluentDomain,
46        namespaces: &[ResolvedNamespace],
47        base_required: bool,
48    ) -> Self {
49        if namespaces.is_empty() {
50            return Self {
51                specs: vec![ModuleResourceSpec::base_for_static_domain(
52                    domain,
53                    base_required,
54                )],
55            };
56        }
57
58        let mut specs = Vec::with_capacity(namespaces.len() + 1);
59        specs.push(ModuleResourceSpec::base_for_static_domain(domain, false));
60        for namespace in namespaces {
61            specs.push(ModuleResourceSpec::namespaced_for_static_domain(
62                domain, namespace, true,
63            ));
64        }
65
66        Self { specs }
67    }
68
69    /// Builds the global/default canonical resource plan for a domain.
70    pub fn for_domain(domain: &str, namespaces: &[&str]) -> Result<Self, ResourcePlanError> {
71        let domain =
72            FluentDomain::try_new(domain).map_err(|details| ResourcePlanError::InvalidDomain {
73                domain: domain.to_string(),
74                details,
75            })?;
76
77        if namespaces.is_empty() {
78            return Ok(Self {
79                specs: vec![ModuleResourceSpec::base_for_domain(&domain, true)],
80            });
81        }
82
83        let mut specs = Vec::with_capacity(namespaces.len() + 1);
84        specs.push(ModuleResourceSpec::base_for_domain(&domain, false));
85
86        let mut seen = HashSet::new();
87        for namespace in namespaces {
88            let namespace = ResolvedNamespace::new(*namespace).map_err(|details| {
89                ResourcePlanError::InvalidNamespace {
90                    namespace: (*namespace).to_string(),
91                    details,
92                }
93            })?;
94
95            if !seen.insert(namespace.clone()) {
96                continue;
97            }
98
99            specs.push(ModuleResourceSpec::namespaced_for_domain(
100                &domain, &namespace, true,
101            ));
102        }
103
104        Ok(Self { specs })
105    }
106
107    /// Builds a sparse resource plan for a single locale.
108    pub fn sparse_for_domain(
109        domain: &str,
110        has_base_resource: bool,
111        namespaces: &[ResolvedNamespace],
112        base_required: bool,
113    ) -> Self {
114        let domain =
115            FluentDomain::try_new(domain).expect("sparse resource domain should be validated");
116        Self::sparse_for_validated_domain(&domain, has_base_resource, namespaces, base_required)
117    }
118
119    /// Builds a sparse resource plan for a single locale from validated static metadata.
120    pub fn sparse_for_static_domain(
121        domain: StaticFluentDomain,
122        has_base_resource: bool,
123        namespaces: &[ResolvedNamespace],
124        base_required: bool,
125    ) -> Self {
126        let mut specs = Vec::with_capacity(namespaces.len() + usize::from(has_base_resource));
127
128        if has_base_resource {
129            specs.push(ModuleResourceSpec::base_for_static_domain(
130                domain,
131                base_required,
132            ));
133        }
134
135        for namespace in namespaces {
136            specs.push(ModuleResourceSpec::namespaced_for_static_domain(
137                domain, namespace, true,
138            ));
139        }
140
141        Self { specs }
142    }
143
144    fn sparse_for_validated_domain(
145        domain: &FluentDomain,
146        has_base_resource: bool,
147        namespaces: &[ResolvedNamespace],
148        base_required: bool,
149    ) -> Self {
150        let mut specs = Vec::with_capacity(namespaces.len() + usize::from(has_base_resource));
151
152        if has_base_resource {
153            specs.push(ModuleResourceSpec::base_for_domain(domain, base_required));
154        }
155
156        for namespace in namespaces {
157            specs.push(ModuleResourceSpec::namespaced_for_domain(
158                domain, namespace, true,
159            ));
160        }
161
162        Self { specs }
163    }
164
165    /// Returns the resource specs in this plan.
166    pub fn specs(&self) -> &[ModuleResourceSpec] {
167        &self.specs
168    }
169
170    /// Converts the plan into its resource specs.
171    pub fn into_specs(self) -> Vec<ModuleResourceSpec> {
172        self.specs
173    }
174}
175
176impl From<ResourcePlan> for Vec<ModuleResourceSpec> {
177    fn from(plan: ResourcePlan) -> Self {
178        plan.into_specs()
179    }
180}
181
182/// Builds the global/default canonical resource plan for a domain.
183///
184/// Contract:
185/// - Without namespaces, `{domain}.ftl` is required.
186/// - With namespaces, `{domain}.ftl` remains an optional mixed-mode resource
187///   and `{domain}/{namespace}.ftl` entries are required.
188/// - Compile-time registrations may provide a sparse per-language plan through
189///   `I18nModuleRegistration::resource_plan_for_language`; managers should use
190///   that plan directly when it is available.
191pub fn try_resource_plan_for(
192    domain: &str,
193    namespaces: &[&str],
194) -> Result<Vec<ModuleResourceSpec>, ResourcePlanError> {
195    ResourcePlan::for_domain(domain, namespaces).map(ResourcePlan::into_specs)
196}
197
198/// Builds the global/default canonical resource plan for a domain.
199///
200/// Use [`try_resource_plan_for`] for dynamic namespace input that should return
201/// typed errors. This constructor is for static module metadata paths where
202/// invalid namespaces are programmer errors and recovery is not useful.
203#[allow(
204    clippy::panic,
205    reason = "static metadata uses literal namespaces; use try_resource_plan_for for dynamic input"
206)]
207pub fn resource_plan_for(domain: &str, namespaces: &[&str]) -> Vec<ModuleResourceSpec> {
208    try_resource_plan_for(domain, namespaces)
209        .unwrap_or_else(|error| panic!("resource_plan_for received {error}"))
210}
211
212/// Returns required resource keys from a resource plan.
213pub fn required_resource_keys_from_plan(plan: &[ModuleResourceSpec]) -> HashSet<ResourceKey> {
214    plan.iter()
215        .filter(|spec| spec.required)
216        .map(|spec| spec.key.clone())
217        .collect()
218}
219
220/// Returns optional resource keys from a resource plan.
221pub fn optional_resource_keys_from_plan(plan: &[ModuleResourceSpec]) -> HashSet<ResourceKey> {
222    plan.iter()
223        .filter(|spec| !spec.required)
224        .map(|spec| spec.key.clone())
225        .collect()
226}
227
228/// Returns true when all required keys are present in the loaded set.
229pub fn locale_is_ready(
230    required_keys: &HashSet<ResourceKey>,
231    loaded_keys: &HashSet<ResourceKey>,
232) -> bool {
233    required_keys.iter().all(|key| loaded_keys.contains(key))
234}