Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 60 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ name = "jsonkdl"
path = "src/main.rs"

[dependencies]
kdl = "6.3.4"
kdl = { version = "6.3.4", features = ["v1"] }
serde_json = "1.0.140"
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ Converts JSON to KDL.
## Usage

```
JSON to KDL Converter
Usage:
jsonkdl <input> <output> [options]
Usage: jsonkdl <input> <output> [options]
Converts JSON to KDL.
By default, KDL spec v2 is used. This can be changed by specifying a flag.

Arguments:
<input> Input JSON file
<output> Output KDL file
<input> Path to input json file
<output> Path to output KDL file

Options:
-f, --force Overwrite existing files
-v, --verbose Verbose output
-1, --kdl-v1 Convert to KDL v1
-2, --kdl-v2 Convert to KDL v2
-f, --force Overwrite output if it exists
-v, --verbose Print extra information during conversion
-h, --help Show this help message
```
76 changes: 54 additions & 22 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ use std::env;
use std::path::Path;

const HELP_TEXT: &str = "\
jsonkdl - convert json to kdl
Usage:
jsonkdl <input> <output> [options]
Usage: jsonkdl <input> <output> [options]
Converts JSON to KDL.
By default, KDL spec v2 is used. This can be changed by specifying a flag.

Arguments:
<input> Path to input json file
<output> Path to output kdl file
<output> Path to output KDL file

Aptions:
Options:
-1, --kdl-v1 Convert to KDL v1
-2, --kdl-v2 Convert to KDL v2
-f, --force Overwrite output if it exists
-v, --verbose Print extra information during conversion
-h, --help Show this help message
Expand All @@ -23,6 +25,7 @@ pub enum CliError {
MissingInput,
MissingOutput,
HelpRequested,
MultipleKdlVersion,
InvalidInputPath(String),
FileExists(String),
InputNotFound(String),
Expand All @@ -37,6 +40,7 @@ pub struct Args {
pub output: String,
pub force: bool,
pub verbose: bool,
pub kdl_version: i32,
}

impl std::fmt::Display for CliError {
Expand All @@ -45,6 +49,7 @@ impl std::fmt::Display for CliError {
CliError::MissingInput => writeln!(f, "missing input path"),
CliError::MissingOutput => writeln!(f, "missing output path"),
CliError::HelpRequested => writeln!(f, "help requested"),
CliError::MultipleKdlVersion => writeln!(f, "specify only one of --kdl-v1 or --kdl-v2"),
CliError::InvalidInputPath(path) => writeln!(f, "not a file: {}", path),
CliError::FileExists(path) => writeln!(f, "file exists: {} (use --force to overwrite)", path),
CliError::InputNotFound(path) => writeln!(f, "no such file: {}", path),
Expand All @@ -71,33 +76,60 @@ impl From<ConversionError> for CliError {
impl Args {
fn parse() -> Result<Self> {
let args: Vec<String> = env::args().collect();
let mut positional: Vec<String> = vec![];
let mut force = false;
let mut verbose = false;
let mut kdl_version = 0;

// If no args besides the program name, show help
if args.len() == 1
|| args.iter().any(|arg| arg == "--help" || arg == "-h")
{
if args.len() == 1 {
return Err(CliError::HelpRequested);
}

let force = args.iter().any(|arg| arg == "--force" || arg == "-f");
let verbose = args.iter().any(|arg| arg == "--verbose" || arg == "-v");
for arg in args.iter().skip(1) {
if !arg.starts_with('-') {
positional.push(arg.into());
} else if arg == "-f" || arg == "--force" {
force = true;
} else if arg == "-v" || arg == "--verbose" {
verbose = true;
} else if arg == "-1" || arg == "--kdl-v1" {
if kdl_version != 0 {
return Err(CliError::MultipleKdlVersion);
}

kdl_version = 1;
} else if arg == "-2" || arg == "--kdl-v2" {
if kdl_version != 0 {
return Err(CliError::MultipleKdlVersion);
}

kdl_version = 2;
}
}

if kdl_version == 0 {
kdl_version = 2;
};

let positional: Vec<String> = args
.iter()
.skip(1)
.filter(|arg| !arg.starts_with('-'))
.cloned()
.collect();
let input = positional
.get(0)
.ok_or(CliError::MissingInput)?
.to_string();

let input = positional.get(0).ok_or(CliError::MissingInput)?.to_string();
let output = positional.get(1).ok_or(CliError::MissingOutput)?.to_string();
let output = positional
.get(1)
.ok_or(CliError::MissingOutput)?
.to_string();

Ok(Self {
let result = Self {
input,
output,
force,
verbose,
})
kdl_version,
};

Ok(result)
}
}

Expand Down Expand Up @@ -130,6 +162,6 @@ pub fn run() -> Result<()> {
return Err(CliError::FileExists(args.output));
}

convert_file_contents(input_path, output_path, args.verbose)?;
convert_file_contents(input_path, output_path, args.verbose, args.kdl_version)?;
Ok(())
}
31 changes: 21 additions & 10 deletions src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ impl From<serde_json::Error> for ConversionError {

pub type Result<T> = std::result::Result<T, ConversionError>;

pub fn convert_file_contents(input: &Path, output: &Path, verbose: bool) -> Result<()> {
pub fn convert_file_contents(input: &Path, output: &Path, verbose: bool, kdl_version: i32) -> Result<()> {
let json_content = fs::read_to_string(input)?;
let json_value: Value = serde_json::from_str(&json_content)?;
let kdl_doc = json_to_kdl(json_value)?;
let kdl_doc = json_to_kdl(json_value, Some(kdl_version))?;

// Create output directory if needed
if let Some(parent) = output.parent() {
Expand All @@ -56,15 +56,15 @@ pub fn convert_file_contents(input: &Path, output: &Path, verbose: bool) -> Resu
fs::write(output, kdl_doc.to_string())?;

if verbose {
println!("Converted {} -> {}", input.display(), output.display());
println!("converted {} -> {}", input.display(), output.display());
}

Ok(())
}

pub fn json_to_kdl(json: Value) -> Result<KdlDocument> {
pub fn json_to_kdl(json: Value, kdl_version: Option<i32>) -> Result<KdlDocument> {
let array = json.as_array().ok_or_else(|| {
ConversionError::InvalidStructure("Document root must be a JSON array".to_string())
ConversionError::InvalidStructure("document root must be json array".to_string())
})?;

let mut document = KdlDocument::new();
Expand All @@ -74,21 +74,32 @@ pub fn json_to_kdl(json: Value) -> Result<KdlDocument> {
document.nodes_mut().push(node);
}

// `kdl_version` will be provided for all root calls
if let Some(kdl_ver_num) = kdl_version {
if kdl_ver_num == 1 {
document.ensure_v1();
} else {
document.ensure_v2();
}
}

document.autoformat();

Ok(document)
}

fn json_value_to_node(value: &Value) -> Result<KdlNode> {
let name = value
.get("name")
.and_then(|n| n.as_str())
.ok_or_else(|| ConversionError::InvalidStructure("`name` must exist and be a string".to_string()))?;
.ok_or_else(|| ConversionError::InvalidStructure("name must be non-empty string".to_string()))?;

let mut node = KdlNode::new(name);

// Handle arguments
if let Some(arguments) = value.get("arguments") {
let args = arguments.as_array().ok_or_else(|| {
ConversionError::InvalidStructure("`arguments` must be an array".to_string())
ConversionError::InvalidStructure("arguments must be an array".to_string())
})?;

for arg in args {
Expand All @@ -100,7 +111,7 @@ fn json_value_to_node(value: &Value) -> Result<KdlNode> {
// Handle properties
if let Some(properties) = value.get("properties") {
let props = properties.as_object().ok_or_else(|| {
ConversionError::InvalidStructure("`properties` must be an object".to_string())
ConversionError::InvalidStructure("properties must be an object".to_string())
})?;

for (key, prop_value) in props {
Expand All @@ -111,7 +122,7 @@ fn json_value_to_node(value: &Value) -> Result<KdlNode> {

// Handle children
if let Some(children) = value.get("children") {
let child_doc = json_to_kdl(children.clone())?;
let child_doc = json_to_kdl(children.clone(), None)?;
node.set_children(child_doc);
}

Expand Down Expand Up @@ -151,7 +162,7 @@ fn json_value_to_entry(value: &Value) -> Result<KdlEntry> {
Value::String(s) => KdlValue::String(s.clone()),
_ => {
return Err(ConversionError::InvalidStructure(
"unsupported JSON value type for KDL conversion".to_string(),
"unsupported json value type for kdl conversion".to_string(),
))
}
};
Expand Down