Skip to main content

webfont_generator/memory/
updates.rs

1use std::collections::{HashMap, HashSet};
2#[cfg(feature = "async")]
3use std::io::Error;
4use std::io::Result;
5use std::sync::OnceLock;
6
7use super::options::{invalid, validate_name};
8use super::{GenerateWebfontsFromSourcesResult, SvgSource, build};
9use crate::incremental::sources::{
10    MembershipChange, SourceUpdate, change_membership, ordered_sources, same_sources,
11    updated_sources,
12};
13use crate::input::{LoadedSvgSource, validate_glyph_names};
14use crate::rendering::css::{TemplateDependencies, template_dependencies};
15
16/// A changed source only; unchanged SVG text stays retained by the result.
17/// Deserializes objects tagged with `type`: `added`, `changed`, or `removed`.
18#[derive(serde::Deserialize)]
19#[serde(tag = "type", rename_all = "camelCase", deny_unknown_fields)]
20pub enum SourceChange {
21    /// Introduce a previously unknown ID with its name and SVG text.
22    Added(SvgSource),
23    /// Replace an existing source; an absent name preserves its old name.
24    Changed {
25        /// Existing opaque ID.
26        id: String,
27        /// Replacement SVG text.
28        svg: String,
29        /// Optional replacement glyph name.
30        name: Option<String>,
31    },
32    /// Delete an existing ID from all designs.
33    Removed {
34        /// Existing opaque ID.
35        id: String,
36    },
37}
38
39/// Complete ordered ID membership for a named variant.
40#[derive(serde::Deserialize)]
41#[serde(rename_all = "camelCase", deny_unknown_fields)]
42pub struct VariantSourceSet {
43    /// Existing variant name; variant configuration cannot change during updates.
44    pub name: String,
45    /// Complete ordered IDs in this design.
46    pub ids: Vec<String>,
47}
48
49/// Complete membership after applying a source change batch.
50/// Deserializes either `{ "sources": [...] }` or `{ "variants": [...] }`.
51#[derive(serde::Deserialize)]
52#[serde(rename_all = "camelCase", deny_unknown_fields)]
53pub enum SourceMembership {
54    /// Ordinary generation: ordered source IDs.
55    Sources(Vec<String>),
56    /// Variant generation: all designs, in original order.
57    Variants(Vec<VariantSourceSet>),
58}
59
60/// An async update failure with the previous usable result, except on worker panic/cancellation.
61#[cfg(feature = "async")]
62pub struct SourceRegenerateError {
63    error: Error,
64    result: Option<Box<GenerateWebfontsFromSourcesResult>>,
65}
66
67#[cfg(feature = "async")]
68impl SourceRegenerateError {
69    pub(super) fn worker(error: tokio::task::JoinError) -> Self {
70        Self {
71            error: Error::other(format!("Source update task failed: {error}")),
72            result: None,
73        }
74    }
75    /// Inspect the underlying validation, generation, or worker error.
76    pub fn error(&self) -> &Error {
77        &self.error
78    }
79    /// Recover the previous result. Worker join failures cannot recover moved state.
80    pub fn into_result(self) -> Option<GenerateWebfontsFromSourcesResult> {
81        self.result.map(|result| *result)
82    }
83    /// Recover both the error and the previous result.
84    pub fn into_parts(self) -> (Error, Option<GenerateWebfontsFromSourcesResult>) {
85        (self.error, self.result.map(|result| *result))
86    }
87}
88
89#[cfg(feature = "async")]
90impl std::fmt::Debug for SourceRegenerateError {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        f.debug_struct("SourceRegenerateError")
93            .field("error", &self.error)
94            .field("recoverable", &self.result.is_some())
95            .finish()
96    }
97}
98#[cfg(feature = "async")]
99impl std::fmt::Display for SourceRegenerateError {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        self.error.fmt(f)
102    }
103}
104#[cfg(feature = "async")]
105impl std::error::Error for SourceRegenerateError {
106    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
107        Some(&self.error)
108    }
109}
110
111impl GenerateWebfontsFromSourcesResult {
112    fn membership_for_change(
113        &self,
114        id: &str,
115        change: MembershipChange<'_>,
116    ) -> Result<SourceMembership> {
117        let names = self.options.variants.as_ref().map(|family| {
118            family
119                .variants
120                .iter()
121                .map(|variant| variant.name.clone())
122                .collect::<Vec<_>>()
123        });
124        let mut sets = self
125            .options
126            .sources
127            .iter()
128            .map(|sources| sources.iter().map(|source| source.id.clone()).collect())
129            .collect::<Vec<_>>();
130        change_membership(&mut sets, names.as_deref(), id, change)?;
131        Ok(match names {
132            Some(names) => SourceMembership::Variants(
133                names
134                    .into_iter()
135                    .zip(sets)
136                    .map(|(name, ids)| VariantSourceSet { name, ids })
137                    .collect(),
138            ),
139            None => SourceMembership::Sources(sets.remove(0)),
140        })
141    }
142
143    /// Append a new source and regenerate. Families require an existing target variant.
144    /// Requires incremental generation. Inherits [`Self::regenerate_sync`]'s rollback semantics.
145    pub fn add_sync(&mut self, source: SvgSource, variant: Option<&str>) -> Result<()> {
146        let membership = self.membership_for_change(&source.id, MembershipChange::Add(variant))?;
147        self.regenerate_sync(membership, vec![SourceChange::Added(source)])
148    }
149
150    /// Replace an existing source in every consuming design, preserving its position.
151    /// An absent name preserves the old name. Inherits [`Self::regenerate_sync`]'s rollback semantics.
152    pub fn update_sync(&mut self, id: String, svg: String, name: Option<String>) -> Result<()> {
153        let membership = self.membership_for_change(&id, MembershipChange::Update)?;
154        self.regenerate_sync(membership, vec![SourceChange::Changed { id, svg, name }])
155    }
156
157    /// Remove an existing ID from every design. Rejects empty designs.
158    /// Inherits [`Self::regenerate_sync`]'s rollback semantics.
159    pub fn remove_sync(&mut self, id: String) -> Result<()> {
160        let membership = self.membership_for_change(&id, MembershipChange::Remove)?;
161        self.regenerate_sync(membership, vec![SourceChange::Removed { id }])
162    }
163
164    /// Consuming async equivalent of [`Self::add_sync`], with [`Self::regenerate`]'s recovery semantics.
165    #[cfg(feature = "async")]
166    pub async fn add(
167        self,
168        source: SvgSource,
169        variant: Option<String>,
170    ) -> std::result::Result<Self, SourceRegenerateError> {
171        let membership = match self
172            .membership_for_change(&source.id, MembershipChange::Add(variant.as_deref()))
173        {
174            Ok(membership) => membership,
175            Err(error) => {
176                return Err(SourceRegenerateError {
177                    error,
178                    result: Some(Box::new(self)),
179                });
180            }
181        };
182        self.regenerate(membership, vec![SourceChange::Added(source)])
183            .await
184    }
185
186    /// Consuming async equivalent of [`Self::update_sync`], with [`Self::regenerate`]'s recovery semantics.
187    #[cfg(feature = "async")]
188    pub async fn update(
189        self,
190        id: String,
191        svg: String,
192        name: Option<String>,
193    ) -> std::result::Result<Self, SourceRegenerateError> {
194        let membership = match self.membership_for_change(&id, MembershipChange::Update) {
195            Ok(membership) => membership,
196            Err(error) => {
197                return Err(SourceRegenerateError {
198                    error,
199                    result: Some(Box::new(self)),
200                });
201            }
202        };
203        self.regenerate(membership, vec![SourceChange::Changed { id, svg, name }])
204            .await
205    }
206
207    /// Consuming async equivalent of [`Self::remove_sync`], with [`Self::regenerate`]'s recovery semantics.
208    #[cfg(feature = "async")]
209    pub async fn remove(self, id: String) -> std::result::Result<Self, SourceRegenerateError> {
210        let membership = match self.membership_for_change(&id, MembershipChange::Remove) {
211            Ok(membership) => membership,
212            Err(error) => {
213                return Err(SourceRegenerateError {
214                    error,
215                    result: Some(Box::new(self)),
216                });
217            }
218        };
219        self.regenerate(membership, vec![SourceChange::Removed { id }])
220            .await
221    }
222
223    /// Apply a changed-source batch and complete ordered membership synchronously.
224    /// Requires `incremental: Some(true)`. On error, buffers, sources and renders remain usable;
225    /// a failed build discards partially mutated geometry caches so a retry starts safely.
226    pub fn regenerate_sync(
227        &mut self,
228        membership: SourceMembership,
229        changes: Vec<SourceChange>,
230    ) -> Result<()> {
231        if self.caches.is_none() {
232            return Err(invalid("Source updates require incremental generation"));
233        }
234        let memberships = match (&self.options.variants, membership) {
235            (None, SourceMembership::Sources(ids)) => vec![ids],
236            (Some(variants), SourceMembership::Variants(sets)) => {
237                if sets.len() != variants.variants.len()
238                    || sets
239                        .iter()
240                        .zip(&variants.variants)
241                        .any(|(set, variant)| set.name != variant.name)
242                {
243                    return Err(invalid("Supply every variant in its original order"));
244                }
245                sets.into_iter().map(|set| set.ids).collect()
246            }
247            _ => {
248                return Err(invalid(
249                    "Source membership does not match the generation mode",
250                ));
251            }
252        };
253        if changes.is_empty()
254            && memberships
255                .iter()
256                .zip(&self.options.sources)
257                .all(|(ids, sources)| {
258                    ids.iter()
259                        .map(String::as_str)
260                        .eq(sources.iter().map(|source| source.id.as_str()))
261                })
262        {
263            return Ok(());
264        }
265        let mut staged = self.options.clone();
266        if let Some(variants) = &mut staged.variants {
267            for (variant, ids) in variants.variants.iter_mut().zip(&memberships) {
268                variant.source_ids = ids.clone();
269            }
270        }
271        let previous = staged
272            .sources
273            .iter()
274            .flatten()
275            .map(|source| (source.id.clone(), source.clone()))
276            .collect::<HashMap<_, _>>();
277        let mut changed_ids = HashSet::new();
278        let mut supplied_ids = HashSet::new();
279        let mut updates = Vec::with_capacity(changes.len());
280        for change in changes {
281            let id = match &change {
282                SourceChange::Added(source) => &source.id,
283                SourceChange::Changed { id, .. } | SourceChange::Removed { id } => id,
284            };
285            if id.is_empty() || !changed_ids.insert(id.clone()) {
286                return Err(invalid("Change IDs must be nonempty and unique"));
287            }
288            match change {
289                SourceChange::Added(source) => {
290                    if previous.contains_key(&source.id) {
291                        return Err(invalid(format!("Source {:?} already exists", source.id)));
292                    }
293                    supplied_ids.insert(source.id.clone());
294                    updates.push(SourceUpdate::Set(LoadedSvgSource {
295                        id: source.id,
296                        glyph_name: source.name,
297                        contents: source.svg.into(),
298                    }));
299                }
300                SourceChange::Changed { id, svg, name } => {
301                    let source = previous
302                        .get(&id)
303                        .ok_or_else(|| invalid(format!("Unknown source {id:?}")))?;
304                    supplied_ids.insert(id.clone());
305                    updates.push(SourceUpdate::Set(LoadedSvgSource {
306                        id,
307                        contents: svg.into(),
308                        glyph_name: name.unwrap_or_else(|| source.glyph_name.clone()),
309                    }));
310                }
311                SourceChange::Removed { id } => {
312                    if !previous.contains_key(&id) {
313                        return Err(invalid(format!("Unknown source {id:?}")));
314                    }
315                    updates.push(SourceUpdate::Remove(id));
316                }
317            }
318        }
319        let previous = previous.into_values().collect::<Vec<_>>();
320        let definitions = updated_sources(&previous, &updates);
321        let mut used = HashSet::new();
322        staged.sources = memberships
323            .into_iter()
324            .map(|ids| {
325                if ids.is_empty() {
326                    return Err(invalid("Each design must contain at least one source"));
327                }
328                let mut seen = HashSet::new();
329                for id in &ids {
330                    if !seen.insert(id.clone()) {
331                        return Err(invalid(format!("Duplicate source ID {id:?}")));
332                    }
333                    used.insert(id.clone());
334                }
335                let sources = ordered_sources(&definitions, &ids)
336                    .map_err(|id| invalid(format!("Unknown membership ID {id:?}")))?;
337                for source in &sources {
338                    validate_name(&source.glyph_name)?;
339                }
340                validate_glyph_names(&sources)?;
341                Ok(sources)
342            })
343            .collect::<Result<Vec<_>>>()?;
344        if supplied_ids.iter().any(|id| !used.contains(id)) {
345            return Err(invalid(
346                "Added or changed sources must appear in final membership",
347            ));
348        }
349        let same = staged
350            .sources
351            .iter()
352            .zip(&self.options.sources)
353            .all(|(new, old)| same_sources(old, new));
354        if same {
355            return Ok(());
356        }
357
358        let fonts = build(&mut staged, self.caches.as_mut(), &updates)?;
359        let names_unchanged = self
360            .options
361            .sources
362            .iter()
363            .flatten()
364            .map(|source| &source.glyph_name)
365            .eq(staged
366                .sources
367                .iter()
368                .flatten()
369                .map(|source| &source.glyph_name));
370        let codepoints_unchanged = self.options.common.codepoints == staged.common.codepoints;
371        let keep_css = matches!(self.css.get(), Some(Ok(_)))
372            && self
373                .options
374                .css_template
375                .as_deref()
376                .map(template_dependencies)
377                .unwrap_or_else(TemplateDependencies::css_default)
378                .can_reuse_css_no_urls(names_unchanged, codepoints_unchanged);
379        let html_dependencies = self
380            .options
381            .html_template
382            .as_deref()
383            .map(template_dependencies)
384            .unwrap_or_else(TemplateDependencies::html_default);
385        let keep_html =
386            html_dependencies.can_reuse_html(names_unchanged, codepoints_unchanged, keep_css)
387                && matches!(self.html.get(), Some(Ok(_)));
388        self.options = staged;
389        self.fonts = fonts;
390        self.shared = OnceLock::new();
391        if !keep_css {
392            self.css = OnceLock::new();
393        }
394        if !keep_html {
395            self.html = OnceLock::new();
396        }
397        Ok(())
398    }
399
400    /// Apply [`Self::regenerate_sync`] on Tokio's blocking pool, returning the updated result.
401    /// Ordinary failures retain the original usable result in [`SourceRegenerateError`].
402    #[cfg(feature = "async")]
403    pub async fn regenerate(
404        mut self,
405        membership: SourceMembership,
406        changes: Vec<SourceChange>,
407    ) -> std::result::Result<Self, SourceRegenerateError> {
408        let runtime = match tokio::runtime::Handle::try_current() {
409            Ok(runtime) => runtime,
410            Err(error) => {
411                return Err(SourceRegenerateError {
412                    error: Error::other(format!("Source update requires a Tokio runtime: {error}")),
413                    result: Some(Box::new(self)),
414                });
415            }
416        };
417        runtime
418            .spawn_blocking(move || match self.regenerate_sync(membership, changes) {
419                Ok(()) => Ok(self),
420                Err(error) => Err(SourceRegenerateError {
421                    error,
422                    result: Some(Box::new(self)),
423                }),
424            })
425            .await
426            .map_err(SourceRegenerateError::worker)?
427    }
428}