Skip to content

Commit

Permalink
Bit of a refactor after feature creep, prepping for much better desig…
Browse files Browse the repository at this point in the history
…ned --light and --superlight modes to get around circular deps, replacing the pretty crappy 'initial' double render approach (#16)
  • Loading branch information
zakstucke authored Feb 23, 2024
1 parent 7776852 commit a5f5a36
Show file tree
Hide file tree
Showing 30 changed files with 1,094 additions and 1,029 deletions.
4 changes: 2 additions & 2 deletions py_rust/src/coerce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ pub enum Coerce {
Bool,
}

pub fn coerce(value: Value, c_type: &Option<Coerce>) -> Result<Value, Zerr> {
pub fn coerce(value: &Value, c_type: &Option<Coerce>) -> Result<Value, Zerr> {
// Always strip whitespace from string inputs:
let value = match value {
Value::String(s) => Value::String(s.trim().to_string()),
_ => value,
_ => value.clone(),
};

if let Some(c_type) = c_type {
Expand Down
105 changes: 105 additions & 0 deletions py_rust/src/config/conf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use std::{fs, path::Path};

use serde::{Deserialize, Serialize};

use super::{context::Context, engine::Engine, tasks::Tasks};
use crate::{init::update_schema_directive_if_needed, prelude::*};

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Config {
// All should be optional to allow empty config file, even though it wouldn't make too much sense!
#[serde(default = "Context::default")]
pub context: Context,
#[serde(default = "Vec::new")]
pub exclude: Vec<String>,
#[serde(default = "Engine::default")]
pub engine: Engine,
#[serde(default = "Vec::new")]
pub ignore_files: Vec<String>,
#[serde(default = "default_matchers")]
pub matchers: Vec<String>,
#[serde(default = "Tasks::default")]
pub tasks: Tasks,
}

fn default_matchers() -> Vec<String> {
vec!["zetch".into()]
}

impl Config {
pub fn ctx_keys(&self) -> Vec<&str> {
let mut keys = Vec::new();

for (key, _) in self.context.stat.iter() {
keys.push(key.as_str());
}

for (key, _) in self.context.env.iter() {
keys.push(key.as_str());
}

for (key, _) in self.context.cli.iter() {
keys.push(key.as_str());
}

keys
}

pub fn from_toml(config_path: &Path) -> Result<Self, Zerr> {
Config::from_toml_inner(config_path).attach_printable_lazy(|| {
format!(
"Error reading config file from '{}'.",
config_path.display()
)
})
}

fn from_toml_inner(config_path: &Path) -> Result<Self, Zerr> {
let contents = autoupdate(config_path)?;

// Decode directly the toml directly into serde/json, using that internally:
let json: serde_json::Value = match toml::from_str(&contents) {
Ok(toml) => toml,
Err(e) => {
return Err(zerr!(
Zerr::ConfigInvalid,
"Invalid toml formatting: '{}'.",
e
))
}
};

// This will check against the json schema,
// can produce much better errors than the toml decoder can, so prevalidate first:
super::validate::pre_validate(&json)?;

// Now deserialize after validation:
let mut config: Config =
serde_json::from_value(json).change_context(Zerr::InternalError)?;

super::validate::post_validate(&mut config, config_path)?;

Ok(config)
}
}

/// Reads & pre-parses the config and updates managed sections, returns updated to save and use if changes needed.
///
/// E.g. currently just updates the schema directive if needs changing.
fn autoupdate(config_path: &Path) -> Result<String, Zerr> {
let mut contents = fs::read_to_string(config_path).change_context(Zerr::InternalError)?;
let mut updated = false;

// Handle the schema directive:
if let Some(new_contents) = update_schema_directive_if_needed(&contents) {
// Re-write schema to file first:
contents = new_contents;
updated = true;
}

if updated {
fs::write(config_path, &contents).change_context(Zerr::InternalError)?;
}

Ok(contents)
}
120 changes: 120 additions & 0 deletions py_rust/src/config/context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use std::{collections::HashMap, path::Path};

use bitbazaar::cli::{Bash, BashErr};
use serde::{Deserialize, Serialize};

use crate::{
coerce::{coerce, Coerce},
prelude::*,
};

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct CtxStaticVar {
pub value: serde_json::Value,
pub coerce: Option<Coerce>,
}

impl CtxStaticVar {
pub fn read(&self) -> Result<serde_json::Value, Zerr> {
coerce(&self.value, &self.coerce)
}
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct CtxEnvVar {
pub env_name: Option<String>,
pub default: Option<serde_json::Value>,
pub coerce: Option<Coerce>,
}

impl CtxEnvVar {
pub fn read(&self, key_name: &str, default_banned: bool) -> Result<serde_json::Value, Zerr> {
let env_name = match &self.env_name {
Some(env_name) => env_name,
None => key_name,
};

let value = match std::env::var(env_name) {
Ok(value) => value,
Err(_) => {
if self.default.is_some() && default_banned {
return Err(zerr!(
Zerr::ContextLoadError,
"Could not find environment variable '{}' and the default has been banned using the 'ban-defaults' cli option.",
env_name
));
} else {
match &self.default {
Some(value) => return Ok(value.clone()),
None => {
return Err(zerr!(
Zerr::ContextLoadError,
"Could not find environment variable '{}' and no default provided.",
env_name
))
}
}
}
}
};

coerce(&serde_json::Value::String(value), &self.coerce)
}
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct CtxCliVar {
pub commands: Vec<String>,
pub coerce: Option<Coerce>,
pub initial: Option<serde_json::Value>,
}

impl CtxCliVar {
pub fn read(&self, config_path: &Path) -> Result<serde_json::Value, Zerr> {
let config_dir = config_path.parent().ok_or_else(|| {
zerr!(
Zerr::InternalError,
"Failed to get parent dir of config file: {}",
config_path.display()
)
})?;

let mut bash = Bash::new().chdir(config_dir);
for command in self.commands.iter() {
bash = bash.cmd(command);
}
let cmd_out = match bash.run() {
Ok(cmd_out) => Ok(cmd_out),
Err(e) => match e.current_context() {
BashErr::InternalError(_) => Err(e.change_context(Zerr::InternalError)),
_ => Err(e.change_context(Zerr::UserCommandError)),
},
}?;
cmd_out.throw_on_bad_code(Zerr::UserCommandError)?;

// Prevent empty output:
let last_cmd_out = cmd_out.last_stdout();
if last_cmd_out.trim().is_empty() {
return Err(zerr!(
Zerr::UserCommandError,
"Implicit None. Final cli command returned nothing.",
)
.attach_printable(cmd_out.fmt_attempted_commands()));
}

coerce(&serde_json::Value::String(last_cmd_out), &self.coerce)
}
}

#[derive(Clone, Debug, Deserialize, Serialize, Default)]
pub struct Context {
#[serde(rename(deserialize = "static"))]
#[serde(default = "HashMap::new")]
pub stat: HashMap<String, CtxStaticVar>,

#[serde(default = "HashMap::new")]
pub env: HashMap<String, CtxEnvVar>,

#[serde(default = "HashMap::new")]
pub cli: HashMap<String, CtxCliVar>,
}
Loading

0 comments on commit a5f5a36

Please sign in to comment.