Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for yaml input/output #24

Merged
merged 4 commits into from
Mar 17, 2023
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
4 changes: 3 additions & 1 deletion config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ edition = "2021"
atty = { version = "0.2" }
clap = { version = "3.2", features = ["derive"] }
crossterm = { version = "0.24.0" }
dsc_lib = { path = "../dsc_lib" }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
serde_yaml = { version = "0.9" }
syntect = { version = "5.0", features = ["default-fancy"], default-features = false }
thiserror = "1.0"
dsc_lib = { path = "../dsc_lib" }
24 changes: 23 additions & 1 deletion config/src/args.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
use clap::{Parser, Subcommand};
use std::str::FromStr;

#[derive(Debug, PartialEq, Eq)]
pub enum OutputFormat {
Json,
PrettyJson,
Yaml,
}

impl FromStr for OutputFormat {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"json" => Ok(OutputFormat::Json),
"prettyjson" => Ok(OutputFormat::PrettyJson),
"yaml" => Ok(OutputFormat::Yaml),
_ => Err(format!("Invalid output format: {}", s)),
}
}
}

/// Struct containing the parsed command line arguments
#[derive(Debug, Parser)]
#[clap(name = "config", version = "0.1.0", about = "Discover and invoke DSC resources", long_about = None)]
pub struct Args {
Expand All @@ -10,6 +30,8 @@ pub struct Args {
/// Whether to use the cache or not
#[clap(short = 'n', long)]
pub no_cache: bool,
#[clap(short = 'f', long)]
pub format: Option<OutputFormat>,
}

#[derive(Debug, PartialEq, Eq, Subcommand)]
Expand Down
117 changes: 102 additions & 15 deletions config/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use args::*;
use atty::Stream;
use clap::Parser;
use dsc_lib::dscresources::dscresource::Invoke;
use dsc_lib::{DscManager, dscresources::dscresource::{DscResource, Invoke}};
use std::io::{self, Read};
use std::process::exit;
use dsc_lib::{DscManager, dscresources::dscresource::DscResource};
use syntect::easy::HighlightLines;
use syntect::parsing::SyntaxSet;
use syntect::highlighting::{ThemeSet, Style};
use syntect::util::{as_24_bit_terminal_escaped, LinesWithEndings};

#[cfg(debug_assertions)]
use crossterm::event;
Expand Down Expand Up @@ -65,13 +68,17 @@ fn main() {
exit(EXIT_JSON_ERROR);
}
};
println!("{}", json);
write_output(&json, &args.format);
// insert newline separating instances if writing to console
if atty::is(Stream::Stdout) {
println!("");
}
}
}
SubCommand::Get { resource, input } => {
// TODO: support streaming stdin which includes resource and input

let input = check_stdin_and_input(&input, &stdin);
let input = get_input(&input, &stdin);
let resource = get_resource(&mut dsc, resource.as_str());
match resource.get(input.as_str()) {
Ok(result) => {
Expand All @@ -83,7 +90,7 @@ fn main() {
exit(EXIT_JSON_ERROR);
}
};
println!("{}", json);
write_output(&json, &args.format);
}
Err(err) => {
eprintln!("Error: {}", err);
Expand All @@ -92,7 +99,7 @@ fn main() {
}
}
SubCommand::Set { resource, input: _ } => {
let input = check_stdin_and_input(&None, &stdin);
let input = get_input(&None, &stdin);
let resource = get_resource(&mut dsc, resource.as_str());
match resource.set(input.as_str()) {
Ok(result) => {
Expand All @@ -104,7 +111,7 @@ fn main() {
exit(EXIT_JSON_ERROR);
}
};
println!("{}", json);
write_output(&json, &args.format);
}
Err(err) => {
eprintln!("Error: {}", err);
Expand All @@ -113,7 +120,7 @@ fn main() {
}
}
SubCommand::Test { resource, input: _ } => {
let input = check_stdin_and_input(&None, &stdin);
let input = get_input(&None, &stdin);
let resource = get_resource(&mut dsc, resource.as_str());
match resource.test(input.as_str()) {
Ok(result) => {
Expand All @@ -125,7 +132,7 @@ fn main() {
exit(EXIT_JSON_ERROR);
}
};
println!("{}", json);
write_output(&json, &args.format);
}
Err(err) => {
eprintln!("Error: {}", err);
Expand All @@ -138,12 +145,74 @@ fn main() {
exit(EXIT_SUCCESS);
}

fn write_output(json: &str, format: &Option<OutputFormat>) {
let mut is_json = true;
match atty::is(Stream::Stdout) {
true => {
let output = match format {
Some(OutputFormat::Json) => json.to_string(),
Some(OutputFormat::PrettyJson) => {
let value: serde_json::Value = match serde_json::from_str(json) {
Ok(value) => value,
Err(err) => {
eprintln!("JSON Error: {}", err);
exit(EXIT_JSON_ERROR);
}
};
match serde_json::to_string_pretty(&value) {
Ok(json) => json,
Err(err) => {
eprintln!("JSON Error: {}", err);
exit(EXIT_JSON_ERROR);
}
}
},
Some(OutputFormat::Yaml) | None => {
is_json = false;
let value: serde_json::Value = match serde_json::from_str(json) {
Ok(value) => value,
Err(err) => {
eprintln!("JSON Error: {}", err);
exit(EXIT_JSON_ERROR);
}
};
match serde_yaml::to_string(&value) {
Ok(yaml) => yaml,
Err(err) => {
eprintln!("YAML Error: {}", err);
exit(EXIT_JSON_ERROR);
}
}
}
};

let ps = SyntaxSet::load_defaults_newlines();
let ts = ThemeSet::load_defaults();
let syntax = match is_json {
true => ps.find_syntax_by_extension("json").unwrap(),
false => ps.find_syntax_by_extension("yaml").unwrap(),
};

let mut h = HighlightLines::new(syntax, &ts.themes["base16-ocean.dark"]);

for line in LinesWithEndings::from(&output) {
let ranges: Vec<(Style, &str)> = h.highlight_line(line, &ps).unwrap();
let escaped = as_24_bit_terminal_escaped(&ranges[..], true);
print!("{}", escaped);
}
},
false => {
println!("{}", json);
}
};
}

fn get_resource(dsc: &mut DscManager, resource: &str) -> DscResource {
// check if resource is JSON or just a name
match serde_json::from_str(resource) {
Ok(resource) => resource,
Err(err) => {
if resource.contains("{") {
if resource.contains('{') {
eprintln!("Not valid resource JSON: {}\nInput was: {}", err, resource);
exit(EXIT_INVALID_ARGS);
}
Expand Down Expand Up @@ -171,7 +240,7 @@ fn get_resource(dsc: &mut DscManager, resource: &str) -> DscResource {
}
}

fn check_stdin_and_input(input: &Option<String>, stdin: &Option<String>) -> String {
fn get_input(input: &Option<String>, stdin: &Option<String>) -> String {
let input = match (input, stdin) {
(Some(_input), Some(_stdin)) => {
eprintln!("Error: Cannot specify both --input and stdin");
Expand All @@ -185,11 +254,29 @@ fn check_stdin_and_input(input: &Option<String>, stdin: &Option<String>) -> Stri
},
};

match serde_json::from_str::<serde_json::Value>(input.as_str()) {
match serde_json::from_str::<serde_json::Value>(&input) {
Ok(_) => input,
Err(err) => {
eprintln!("Input JSON Error: {}", err);
exit(EXIT_INVALID_ARGS);
Err(json_err) => {
match serde_yaml::from_str::<serde_yaml::Value>(&input) {
Ok(yaml) => {
match serde_json::to_string(&yaml) {
Ok(json) => json,
Err(err) => {
eprintln!("Error: Cannot convert YAML to JSON: {}", err);
exit(EXIT_INVALID_ARGS);
}
}
},
Err(err) => {
if input.contains('{') {
eprintln!("Error: Input is not valid JSON: {}", json_err);
}
else {
eprintln!("Error: Input is not valid YAML: {}", err);
}
exit(EXIT_INVALID_ARGS);
}
}
}
}
}
Expand Down
22 changes: 22 additions & 0 deletions config/tests/config_args.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Describe 'config argument tests' {
It 'input is <type>' -Skip:(!$IsWindows) -TestCases @(
@{ type = 'yaml'; text = @'
keyPath: HKLM\Software\Microsoft\Windows NT\CurrentVersion
valueName: ProductName
'@ }
@{ type = 'json'; text = @'
{
"keyPath": "HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion",
"valueName": "ProductName"
}
'@ }
) {
param($text)
$output = $text | config get -r registry
$output = $output | ConvertFrom-Json
$output.actual_state.'$id' | Should -BeExactly 'https://developer.microsoft.com/json-schemas/windows/registry/20230303/Microsoft.Windows.Registry.schema.json'
$output.actual_state.keyPath | Should -BeExactly 'HKLM\Software\Microsoft\Windows NT\CurrentVersion'
$output.actual_state.valueName | Should -BeExactly 'ProductName'
$output.actual_state.valueData.String | Should -Match 'Windows .*'
}
}