Skip to content

Commit 7499971

Browse files
committed
fix: --outSAMunmapped now correctly takes multiple args
1 parent 6a35eee commit 7499971

2 files changed

Lines changed: 226 additions & 131 deletions

File tree

src/params.rs renamed to src/params/mod.rs

Lines changed: 47 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ use std::collections::HashSet;
22
use std::num::NonZeroUsize;
33
use std::path::PathBuf;
44

5-
use clap::Parser;
5+
use clap::{CommandFactory, Parser};
6+
7+
mod sam;
8+
9+
pub use sam::{OutSamFormat, OutSamSortOrder, OutSamType, OutSamUnmapped};
610

711
// ---------------------------------------------------------------------------
812
// Run mode enum
@@ -102,108 +106,6 @@ impl std::str::FromStr for IntronStrandFilter {
102106
}
103107
}
104108

105-
// ---------------------------------------------------------------------------
106-
// SAM output type enums
107-
// ---------------------------------------------------------------------------
108-
109-
/// STAR's `--outSAMtype` format component.
110-
#[derive(Debug, Clone, PartialEq, Eq, Default)]
111-
pub enum OutSamFormat {
112-
#[default]
113-
Sam,
114-
Bam,
115-
None,
116-
}
117-
118-
/// STAR's `--outSAMtype` sort order component (only applies to BAM).
119-
#[derive(Debug, Clone, PartialEq, Eq)]
120-
pub enum OutSamSortOrder {
121-
Unsorted,
122-
SortedByCoordinate,
123-
}
124-
125-
/// Combined `--outSAMtype` value.
126-
#[derive(Debug, Clone, PartialEq, Eq, Default)]
127-
pub struct OutSamType {
128-
pub format: OutSamFormat,
129-
pub sort_order: Option<OutSamSortOrder>,
130-
}
131-
132-
impl std::fmt::Display for OutSamType {
133-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134-
match (&self.format, &self.sort_order) {
135-
(OutSamFormat::Sam, _) => write!(f, "SAM"),
136-
(OutSamFormat::None, _) => write!(f, "None"),
137-
(OutSamFormat::Bam, Some(OutSamSortOrder::SortedByCoordinate)) => {
138-
write!(f, "BAM SortedByCoordinate")
139-
}
140-
(OutSamFormat::Bam, _) => write!(f, "BAM Unsorted"),
141-
}
142-
}
143-
}
144-
145-
impl clap::FromArgMatches for OutSamType {
146-
fn from_arg_matches(matches: &clap::ArgMatches) -> Result<Self, clap::Error> {
147-
let mut s = Self::default();
148-
s.update_from_arg_matches(matches)?;
149-
Ok(s)
150-
}
151-
152-
fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> {
153-
let Some(values) = matches.get_many::<String>("outSAMtype") else {
154-
return Ok(());
155-
};
156-
let tokens: Vec<&str> = values.map(String::as_str).collect();
157-
*self = match tokens.as_slice() {
158-
["SAM"] => Self {
159-
format: OutSamFormat::Sam,
160-
sort_order: None,
161-
},
162-
["None"] => Self {
163-
format: OutSamFormat::None,
164-
sort_order: None,
165-
},
166-
["BAM", "Unsorted"] => Self {
167-
format: OutSamFormat::Bam,
168-
sort_order: Some(OutSamSortOrder::Unsorted),
169-
},
170-
["BAM", "SortedByCoordinate"] => Self {
171-
format: OutSamFormat::Bam,
172-
sort_order: Some(OutSamSortOrder::SortedByCoordinate),
173-
},
174-
other => {
175-
return Err(clap::Error::raw(
176-
clap::error::ErrorKind::InvalidValue,
177-
format!(
178-
"invalid value '{}' for '--outSAMtype': expected 'SAM', 'None', 'BAM Unsorted', or 'BAM SortedByCoordinate'\n",
179-
other.join(" ")
180-
),
181-
));
182-
}
183-
};
184-
Ok(())
185-
}
186-
}
187-
188-
impl clap::Args for OutSamType {
189-
fn augment_args(cmd: clap::Command) -> clap::Command {
190-
cmd.arg(
191-
clap::Arg::new("outSAMtype")
192-
.long("outSAMtype")
193-
.num_args(1..=2)
194-
.default_values(["SAM"])
195-
.help(
196-
"Output type: SAM, BAM Unsorted, BAM SortedByCoordinate, None. \
197-
Provide as space-separated tokens, e.g. \"BAM SortedByCoordinate\".",
198-
),
199-
)
200-
}
201-
202-
fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
203-
Self::augment_args(cmd)
204-
}
205-
}
206-
207109
// ---------------------------------------------------------------------------
208110
// Standard output streaming
209111
// ---------------------------------------------------------------------------
@@ -257,30 +159,6 @@ impl std::str::FromStr for OutReadsUnmapped {
257159
}
258160
}
259161

260-
// ---------------------------------------------------------------------------
261-
// SAM unmapped output
262-
// ---------------------------------------------------------------------------
263-
264-
#[derive(Debug, Clone, PartialEq, Eq, Default)]
265-
pub enum OutSamUnmapped {
266-
#[default]
267-
None,
268-
Within,
269-
WithinKeepPairs,
270-
}
271-
272-
impl std::str::FromStr for OutSamUnmapped {
273-
type Err = String;
274-
fn from_str(s: &str) -> Result<Self, Self::Err> {
275-
match s {
276-
"None" => Ok(Self::None),
277-
"Within" => Ok(Self::Within),
278-
"Within KeepPairs" => Ok(Self::WithinKeepPairs),
279-
_ => Err(format!("unknown outSAMunmapped value: '{s}'")),
280-
}
281-
}
282-
}
283-
284162
// ---------------------------------------------------------------------------
285163
// Output filter type
286164
// ---------------------------------------------------------------------------
@@ -435,8 +313,7 @@ pub struct Parameters {
435313
#[arg(long = "outSAMattrRGline", num_args = 1.., default_values_t = vec!["-".to_string()])]
436314
pub out_sam_attr_rg_line: Vec<String>,
437315

438-
/// Unmapped reads in SAM output: None or Within
439-
#[arg(long = "outSAMunmapped", default_value = "None")]
316+
#[command(flatten)]
440317
pub out_sam_unmapped: OutSamUnmapped,
441318

442319
/// Output unmapped reads to FASTQ file(s): None or Fastx
@@ -943,8 +820,13 @@ impl Parameters {
943820
pub fn parse_from<T: Into<std::ffi::OsString> + Clone>(
944821
args: impl IntoIterator<Item = T>,
945822
) -> Self {
946-
Self::try_parse_from(args)
947-
.unwrap_or_else(|e| if cfg!(test) { panic!("{e}") } else { e.exit() })
823+
Self::try_parse_from(args).unwrap_or_else(|e| {
824+
if cfg!(test) {
825+
panic!("{e}")
826+
} else {
827+
e.format(&mut <Self as CommandFactory>::command()).exit()
828+
}
829+
})
948830
}
949831

950832
/// Parse and validate parameter combinations.
@@ -1262,6 +1144,40 @@ mod tests {
12621144
assert!(try_parse(&["--readFilesIn", "r.fq", "--outSAMtype", "BAM"]).is_err());
12631145
}
12641146

1147+
#[test]
1148+
fn out_sam_unmapped_parsing() {
1149+
let p = try_parse(&["--readFilesIn", "r.fq"]).unwrap();
1150+
assert_eq!(p.out_sam_unmapped, OutSamUnmapped::None);
1151+
1152+
let p = try_parse(&["--readFilesIn", "r.fq", "--outSAMunmapped", "None"]).unwrap();
1153+
assert_eq!(p.out_sam_unmapped, OutSamUnmapped::None);
1154+
1155+
let p = try_parse(&["--readFilesIn", "r.fq", "--outSAMunmapped", "Within"]).unwrap();
1156+
assert_eq!(p.out_sam_unmapped, OutSamUnmapped::Within);
1157+
1158+
let p = try_parse(&[
1159+
"--readFilesIn",
1160+
"r.fq",
1161+
"--outSAMunmapped",
1162+
"Within",
1163+
"KeepPairs",
1164+
])
1165+
.unwrap();
1166+
assert_eq!(p.out_sam_unmapped, OutSamUnmapped::WithinKeepPairs);
1167+
1168+
assert!(try_parse(&["--readFilesIn", "r.fq", "--outSAMunmapped", "Bogus"]).is_err());
1169+
assert!(
1170+
try_parse(&[
1171+
"--readFilesIn",
1172+
"r.fq",
1173+
"--outSAMunmapped",
1174+
"Within",
1175+
"Bogus"
1176+
])
1177+
.is_err()
1178+
);
1179+
}
1180+
12651181
#[test]
12661182
fn chimeric_params() {
12671183
let p = try_parse(&[

src/params/sam.rs

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// ---------------------------------------------------------------------------
2+
// SAM output type enums
3+
// ---------------------------------------------------------------------------
4+
5+
use clap::error::{ContextKind, ContextValue, ErrorKind};
6+
7+
/// STAR's `--outSAMtype` format component.
8+
#[derive(Debug, Clone, PartialEq, Eq, Default)]
9+
pub enum OutSamFormat {
10+
#[default]
11+
Sam,
12+
Bam,
13+
None,
14+
}
15+
16+
/// STAR's `--outSAMtype` sort order component (only applies to BAM).
17+
#[derive(Debug, Clone, PartialEq, Eq)]
18+
pub enum OutSamSortOrder {
19+
Unsorted,
20+
SortedByCoordinate,
21+
}
22+
23+
/// Combined `--outSAMtype` value.
24+
#[derive(Debug, Clone, PartialEq, Eq, Default)]
25+
pub struct OutSamType {
26+
pub format: OutSamFormat,
27+
pub sort_order: Option<OutSamSortOrder>,
28+
}
29+
30+
impl std::fmt::Display for OutSamType {
31+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32+
match (&self.format, &self.sort_order) {
33+
(OutSamFormat::Sam, _) => write!(f, "SAM"),
34+
(OutSamFormat::None, _) => write!(f, "None"),
35+
(OutSamFormat::Bam, Some(OutSamSortOrder::SortedByCoordinate)) => {
36+
write!(f, "BAM SortedByCoordinate")
37+
}
38+
(OutSamFormat::Bam, _) => write!(f, "BAM Unsorted"),
39+
}
40+
}
41+
}
42+
43+
impl clap::FromArgMatches for OutSamType {
44+
fn from_arg_matches(matches: &clap::ArgMatches) -> Result<Self, clap::Error> {
45+
let mut s = Self::default();
46+
s.update_from_arg_matches(matches)?;
47+
Ok(s)
48+
}
49+
50+
fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> {
51+
let Some(values) = matches.get_many::<String>("outSAMtype") else {
52+
return Ok(());
53+
};
54+
let tokens: Vec<&str> = values.map(String::as_str).collect();
55+
*self = match tokens.as_slice() {
56+
["SAM"] => Self {
57+
format: OutSamFormat::Sam,
58+
sort_order: None,
59+
},
60+
["None"] => Self {
61+
format: OutSamFormat::None,
62+
sort_order: None,
63+
},
64+
["BAM", "Unsorted"] => Self {
65+
format: OutSamFormat::Bam,
66+
sort_order: Some(OutSamSortOrder::Unsorted),
67+
},
68+
["BAM", "SortedByCoordinate"] => Self {
69+
format: OutSamFormat::Bam,
70+
sort_order: Some(OutSamSortOrder::SortedByCoordinate),
71+
},
72+
other => {
73+
return Err(invalid_multi_arg(
74+
other,
75+
&["SAM", "None", "BAM Unsorted", "BAM SortedByCoordinate"],
76+
));
77+
}
78+
};
79+
Ok(())
80+
}
81+
}
82+
83+
impl clap::Args for OutSamType {
84+
fn augment_args(cmd: clap::Command) -> clap::Command {
85+
cmd.arg(
86+
clap::Arg::new("outSAMtype")
87+
.long("outSAMtype")
88+
.num_args(1..=2)
89+
.default_values(["SAM"])
90+
.help(
91+
"Output type: SAM, BAM Unsorted, BAM SortedByCoordinate, None. \
92+
Provide as space-separated tokens, e.g. \"BAM SortedByCoordinate\".",
93+
),
94+
)
95+
}
96+
97+
fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
98+
Self::augment_args(cmd)
99+
}
100+
}
101+
102+
// ---------------------------------------------------------------------------
103+
// SAM unmapped output
104+
// ---------------------------------------------------------------------------
105+
106+
/// STAR’s `--outSAMunmapped` value
107+
#[derive(Debug, Clone, PartialEq, Eq, Default)]
108+
pub enum OutSamUnmapped {
109+
#[default]
110+
None,
111+
Within,
112+
WithinKeepPairs,
113+
}
114+
115+
impl clap::FromArgMatches for OutSamUnmapped {
116+
fn from_arg_matches(matches: &clap::ArgMatches) -> Result<Self, clap::Error> {
117+
let mut s = Self::default();
118+
s.update_from_arg_matches(matches)?;
119+
Ok(s)
120+
}
121+
122+
fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> {
123+
let Some(values) = matches.get_many::<String>("outSAMunmapped") else {
124+
return Ok(());
125+
};
126+
let tokens: Vec<&str> = values.map(String::as_str).collect();
127+
*self = match tokens.as_slice() {
128+
["None"] => Self::None,
129+
["Within"] => Self::Within,
130+
["Within", "KeepPairs"] => Self::WithinKeepPairs,
131+
other => {
132+
return Err(invalid_multi_arg(
133+
other,
134+
&["None", "Within", "Within KeepPairs"],
135+
));
136+
}
137+
};
138+
Ok(())
139+
}
140+
}
141+
142+
impl clap::Args for OutSamUnmapped {
143+
fn augment_args(cmd: clap::Command) -> clap::Command {
144+
cmd.arg(
145+
clap::Arg::new("outSAMunmapped")
146+
.long("outSAMunmapped")
147+
.num_args(1..=2)
148+
.default_values(["None"])
149+
.help(
150+
"Unmapped reads in SAM output: None, Within, or Within KeepPairs. \
151+
Provide as space-separated tokens.",
152+
),
153+
)
154+
}
155+
156+
fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
157+
Self::augment_args(cmd)
158+
}
159+
}
160+
161+
// Helpers
162+
163+
fn invalid_multi_arg(other: &[&str], valid: &[&str]) -> clap::error::Error {
164+
let mut err = clap::Error::new(ErrorKind::InvalidValue);
165+
err.insert(
166+
ContextKind::InvalidArg,
167+
ContextValue::String("--outSAMtype".into()),
168+
);
169+
err.insert(
170+
ContextKind::InvalidValue,
171+
ContextValue::String(other.join(" ")),
172+
);
173+
err.insert(
174+
ContextKind::ValidValue,
175+
// replace spaces with an invisible non-whitespace character to prevent clap from adding quotes
176+
ContextValue::Strings(valid.iter().map(|s| s.replace(' ', "\u{2800}")).collect()),
177+
);
178+
err
179+
}

0 commit comments

Comments
 (0)