1use 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
16pub const WIF_DEVELOPERS: &str = "wif@mhsoft.com";
18pub const WIF_DATE: &str = "April 20, 1997";
20pub const WIF_VERSION: &str = "1.1";
22
23#[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 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 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 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 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 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 ini_map.insert(Section::Contents.to_string(), IndexMap::new());
153 inner.shift_remove_entry(&Section::Contents.index_map_key());
154
155 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 for (key, section) in inner {
169 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 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 #[must_use]
188 pub fn writes(&self) -> String {
189 self.to_ini().pretty_writes(&Self::write_options())
190 }
191
192 #[must_use]
194 pub const fn threading(&self) -> Option<&WifSequence<Vec<usize>>> {
195 self.threading.as_ref()
196 }
197
198 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 #[must_use]
211 pub const fn treadling(&self) -> Option<&WifSequence<Vec<usize>>> {
212 self.treadling.as_ref()
213 }
214
215 #[must_use]
217 pub const fn lift_plan(&self) -> Option<&WifSequence<Vec<usize>>> {
218 self.lift_plan.as_ref()
219 }
220
221 #[must_use]
223 pub const fn tie_up(&self) -> Option<&WifSequence<Vec<usize>>> {
224 self.tie_up.as_ref()
225 }
226
227 #[must_use]
229 pub const fn color_palette(&self) -> Option<&ColorPalette> {
230 self.color_palette.as_ref()
231 }
232
233 #[must_use]
235 pub const fn weaving(&self) -> Option<&Weaving> {
236 self.weaving.as_ref()
237 }
238
239 #[must_use]
241 pub const fn warp(&self) -> Option<&WarpWeft> {
242 self.warp.as_ref()
243 }
244
245 #[must_use]
247 pub const fn weft(&self) -> Option<&WarpWeft> {
248 self.weft.as_ref()
249 }
250
251 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 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(§ion.to_string().to_lowercase()))
300 }
301 }
302
303 #[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#[derive(Error, Debug, Clone, PartialEq, Eq)]
321pub enum ParseError {
322 #[error("Required field {0} is missing")]
324 MissingField(String),
325 #[error("Key {0} has no value")]
327 MissingValue(String),
328 #[error("Keys must be positive integers, found {0}")]
330 BadIntegerKey(String),
331 #[error("Values must be {expected_type}, found {value} at key {key}")]
333 BadValueType {
334 key: String,
336 value: String,
338 expected_type: String,
340 },
341 #[error("When {dependent_section} is present, {missing_section} must also be present")]
343 MissingDependentSection {
344 missing_section: Section,
346 dependent_section: Section,
348 },
349}
350
351#[derive(Error, Debug, PartialEq, Eq, Clone, Copy)]
353pub enum SequenceError {
354 #[error("Found 0 index at entry {0}")]
356 Zero(usize),
357
358 #[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 last_ok_index: usize,
366 out_of_order_position: usize,
368 out_of_order_index: usize,
370 },
371
372 #[error(
374 "Duplicate index {duplicate_index} at positions {last_ok_position} and {error_position}"
375 )]
376 Repeat {
377 last_ok_position: usize,
379 error_position: usize,
381 duplicate_index: usize,
383 },
384}
385
386#[derive(EnumString, Debug, PartialEq, Eq, Hash, Clone, EnumIter, IntoStaticStr, Display, Copy)]
388#[strum(use_phf, serialize_all = "UPPERCASE")]
389pub enum Section {
390 #[strum(serialize = "WIF")]
392 Header,
393 Contents,
395 #[strum(serialize = "COLOR PALETTE")]
397 ColorPalette,
398 #[strum(serialize = "WEFT SYMBOL PALETTE")]
400 WeftSymbolPalette,
401 #[strum(serialize = "WARP SYMBOL PALETTE")]
403 WarpSymbolPalette,
404 Text,
406 Weaving,
408 Warp,
410 Weft,
412 Notes,
414 TieUp,
416 #[strum(serialize = "COLOR TABLE")]
418 ColorTable,
419 #[strum(serialize = "WARP SYMBOL TABLE")]
421 WarpSymbolTable,
422 #[strum(serialize = "WEFT SYMBOL TABLE")]
424 WeftSymbolTable,
425 Threading,
427 #[strum(serialize = "WARP THICKNESS")]
429 WarpThickness,
430 #[strum(serialize = "WARP THICKNESS ZOOM")]
432 WarpThicknessZoom,
433 #[strum(serialize = "WARP SPACING")]
435 WarpSpacing,
436 #[strum(serialize = "WARP SPACING ZOOM")]
438 WarpSpacingZoom,
439 #[strum(serialize = "WARP COLORS")]
441 WarpColors,
442 #[strum(serialize = "WARP SYMBOLS")]
444 WarpSymbols,
445 Treadling,
447 LiftPlan,
449 #[strum(serialize = "WEFT THICKNESS")]
451 WeftThickness,
452 #[strum(serialize = "WEFT THICKNESS ZOOM")]
454 WeftThicknessZoom,
455 #[strum(serialize = "WEFT SPACING")]
457 WeftSpacing,
458 #[strum(serialize = "WEFT SPACING ZOOM")]
460 WeftSpacingZoom,
461 #[strum(serialize = "WEFT COLORS")]
463 WeftColors,
464 #[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}