mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 23:07:46 +02:00
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}`.
This commit is contained in:
@ -5,7 +5,7 @@ use base64::{alphabet, Engine};
|
||||
use nu_protocol::ast::Call;
|
||||
use nu_protocol::engine::{EngineState, Stack};
|
||||
use nu_protocol::{
|
||||
BufferedReader, IntoPipelineData, PipelineData, RawStream, ShellError, Span, Value,
|
||||
record, BufferedReader, IntoPipelineData, PipelineData, RawStream, ShellError, Span, Value,
|
||||
};
|
||||
use ureq::{Error, ErrorKind, Request, Response};
|
||||
|
||||
@ -183,12 +183,12 @@ pub fn send_request(
|
||||
let data = value_to_json_value(&body)?;
|
||||
send_cancellable_request(&request_url, Box::new(|| request.send_json(data)), ctrl_c)
|
||||
}
|
||||
Value::Record { cols, vals, .. } if body_type == BodyType::Form => {
|
||||
let mut data: Vec<(String, String)> = Vec::with_capacity(cols.len());
|
||||
Value::Record { val, .. } if body_type == BodyType::Form => {
|
||||
let mut data: Vec<(String, String)> = Vec::with_capacity(val.len());
|
||||
|
||||
for (col, val) in cols.iter().zip(vals.iter()) {
|
||||
for (col, val) in val {
|
||||
let val_string = val.as_string()?;
|
||||
data.push((col.clone(), val_string))
|
||||
data.push((col, val_string))
|
||||
}
|
||||
|
||||
let request_fn = move || {
|
||||
@ -296,8 +296,8 @@ pub fn request_add_custom_headers(
|
||||
let mut custom_headers: HashMap<String, Value> = HashMap::new();
|
||||
|
||||
match &headers {
|
||||
Value::Record { cols, vals, .. } => {
|
||||
for (k, v) in cols.iter().zip(vals.iter()) {
|
||||
Value::Record { val, .. } => {
|
||||
for (k, v) in val {
|
||||
custom_headers.insert(k.to_string(), v.clone());
|
||||
}
|
||||
}
|
||||
@ -306,8 +306,8 @@ pub fn request_add_custom_headers(
|
||||
if table.len() == 1 {
|
||||
// single row([key1 key2]; [val1 val2])
|
||||
match &table[0] {
|
||||
Value::Record { cols, vals, .. } => {
|
||||
for (k, v) in cols.iter().zip(vals.iter()) {
|
||||
Value::Record { val, .. } => {
|
||||
for (k, v) in val {
|
||||
custom_headers.insert(k.to_string(), v.clone());
|
||||
}
|
||||
}
|
||||
@ -498,25 +498,19 @@ fn request_handle_response_content(
|
||||
Err(_) => Value::nothing(span),
|
||||
};
|
||||
|
||||
let headers = Value::Record {
|
||||
cols: vec!["request".to_string(), "response".to_string()],
|
||||
vals: vec![request_headers_value, response_headers_value],
|
||||
span,
|
||||
let headers = record! {
|
||||
"request" => request_headers_value,
|
||||
"response" => response_headers_value,
|
||||
};
|
||||
|
||||
let full_response = Value::Record {
|
||||
cols: vec![
|
||||
"headers".to_string(),
|
||||
"body".to_string(),
|
||||
"status".to_string(),
|
||||
],
|
||||
vals: vec![
|
||||
headers,
|
||||
consume_response_body(resp)?.into_value(span),
|
||||
Value::int(response_status as i64, span),
|
||||
],
|
||||
let full_response = Value::record(
|
||||
record! {
|
||||
"headers" => Value::record(headers, span),
|
||||
"body" => consume_response_body(resp)?.into_value(span),
|
||||
"status" => Value::int(response_status as i64, span),
|
||||
},
|
||||
span,
|
||||
};
|
||||
);
|
||||
|
||||
Ok(full_response.into_pipeline_data())
|
||||
} else {
|
||||
@ -597,15 +591,14 @@ fn extract_response_headers(response: &Response) -> Headers {
|
||||
}
|
||||
|
||||
fn headers_to_nu(headers: &Headers, span: Span) -> Result<PipelineData, ShellError> {
|
||||
let cols = vec!["name".to_string(), "value".to_string()];
|
||||
let mut vals = Vec::with_capacity(headers.len());
|
||||
|
||||
for (name, values) in headers {
|
||||
let is_duplicate = vals.iter().any(|val| {
|
||||
if let Value::Record { vals, .. } = val {
|
||||
if let Value::Record { val, .. } = val {
|
||||
if let Some(Value::String {
|
||||
val: header_name, ..
|
||||
}) = vals.get(0)
|
||||
}) = val.vals.get(0)
|
||||
{
|
||||
return name == header_name;
|
||||
}
|
||||
@ -616,11 +609,11 @@ fn headers_to_nu(headers: &Headers, span: Span) -> Result<PipelineData, ShellErr
|
||||
// A single header can hold multiple values
|
||||
// This interface is why we needed to check if we've already parsed this header name.
|
||||
for str_value in values {
|
||||
let header = vec![
|
||||
Value::string(name, span),
|
||||
Value::string(str_value.to_string(), span),
|
||||
];
|
||||
vals.push(Value::record(cols.clone(), header, span));
|
||||
let record = record! {
|
||||
"name" => Value::string(name, span),
|
||||
"value" => Value::string(str_value, span),
|
||||
};
|
||||
vals.push(Value::record(record, span));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,13 +65,9 @@ fn to_url(input: PipelineData, head: Span) -> Result<PipelineData, ShellError> {
|
||||
let output: Result<String, ShellError> = input
|
||||
.into_iter()
|
||||
.map(move |value| match value {
|
||||
Value::Record {
|
||||
ref cols,
|
||||
ref vals,
|
||||
span,
|
||||
} => {
|
||||
Value::Record { ref val, span } => {
|
||||
let mut row_vec = vec![];
|
||||
for (k, v) in cols.iter().zip(vals.iter()) {
|
||||
for (k, v) in val {
|
||||
match v.as_string() {
|
||||
Ok(s) => {
|
||||
row_vec.push((k.clone(), s.to_string()));
|
||||
|
@ -92,16 +92,11 @@ impl Command for SubCommand {
|
||||
let output: Result<String, ShellError> = input
|
||||
.into_iter()
|
||||
.map(move |value| match value {
|
||||
Value::Record {
|
||||
ref cols,
|
||||
ref vals,
|
||||
span,
|
||||
} => {
|
||||
let url_components = cols
|
||||
.iter()
|
||||
.zip(vals.iter())
|
||||
Value::Record { val, span } => {
|
||||
let url_components = val
|
||||
.into_iter()
|
||||
.try_fold(UrlComponents::new(), |url, (k, v)| {
|
||||
url.add_component(k.clone(), v.clone(), span)
|
||||
url.add_component(k, v, span)
|
||||
});
|
||||
|
||||
url_components?.to_url(span)
|
||||
@ -176,14 +171,9 @@ impl UrlComponents {
|
||||
|
||||
if key == "params" {
|
||||
return match value {
|
||||
Value::Record {
|
||||
ref cols,
|
||||
ref vals,
|
||||
span,
|
||||
} => {
|
||||
let mut qs = cols
|
||||
Value::Record { ref val, span } => {
|
||||
let mut qs = val
|
||||
.iter()
|
||||
.zip(vals.iter())
|
||||
.map(|(k, v)| match v.as_string() {
|
||||
Ok(val) => Ok(format!("{k}={val}")),
|
||||
Err(err) => Err(err),
|
||||
|
@ -2,7 +2,8 @@ use super::url;
|
||||
use nu_protocol::ast::Call;
|
||||
use nu_protocol::engine::{Command, EngineState, Stack};
|
||||
use nu_protocol::{
|
||||
Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value,
|
||||
record, Category, Example, PipelineData, Record, ShellError, Signature, Span, SyntaxShape,
|
||||
Type, Value,
|
||||
};
|
||||
|
||||
use url::Url;
|
||||
@ -55,7 +56,7 @@ impl Command for SubCommand {
|
||||
vec![Example {
|
||||
description: "Parses a url",
|
||||
example: "'http://user123:pass567@www.example.com:8081/foo/bar?param1=section&p2=&f[name]=vldc#hello' | url parse",
|
||||
result: Some(Value::Record {
|
||||
result: Some(Value::test_record(Record {
|
||||
cols: vec![
|
||||
"scheme".to_string(),
|
||||
"username".to_string(),
|
||||
@ -76,18 +77,16 @@ impl Command for SubCommand {
|
||||
Value::test_string("/foo/bar"),
|
||||
Value::test_string("param1=section&p2=&f[name]=vldc"),
|
||||
Value::test_string("hello"),
|
||||
Value::Record {
|
||||
Value::test_record(Record {
|
||||
cols: vec!["param1".to_string(), "p2".to_string(), "f[name]".to_string()],
|
||||
vals: vec![
|
||||
Value::test_string("section"),
|
||||
Value::test_string(""),
|
||||
Value::test_string("vldc"),
|
||||
],
|
||||
span: Span::test_data(),
|
||||
},
|
||||
}),
|
||||
],
|
||||
span: Span::test_data(),
|
||||
}),
|
||||
})),
|
||||
}]
|
||||
}
|
||||
}
|
||||
@ -106,80 +105,31 @@ fn parse(value: Value, head: Span, engine_state: &EngineState) -> Result<Pipelin
|
||||
|
||||
match result_url {
|
||||
Ok(url) => {
|
||||
let cols = vec![
|
||||
String::from("scheme"),
|
||||
String::from("username"),
|
||||
String::from("password"),
|
||||
String::from("host"),
|
||||
String::from("port"),
|
||||
String::from("path"),
|
||||
String::from("query"),
|
||||
String::from("fragment"),
|
||||
String::from("params"),
|
||||
];
|
||||
let mut vals: Vec<Value> = vec![
|
||||
Value::String {
|
||||
val: String::from(url.scheme()),
|
||||
span: head,
|
||||
},
|
||||
Value::String {
|
||||
val: String::from(url.username()),
|
||||
span: head,
|
||||
},
|
||||
Value::String {
|
||||
val: String::from(url.password().unwrap_or("")),
|
||||
span: head,
|
||||
},
|
||||
Value::String {
|
||||
val: String::from(url.host_str().unwrap_or("")),
|
||||
span: head,
|
||||
},
|
||||
Value::String {
|
||||
val: url
|
||||
.port()
|
||||
.map(|p| p.to_string())
|
||||
.unwrap_or_else(|| "".into()),
|
||||
span: head,
|
||||
},
|
||||
Value::String {
|
||||
val: String::from(url.path()),
|
||||
span: head,
|
||||
},
|
||||
Value::String {
|
||||
val: String::from(url.query().unwrap_or("")),
|
||||
span: head,
|
||||
},
|
||||
Value::String {
|
||||
val: String::from(url.fragment().unwrap_or("")),
|
||||
span: head,
|
||||
},
|
||||
];
|
||||
|
||||
let params =
|
||||
serde_urlencoded::from_str::<Vec<(String, String)>>(url.query().unwrap_or(""));
|
||||
match params {
|
||||
Ok(result) => {
|
||||
let (param_cols, param_vals) = result
|
||||
let params = result
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, Value::String { val: v, span: head }))
|
||||
.unzip();
|
||||
.map(|(k, v)| (k, Value::string(v, head)))
|
||||
.collect();
|
||||
|
||||
vals.push(Value::Record {
|
||||
cols: param_cols,
|
||||
vals: param_vals,
|
||||
span: head,
|
||||
});
|
||||
let port = url.port().map(|p| p.to_string()).unwrap_or_default();
|
||||
|
||||
Ok(PipelineData::Value(
|
||||
Value::Record {
|
||||
cols,
|
||||
vals,
|
||||
span: head,
|
||||
},
|
||||
None,
|
||||
))
|
||||
let record = record! {
|
||||
"scheme" => Value::string(url.scheme(), head),
|
||||
"username" => Value::string(url.username(), head),
|
||||
"password" => Value::string(url.password().unwrap_or(""), head),
|
||||
"host" => Value::string(url.host_str().unwrap_or(""), head),
|
||||
"port" => Value::string(port, head),
|
||||
"path" => Value::string(url.path(), head),
|
||||
"query" => Value::string(url.query().unwrap_or(""), head),
|
||||
"fragment" => Value::string(url.fragment().unwrap_or(""), head),
|
||||
"params" => Value::record(params, head),
|
||||
};
|
||||
|
||||
Ok(PipelineData::Value(Value::record(record, head), None))
|
||||
}
|
||||
|
||||
_ => Err(ShellError::UnsupportedInput(
|
||||
"String not compatible with url-encoding".to_string(),
|
||||
"value originates from here".into(),
|
||||
|
Reference in New Issue
Block a user