1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
use anyhow::{bail, Context as _, Result};
use std::fmt;
use std::fs::File;
use std::path::Path;

use crate::{Position, Rule};

mod plaintext;
pub use plaintext::{Plaintext, PlaintextBuilder};

mod rle;
pub use rle::{Rle, RleBuilder};

/// Provides several methods for Conway's Game of Life pattern file formats.
pub trait Format: fmt::Display {
    /// Returns the rule.
    ///
    /// # Examples
    ///
    /// ```
    /// use life_backend::{Format, Rule};
    /// use life_backend::format::Rle;
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let pattern = "\
    ///     #N T-tetromino\n\
    ///     x = 3, y = 2, rule = B3/S23\n\
    ///     3o$bo!\n\
    /// ";
    /// let handler: Box<dyn Format> = Box::new(pattern.parse::<Rle>()?);
    /// assert_eq!(handler.rule(), Rule::conways_life());
    /// # Ok(())
    /// # }
    /// ```
    ///
    fn rule(&self) -> Rule;

    /// Creates an owning iterator over the series of live cell positions in ascending order.
    ///
    /// # Examples
    ///
    /// ```
    /// use life_backend::{Format, Position, Rule};
    /// use life_backend::format::Rle;
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let pattern = "\
    ///     #N T-tetromino\n\
    ///     x = 3, y = 2, rule = B3/S23\n\
    ///     3o$bo!\n\
    /// ";
    /// let handler: Box<dyn Format> = Box::new(pattern.parse::<Rle>()?);
    /// assert!(handler.live_cells().eq([Position(0, 0), Position(1, 0), Position(2, 0), Position(1, 1)]));
    /// # Ok(())
    /// # }
    /// ```
    ///
    fn live_cells(&self) -> Box<dyn Iterator<Item = Position<usize>> + '_>;
}

/// Attempts to open a file with the file format handler specified by the file extension.
///
/// # Examples
///
/// ```
/// use life_backend::format;
/// use life_backend::Rule;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let path = "patterns/rpentomino.cells";
/// let handler = format::open(path)?;
/// assert_eq!(handler.rule(), Rule::conways_life());
/// assert_eq!(handler.live_cells().count(), 5);
/// # Ok(())
/// # }
/// ```
///
/// ```
/// use life_backend::format;
/// use life_backend::Rule;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let path = "patterns/bheptomino.rle";
/// let handler = format::open(path)?;
/// assert_eq!(handler.rule(), Rule::conways_life());
/// assert_eq!(handler.live_cells().count(), 7);
/// # Ok(())
/// # }
/// ```
///
pub fn open<P>(path: P) -> Result<Box<dyn Format>>
where
    P: AsRef<Path>,
{
    let path_for_display = path.as_ref().to_owned();
    let ext = path
        .as_ref()
        .extension()
        .with_context(|| format!("\"{}\" has no extension", path_for_display.display()))?
        .to_owned();
    let file = File::open(path).with_context(|| format!("Failed to open \"{}\"", path_for_display.display()))?;
    let result: Box<dyn Format> = if ext.as_os_str() == "cells" {
        Box::new(Plaintext::new(file)?)
    } else if ext.as_os_str() == "rle" {
        Box::new(Rle::new(file)?)
    } else {
        bail!("\"{}\" has unknown extension", path_for_display.display());
    };
    Ok(result)
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn open_no_extension() {
        let path = "patterns/rpentomino";
        let result = open(path);
        assert!(result.is_err());
    }
    #[test]
    fn open_unknown_extension() {
        let path = "patterns/rpentomino.unknown";
        let result = open(path);
        assert!(result.is_err());
    }
}