wif_weave/
wif.rs

1//! The wif module provides classes needed to extract data from a `.wif` file
2
3use crate::wif::data::WifSequence;
4use crate::wif::sections::WarpWeft;
5use configparser::ini::{Ini, WriteOptions};
6use data::WifParseable;
7use indexmap::IndexMap;
8use sections::{ColorPalette, Weaving};
9use std::collections::{HashMap, HashSet};
10use std::io;
11use std::path::Path;
12use std::str::FromStr;
13use strum::{Display, EnumIter, EnumString, IntoEnumIterator, IntoStaticStr};
14use thiserror::Error;
15
16/// Default value for the developers field in the WIF header
17pub const WIF_DEVELOPERS: &str = "wif@mhsoft.com";
18/// Default value for the date field in the WIF header
19pub const WIF_DATE: &str = "April 20, 1997";
20/// Current version of `.wif`
21pub const WIF_VERSION: &str = "1.1";
22
23/// Representation of the data in a `.wif` file
24#[derive(Debug, Clone, Default, PartialEq, Eq)]
25pub struct Wif {
26    inner_map: IndexMap<String, IndexMap<String, Option<String>>>,
27    treadling: Option<WifSequence<Vec<usize>>>,
28    threading: Option<WifSequence<Vec<usize>>>,
29    lift_plan: Option<WifSequence<Vec<usize>>>,
30    color_palette: Option<ColorPalette>,
31    tie_up: Option<WifSequence<Vec<usize>>>,
32    weaving: Option<Weaving>,
33    warp: Option<WarpWeft>,
34    weft: Option<WarpWeft>,
35}
36
37pub mod data;
38
39pub mod sections;
40
41impl Wif {
42    #[cfg(feature = "async")]
43    /// Asynchronously read a file and parse it into a [Wif]
44    /// # Errors
45    /// Returns an error if file reading or ini parsing fails
46    pub async fn load_async<T>(path: T) -> Result<(Self, HashMap<Section, Vec<ParseError>>), String>
47    where
48        T: AsRef<Path> + Send + Sync,
49    {
50        let mut ini = Ini::new();
51        let map = ini.load_async(path).await?;
52        Ok(Self::from_ini(map))
53    }
54
55    #[cfg(feature = "async")]
56    /// Asynchronously write to a `.wif` file
57    ///
58    /// # Errors
59    /// returns an [`io::Error`] if file writing fails
60    pub async fn write_async<T>(&self, path: T) -> Result<(), io::Error>
61    where
62        T: AsRef<Path> + Send + Sync,
63    {
64        self.to_ini()
65            .pretty_write_async(path, &Self::write_options())
66            .await
67    }
68
69    fn write_options() -> WriteOptions {
70        let mut options = WriteOptions::new();
71        options.blank_lines_between_sections = 1;
72        options
73    }
74
75    fn from_ini(
76        mut map: IndexMap<String, IndexMap<String, Option<String>>>,
77    ) -> (Self, HashMap<Section, Vec<ParseError>>) {
78        let mut errors = HashMap::new();
79        map.shift_remove_entry(&Section::Contents.index_map_key());
80
81        (
82            Self {
83                treadling: Section::Treadling.parse_and_pop(&mut map, &mut errors),
84                threading: Section::Threading.parse_and_pop(&mut map, &mut errors),
85                lift_plan: Section::LiftPlan.parse_and_pop(&mut map, &mut errors),
86                color_palette: ColorPalette::maybe_build(
87                    Section::ColorPalette.parse_and_pop(&mut map, &mut errors),
88                    Section::ColorTable.parse_and_pop(&mut map, &mut errors),
89                ),
90                tie_up: Section::TieUp.parse_and_pop(&mut map, &mut errors),
91                weaving: Section::Weaving.parse_and_pop(&mut map, &mut errors),
92                warp: Section::Warp.parse_and_pop(&mut map, &mut errors),
93                weft: Section::Weft.parse_and_pop(&mut map, &mut errors),
94                inner_map: map,
95            },
96            errors,
97        )
98    }
99
100    /// Construct a [Wif] from a file.
101    ///
102    /// # Errors
103    ///
104    /// Returns an error if there's an issue reading the file or if the contents don't match the INI spec
105    ///
106    /// Errors with the Wif specification will be collected into the second value of the return, but the other
107    /// sections will still be parsed.
108    pub fn load<T: AsRef<Path>>(
109        path: T,
110    ) -> Result<(Self, HashMap<Section, Vec<ParseError>>), String> {
111        let mut ini = Ini::new();
112        let map = ini.load(path)?;
113        Ok(Self::from_ini(map))
114    }
115
116    /// Construct a [Wif] from the string contents of a `.wif`
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if the text doesn't match the INI format
121    pub fn read(text: String) -> Result<(Self, HashMap<Section, Vec<ParseError>>), String> {
122        let mut ini = Ini::new();
123        let map = ini.read(text)?;
124        Ok(Self::from_ini(map))
125    }
126
127    fn to_ini(&self) -> Ini {
128        let mut ini = Ini::new_cs();
129        let ini_map = ini.get_mut_map();
130        let mut inner = self.inner_map.clone();
131
132        // Populate header
133        let mut header = inner
134            .shift_remove_entry(&Section::Header.to_string())
135            .map(|e| e.1)
136            .unwrap_or_default();
137        header
138            .entry(String::from("Version"))
139            .or_insert(Some(WIF_VERSION.to_owned()));
140        header
141            .entry(String::from("Date"))
142            .or_insert(Some(WIF_DATE.to_owned()));
143        header
144            .entry(String::from("Developers"))
145            .or_insert(Some(WIF_DEVELOPERS.to_owned()));
146        header
147            .entry(String::from("Source Program"))
148            .or_insert(Some(String::from("wif-weave")));
149        ini_map.insert(Section::Header.to_string(), header);
150
151        // Create contents
152        ini_map.insert(Section::Contents.to_string(), IndexMap::new());
153        inner.shift_remove_entry(&Section::Contents.index_map_key());
154
155        // Add parsed sections
156        Section::Threading.push_and_mark(ini_map, self.threading());
157        Section::Treadling.push_and_mark(ini_map, self.treadling());
158        Section::LiftPlan.push_and_mark(ini_map, self.lift_plan());
159        Section::TieUp.push_and_mark(ini_map, self.tie_up());
160        if let Some(palette) = self.color_palette() {
161            palette.push_and_mark(ini_map);
162        }
163        Section::Weaving.push_and_mark(ini_map, self.weaving());
164        Section::Warp.push_and_mark(ini_map, self.warp());
165        Section::Weft.push_and_mark(ini_map, self.weft());
166
167        // insert other sections
168        for (key, section) in inner {
169            // only insert valid sections
170            if let Ok(wif_section) = Section::from_str(key.to_uppercase().as_str()) {
171                ini_map.insert(wif_section.to_string(), section);
172            }
173        }
174
175        ini
176    }
177
178    /// Write to a `.wif` file
179    ///
180    /// # Errors
181    /// Returns an error if there's an issue writing to the file
182    pub fn write<T: AsRef<Path>>(&self, path: T) -> Result<(), io::Error> {
183        self.to_ini().pretty_write(path, &Self::write_options())
184    }
185
186    /// Write to a string in the `.wif` format
187    #[must_use]
188    pub fn writes(&self) -> String {
189        self.to_ini().pretty_writes(&Self::write_options())
190    }
191
192    /// Returns the threading sequence if present
193    #[must_use]
194    pub const fn threading(&self) -> Option<&WifSequence<Vec<usize>>> {
195        self.threading.as_ref()
196    }
197
198    /// If all threads only go through one heddle (standard), returns the threading.
199    ///
200    /// # Errors
201    /// Returns the first index with multiple heddles
202    pub fn single_threading(&self) -> Result<Option<WifSequence<usize>>, usize> {
203        self.threading
204            .as_ref()
205            .map(WifSequence::to_single_sequence)
206            .transpose()
207    }
208
209    /// Returns the treadling sequence if present
210    #[must_use]
211    pub const fn treadling(&self) -> Option<&WifSequence<Vec<usize>>> {
212        self.treadling.as_ref()
213    }
214
215    /// Returns the lift plan if present
216    #[must_use]
217    pub const fn lift_plan(&self) -> Option<&WifSequence<Vec<usize>>> {
218        self.lift_plan.as_ref()
219    }
220
221    /// Returns the tie-up if present
222    #[must_use]
223    pub const fn tie_up(&self) -> Option<&WifSequence<Vec<usize>>> {
224        self.tie_up.as_ref()
225    }
226
227    /// Returns the color palette if present. Corresponds to [`ColorPalette`][WifSection::ColorPalette] and [`ColorTable`][WifSection::ColorTable]
228    #[must_use]
229    pub const fn color_palette(&self) -> Option<&ColorPalette> {
230        self.color_palette.as_ref()
231    }
232
233    /// Returns the weaving section if present
234    #[must_use]
235    pub const fn weaving(&self) -> Option<&Weaving> {
236        self.weaving.as_ref()
237    }
238
239    /// Returns the warp section if present
240    #[must_use]
241    pub const fn warp(&self) -> Option<&WarpWeft> {
242        self.warp.as_ref()
243    }
244
245    /// Returns the weft section if present
246    #[must_use]
247    pub const fn weft(&self) -> Option<&WarpWeft> {
248        self.weft.as_ref()
249    }
250
251    /// Returns list of all sections present in the original `.wif`
252    pub fn contents(&self) -> HashSet<Section> {
253        let mut contents = HashSet::new();
254        if self.treadling.is_some() {
255            contents.insert(Section::Treadling);
256        }
257        if self.threading.is_some() {
258            contents.insert(Section::Threading);
259        }
260        if self.tie_up.is_some() {
261            contents.insert(Section::TieUp);
262        }
263        if self.lift_plan.is_some() {
264            contents.insert(Section::LiftPlan);
265        }
266        if self.color_palette.as_ref().map(|p| p.colors()).is_some() {
267            contents.insert(Section::ColorTable);
268        }
269        if self
270            .color_palette
271            .as_ref()
272            .map(ColorPalette::color_range)
273            .is_some()
274        {
275            contents.insert(Section::ColorPalette);
276        }
277
278        Section::iter().for_each(|sec| {
279            if self.inner_map.contains_key(&sec.to_string().to_lowercase()) {
280                contents.insert(sec);
281            }
282        });
283
284        contents
285    }
286
287    /// Get the raw data for a section that is not yet parsed.
288    ///
289    /// # Errors
290    ///
291    /// Returns an error if the section has a specific method to retrieve it.
292    pub fn get_section(
293        &self,
294        section: Section,
295    ) -> Result<Option<&IndexMap<String, Option<String>>>, String> {
296        if Self::implemented(section) {
297            Err(String::from("Must be retrieved with specific method"))
298        } else {
299            Ok(self.inner_map.get(&section.to_string().to_lowercase()))
300        }
301    }
302
303    /// Returns whether the given section can be retrieved via a specialized method or via [`get_section`](Self::get_section)
304    #[must_use]
305    pub const fn implemented(section: Section) -> bool {
306        matches!(
307            section,
308            Section::Contents
309                | Section::ColorPalette
310                | Section::ColorTable
311                | Section::Threading
312                | Section::Treadling
313                | Section::TieUp
314                | Section::LiftPlan
315        )
316    }
317}
318
319/// Error when parsing Wif file
320#[derive(Error, Debug, Clone, PartialEq, Eq)]
321pub enum ParseError {
322    /// A required field is missing
323    #[error("Required field {0} is missing")]
324    MissingField(String),
325    /// Missing value for a key
326    #[error("Key {0} has no value")]
327    MissingValue(String),
328    /// Non integer keys in a section that only allows integers
329    #[error("Keys must be positive integers, found {0}")]
330    BadIntegerKey(String),
331    /// Values of the wrong type for the section+key
332    #[error("Values must be {expected_type}, found {value} at key {key}")]
333    BadValueType {
334        /// key where the issue occurred
335        key: String,
336        /// bad value
337        value: String,
338        /// expected type
339        expected_type: String,
340    },
341    /// When a section is missing but is required when another section is present
342    #[error("When {dependent_section} is present, {missing_section} must also be present")]
343    MissingDependentSection {
344        /// missing section
345        missing_section: Section,
346        /// section that makes the missing section required
347        dependent_section: Section,
348    },
349}
350
351/// Validation errors for [`WifSequence`]
352#[derive(Error, Debug, PartialEq, Eq, Clone, Copy)]
353pub enum SequenceError {
354    /// An entry with an index of 0 was found in the sequence
355    #[error("Found 0 index at entry {0}")]
356    Zero(usize),
357
358    /// An entry with an index less than the previous index was found in the sequence
359    #[error(
360        "Out of order entry at position {out_of_order_position}, index {out_of_order_index} is \
361        smaller than previous index {last_ok_index}"
362    )]
363    OutOfOrder {
364        /// Index of last valid entry
365        last_ok_index: usize,
366        /// Position of error entry
367        out_of_order_position: usize,
368        /// Index of error entry
369        out_of_order_index: usize,
370    },
371
372    /// An entry with the same index as the previous entry was found in the sequence
373    #[error(
374        "Duplicate index {duplicate_index} at positions {last_ok_position} and {error_position}"
375    )]
376    Repeat {
377        /// Position of last valid entry
378        last_ok_position: usize,
379        /// Position of error entry
380        error_position: usize,
381        /// Duplicate index
382        duplicate_index: usize,
383    },
384}
385
386/// Enum of all the possible sections in a `.wif` document (excluding private sections)
387#[derive(EnumString, Debug, PartialEq, Eq, Hash, Clone, EnumIter, IntoStaticStr, Display, Copy)]
388#[strum(use_phf, serialize_all = "UPPERCASE")]
389pub enum Section {
390    /// `WIF` section
391    #[strum(serialize = "WIF")]
392    Header,
393    /// `CONTENTS` section
394    Contents,
395    /// `COLOR PALETTE` section
396    #[strum(serialize = "COLOR PALETTE")]
397    ColorPalette,
398    /// `WEFT SYMBOL PALETTE` section
399    #[strum(serialize = "WEFT SYMBOL PALETTE")]
400    WeftSymbolPalette,
401    /// `WARP SYMBOL PALETTE` section
402    #[strum(serialize = "WARP SYMBOL PALETTE")]
403    WarpSymbolPalette,
404    /// `TEXT` section
405    Text,
406    /// `WEAVING` section
407    Weaving,
408    /// `WARP` section
409    Warp,
410    /// `WEFT` section
411    Weft,
412    /// `Notes` section
413    Notes,
414    /// `TIEUP` section
415    TieUp,
416    /// `COLOR TABLE` section
417    #[strum(serialize = "COLOR TABLE")]
418    ColorTable,
419    /// `WARP SYMBOL TABLE` section
420    #[strum(serialize = "WARP SYMBOL TABLE")]
421    WarpSymbolTable,
422    /// `WEFT SYMBOL TABLE` section
423    #[strum(serialize = "WEFT SYMBOL TABLE")]
424    WeftSymbolTable,
425    /// `THREADING` section
426    Threading,
427    /// `WARP THICKNESS` section
428    #[strum(serialize = "WARP THICKNESS")]
429    WarpThickness,
430    /// `WARP THICKNESS ZOOM` section
431    #[strum(serialize = "WARP THICKNESS ZOOM")]
432    WarpThicknessZoom,
433    /// `WARP SPACING` section
434    #[strum(serialize = "WARP SPACING")]
435    WarpSpacing,
436    /// `WARP SPACING ZOOM` section
437    #[strum(serialize = "WARP SPACING ZOOM")]
438    WarpSpacingZoom,
439    /// `WARP COLORS` section
440    #[strum(serialize = "WARP COLORS")]
441    WarpColors,
442    /// `WARP SYMBOLS` section
443    #[strum(serialize = "WARP SYMBOLS")]
444    WarpSymbols,
445    /// `TREADLING` section
446    Treadling,
447    /// `LIFTPLAN` section
448    LiftPlan,
449    /// `WEFT THICKNESS` section
450    #[strum(serialize = "WEFT THICKNESS")]
451    WeftThickness,
452    /// `WEFT THICKNESS ZOOM` section
453    #[strum(serialize = "WEFT THICKNESS ZOOM")]
454    WeftThicknessZoom,
455    /// `WEFT SPACING` section
456    #[strum(serialize = "WEFT SPACING")]
457    WeftSpacing,
458    /// `WEFT SPACING ZOOM` section
459    #[strum(serialize = "WEFT SPACING ZOOM")]
460    WeftSpacingZoom,
461    /// `WEFT COLORS` section
462    #[strum(serialize = "WEFT COLORS")]
463    WeftColors,
464    /// `WEFT SYMBOLS` section
465    #[strum(serialize = "WEFT SYMBOLS")]
466    WeftSymbols,
467}
468
469impl Section {
470    fn index_map_key(self) -> String {
471        self.to_string().to_lowercase()
472    }
473    fn get_data(
474        self,
475        map: &IndexMap<String, IndexMap<String, Option<String>>>,
476    ) -> Option<&IndexMap<String, Option<String>>> {
477        map.get(&self.index_map_key())
478    }
479
480    fn parse_and_pop<T: WifParseable>(
481        self,
482        map: &mut IndexMap<String, IndexMap<String, Option<String>>>,
483        error_map: &mut HashMap<Self, Vec<ParseError>>,
484    ) -> Option<T> {
485        let data = self.get_data(map)?;
486        let (section, errs) = T::from_index_map(data);
487        if !errs.is_empty() {
488            error_map.insert(self, errs);
489        }
490        map.shift_remove_entry(&self.index_map_key());
491        Some(section)
492    }
493
494    fn push_and_mark<T: WifParseable>(
495        self,
496        map: &mut IndexMap<String, IndexMap<String, Option<String>>>,
497        section: Option<&T>,
498    ) {
499        if let Some(section) = section.as_ref() {
500            map.insert(self.to_string(), section.to_index_map());
501            self.mark_present(map);
502        }
503    }
504
505    fn mark_present(self, map: &mut IndexMap<String, IndexMap<String, Option<String>>>) {
506        map.entry(Self::Contents.to_string())
507            .or_default()
508            .insert(self.to_string(), Some(String::from("1")));
509    }
510}
511
512#[cfg(test)]
513mod tests {
514    use super::*;
515    use data::WifValue;
516
517    #[test]
518    fn parse_usize() {
519        assert_eq!(usize::parse("1", "").unwrap(), 1);
520        assert_eq!(usize::parse("  1   ", "").unwrap(), 1);
521        assert_eq!(
522            usize::parse("-1", "").unwrap_err(),
523            ParseError::BadValueType {
524                key: String::new(),
525                value: String::from("-1"),
526                expected_type: String::from("non-negative integer")
527            }
528        );
529        assert_eq!(
530            usize::parse("a", "").unwrap_err(),
531            ParseError::BadValueType {
532                key: String::new(),
533                value: String::from("a"),
534                expected_type: String::from("non-negative integer")
535            }
536        );
537    }
538
539    #[test]
540    fn all_wif_fields_in_enum() {
541        let fields = [
542            "WIF",
543            "CONTENTS",
544            "COLOR PALETTE",
545            "WARP SYMBOL PALETTE",
546            "WEFT SYMBOL PALETTE",
547            "TEXT",
548            "WEAVING",
549            "WARP",
550            "WEFT",
551            "NOTES",
552            "TIEUP",
553            "COLOR TABLE",
554            "WARP SYMBOL TABLE",
555            "WEFT SYMBOL TABLE",
556            "THREADING",
557            "WARP THICKNESS",
558            "WARP THICKNESS ZOOM",
559            "WARP SPACING",
560            "WARP SPACING ZOOM",
561            "WARP COLORS",
562            "WARP SYMBOLS",
563            "TREADLING",
564            "LIFTPLAN",
565            "WEFT THICKNESS",
566            "WEFT THICKNESS ZOOM",
567            "WEFT SPACING",
568            "WEFT SPACING ZOOM",
569            "WEFT COLORS",
570            "WEFT SYMBOLS",
571        ];
572
573        for field in fields {
574            assert!(Section::from_str(field).is_ok(), "{field} is not in enum");
575        }
576    }
577}