Encoding and decoding of `settings::Settings` now handled in macros to avoid code repetition. I also wish rewrite them both in a combined macro that would either encode or decode the struct depending on how it was called. Replaced some log warnings with errors Now if type extension is not valid, the code will still attempt to decode it, first as JSON data, then as Yaml data. If both fail, then an error is returned. For this, I added the Display trait to the `settings::Settings` struct which allows the FromStr trait, and due to conflicting implementation, I removed the From trait and implemented manually the `from` method with the same signature -- the struct just lacks the trait that comes with it.
300 lines
9.3 KiB
Rust
300 lines
9.3 KiB
Rust
extern crate serde;
|
||
extern crate serde_json;
|
||
extern crate serde_yaml;
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
extern crate log;
|
||
use log::{error, info};
|
||
|
||
// mod utils;
|
||
pub mod utils;
|
||
use utils::SettingsType;
|
||
|
||
/// Current version of the ruleset. It will help determine if the ruleset is
|
||
/// outdated or from a more recent version of the software than the one being in
|
||
/// use.
|
||
pub const RULESET_CURRENT_VERSION: i32 = 1;
|
||
|
||
/// Encode a [`Settings`] struct to a filetype, returns a
|
||
/// `std::result::Result<std::string::String, std::io::Error>`
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `funcrate` - `serde`-compatible crate to use, must implement `to_string`
|
||
/// * `content` - content to encode, must be `Settings` struct
|
||
///
|
||
/// # Example
|
||
///
|
||
/// ```ignore
|
||
/// # use lang_evolve_core::settings::*;
|
||
/// # use lang_evolve_core::encode_settings;
|
||
/// use std::io::{Error, ErrorKind};
|
||
/// use std::path::Path;
|
||
/// let filetype = utils::get_file_type(Path::new("./path/to/file.json"));
|
||
/// let s = Settings::new();
|
||
/// let content = match filetype {
|
||
/// utils::SettingsType::Yaml => encode_settings!(serde_yaml, &s).unwrap(),
|
||
/// utils::SettingsType::Json => encode_settings!(serde_json, &s).unwrap(),
|
||
/// _ => panic!("Could not encode settings"),
|
||
/// };
|
||
/// ```
|
||
///
|
||
/// [`Settings`]: ./settings/struct.Settings.html
|
||
#[macro_export(local_inner_macros)]
|
||
macro_rules! encode_settings {
|
||
($funcrate:ident, $content:expr) => {
|
||
match $funcrate::to_string($content) {
|
||
Err(e) => {
|
||
log::error!("Could not serialize settings: {}", e.to_string());
|
||
return Err(std::io::Error::new(
|
||
std::io::ErrorKind::InvalidData,
|
||
e,
|
||
));
|
||
}
|
||
Ok(val) => val,
|
||
}
|
||
};
|
||
}
|
||
|
||
/// Decode a [`Settings`] struct from a `std::std::String`, returns a
|
||
/// std::result::Result<lang_evolve_core::settings::Settings, std::io::Error>
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `funcrate` - `serde`-compatible crate to use, mus implement `from_string`
|
||
/// * `content` - `&str` to decode into a [`Settings`]
|
||
///
|
||
/// # Example
|
||
///
|
||
/// ```ignore
|
||
/// # use lang_evolve_core::decode_settings;
|
||
/// let str = r#"{"version":"1","categories":[],"rules":[]}"#;
|
||
/// let settings = decode_settings!(serde_json, str);
|
||
/// ```
|
||
///
|
||
/// [`Settings`]: ./settings/struct.Settings.html
|
||
#[macro_export(local_inner_macros)]
|
||
macro_rules! decode_settings {
|
||
($funcrate:ident, $content:expr) => {
|
||
match $funcrate::from_str($content) {
|
||
Err(e) => {
|
||
log::error!("Could not import settings: {}", e.to_string());
|
||
return Err(std::io::Error::new(
|
||
std::io::ErrorKind::InvalidInput,
|
||
e,
|
||
));
|
||
}
|
||
Ok(val) => val,
|
||
}
|
||
};
|
||
}
|
||
|
||
#[derive(Debug, Deserialize, Serialize)]
|
||
pub struct Settings {
|
||
#[serde(default = "Settings::get_ruleset_version")]
|
||
version: String,
|
||
#[serde(default)]
|
||
categories: Vec<(String, String)>,
|
||
#[serde(default)]
|
||
rules: Vec<(String, String)>,
|
||
}
|
||
|
||
/// Representation inside the crate of LangEvolve’s settings.
|
||
impl Settings {
|
||
/// Creates a new empty instance of `Settings`
|
||
///
|
||
/// # Example
|
||
///
|
||
/// ```
|
||
/// let s = lang_evolve_core::settings::Settings::new();
|
||
/// let content_yaml = r#"---
|
||
/// version: "1"
|
||
/// categories: []
|
||
/// rules: []"#;
|
||
/// let content_json = r#"{"version":"1","categories":[],"rules":[]}"#;
|
||
/// assert_eq!(content_yaml, serde_yaml::to_string(&s).unwrap());
|
||
/// assert_eq!(content_json, serde_json::to_string(&s).unwrap());
|
||
/// ```
|
||
pub fn new() -> Self {
|
||
Self {
|
||
version: Self::get_ruleset_version(),
|
||
categories: Vec::new(),
|
||
rules: Vec::new(),
|
||
}
|
||
}
|
||
|
||
/// Import settings from an imput file. The currently allowed file formats
|
||
/// are:
|
||
/// - JSON - with the `.json` extension
|
||
/// - Yaml - with the `.yaml` or `.yml` extension
|
||
/// The format will be automatically detected based on the filename
|
||
/// extension.
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `path` - File to open and load settings from
|
||
///
|
||
/// # Example
|
||
///
|
||
/// ```no_run
|
||
/// use std::path::Path;
|
||
/// use lang_evolve_core::settings::Settings;
|
||
/// let path_json = Path::new("settings.json");
|
||
/// let _s_json = Settings::import(&path_json).unwrap();
|
||
///
|
||
/// let path_yaml = Path::new("settings.yaml");
|
||
/// let _s_yaml = Settings::import(&path_yaml).unwrap();
|
||
///
|
||
/// let path_yml = Path::new("settings.yml");
|
||
/// let _s_yml = Settings::import(&path_yml).unwrap();
|
||
/// ```
|
||
pub fn import(path: &std::path::Path) -> std::io::Result<Self> {
|
||
use utils::SettingsType::{Json, Yaml};
|
||
let file_type = utils::get_file_type(&path);
|
||
let content = utils::read_file(&path)?;
|
||
|
||
let settings: Settings = match file_type {
|
||
Yaml => decode_settings!(serde_yaml, &content),
|
||
Json => decode_settings!(serde_json, &content),
|
||
// Attempt to decode anyway
|
||
_ => match Settings::from_str(&content.as_str()) {
|
||
Ok(val) => val,
|
||
Err(e) => {
|
||
error!(
|
||
"Could not decode input {}: {}",
|
||
content,
|
||
e.to_string()
|
||
);
|
||
return Err(std::io::Error::new(
|
||
std::io::ErrorKind::InvalidData,
|
||
e,
|
||
));
|
||
}
|
||
},
|
||
};
|
||
info!("Successfuly imported {}", path.display());
|
||
Ok(settings)
|
||
}
|
||
|
||
/// Export the current rules to a file. The allowed file formats are either
|
||
/// a YAML file or a Json file, hence the allowed filename extension are:
|
||
/// * "yml" or "yaml" for Yaml files
|
||
/// * "json" for Json files
|
||
/// The format is detected automatically depending on the extension of the
|
||
/// filename.
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `path` - Path to write and export settings to
|
||
///
|
||
/// # Example
|
||
///
|
||
/// ```
|
||
/// use std::path::Path;
|
||
/// let s = lang_evolve_core::settings::Settings::new();
|
||
///
|
||
/// // Export to JSON
|
||
/// let path_json = Path::new("./output.json");
|
||
/// s.export(&path_json).unwrap();
|
||
///
|
||
/// // Export to Yaml, both ".yml" and ".yaml" work
|
||
/// let path_yaml = Path::new("./output.yaml");
|
||
/// s.export(&path_yaml).unwrap();
|
||
/// let path_yml = Path::new("./output.yml");
|
||
/// s.export(&path_yml).unwrap();
|
||
/// ```
|
||
pub fn export(&self, path: &std::path::Path) -> std::io::Result<()> {
|
||
let filetype = utils::get_file_type(&path);
|
||
let content = match filetype {
|
||
SettingsType::Yaml => encode_settings!(serde_yaml, &self),
|
||
SettingsType::Json => encode_settings!(serde_json, &self),
|
||
_ => {
|
||
error!("Unknown filetype {}", path.to_str().unwrap());
|
||
return Err(std::io::Error::new(
|
||
std::io::ErrorKind::InvalidData,
|
||
"Unknown file type",
|
||
));
|
||
}
|
||
};
|
||
info!("Successfuly exported settings to {}", path.display());
|
||
utils::write_file(&path, &content)
|
||
}
|
||
|
||
/// Get the current ruleset version of LangEvolve.
|
||
fn get_ruleset_version() -> String {
|
||
RULESET_CURRENT_VERSION.to_string()
|
||
}
|
||
}
|
||
|
||
use std::str::FromStr;
|
||
impl FromStr for Settings {
|
||
type Err = serde_yaml::Error;
|
||
|
||
/// Decode a litteral string into a `Settings` struct. Works only for
|
||
/// supported file types described in `SettingsType`. It will try to decode
|
||
/// the input `s` by any mean known by `SettingsType`.
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `s` - litteral string to decode into a `Settings` struct
|
||
///
|
||
/// # Example
|
||
///
|
||
/// ```
|
||
/// # use std::str::FromStr;
|
||
/// let s = r#"{"version":"1","categories":[],"rules":[]}"#;
|
||
/// let settings = lang_evolve_core::settings::Settings::from_str(s).unwrap();
|
||
/// ```
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||
match serde_json::from_str::<Settings>(s) {
|
||
Ok(val) => Ok(val),
|
||
Err(_) => match serde_yaml::from_str::<Settings>(s) {
|
||
Ok(val) => Ok(val),
|
||
Err(e) => {
|
||
error!("Could not decode input {}: {}", s, e.to_string());
|
||
return Err(e);
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
use std::fmt;
|
||
use std::fmt::Display;
|
||
impl Display for Settings {
|
||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||
write!(f, "{}", serde_json::to_string(&self).unwrap())
|
||
}
|
||
}
|
||
|
||
impl PartialEq for Settings {
|
||
fn eq(&self, other: &Self) -> bool {
|
||
self.version == other.version
|
||
&& self.categories == other.categories
|
||
&& self.rules == other.rules
|
||
}
|
||
}
|
||
|
||
impl Eq for Settings {}
|
||
|
||
#[test]
|
||
fn write_settings() {
|
||
let s = Settings::new();
|
||
let path = std::path::Path::new("test.yaml");
|
||
let settings = r#"---
|
||
version: "1"
|
||
categories: []
|
||
rules: []"#;
|
||
utils::write_file(&path, &serde_yaml::to_string(&s).unwrap()).unwrap();
|
||
assert_eq!(settings, utils::read_file(&path).unwrap());
|
||
}
|
||
|
||
#[test]
|
||
fn read_settings() {
|
||
let s1 = Settings::new();
|
||
let path = std::path::Path::new("test.yml");
|
||
s1.export(&path).unwrap();
|
||
let s2 = Settings::import(&path).unwrap();
|
||
assert_eq!(s1, s2);
|
||
}
|