Skip to content

Commit 885ddb6

Browse files
committed
Remove support for "dcm cell-path" to select from record
1 parent f031205 commit 885ddb6

4 files changed

Lines changed: 107 additions & 57 deletions

File tree

README.md

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ I'm still trying to figure out what is the most useful way of using this plugin.
1111
send feedback in [Discussions](https://github.com/realcundo/nu_plugin_dcm/discussions) or report problems in [Issues](https://github.com/realcundo/nu_plugin_dcm/issues).
1212

1313
## Usage
14-
`dcm` plugin reads its input from single values, from specific columns, or from list of values:
15-
- `dcm`: expects a string/filename or binary DICOM data
16-
- `dcm $column_name`: reads a string/filename or binary DICOM data from `$column`. This is
17-
equivalent to `get $column | dcm`.
18-
- `ls *.dcm | select name | dcm`: reads all files foun dby `ls` and returns a list of records.
14+
`dcm` plugin reads its input from single values, or from list of values:
15+
- `dcm`: expects a string/filename, file record (must contain `name` and `type`), dicomweb record, or binary DICOM data
16+
- `ls *.dcm | dcm`: process a list of files, resulting in a list of dicom records
17+
- `ls *.dcm | select name type | dcm`: process a list of files specified by their filename, resulting in a list of dicom records
18+
- `open --raw file.dcm | into binary | dcm`: process a binary stream, resulting in a dicom record
19+
- `open dicomweb.json | dcm`: process a dicomweb record, resulting in a dicom record
1920

2021
See Examples for more details.
2122

@@ -42,11 +43,14 @@ See Examples for more details.
4243

4344
### Output DICOM file as a record/table (list of records)
4445
```sh
45-
echo file.dcm | dcm # uses filename/string to specify which file to open
46-
open --raw file.dcm | dcm # pass binary data to `dcm`
47-
ls file.dcm | dcm name # use `name` column as the filename (equivalent of `ls file.dcm | select name | dcm`)
48-
echo file.dcm | wrap foo | dcm foo # use `foo` column as the filename
49-
open -r file.dcm | into binary | wrap foo | dcm foo # use `foo` column as binary data (see Known Limitations for details)
46+
echo file.dcm | dcm # uses filename/string to specify which file to open
47+
"file.dcm" | dcm # same asa above, uses filename/string to specify which file to open
48+
open --raw file.dcm | dcm # pass (hopefully) binary data to `dcm`
49+
open --raw file.dcm | into binary | dcm # pass binary data to `dcm`
50+
ls file.dcm | dcm # use file records as the filename
51+
ls file.dcm | select name type | dcm # use file record-like records as the filename
52+
# ls file.dcm | select name | dcm # fails because the record only contains `name` field.
53+
ls file.dcm | get name | dcm # use a list of filenames (list of strings, rather than a list of records)
5054
```
5155
5256
### Dump DICOM file as a JSON/YAML document
@@ -57,16 +61,16 @@ open -r file.dcm | dcm | to yaml
5761
5862
### Dump all DICOM files in the current directory to a JSON/YAML document
5963
```sh
60-
ls *.dcm | dcm name | to json --indent 2
61-
ls *.dcm | dcm name | to yaml
64+
ls *.dcm | dcm | to json --indent 2
65+
ls *.dcm | dcm | to yaml
6266
```
6367
6468
### Find all files in the current directory and subdirectories, parse them and group by Modality
6569
6670
```sh
6771
ls **/* |
6872
where type == file |
69-
dcm name -e error |
73+
dcm -e error |
7074
where error == "" |
7175
select --ignore-errors SOPInstanceUID Modality |
7276
group-by Modality
@@ -79,7 +83,7 @@ let files = (ls | where type == file)
7983
$files |
8084
select name size |
8185
merge ($files |
82-
dcm name -e error |
86+
dcm -e error |
8387
select --ignore-errors SOPInstanceUID Modality error
8488
) |
8589
sort-by size

src/plugin.rs

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ use dicom::object::DefaultDicomObject;
1010
use dicom::object::StandardDataDictionary;
1111
use indexmap::IndexMap;
1212
use nu_plugin::{EngineInterface, Plugin, PluginCommand};
13-
use nu_protocol::ast::CellPath;
1413
use nu_protocol::{Category, Example, LabeledError, PipelineData, Record, ShellError, Signature, Span, SyntaxShape, Value};
1514

1615
#[derive(Default)]
@@ -90,11 +89,6 @@ impl PluginCommand for DcmPluginCommand {
9089
SyntaxShape::String,
9190
"If an error occurs when Dicom object is parsed, the error message will be inserted in this column instead producing an error result.",
9291
Some('e'))
93-
.optional(
94-
"column",
95-
SyntaxShape::CellPath,
96-
"Optional column name to use as Dicom source",
97-
)
9892
.category(Category::Formats) // More appropriate category
9993
.search_terms(vec!["dicom".to_string(), "medical".to_string(), "parse".to_string()])
10094
.description("Parse DICOM files and binary data")
@@ -113,9 +107,9 @@ impl PluginCommand for DcmPluginCommand {
113107
("ImageType".to_string(), Value::test_string("ORIGINAL\\PRIMARY")),
114108
]))),
115109
},
116-
Example { description: "Parse DICOM files from a list", example: "ls *.dcm | dcm name", result: None },
110+
Example { description: "Parse DICOM files from a list", example: "ls *.dcm | dcm", result: None },
117111
Example { description: "Parse a specific file by filename", example: "\"file.dcm\" | dcm", result: None },
118-
Example { description: "Parse with error handling", example: "ls *.dcm | dcm name --error parse_error", result: None },
112+
Example { description: "Parse with error handling", example: "ls *.dcm | dcm --error parse_error", result: None },
119113
]
120114
}
121115

@@ -147,14 +141,6 @@ impl PluginCommand for DcmPluginCommand {
147141
input.into_value(span)?
148142
};
149143

150-
// parse args, nu makes sure the num of args and type is correct
151-
let source_column = call.nth(0);
152-
let source_column = if let Some(Value::CellPath { val, .. }) = source_column {
153-
Some(val)
154-
} else {
155-
None
156-
};
157-
158144
let error_column = call.get_flag_value("error");
159145
let error_column = if let Some(Value::String { val, .. }) = error_column {
160146
Some(val)
@@ -163,7 +149,7 @@ impl PluginCommand for DcmPluginCommand {
163149
};
164150

165151
// run
166-
let output = self.run_filter(plugin, current_dir.as_deref(), &input, source_column, error_column)?;
152+
let output = self.run_filter(plugin, current_dir.as_deref(), &input, error_column)?;
167153

168154
// Forward DataSource metadata from input to output, but clear any content type. This keeps the source.
169155
let output_metadata = input_metadata.map(|m| m.with_content_type(None));
@@ -178,19 +164,9 @@ impl DcmPluginCommand {
178164
plugin: &DcmPlugin,
179165
current_dir: Result<&Path, &ShellError>,
180166
value: &Value,
181-
source_column: Option<CellPath>,
182167
error_column: Option<String>,
183168
) -> Result<Value, LabeledError> {
184-
// use source column if known
185-
if let Some(source_column) = source_column {
186-
let value = value.follow_cell_path(&source_column.members)?;
187-
188-
// AFAIK a list, process_value will handle it
189-
self.process_value(plugin, current_dir, &value, &error_column)
190-
} else {
191-
// expect a primitive value if column is not known
192-
self.process_value(plugin, current_dir, value, &error_column)
193-
}
169+
self.process_value(plugin, current_dir, value, &error_column)
194170
}
195171

196172
fn process_value(
@@ -257,9 +233,17 @@ impl DcmPluginCommand {
257233

258234
if let (Some(record_type), Some(record_name)) = (record_type, record_name) {
259235
if record_type == "file" {
260-
return Err(LabeledError::new("Cannot process file records directly")
261-
.with_help("Extract the filename first using: `dcm name`, `get name | dcm`, or `select name | dcm`")
262-
.with_label(format!("Found file record with name: '{record_name}'"), *internal_span));
236+
// make absolute if needed
237+
let file = resolve_path(record_name, current_dir, value.span())?;
238+
239+
// merge with file-reading above (Value::String)?
240+
let obj = read_dcm_file(&file).map_err(|e| {
241+
let text = format!("{} [file {}]", e, file.to_string_lossy());
242+
243+
LabeledError::new("`dcm` expects valid DICOM binary data").with_label(text, *internal_span)
244+
})?;
245+
246+
return self.process_dicom_object(plugin, internal_span, obj, error_column);
263247
}
264248
}
265249

tests/integration_tests.rs

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,39 @@ fn test_command_vector_open() -> Result<(), nu_protocol::ShellError> {
6767
fn test_command_ls() -> Result<(), nu_protocol::ShellError> {
6868
let mut plugin_test = setup_plugin_for_test(vec![Box::new(nu_command::Ls)])?;
6969

70-
let result = plugin_test.eval("ls *.dcm | dcm name")?;
70+
let result = plugin_test.eval("ls *.dcm | dcm")?;
71+
let result = result.into_value(Span::test_data())?;
72+
73+
// TODO actually test the result
74+
assert!(
75+
result
76+
.as_list()
77+
.is_ok()
78+
);
79+
Ok(())
80+
}
81+
82+
#[test]
83+
fn test_command_ls_select_name() -> Result<(), nu_protocol::ShellError> {
84+
let mut plugin_test = setup_plugin_for_test(vec![Box::new(nu_command::Ls), Box::new(nu_command::Select)])?;
85+
86+
let result = plugin_test.eval("ls *.dcm | select name | dcm")?;
87+
let result = result.into_value(Span::test_data())?;
88+
89+
// TODO actually test the result
90+
assert!(
91+
result
92+
.as_list()
93+
.is_ok()
94+
);
95+
Ok(())
96+
}
97+
98+
#[test]
99+
fn test_command_ls_get_name() -> Result<(), nu_protocol::ShellError> {
100+
let mut plugin_test = setup_plugin_for_test(vec![Box::new(nu_command::Ls), Box::new(nu_command::Get)])?;
101+
102+
let result = plugin_test.eval("ls *.dcm | get name | dcm")?;
71103
let result = result.into_value(Span::test_data())?;
72104

73105
// TODO actually test the result
@@ -143,7 +175,7 @@ fn test_string_file_path_input() -> Result<(), Box<dyn std::error::Error>> {
143175

144176
Ok(())
145177
}
146-
/// Simulate `ls *.dcm | dcm name` matching a single file
178+
/// Simulate `ls *.dcm | dcm` matching a single file
147179
#[test]
148180
fn test_scalar_record_input() -> Result<(), Box<dyn std::error::Error>> {
149181
let mut plugin_test = setup_plugin_for_test(vec![])?;
@@ -152,7 +184,7 @@ fn test_scalar_record_input() -> Result<(), Box<dyn std::error::Error>> {
152184
let file_record = create_file_record_value("ImplicitVRLittleEndian-Preamble.dcm");
153185

154186
// Test with column path 'name'
155-
let pipeline_data = plugin_test.eval_with("dcm name", file_record.into_pipeline_data())?;
187+
let pipeline_data = plugin_test.eval_with("dcm", file_record.into_pipeline_data())?;
156188
let result = pipeline_data.into_value(Span::test_data())?;
157189

158190
assert_eq!(get_string_by_cell_path(&result, "TransferSyntax"), "1.2.840.10008.1.2");
@@ -161,7 +193,7 @@ fn test_scalar_record_input() -> Result<(), Box<dyn std::error::Error>> {
161193
Ok(())
162194
}
163195

164-
/// Simulate `ls *.dcm | dcm name` with multiple matching files
196+
/// Simulate `ls *.dcm | dcm` with multiple matching files
165197
#[test]
166198
fn test_vector_record_input() -> Result<(), Box<dyn std::error::Error>> {
167199
let mut plugin_test = setup_plugin_for_test(vec![])?;
@@ -177,7 +209,7 @@ fn test_vector_record_input() -> Result<(), Box<dyn std::error::Error>> {
177209
let list_value = Value::list(file_records, Span::test_data());
178210

179211
// Test with column path 'name'
180-
let pipeline_data = plugin_test.eval_with("dcm name", list_value.into_pipeline_data())?;
212+
let pipeline_data = plugin_test.eval_with("dcm", list_value.into_pipeline_data())?;
181213
let result = pipeline_data.into_value(Span::test_data())?;
182214

183215
assert_eq!(get_string_by_cell_path(&result, "0.PatientName"), "ExplicitVRLittleEndian-Preamble");
@@ -191,3 +223,33 @@ fn test_vector_record_input() -> Result<(), Box<dyn std::error::Error>> {
191223

192224
Ok(())
193225
}
226+
227+
/// Fail on extra parameters being passed to dcm
228+
#[test]
229+
fn test_fail_on_extra_parameters() -> Result<(), Box<dyn std::error::Error>> {
230+
let mut plugin_test = setup_plugin_for_test(vec![])?;
231+
232+
// Create a record that simulates what 'ls' would produce
233+
let file_record = create_file_record_value("ImplicitVRLittleEndian-Preamble.dcm");
234+
235+
// Test with column path 'name'
236+
let pipeline_data = plugin_test.eval_with("dcm name", file_record.into_pipeline_data());
237+
238+
let error = pipeline_data.unwrap_err();
239+
if let nu_protocol::ShellError::LabeledError(labeled_error) = error {
240+
assert_eq!(labeled_error.msg, "Example failed to parse"); // coming from plugin test
241+
242+
let inner_error = labeled_error
243+
.inner
244+
.get(0)
245+
.expect("Expected an inner error");
246+
247+
dbg!(&inner_error);
248+
assert_eq!(inner_error.msg, "Extra positional argument.");
249+
assert_eq!(inner_error.help, Some("Usage: dcm {flags} ".to_string()));
250+
} else {
251+
panic!("Unexpected error {:?}", error);
252+
}
253+
254+
Ok(())
255+
}

tests/simple.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ fn no_input_without_errors() {
1616
let p = DcmPlugin::default();
1717
let cmd = DcmPluginCommand;
1818

19-
let actual = cmd.run_filter(&p, current_dir, &Value::test_nothing(), None, None);
19+
let actual = cmd.run_filter(&p, current_dir, &Value::test_nothing(), None);
2020

2121
let expected =
2222
Err(LabeledError::new("Unrecognized type in stream")
@@ -34,7 +34,7 @@ fn read_explicit_vr_big_endian_preamble() {
3434
let p = DcmPlugin::default();
3535
let cmd = DcmPluginCommand;
3636

37-
let actual = cmd.run_filter(&p, current_dir, &filepath(filename), None, None);
37+
let actual = cmd.run_filter(&p, current_dir, &filepath(filename), None);
3838
let actual_value = actual.unwrap();
3939

4040
assert_eq!(get_string_by_cell_path(&actual_value, "TransferSyntax"), "1.2.840.10008.1.2.2");
@@ -52,7 +52,7 @@ fn read_explicit_vr_little_endian_preamble() {
5252
let p = DcmPlugin::default();
5353
let cmd = DcmPluginCommand;
5454

55-
let actual = cmd.run_filter(&p, current_dir, &filepath(filename), None, None);
55+
let actual = cmd.run_filter(&p, current_dir, &filepath(filename), None);
5656
let actual_value = actual.unwrap();
5757

5858
assert_eq!(get_string_by_cell_path(&actual_value, "TransferSyntax"), "1.2.840.10008.1.2.1");
@@ -69,7 +69,7 @@ fn read_implicit_vr_little_endian_preamble() {
6969

7070
let p = DcmPlugin::default();
7171
let cmd = DcmPluginCommand;
72-
let actual = cmd.run_filter(&p, current_dir, &filepath(filename), None, None);
72+
let actual = cmd.run_filter(&p, current_dir, &filepath(filename), None);
7373
let actual_value = actual.unwrap();
7474

7575
assert_eq!(get_string_by_cell_path(&actual_value, "TransferSyntax"), "1.2.840.10008.1.2");
@@ -87,7 +87,7 @@ fn read_explicit_vr_big_endian_no_preamble() {
8787

8888
let p = DcmPlugin::default();
8989
let cmd = DcmPluginCommand;
90-
let _actual = cmd.run_filter(&p, current_dir, &filepath(filename), None, None);
90+
let _actual = cmd.run_filter(&p, current_dir, &filepath(filename), None);
9191

9292
todo!()
9393
}
@@ -101,7 +101,7 @@ fn read_explicit_vr_little_endian_no_preamble() {
101101

102102
let p = DcmPlugin::default();
103103
let cmd = DcmPluginCommand;
104-
let _actual = cmd.run_filter(&p, current_dir, &filepath(filename), None, None);
104+
let _actual = cmd.run_filter(&p, current_dir, &filepath(filename), None);
105105

106106
todo!()
107107
}
@@ -115,7 +115,7 @@ fn read_implicit_vr_little_endian_no_preamble() {
115115

116116
let p = DcmPlugin::default();
117117
let cmd = DcmPluginCommand;
118-
let _actual = cmd.run_filter(&p, current_dir, &filepath(filename), None, None);
118+
let _actual = cmd.run_filter(&p, current_dir, &filepath(filename), None);
119119

120120
todo!()
121121
}

0 commit comments

Comments
 (0)