sokoban-elements 0.2.1

Parsing and writing Sokoban, Hexoban, Trioban and Octoban file formats.
Documentation
use std::mem;

use itertools::Itertools;

use super::super::IoReadError;
use crate::{Collection, GameBoard, Puzzle, SavedGame, SokobanPlus, Tessellation};

pub const AUTHOR: &str = "Author";
pub const TITLE: &str = "Title";
pub const COLLECTION: &str = "Collection";
pub const GOALORDER: &str = "goalorder";
pub const BOXORDER: &str = "boxorder";
pub const SOLVER: &str = "Solver";
pub const VARIANT: &str = "Game";
pub const DATE_CREATED: &str = "Date Created";
pub const DATE_OF_LAST_CHANGE: &str = "Date of Last Change";
pub const RAW_FILE_NOTES: &str = "::";
pub const TAG_DELIMITERS: &str = "=:";

#[derive(Debug, Clone)]
pub struct NumberedLine {
    pub idx: usize,
    pub ln:  String,
}

#[derive(Debug, Clone)]
pub struct SavedGameData {
    pub ln_idx: usize,
    pub moves:  String,
    pub title:  String,
    pub solver: String,
    pub notes:  Vec<NumberedLine>,
}

impl SavedGameData {
    pub fn new(ln_idx: usize) -> Self {
        Self {
            ln_idx,
            moves: String::new(),
            title: String::new(),
            solver: String::new(),
            notes: Vec::new(),
        }
    }

    pub fn create_saved_game(
        &mut self, tessellation: Tessellation,
    ) -> Result<SavedGame, IoReadError> {
        match SavedGame::parsed(&self.moves, tessellation) {
            Ok(mut saved_game) => {
                mem::swap(&mut saved_game.title, &mut self.title);
                mem::swap(&mut saved_game.solved_by, &mut self.solver);
                saved_game.notes = self.notes.iter().map(|num_ln| &num_ln.ln).join("\n");

                Ok(saved_game)
            }
            Err(e) => Err(IoReadError::new_saved_game_syntax_error(self.ln_idx, e)),
        }
    }
}

#[derive(Debug, Clone)]
pub struct PuzzleData {
    pub board_ln_idx:   usize,
    pub sokplus_ln_idx: usize,
    pub board:          String,
    pub tessellation:   Tessellation,
    pub title:          String,
    pub author:         String,
    pub boxorder:       String,
    pub goalorder:      String,
    pub notes:          Vec<NumberedLine>,
    pub saved_games:    Vec<SavedGameData>,
}

impl PuzzleData {
    pub fn new(board_ln_idx: usize) -> Self {
        Self {
            board_ln_idx,
            sokplus_ln_idx: 0,
            board: String::new(),
            tessellation: Tessellation::Sokoban,
            title: String::new(),
            author: String::new(),
            boxorder: String::new(),
            goalorder: String::new(),
            notes: Vec::new(),
            saved_games: Vec::new(),
        }
    }

    pub fn create_puzzle(
        &mut self, legacy_sokoban_plus: bool, tessellation: Tessellation,
    ) -> Result<Puzzle, IoReadError> {
        match GameBoard::parsed(&self.board, tessellation) {
            Ok(board) => {
                let mut puzzle = Puzzle {
                    board,
                    title: String::default(),
                    author: String::default(),
                    notes: String::default(),
                    saved_games: Vec::default(),
                    sokoban_plus: None,
                };

                mem::swap(&mut puzzle.title, &mut self.title);
                mem::swap(&mut puzzle.author, &mut self.author);
                puzzle.notes = self.notes.iter().map(|num_ln| &num_ln.ln).join("\n");

                let result = if legacy_sokoban_plus {
                    SokobanPlus::parsed_legacy(&self.boxorder, &self.goalorder).map_err(|e| {
                        IoReadError::new_sokoban_plus_syntax_error(self.sokplus_ln_idx, e)
                    })?
                } else {
                    SokobanPlus::parsed(&self.boxorder, &self.goalorder).map_err(|e| {
                        IoReadError::new_sokoban_plus_syntax_error(self.sokplus_ln_idx, e)
                    })?
                };
                if !result.is_empty() {
                    puzzle.sokoban_plus = Some(result);
                }

                Ok(puzzle)
            }
            Err(e) => Err(IoReadError::new_puzzle_syntax_error(self.board_ln_idx, e)),
        }
    }
}

#[derive(Debug, Clone)]
pub struct CollectionData {
    pub title:                      String,
    pub author:                     String,
    pub created_at:                 String,
    pub updated_at:                 String,
    pub notes:                      Vec<NumberedLine>,
    pub header_tessellation_hint:   Option<Tessellation>,
    pub was_tessellation_in_header: bool,
    pub puzzles:                    Vec<PuzzleData>,
}

impl CollectionData {
    pub fn new(tessellation_hint: Option<Tessellation>) -> Self {
        Self {
            title:                      String::new(),
            author:                     String::new(),
            created_at:                 String::new(),
            updated_at:                 String::new(),
            notes:                      Vec::new(),
            header_tessellation_hint:   tessellation_hint,
            was_tessellation_in_header: false,
            puzzles:                    Vec::new(),
        }
    }

    pub fn create_collection(
        &mut self, legacy_sokoban_plus: bool,
    ) -> Result<Collection, IoReadError> {
        let mut collection = Collection {
            title:      mem::take(&mut self.title),
            author:     mem::take(&mut self.author),
            created_at: mem::take(&mut self.created_at),
            updated_at: mem::take(&mut self.updated_at),
            email:      String::new(),
            url:        String::new(),
            notes:      self.notes.iter().map(|num_ln| &num_ln.ln).join("\n"),
            puzzles:    Vec::new(),
        };

        for puzzle_data in &mut self.puzzles {
            let mut puzzle =
                puzzle_data.create_puzzle(legacy_sokoban_plus, puzzle_data.tessellation)?;
            for saved_game_data in &mut puzzle_data.saved_games {
                let saved_game = saved_game_data.create_saved_game(puzzle_data.tessellation)?;
                puzzle.saved_games.push(saved_game);
            }
            collection.puzzles.push(puzzle);
        }

        Ok(collection)
    }
}