Add pipeline span to metadata (#16014)

# Description

This PR makes the span of a pipeline accessible through `metadata`,
meaning it's possible to get the span of a pipeline without collecting
it.

Examples:
```nushell
ls | metadata
# => ╭────────┬────────────────────╮
# => │        │ ╭───────┬────────╮ │
# => │ span   │ │ start │ 170218 │ │
# => │        │ │ end   │ 170220 │ │
# => │        │ ╰───────┴────────╯ │
# => │ source │ ls                 │
# => ╰────────┴────────────────────╯
```

```nushell
ls | metadata access {|meta|
  error make {msg: "error", label: {text: "here", span: $meta.span}}
}
# => Error:   × error
# =>    ╭─[entry #7:1:1]
# =>  1 │ ls | metadata access {|meta|
# =>    · ─┬
# =>    ·  ╰── here
# =>  2 │   error make {msg: "error", label: {text: "here", span: $meta.span}}
# =>    ╰────
```

Here's an example that wouldn't be possible before, since you would have
to use `metadata $in` to get the span, collecting the (infinite) stream

```nushell
generate {|x=0| {out: 0, next: 0} } | metadata access {|meta|
  # do whatever with stream
  error make {msg: "error", label: {text: "here", span: $meta.span}}
}
# => Error:   × error
# =>    ╭─[entry #16:1:1]
# =>  1 │ generate {|x=0| {out: 0, next: 0} } | metadata access {|meta|
# =>    · ────┬───
# =>    ·     ╰── here
# =>  2 │   # do whatever with stream
# =>    ╰────
```

I haven't done the tests or anything yet since I'm not sure how we feel
about having this as part of the normal metadata, rather than a new
command like `metadata span` or something. We could also have a
`metadata access` like functionality for that with an optional closure
argument potentially.

# User-Facing Changes

* The span of a pipeline is now available through `metadata` and
`metadata access` without collecting a stream.

# Tests + Formatting

TODO

# After Submitting

N/A
This commit is contained in:
132ikl
2025-06-30 17:17:43 -04:00
committed by GitHub
parent 082e8d0de8
commit f4136aa3f4
22 changed files with 70 additions and 64 deletions

View File

@ -87,10 +87,9 @@ impl Command for Metadata {
.into_pipeline_data(),
)
}
None => Ok(
Value::record(build_metadata_record(input.metadata().as_ref(), head), head)
.into_pipeline_data(),
),
None => {
Ok(Value::record(build_metadata_record(&input, head), head).into_pipeline_data())
}
}
}
@ -116,19 +115,7 @@ fn build_metadata_record_value(
head: Span,
) -> Value {
let mut record = Record::new();
let span = arg.span();
record.push(
"span",
Value::record(
record! {
"start" => Value::int(span.start as i64,span),
"end" => Value::int(span.end as i64, span),
},
head,
),
);
record.push("span", arg.span().into_value(head));
Value::record(extend_record_with_metadata(record, metadata, head), head)
}

View File

@ -42,10 +42,7 @@ impl Command for MetadataAccess {
// `ClosureEvalOnce` is not used as it uses `Stack::captures_to_stack` rather than
// `Stack::captures_to_stack_preserve_out_dest`. This command shouldn't collect streams
let mut callee_stack = caller_stack.captures_to_stack_preserve_out_dest(closure.captures);
let metadata_record = Value::record(
build_metadata_record(input.metadata().as_ref(), call.head),
call.head,
);
let metadata_record = Value::record(build_metadata_record(&input, call.head), call.head);
if let Some(var_id) = block.signature.get_positional(0).and_then(|var| var.var_id) {
callee_stack.add_var(var_id, metadata_record)
@ -58,12 +55,10 @@ impl Command for MetadataAccess {
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Access metadata and data from a stream together",
example: r#"{foo: bar} | to json --raw | metadata access {|meta| {in: $in, meta: $meta}}"#,
example: r#"{foo: bar} | to json --raw | metadata access {|meta| {in: $in, content: $meta.content_type}}"#,
result: Some(Value::test_record(record! {
"in" => Value::test_string(r#"{"foo":"bar"}"#),
"meta" => Value::test_record(record! {
"content_type" => Value::test_string(r#"application/json"#)
})
"content" => Value::test_string(r#"application/json"#)
})),
}]
}

View File

@ -79,15 +79,13 @@ impl Command for MetadataSet {
},
Example {
description: "Set the metadata of a file path",
example: "'crates' | metadata set --datasource-filepath $'(pwd)/crates' | metadata",
example: "'crates' | metadata set --datasource-filepath $'(pwd)/crates'",
result: None,
},
Example {
description: "Set the metadata of a file path",
example: "'crates' | metadata set --content-type text/plain | metadata",
result: Some(Value::test_record(record! {
"content_type" => Value::test_string("text/plain"),
})),
example: "'crates' | metadata set --content-type text/plain | metadata | get content_type",
result: Some(Value::test_string("text/plain")),
},
]
}

View File

@ -1,4 +1,4 @@
use nu_protocol::{DataSource, PipelineMetadata, Record, Span, Value};
use nu_protocol::{DataSource, IntoValue, PipelineData, PipelineMetadata, Record, Span, Value};
pub fn extend_record_with_metadata(
mut record: Record,
@ -29,6 +29,10 @@ pub fn extend_record_with_metadata(
record
}
pub fn build_metadata_record(metadata: Option<&PipelineMetadata>, head: Span) -> Record {
extend_record_with_metadata(Record::new(), metadata, head)
pub fn build_metadata_record(pipeline: &PipelineData, head: Span) -> Record {
let mut record = Record::new();
if let Some(span) = pipeline.span() {
record.insert("span", span.into_value(head));
}
extend_record_with_metadata(record, pipeline.metadata().as_ref(), head)
}

View File

@ -203,6 +203,7 @@ mod test {
use super::*;
use crate::Reject;
use crate::{Metadata, MetadataSet};
#[test]
@ -221,6 +222,7 @@ mod test {
working_set.add_decl(Box::new(FromCsv {}));
working_set.add_decl(Box::new(Metadata {}));
working_set.add_decl(Box::new(MetadataSet {}));
working_set.add_decl(Box::new(Reject {}));
working_set.render()
};
@ -229,7 +231,7 @@ mod test {
.merge_delta(delta)
.expect("Error merging delta");
let cmd = r#""a,b\n1,2" | metadata set --content-type 'text/csv' --datasource-ls | from csv | metadata | $in"#;
let cmd = r#""a,b\n1,2" | metadata set --content-type 'text/csv' --datasource-ls | from csv | metadata | reject span | $in"#;
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),

View File

@ -248,6 +248,7 @@ fn convert_string_to_value_strict(string_input: &str, span: Span) -> Result<Valu
mod test {
use nu_cmd_lang::eval_pipeline_without_terminal_expression;
use crate::Reject;
use crate::{Metadata, MetadataSet};
use super::*;
@ -268,6 +269,7 @@ mod test {
working_set.add_decl(Box::new(FromJson {}));
working_set.add_decl(Box::new(Metadata {}));
working_set.add_decl(Box::new(MetadataSet {}));
working_set.add_decl(Box::new(Reject {}));
working_set.render()
};
@ -276,7 +278,7 @@ mod test {
.merge_delta(delta)
.expect("Error merging delta");
let cmd = r#"'{"a":1,"b":2}' | metadata set --content-type 'application/json' --datasource-ls | from json | metadata | $in"#;
let cmd = r#"'{"a":1,"b":2}' | metadata set --content-type 'application/json' --datasource-ls | from json | metadata | reject span | $in"#;
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),

View File

@ -514,6 +514,7 @@ fn assert_eof(input: &mut impl io::Read, span: Span) -> Result<(), ShellError> {
mod test {
use nu_cmd_lang::eval_pipeline_without_terminal_expression;
use crate::Reject;
use crate::{Metadata, MetadataSet, ToMsgpack};
use super::*;
@ -535,6 +536,7 @@ mod test {
working_set.add_decl(Box::new(FromMsgpack {}));
working_set.add_decl(Box::new(Metadata {}));
working_set.add_decl(Box::new(MetadataSet {}));
working_set.add_decl(Box::new(Reject {}));
working_set.render()
};
@ -543,7 +545,7 @@ mod test {
.merge_delta(delta)
.expect("Error merging delta");
let cmd = r#"{a: 1 b: 2} | to msgpack | metadata set --datasource-ls | from msgpack | metadata | $in"#;
let cmd = r#"{a: 1 b: 2} | to msgpack | metadata set --datasource-ls | from msgpack | metadata | reject span | $in"#;
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),

View File

@ -74,6 +74,7 @@ impl Command for FromNuon {
mod test {
use nu_cmd_lang::eval_pipeline_without_terminal_expression;
use crate::Reject;
use crate::{Metadata, MetadataSet};
use super::*;
@ -94,6 +95,7 @@ mod test {
working_set.add_decl(Box::new(FromNuon {}));
working_set.add_decl(Box::new(Metadata {}));
working_set.add_decl(Box::new(MetadataSet {}));
working_set.add_decl(Box::new(Reject {}));
working_set.render()
};
@ -102,7 +104,7 @@ mod test {
.merge_delta(delta)
.expect("Error merging delta");
let cmd = r#"'[[a, b]; [1, 2]]' | metadata set --content-type 'application/x-nuon' --datasource-ls | from nuon | metadata | $in"#;
let cmd = r#"'[[a, b]; [1, 2]]' | metadata set --content-type 'application/x-nuon' --datasource-ls | from nuon | metadata | reject span | $in"#;
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),

View File

@ -145,6 +145,7 @@ pub fn convert_string_to_value(string_input: String, span: Span) -> Result<Value
#[cfg(test)]
mod tests {
use crate::Reject;
use crate::{Metadata, MetadataSet};
use super::*;
@ -345,6 +346,7 @@ mod tests {
working_set.add_decl(Box::new(FromToml {}));
working_set.add_decl(Box::new(Metadata {}));
working_set.add_decl(Box::new(MetadataSet {}));
working_set.add_decl(Box::new(Reject {}));
working_set.render()
};
@ -353,7 +355,7 @@ mod tests {
.merge_delta(delta)
.expect("Error merging delta");
let cmd = r#""[a]\nb = 1\nc = 1" | metadata set --content-type 'text/x-toml' --datasource-ls | from toml | metadata | $in"#;
let cmd = r#""[a]\nb = 1\nc = 1" | metadata set --content-type 'text/x-toml' --datasource-ls | from toml | metadata | reject span | $in"#;
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),

View File

@ -160,6 +160,7 @@ fn from_tsv(
mod test {
use nu_cmd_lang::eval_pipeline_without_terminal_expression;
use crate::Reject;
use crate::{Metadata, MetadataSet};
use super::*;
@ -180,6 +181,7 @@ mod test {
working_set.add_decl(Box::new(FromTsv {}));
working_set.add_decl(Box::new(Metadata {}));
working_set.add_decl(Box::new(MetadataSet {}));
working_set.add_decl(Box::new(Reject {}));
working_set.render()
};
@ -188,7 +190,7 @@ mod test {
.merge_delta(delta)
.expect("Error merging delta");
let cmd = r#""a\tb\n1\t2" | metadata set --content-type 'text/tab-separated-values' --datasource-ls | from tsv | metadata | $in"#;
let cmd = r#""a\tb\n1\t2" | metadata set --content-type 'text/tab-separated-values' --datasource-ls | from tsv | metadata | reject span | $in"#;
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),

View File

@ -370,6 +370,7 @@ fn make_xml_error_spanned(msg: impl Into<String>, src: String, pos: TextPos) ->
mod tests {
use crate::Metadata;
use crate::MetadataSet;
use crate::Reject;
use super::*;
@ -541,6 +542,7 @@ mod tests {
working_set.add_decl(Box::new(FromXml {}));
working_set.add_decl(Box::new(Metadata {}));
working_set.add_decl(Box::new(MetadataSet {}));
working_set.add_decl(Box::new(Reject {}));
working_set.render()
};
@ -552,7 +554,7 @@ mod tests {
let cmd = r#"'<?xml version="1.0" encoding="UTF-8"?>
<note>
<remember>Event</remember>
</note>' | metadata set --content-type 'application/xml' --datasource-ls | from xml | metadata | $in"#;
</note>' | metadata set --content-type 'application/xml' --datasource-ls | from xml | metadata | reject span | $in"#;
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),

View File

@ -243,6 +243,7 @@ fn from_yaml(input: PipelineData, head: Span) -> Result<PipelineData, ShellError
#[cfg(test)]
mod test {
use crate::Reject;
use crate::{Metadata, MetadataSet};
use super::*;
@ -409,6 +410,7 @@ mod test {
working_set.add_decl(Box::new(FromYaml {}));
working_set.add_decl(Box::new(Metadata {}));
working_set.add_decl(Box::new(MetadataSet {}));
working_set.add_decl(Box::new(Reject {}));
working_set.render()
};
@ -417,7 +419,7 @@ mod test {
.merge_delta(delta)
.expect("Error merging delta");
let cmd = r#""a: 1\nb: 2" | metadata set --content-type 'application/yaml' --datasource-ls | from yaml | metadata | $in"#;
let cmd = r#""a: 1\nb: 2" | metadata set --content-type 'application/yaml' --datasource-ls | from yaml | metadata | reject span | $in"#;
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),

View File

@ -166,14 +166,14 @@ mod test {
.merge_delta(delta)
.expect("Error merging delta");
let cmd = "{a: 1 b: 2} | to csv | metadata | get content_type";
let cmd = "{a: 1 b: 2} | to csv | metadata | get content_type | $in";
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),
&mut engine_state,
);
assert_eq!(
Value::test_record(record!("content_type" => Value::test_string("text/csv"))),
Value::test_string("text/csv"),
result.expect("There should be a result")
);
}

View File

@ -229,14 +229,14 @@ mod test {
.merge_delta(delta)
.expect("Error merging delta");
let cmd = "{a: 1 b: 2} | to json | metadata | get content_type";
let cmd = "{a: 1 b: 2} | to json | metadata | get content_type | $in";
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),
&mut engine_state,
);
assert_eq!(
Value::test_record(record!("content_type" => Value::test_string("application/json"))),
Value::test_string("application/json"),
result.expect("There should be a result")
);
}

View File

@ -904,14 +904,14 @@ mod tests {
.merge_delta(delta)
.expect("Error merging delta");
let cmd = "{a: 1 b: 2} | to md | metadata | get content_type";
let cmd = "{a: 1 b: 2} | to md | metadata | get content_type | $in";
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),
&mut engine_state,
);
assert_eq!(
Value::test_record(record!("content_type" => Value::test_string("text/markdown"))),
Value::test_string("text/markdown"),
result.expect("There should be a result")
);
}

View File

@ -348,16 +348,14 @@ mod test {
.merge_delta(delta)
.expect("Error merging delta");
let cmd = "{a: 1 b: 2} | to msgpack | metadata | get content_type";
let cmd = "{a: 1 b: 2} | to msgpack | metadata | get content_type | $in";
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),
&mut engine_state,
);
assert_eq!(
Value::test_record(
record!("content_type" => Value::test_string("application/x-msgpack"))
),
Value::test_string("application/x-msgpack"),
result.expect("There should be a result")
);
}

View File

@ -143,14 +143,14 @@ mod test {
.merge_delta(delta)
.expect("Error merging delta");
let cmd = "{a: 1 b: 2} | to nuon | metadata | get content_type";
let cmd = "{a: 1 b: 2} | to nuon | metadata | get content_type | $in";
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),
&mut engine_state,
);
assert_eq!(
Value::test_record(record!("content_type" => Value::test_string("application/x-nuon"))),
Value::test_string("application/x-nuon"),
result.expect("There should be a result")
);
}

View File

@ -273,14 +273,14 @@ mod test {
.merge_delta(delta)
.expect("Error merging delta");
let cmd = "{a: 1 b: 2} | to text | metadata | get content_type";
let cmd = "{a: 1 b: 2} | to text | metadata | get content_type | $in";
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),
&mut engine_state,
);
assert_eq!(
Value::test_record(record!("content_type" => Value::test_string("text/plain"))),
Value::test_string("text/plain"),
result.expect("There should be a result")
);
}

View File

@ -132,16 +132,14 @@ mod test {
.merge_delta(delta)
.expect("Error merging delta");
let cmd = "{a: 1 b: 2} | to tsv | metadata | get content_type";
let cmd = "{a: 1 b: 2} | to tsv | metadata | get content_type | $in";
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),
&mut engine_state,
);
assert_eq!(
Value::test_record(
record!("content_type" => Value::test_string("text/tab-separated-values"))
),
Value::test_string("text/tab-separated-values"),
result.expect("There should be a result")
);
}

View File

@ -538,14 +538,14 @@ mod test {
.merge_delta(delta)
.expect("Error merging delta");
let cmd = "{tag: note attributes: {} content : [{tag: remember attributes: {} content : [{tag: null attributes: null content : Event}]}]} | to xml | metadata | get content_type";
let cmd = "{tag: note attributes: {} content : [{tag: remember attributes: {} content : [{tag: null attributes: null content : Event}]}]} | to xml | metadata | get content_type | $in";
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),
&mut engine_state,
);
assert_eq!(
Value::test_record(record!("content_type" => Value::test_string("application/xml"))),
Value::test_string("application/xml"),
result.expect("There should be a result")
);
}

View File

@ -233,14 +233,14 @@ mod test {
.merge_delta(delta)
.expect("Error merging delta");
let cmd = "{a: 1 b: 2} | to yaml | metadata | get content_type";
let cmd = "{a: 1 b: 2} | to yaml | metadata | get content_type | $in";
let result = eval_pipeline_without_terminal_expression(
cmd,
std::env::temp_dir().as_ref(),
&mut engine_state,
);
assert_eq!(
Value::test_record(record!("content_type" => Value::test_string("application/yaml"))),
Value::test_string("application/yaml"),
result.expect("There should be a result")
);
}

View File

@ -1,5 +1,5 @@
//! [`Span`] to point to sections of source code and the [`Spanned`] wrapper type
use crate::SpanId;
use crate::{IntoValue, SpanId, Value, record};
use miette::SourceSpan;
use serde::{Deserialize, Serialize};
use std::ops::Deref;
@ -277,6 +277,16 @@ impl Span {
}
}
impl IntoValue for Span {
fn into_value(self, span: Span) -> Value {
let record = record! {
"start" => Value::int(self.start as i64, self),
"end" => Value::int(self.end as i64, self),
};
record.into_value(span)
}
}
impl From<Span> for SourceSpan {
fn from(s: Span) -> Self {
Self::new(s.start.into(), s.end - s.start)