Files
nushell/crates/nu-command/src/formats/to/toml.rs
Ian Manske 8da27a1a09 Create Record type (#10103)
# Description
This PR creates a new `Record` type to reduce duplicate code and
possibly bugs as well. (This is an edited version of #9648.)
- `Record` implements `FromIterator` and `IntoIterator` and so can be
iterated over or collected into. For example, this helps with
conversions to and from (hash)maps. (Also, no more
`cols.iter().zip(vals)`!)
- `Record` has a `push(col, val)` function to help insure that the
number of columns is equal to the number of values. I caught a few
potential bugs thanks to this (e.g. in the `ls` command).
- Finally, this PR also adds a `record!` macro that helps simplify
record creation. It is used like so:
   ```rust
   record! {
       "key1" => some_value,
       "key2" => Value::string("text", span),
       "key3" => Value::int(optional_int.unwrap_or(0), span),
       "key4" => Value::bool(config.setting, span),
   }
   ```
Since macros hinder formatting, etc., the right hand side values should
be relatively short and sweet like the examples above.

Where possible, prefer `record!` or `.collect()` on an iterator instead
of multiple `Record::push`s, since the first two automatically set the
record capacity and do less work overall.

# User-Facing Changes
Besides the changes in `nu-protocol` the only other breaking changes are
to `nu-table::{ExpandedTable::build_map, JustTable::kv_table}`.
2023-08-25 07:50:29 +12:00

234 lines
7.4 KiB
Rust

use nu_protocol::ast::{Call, PathMember};
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Type, Value,
};
#[derive(Clone)]
pub struct ToToml;
impl Command for ToToml {
fn name(&self) -> &str {
"to toml"
}
fn signature(&self) -> Signature {
Signature::build("to toml")
.input_output_types(vec![(Type::Record(vec![]), Type::String)])
.category(Category::Formats)
}
fn usage(&self) -> &str {
"Convert record into .toml text."
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Outputs an TOML string representing the contents of this record",
example: r#"{foo: 1 bar: 'qwe'} | to toml"#,
result: Some(Value::test_string("bar = \"qwe\"\nfoo = 1\n")),
}]
}
fn run(
&self,
engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
to_toml(engine_state, input, head)
}
}
// Helper method to recursively convert nu_protocol::Value -> toml::Value
// This shouldn't be called at the top-level
fn helper(engine_state: &EngineState, v: &Value) -> Result<toml::Value, ShellError> {
Ok(match &v {
Value::Bool { val, .. } => toml::Value::Boolean(*val),
Value::Int { val, .. } => toml::Value::Integer(*val),
Value::Filesize { val, .. } => toml::Value::Integer(*val),
Value::Duration { val, .. } => toml::Value::String(val.to_string()),
Value::Date { val, .. } => toml::Value::String(val.to_string()),
Value::Range { .. } => toml::Value::String("<Range>".to_string()),
Value::Float { val, .. } => toml::Value::Float(*val),
Value::String { val, .. } => toml::Value::String(val.clone()),
Value::Record { val, .. } => {
let mut m = toml::map::Map::new();
for (k, v) in val {
m.insert(k.clone(), helper(engine_state, v)?);
}
toml::Value::Table(m)
}
Value::LazyRecord { val, .. } => {
let collected = val.collect()?;
helper(engine_state, &collected)?
}
Value::List { vals, .. } => toml::Value::Array(toml_list(engine_state, vals)?),
Value::Block { span, .. } => {
let code = engine_state.get_span_contents(*span);
let code = String::from_utf8_lossy(code).to_string();
toml::Value::String(code)
}
Value::Closure { span, .. } => {
let code = engine_state.get_span_contents(*span);
let code = String::from_utf8_lossy(code).to_string();
toml::Value::String(code)
}
Value::Nothing { .. } => toml::Value::String("<Nothing>".to_string()),
Value::Error { error } => return Err(*error.clone()),
Value::Binary { val, .. } => toml::Value::Array(
val.iter()
.map(|x| toml::Value::Integer(*x as i64))
.collect(),
),
Value::CellPath { val, .. } => toml::Value::Array(
val.members
.iter()
.map(|x| match &x {
PathMember::String { val, .. } => Ok(toml::Value::String(val.clone())),
PathMember::Int { val, .. } => Ok(toml::Value::Integer(*val as i64)),
})
.collect::<Result<Vec<toml::Value>, ShellError>>()?,
),
Value::CustomValue { .. } => toml::Value::String("<Custom Value>".to_string()),
Value::MatchPattern { .. } => toml::Value::String("<Match Pattern>".to_string()),
})
}
fn toml_list(engine_state: &EngineState, input: &[Value]) -> Result<Vec<toml::Value>, ShellError> {
let mut out = vec![];
for value in input {
out.push(helper(engine_state, value)?);
}
Ok(out)
}
fn toml_into_pipeline_data(
toml_value: &toml::Value,
value_type: Type,
span: Span,
) -> Result<PipelineData, ShellError> {
match toml::to_string(&toml_value) {
Ok(serde_toml_string) => Ok(Value::String {
val: serde_toml_string,
span,
}
.into_pipeline_data()),
_ => Ok(Value::Error {
error: Box::new(ShellError::CantConvert {
to_type: "TOML".into(),
from_type: value_type.to_string(),
span,
help: None,
}),
}
.into_pipeline_data()),
}
}
fn value_to_toml_value(
engine_state: &EngineState,
v: &Value,
head: Span,
) -> Result<toml::Value, ShellError> {
match v {
Value::Record { .. } => helper(engine_state, v),
// Propagate existing errors
Value::Error { error } => Err(*error.clone()),
_ => Err(ShellError::UnsupportedInput(
format!("{:?} is not valid top-level TOML", v.get_type()),
"value originates from here".into(),
head,
v.expect_span(),
)),
}
}
fn to_toml(
engine_state: &EngineState,
input: PipelineData,
span: Span,
) -> Result<PipelineData, ShellError> {
let value = input.into_value(span);
let toml_value = value_to_toml_value(engine_state, &value, span)?;
match toml_value {
toml::Value::Array(ref vec) => match vec[..] {
[toml::Value::Table(_)] => toml_into_pipeline_data(
vec.iter().next().expect("this should never trigger"),
value.get_type(),
span,
),
_ => toml_into_pipeline_data(&toml_value, value.get_type(), span),
},
_ => toml_into_pipeline_data(&toml_value, value.get_type(), span),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(ToToml {})
}
#[test]
fn test_value_to_toml_value() {
//
// Positive Tests
//
let engine_state = EngineState::new();
let mut m = indexmap::IndexMap::new();
m.insert("rust".to_owned(), Value::test_string("editor"));
m.insert("is".to_owned(), Value::nothing(Span::test_data()));
m.insert(
"features".to_owned(),
Value::List {
vals: vec![Value::test_string("hello"), Value::test_string("array")],
span: Span::test_data(),
},
);
let tv = value_to_toml_value(
&engine_state,
&Value::record(m.into_iter().collect(), Span::test_data()),
Span::test_data(),
)
.expect("Expected Ok from valid TOML dictionary");
assert_eq!(
tv.get("features"),
Some(&toml::Value::Array(vec![
toml::Value::String("hello".to_owned()),
toml::Value::String("array".to_owned())
]))
);
//
// Negative Tests
//
value_to_toml_value(
&engine_state,
&Value::test_string("not_valid"),
Span::test_data(),
)
.expect_err("Expected non-valid toml (String) to cause error!");
value_to_toml_value(
&engine_state,
&Value::List {
vals: vec![Value::test_string("1")],
span: Span::test_data(),
},
Span::test_data(),
)
.expect_err("Expected non-valid toml (Table) to cause error!");
}
}