use std::{ fmt::Display, fs::File, io::{Read, Write}, }; use anyhow::{Context, Result}; use clap::Parser; use serde_json::Value; fn main() -> Result<()> { let args = Args::parse(); let mut reader: Box = match args.input { Some(ref filename) => Box::new( File::open(filename).with_context(|| format!("Could not open file `{filename}`"))?, ), None => Box::new(std::io::stdin()), }; let mut buffer = String::new(); let filename = args.input.unwrap_or("STDIN".to_string()); reader .read_to_string(&mut buffer) .with_context(|| format!("Could not read `{filename}`"))?; let json: Value = serde_json::from_str(&buffer) .with_context(|| format!("`{filename}` does not contain valid JSON"))?; let mut vars: Vec = vec![]; let separator = args.separator.unwrap_or("__".to_string()); JsonParser::parse(&mut vars, "", json, &separator); let environ = vars .iter() .map(|v| v.to_string()) .collect::>() .join("\n"); let mut writer: Box = match args.output { Some(ref filename) => Box::new( File::create(filename).with_context(|| format!("Could not open file `{filename}`"))?, ), None => Box::new(std::io::stdout()), }; writer .write_all(environ.as_bytes()) .with_context(|| "".to_string())?; Ok(()) } #[derive(Debug, Parser)] #[command(name = "json2env", version, about)] struct Args { /// Input file, defaults to STDIN if not specified #[arg(short, long, value_name = "FILE")] input: Option, /// Output file, defaults to STDOUT if not specified #[arg(short, long, value_name = "FILE")] output: Option, /// Separator for nested keys, defaults to double underscore (__) #[arg(short, long, value_name = "STRING")] separator: Option, } struct JsonParser; impl JsonParser { fn parse(lines: &mut Vec, key: &str, value: Value, separator: &str) { match value { Value::Array(array) => { for (index, item) in array.into_iter().enumerate() { let key = Self::build_key(key, index.to_string(), separator); Self::parse(lines, key.as_str(), item, separator) } } Value::Object(object) => { for (name, value) in object { let key = Self::build_key(key, name, separator); Self::parse(lines, key.as_str(), value, separator) } } _ => lines.push(EnvVar::new(key.trim().to_owned(), value)), } } fn build_key(prefix: &str, key: String, separator: &str) -> String { match prefix.is_empty() { true => key, false => format!("{prefix}{separator}{key}"), } } } #[derive(Debug, PartialEq, Eq)] struct EnvVar { name: String, value: Value, } impl EnvVar { fn new(name: String, value: Value) -> Self { Self { name, value } } } impl Display for EnvVar { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let key = self.name.clone(); match self.value.clone() { Value::Null => write!(f, "{key}=null"), Value::Bool(bool) => write!(f, "{key}={bool}",), Value::Number(number) => write!(f, "{key}={number}"), Value::String(string) => write!(f, r#"{key}="{}""#, string.replace('"', r#"\""#)), _ => write!(f, ""), } } } #[cfg(test)] mod tests { use serde_json::json; use crate::{EnvVar, JsonParser}; const KEY: &str = r#""key""#; #[test] fn build_key_should_leave_key_unchanged_when_prefix_is_empty() { // ARRANGE let separator = ""; let input = KEY.to_owned(); let expected = KEY; // ACT let result = JsonParser::build_key("", input, separator); // ASSERT assert_eq!(result, expected); } #[test] fn build_key_should_leave_prepend_prefix_with_separator() { // ARRANGE let separator = "_"; let input = KEY.to_owned(); let expected = format!("prefix{separator}{KEY}"); // ACT let actual = JsonParser::build_key("prefix", input, separator); // ASSERT assert_eq!(actual, expected); } #[test] fn bool_env_var_should_be_formatted_correctly() { // ARRANGE let input = EnvVar::new(KEY.to_owned(), json!(true)); // ACT let result = input.to_string(); // ASSERT assert_eq!(result, r#""key"=true"#) } #[test] fn numeric_env_var_should_be_formatted_correctly() { // ARRANGE let input = EnvVar::new(KEY.to_owned(), json!(1.0)); // ACT let result = input.to_string(); // ASSERT assert_eq!(result, r#""key"=1.0"#) } #[test] fn string_env_var_should_be_formatted_correctly() { // ARRANGE let input = EnvVar::new(KEY.to_owned(), json!("hello")); // ACT let result = input.to_string(); // ASSERT assert_eq!(result, r#""key"="hello""#) } #[test] fn array_env_var_should_be_formatted_correctly() { // ARRANGE let input = EnvVar::new(KEY.to_owned(), json!([1, 2])); // ACT let result = input.to_string(); // ASSERT assert_eq!(result, "") } #[test] fn object_env_var_should_be_formatted_correctly() { // ARRANGE let input = EnvVar::new(KEY.to_owned(), json!({ "key": "value" })); // ACT let result = input.to_string(); // ASSERT assert_eq!(result, "") } }