144 lines
3.9 KiB
Rust
144 lines
3.9 KiB
Rust
//! JSON-based project file persistence with versioned format.
|
|
|
|
use std::fs;
|
|
use std::io;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::project::{Bank, PatternSpeed, Project};
|
|
|
|
const VERSION: u8 = 1;
|
|
const EXTENSION: &str = "cagire";
|
|
|
|
fn ensure_extension(path: &Path) -> PathBuf {
|
|
if path.extension().map(|e| e == EXTENSION).unwrap_or(false) {
|
|
path.to_path_buf()
|
|
} else {
|
|
path.with_extension(EXTENSION)
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
struct ProjectFile {
|
|
version: u8,
|
|
banks: Vec<Bank>,
|
|
#[serde(default)]
|
|
sample_paths: Vec<PathBuf>,
|
|
#[serde(default = "default_tempo")]
|
|
tempo: f64,
|
|
#[serde(default)]
|
|
playing_patterns: Vec<(usize, usize)>,
|
|
#[serde(default, skip_serializing_if = "String::is_empty")]
|
|
prelude: String,
|
|
#[serde(default, skip_serializing_if = "String::is_empty")]
|
|
script: String,
|
|
#[serde(default, skip_serializing_if = "is_default_speed")]
|
|
script_speed: PatternSpeed,
|
|
#[serde(default = "default_script_length", skip_serializing_if = "is_default_script_length")]
|
|
script_length: usize,
|
|
}
|
|
|
|
fn is_default_speed(s: &PatternSpeed) -> bool {
|
|
*s == PatternSpeed::default()
|
|
}
|
|
|
|
fn default_script_length() -> usize {
|
|
16
|
|
}
|
|
|
|
fn is_default_script_length(n: &usize) -> bool {
|
|
*n == default_script_length()
|
|
}
|
|
|
|
fn default_tempo() -> f64 {
|
|
120.0
|
|
}
|
|
|
|
impl From<&Project> for ProjectFile {
|
|
fn from(project: &Project) -> Self {
|
|
Self {
|
|
version: VERSION,
|
|
banks: project.banks.clone(),
|
|
sample_paths: project.sample_paths.clone(),
|
|
tempo: project.tempo,
|
|
playing_patterns: project.playing_patterns.clone(),
|
|
prelude: project.prelude.clone(),
|
|
script: project.script.clone(),
|
|
script_speed: project.script_speed,
|
|
script_length: project.script_length,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<ProjectFile> for Project {
|
|
fn from(file: ProjectFile) -> Self {
|
|
let mut project = Self {
|
|
banks: file.banks,
|
|
sample_paths: file.sample_paths,
|
|
tempo: file.tempo,
|
|
playing_patterns: file.playing_patterns,
|
|
prelude: file.prelude,
|
|
script: file.script,
|
|
script_speed: file.script_speed,
|
|
script_length: file.script_length,
|
|
};
|
|
project.normalize();
|
|
project
|
|
}
|
|
}
|
|
|
|
/// Error returned by project save/load operations.
|
|
#[derive(Debug)]
|
|
pub enum FileError {
|
|
Io(io::Error),
|
|
Json(serde_json::Error),
|
|
Version(u8),
|
|
}
|
|
|
|
impl std::fmt::Display for FileError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
FileError::Io(e) => write!(f, "IO error: {e}"),
|
|
FileError::Json(e) => write!(f, "JSON error: {e}"),
|
|
FileError::Version(v) => write!(f, "Unsupported version: {v}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<io::Error> for FileError {
|
|
fn from(e: io::Error) -> Self {
|
|
FileError::Io(e)
|
|
}
|
|
}
|
|
|
|
impl From<serde_json::Error> for FileError {
|
|
fn from(e: serde_json::Error) -> Self {
|
|
FileError::Json(e)
|
|
}
|
|
}
|
|
|
|
/// Write a project to disk as pretty-printed JSON, returning the final path.
|
|
pub fn save(project: &Project, path: &Path) -> Result<PathBuf, FileError> {
|
|
let path = ensure_extension(path);
|
|
let file = ProjectFile::from(project);
|
|
let json = serde_json::to_string_pretty(&file)?;
|
|
fs::write(&path, json)?;
|
|
Ok(path)
|
|
}
|
|
|
|
/// Read a project from a `.cagire` file on disk.
|
|
pub fn load(path: &Path) -> Result<Project, FileError> {
|
|
let json = fs::read_to_string(path)?;
|
|
load_str(&json)
|
|
}
|
|
|
|
/// Parse a project from a JSON string.
|
|
pub fn load_str(json: &str) -> Result<Project, FileError> {
|
|
let file: ProjectFile = serde_json::from_str(json)?;
|
|
if file.version > VERSION {
|
|
return Err(FileError::Version(file.version));
|
|
}
|
|
Ok(Project::from(file))
|
|
}
|