rust-annovar 0.1.0.beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Cargo.lock +1034 -0
- data/Cargo.toml +33 -0
- data/LICENSE-APACHE +15 -0
- data/LICENSE-MIT +21 -0
- data/README.md +298 -0
- data/bin/rust-annovar +6 -0
- data/ext/rust_annovar/extconf.rb +34 -0
- data/lib/rust_annovar/version.rb +5 -0
- data/lib/rust_annovar.rb +35 -0
- data/src/database.rs +592 -0
- data/src/gene.rs +520 -0
- data/src/io.rs +217 -0
- data/src/lib.rs +12 -0
- data/src/main.rs +376 -0
- data/src/model.rs +123 -0
- data/src/pipeline.rs +288 -0
- metadata +68 -0
data/src/database.rs
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
use crate::model::{Annotation, AnnotationKind, Variant, normalize_chrom};
|
|
2
|
+
use anyhow::{Context, Result, bail};
|
|
3
|
+
use serde::{Deserialize, Serialize};
|
|
4
|
+
use sha2::{Digest, Sha256};
|
|
5
|
+
use std::collections::{BTreeMap, HashMap, HashSet};
|
|
6
|
+
use std::fs::{self, File};
|
|
7
|
+
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
|
|
8
|
+
use std::path::{Path, PathBuf};
|
|
9
|
+
use std::time::UNIX_EPOCH;
|
|
10
|
+
|
|
11
|
+
#[derive(Debug, Clone)]
|
|
12
|
+
pub struct FilterRecord {
|
|
13
|
+
pub variant: Variant,
|
|
14
|
+
pub values: Vec<String>,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#[derive(Debug, Clone)]
|
|
18
|
+
pub struct RegionRecord {
|
|
19
|
+
pub chrom: String,
|
|
20
|
+
pub start: u64,
|
|
21
|
+
pub end: u64,
|
|
22
|
+
pub values: Vec<String>,
|
|
23
|
+
pub score: Option<f64>,
|
|
24
|
+
pub name: Option<String>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#[derive(Debug)]
|
|
28
|
+
pub struct FilterDatabase {
|
|
29
|
+
pub headers: Vec<String>,
|
|
30
|
+
records: HashMap<OwnedKey, Vec<Vec<String>>>,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
34
|
+
struct OwnedKey {
|
|
35
|
+
chrom: String,
|
|
36
|
+
start: u64,
|
|
37
|
+
end: u64,
|
|
38
|
+
reference: String,
|
|
39
|
+
alternate: String,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
impl FilterDatabase {
|
|
43
|
+
pub fn load(path: &Path) -> Result<Self> {
|
|
44
|
+
let reader = open_text(path)?;
|
|
45
|
+
let mut headers = Vec::new();
|
|
46
|
+
let mut records: HashMap<OwnedKey, Vec<Vec<String>>> = HashMap::new();
|
|
47
|
+
for (line_no, line) in reader.lines().enumerate() {
|
|
48
|
+
let line = line?;
|
|
49
|
+
if line.trim().is_empty() {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if line.starts_with('#') {
|
|
53
|
+
if headers.is_empty() && !line.starts_with("##") {
|
|
54
|
+
headers = line
|
|
55
|
+
.trim_start_matches('#')
|
|
56
|
+
.split('\t')
|
|
57
|
+
.skip(5)
|
|
58
|
+
.map(str::to_string)
|
|
59
|
+
.collect();
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
let fields: Vec<&str> = line.split('\t').collect();
|
|
64
|
+
if fields.len() < 5 {
|
|
65
|
+
bail!(
|
|
66
|
+
"{}:{}: filter database needs Chr,Start,End,Ref,Alt",
|
|
67
|
+
path.display(),
|
|
68
|
+
line_no + 1
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
let start: u64 = fields[1]
|
|
72
|
+
.parse()
|
|
73
|
+
.with_context(|| format!("invalid database start at line {}", line_no + 1))?;
|
|
74
|
+
let end: u64 = fields[2]
|
|
75
|
+
.parse()
|
|
76
|
+
.with_context(|| format!("invalid database end at line {}", line_no + 1))?;
|
|
77
|
+
let reference = db_allele(fields[3]);
|
|
78
|
+
let alternate = db_allele(fields[4]);
|
|
79
|
+
let internal_start = if reference.is_empty() {
|
|
80
|
+
start
|
|
81
|
+
} else {
|
|
82
|
+
start
|
|
83
|
+
.checked_sub(1)
|
|
84
|
+
.context("database coordinates must be one-based")?
|
|
85
|
+
};
|
|
86
|
+
let internal_end = if reference.is_empty() { start } else { end };
|
|
87
|
+
let key = OwnedKey {
|
|
88
|
+
chrom: normalize_chrom(fields[0]),
|
|
89
|
+
start: internal_start,
|
|
90
|
+
end: internal_end,
|
|
91
|
+
reference,
|
|
92
|
+
alternate,
|
|
93
|
+
};
|
|
94
|
+
records
|
|
95
|
+
.entry(key)
|
|
96
|
+
.or_default()
|
|
97
|
+
.push(fields[5..].iter().map(|v| (*v).to_string()).collect());
|
|
98
|
+
}
|
|
99
|
+
if headers.is_empty() {
|
|
100
|
+
let width = records
|
|
101
|
+
.values()
|
|
102
|
+
.next()
|
|
103
|
+
.and_then(|v| v.first())
|
|
104
|
+
.map_or(1, Vec::len);
|
|
105
|
+
headers = (1..=width)
|
|
106
|
+
.map(|i| {
|
|
107
|
+
if width == 1 {
|
|
108
|
+
"value".into()
|
|
109
|
+
} else {
|
|
110
|
+
format!("value{i}")
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
.collect();
|
|
114
|
+
if headers.is_empty() {
|
|
115
|
+
headers.push("match".to_string());
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
Ok(Self { headers, records })
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Load only source-file bins needed by the query when a current
|
|
122
|
+
/// `.fai.json` sidecar is available. Falls back to a full load otherwise.
|
|
123
|
+
pub fn load_for_variants(path: &Path, variants: &[Variant]) -> Result<Self> {
|
|
124
|
+
let index_path = path.with_extension("fai.json");
|
|
125
|
+
let manifest = File::open(&index_path)
|
|
126
|
+
.ok()
|
|
127
|
+
.and_then(|file| serde_json::from_reader::<_, IndexManifest>(file).ok());
|
|
128
|
+
let Some(manifest) = manifest else {
|
|
129
|
+
return Self::load(path);
|
|
130
|
+
};
|
|
131
|
+
if manifest.bins.is_empty()
|
|
132
|
+
|| !manifest.is_current()?
|
|
133
|
+
|| path.extension().is_some_and(|ext| ext == "gz")
|
|
134
|
+
{
|
|
135
|
+
return Self::load(path);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let mut requested = HashSet::new();
|
|
139
|
+
for variant in variants {
|
|
140
|
+
requested.insert((variant.chrom.clone(), variant.start / manifest.bin_size));
|
|
141
|
+
requested.insert((
|
|
142
|
+
variant.chrom.clone(),
|
|
143
|
+
variant.start.saturating_sub(1) / manifest.bin_size,
|
|
144
|
+
));
|
|
145
|
+
}
|
|
146
|
+
let mut ranges = requested
|
|
147
|
+
.into_iter()
|
|
148
|
+
.filter_map(|(chrom, bin)| manifest.bins.get(&chrom)?.get(&bin).copied())
|
|
149
|
+
.collect::<Vec<_>>();
|
|
150
|
+
ranges.sort_by_key(|range| range.start);
|
|
151
|
+
ranges.dedup();
|
|
152
|
+
|
|
153
|
+
let headers = read_filter_headers(path)?;
|
|
154
|
+
let mut records: HashMap<OwnedKey, Vec<Vec<String>>> = HashMap::new();
|
|
155
|
+
let mut file = File::open(path)?;
|
|
156
|
+
for range in ranges {
|
|
157
|
+
file.seek(SeekFrom::Start(range.start))?;
|
|
158
|
+
let mut bytes = vec![0; usize::try_from(range.end - range.start)?];
|
|
159
|
+
file.read_exact(&mut bytes)?;
|
|
160
|
+
for line in String::from_utf8(bytes)?.lines() {
|
|
161
|
+
if line.is_empty() || line.starts_with('#') {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
let (key, values) = parse_filter_record(line)
|
|
165
|
+
.with_context(|| format!("invalid indexed record in {}", path.display()))?;
|
|
166
|
+
records.entry(key).or_default().push(values);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
let headers = infer_filter_headers(headers, &records);
|
|
170
|
+
Ok(Self { headers, records })
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
pub fn annotate(&self, variant: &Variant, protocol: &str) -> Option<Annotation> {
|
|
174
|
+
let key = OwnedKey {
|
|
175
|
+
chrom: variant.chrom.clone(),
|
|
176
|
+
start: variant.start,
|
|
177
|
+
end: variant.end,
|
|
178
|
+
reference: variant.reference.clone(),
|
|
179
|
+
alternate: variant.alternate.clone(),
|
|
180
|
+
};
|
|
181
|
+
self.records.get(&key).map(|matches| Annotation {
|
|
182
|
+
protocol: protocol.to_string(),
|
|
183
|
+
kind: AnnotationKind::Filter,
|
|
184
|
+
values: transpose_join(matches),
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#[derive(Debug)]
|
|
190
|
+
pub struct RegionDatabase {
|
|
191
|
+
pub headers: Vec<String>,
|
|
192
|
+
by_chrom: HashMap<String, Vec<RegionRecord>>,
|
|
193
|
+
gff3: bool,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
impl RegionDatabase {
|
|
197
|
+
pub fn load(path: &Path) -> Result<Self> {
|
|
198
|
+
let reader = open_text(path)?;
|
|
199
|
+
let mut headers = Vec::new();
|
|
200
|
+
let mut by_chrom: HashMap<String, Vec<RegionRecord>> = HashMap::new();
|
|
201
|
+
let mut value_width = 0usize;
|
|
202
|
+
let mut gff3 = false;
|
|
203
|
+
for (line_no, line) in reader.lines().enumerate() {
|
|
204
|
+
let line = line?;
|
|
205
|
+
if line.trim().is_empty() {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if line.starts_with('#') {
|
|
209
|
+
if headers.is_empty() {
|
|
210
|
+
let columns = line.trim_start_matches('#').split('\t').collect::<Vec<_>>();
|
|
211
|
+
let coordinate_columns = if columns.first().is_some_and(|value| *value == "bin")
|
|
212
|
+
{
|
|
213
|
+
4
|
|
214
|
+
} else {
|
|
215
|
+
3
|
|
216
|
+
};
|
|
217
|
+
headers = columns
|
|
218
|
+
.into_iter()
|
|
219
|
+
.skip(coordinate_columns)
|
|
220
|
+
.map(str::to_string)
|
|
221
|
+
.collect();
|
|
222
|
+
}
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
let fields: Vec<&str> = line.split('\t').collect();
|
|
226
|
+
if fields.len() < 3 {
|
|
227
|
+
bail!(
|
|
228
|
+
"{}:{}: region database needs at least three columns",
|
|
229
|
+
path.display(),
|
|
230
|
+
line_no + 1
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
let (chrom, start, end, values, score, name) = if is_gff3(&fields) {
|
|
234
|
+
gff3 = true;
|
|
235
|
+
let start = fields[3]
|
|
236
|
+
.parse::<u64>()?
|
|
237
|
+
.checked_sub(1)
|
|
238
|
+
.context("GFF3 coordinates must be one-based")?;
|
|
239
|
+
(
|
|
240
|
+
normalize_chrom(fields[0]),
|
|
241
|
+
start,
|
|
242
|
+
fields[4].parse()?,
|
|
243
|
+
Vec::new(),
|
|
244
|
+
fields[5].parse::<f64>().ok(),
|
|
245
|
+
Some(gff3_id(fields[8])),
|
|
246
|
+
)
|
|
247
|
+
} else {
|
|
248
|
+
let (chrom_col, start_col, end_col, value_col) = detect_region_columns(&fields)?;
|
|
249
|
+
(
|
|
250
|
+
normalize_chrom(fields[chrom_col]),
|
|
251
|
+
fields[start_col].parse()?,
|
|
252
|
+
fields[end_col].parse()?,
|
|
253
|
+
fields[value_col..]
|
|
254
|
+
.iter()
|
|
255
|
+
.map(|v| (*v).to_string())
|
|
256
|
+
.collect(),
|
|
257
|
+
None,
|
|
258
|
+
None,
|
|
259
|
+
)
|
|
260
|
+
};
|
|
261
|
+
value_width = value_width.max(values.len());
|
|
262
|
+
by_chrom
|
|
263
|
+
.entry(chrom.clone())
|
|
264
|
+
.or_default()
|
|
265
|
+
.push(RegionRecord {
|
|
266
|
+
chrom,
|
|
267
|
+
start,
|
|
268
|
+
end,
|
|
269
|
+
values,
|
|
270
|
+
score,
|
|
271
|
+
name,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
for records in by_chrom.values_mut() {
|
|
275
|
+
records.sort_by_key(|record| record.start);
|
|
276
|
+
}
|
|
277
|
+
if gff3 {
|
|
278
|
+
headers = vec!["value".to_string()];
|
|
279
|
+
} else if headers.is_empty() {
|
|
280
|
+
headers = (1..=value_width.max(1))
|
|
281
|
+
.map(|index| {
|
|
282
|
+
if value_width <= 1 {
|
|
283
|
+
"value".to_string()
|
|
284
|
+
} else {
|
|
285
|
+
format!("value{index}")
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
.collect();
|
|
289
|
+
} else {
|
|
290
|
+
while headers.len() < value_width {
|
|
291
|
+
headers.push(format!("value{}", headers.len() + 1));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
Ok(Self {
|
|
295
|
+
headers,
|
|
296
|
+
by_chrom,
|
|
297
|
+
gff3,
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
pub fn annotate(
|
|
302
|
+
&self,
|
|
303
|
+
variant: &Variant,
|
|
304
|
+
protocol: &str,
|
|
305
|
+
min_overlap: f64,
|
|
306
|
+
) -> Option<Annotation> {
|
|
307
|
+
let records = self.by_chrom.get(&variant.chrom)?;
|
|
308
|
+
let query_len = (variant.end.saturating_sub(variant.start)).max(1) as f64;
|
|
309
|
+
let mut hits: Vec<&RegionRecord> = Vec::new();
|
|
310
|
+
for record in records {
|
|
311
|
+
if record.start > variant.end {
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
if variant.overlaps(record.start, record.end) {
|
|
315
|
+
let overlap = variant
|
|
316
|
+
.end
|
|
317
|
+
.min(record.end)
|
|
318
|
+
.saturating_sub(variant.start.max(record.start))
|
|
319
|
+
.max(1) as f64;
|
|
320
|
+
if overlap / query_len >= min_overlap {
|
|
321
|
+
hits.push(record);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if hits.is_empty() {
|
|
326
|
+
return None;
|
|
327
|
+
}
|
|
328
|
+
let values = if self.gff3 {
|
|
329
|
+
let max_score = hits
|
|
330
|
+
.iter()
|
|
331
|
+
.filter_map(|record| record.score)
|
|
332
|
+
.max_by(f64::total_cmp);
|
|
333
|
+
let names = hits
|
|
334
|
+
.iter()
|
|
335
|
+
.filter(|record| max_score.is_none() || record.score == max_score)
|
|
336
|
+
.filter_map(|record| record.name.as_deref())
|
|
337
|
+
.collect::<Vec<_>>()
|
|
338
|
+
.join(",");
|
|
339
|
+
let mut value = String::new();
|
|
340
|
+
if let Some(score) = max_score {
|
|
341
|
+
value.push_str(&format!("Score={score};"));
|
|
342
|
+
}
|
|
343
|
+
if !names.is_empty() {
|
|
344
|
+
value.push_str(&format!("Name={names}"));
|
|
345
|
+
}
|
|
346
|
+
vec![value]
|
|
347
|
+
} else {
|
|
348
|
+
transpose_join(
|
|
349
|
+
&hits
|
|
350
|
+
.iter()
|
|
351
|
+
.map(|record| record.values.clone())
|
|
352
|
+
.collect::<Vec<_>>(),
|
|
353
|
+
)
|
|
354
|
+
};
|
|
355
|
+
Some(Annotation {
|
|
356
|
+
protocol: protocol.to_string(),
|
|
357
|
+
kind: AnnotationKind::Region,
|
|
358
|
+
values,
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
364
|
+
pub struct IndexManifest {
|
|
365
|
+
pub format_version: u32,
|
|
366
|
+
pub source: PathBuf,
|
|
367
|
+
pub source_size: u64,
|
|
368
|
+
pub modified_unix_seconds: u64,
|
|
369
|
+
pub prefix_sha256: String,
|
|
370
|
+
pub kind: String,
|
|
371
|
+
pub bin_size: u64,
|
|
372
|
+
pub bins: HashMap<String, BTreeMap<u64, ByteRange>>,
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
376
|
+
pub struct ByteRange {
|
|
377
|
+
pub start: u64,
|
|
378
|
+
pub end: u64,
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
impl IndexManifest {
|
|
382
|
+
pub fn build(source: &Path, kind: &str) -> Result<Self> {
|
|
383
|
+
let (source_size, modified_unix_seconds, prefix_sha256) = source_fingerprint(source)?;
|
|
384
|
+
let bin_size = 1_000_000;
|
|
385
|
+
let bins = if matches!(kind, "filter" | "generic")
|
|
386
|
+
&& source.extension().is_none_or(|ext| ext != "gz")
|
|
387
|
+
{
|
|
388
|
+
build_filter_bins(source, bin_size)?
|
|
389
|
+
} else {
|
|
390
|
+
HashMap::new()
|
|
391
|
+
};
|
|
392
|
+
Ok(Self {
|
|
393
|
+
format_version: 1,
|
|
394
|
+
source: source.to_path_buf(),
|
|
395
|
+
source_size,
|
|
396
|
+
modified_unix_seconds,
|
|
397
|
+
prefix_sha256,
|
|
398
|
+
kind: kind.to_string(),
|
|
399
|
+
bin_size,
|
|
400
|
+
bins,
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
pub fn write(&self, path: &Path) -> Result<()> {
|
|
405
|
+
let temp = path.with_extension("tmp");
|
|
406
|
+
let mut writer = File::create(&temp)?;
|
|
407
|
+
serde_json::to_writer_pretty(&mut writer, self)?;
|
|
408
|
+
writeln!(writer)?;
|
|
409
|
+
fs::rename(temp, path)?;
|
|
410
|
+
Ok(())
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
pub fn is_current(&self) -> Result<bool> {
|
|
414
|
+
let (size, modified, sha256) = source_fingerprint(&self.source)?;
|
|
415
|
+
Ok(size == self.source_size
|
|
416
|
+
&& modified == self.modified_unix_seconds
|
|
417
|
+
&& sha256 == self.prefix_sha256)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
fn source_fingerprint(source: &Path) -> Result<(u64, u64, String)> {
|
|
422
|
+
let metadata = fs::metadata(source)?;
|
|
423
|
+
let modified = metadata.modified()?.duration_since(UNIX_EPOCH)?.as_secs();
|
|
424
|
+
let mut reader = File::open(source)?;
|
|
425
|
+
let mut limited = std::io::Read::by_ref(&mut reader).take(1024 * 1024);
|
|
426
|
+
let mut hasher = Sha256::new();
|
|
427
|
+
std::io::copy(&mut limited, &mut hasher)?;
|
|
428
|
+
Ok((metadata.len(), modified, format!("{:x}", hasher.finalize())))
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
fn build_filter_bins(
|
|
432
|
+
source: &Path,
|
|
433
|
+
bin_size: u64,
|
|
434
|
+
) -> Result<HashMap<String, BTreeMap<u64, ByteRange>>> {
|
|
435
|
+
let mut reader = BufReader::new(File::open(source)?);
|
|
436
|
+
let mut bins: HashMap<String, BTreeMap<u64, ByteRange>> = HashMap::new();
|
|
437
|
+
let mut line = String::new();
|
|
438
|
+
loop {
|
|
439
|
+
let start = reader.stream_position()?;
|
|
440
|
+
line.clear();
|
|
441
|
+
if reader.read_line(&mut line)? == 0 {
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
let end = reader.stream_position()?;
|
|
445
|
+
if line.trim().is_empty() || line.starts_with('#') {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
let fields = line.split('\t').collect::<Vec<_>>();
|
|
449
|
+
if fields.len() < 2 {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
let Ok(position) = fields[1].parse::<u64>() else {
|
|
453
|
+
continue;
|
|
454
|
+
};
|
|
455
|
+
let chrom = normalize_chrom(fields[0]);
|
|
456
|
+
let bin = position.saturating_sub(1) / bin_size;
|
|
457
|
+
bins.entry(chrom)
|
|
458
|
+
.or_default()
|
|
459
|
+
.entry(bin)
|
|
460
|
+
.and_modify(|range| range.end = end)
|
|
461
|
+
.or_insert(ByteRange { start, end });
|
|
462
|
+
}
|
|
463
|
+
Ok(bins)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
fn open_text(path: &Path) -> Result<Box<dyn BufRead>> {
|
|
467
|
+
crate::io::open_reader(path)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
fn parse_filter_record(line: &str) -> Result<(OwnedKey, Vec<String>)> {
|
|
471
|
+
let fields: Vec<&str> = line.split('\t').collect();
|
|
472
|
+
if fields.len() < 5 {
|
|
473
|
+
bail!("filter database needs Chr,Start,End,Ref,Alt");
|
|
474
|
+
}
|
|
475
|
+
let start: u64 = fields[1].parse()?;
|
|
476
|
+
let end: u64 = fields[2].parse()?;
|
|
477
|
+
let reference = db_allele(fields[3]);
|
|
478
|
+
let alternate = db_allele(fields[4]);
|
|
479
|
+
let internal_start = if reference.is_empty() {
|
|
480
|
+
start
|
|
481
|
+
} else {
|
|
482
|
+
start
|
|
483
|
+
.checked_sub(1)
|
|
484
|
+
.context("database coordinates must be one-based")?
|
|
485
|
+
};
|
|
486
|
+
let internal_end = if reference.is_empty() { start } else { end };
|
|
487
|
+
Ok((
|
|
488
|
+
OwnedKey {
|
|
489
|
+
chrom: normalize_chrom(fields[0]),
|
|
490
|
+
start: internal_start,
|
|
491
|
+
end: internal_end,
|
|
492
|
+
reference,
|
|
493
|
+
alternate,
|
|
494
|
+
},
|
|
495
|
+
fields[5..]
|
|
496
|
+
.iter()
|
|
497
|
+
.map(|value| (*value).to_string())
|
|
498
|
+
.collect(),
|
|
499
|
+
))
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
fn read_filter_headers(path: &Path) -> Result<Vec<String>> {
|
|
503
|
+
for line in open_text(path)?.lines() {
|
|
504
|
+
let line = line?;
|
|
505
|
+
if line.starts_with('#') {
|
|
506
|
+
return Ok(line
|
|
507
|
+
.trim_start_matches('#')
|
|
508
|
+
.split('\t')
|
|
509
|
+
.skip(5)
|
|
510
|
+
.map(str::to_string)
|
|
511
|
+
.collect());
|
|
512
|
+
}
|
|
513
|
+
if !line.trim().is_empty() {
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
Ok(Vec::new())
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
fn infer_filter_headers(
|
|
521
|
+
mut headers: Vec<String>,
|
|
522
|
+
records: &HashMap<OwnedKey, Vec<Vec<String>>>,
|
|
523
|
+
) -> Vec<String> {
|
|
524
|
+
if headers.is_empty() {
|
|
525
|
+
let width = records
|
|
526
|
+
.values()
|
|
527
|
+
.next()
|
|
528
|
+
.and_then(|values| values.first())
|
|
529
|
+
.map_or(1, Vec::len)
|
|
530
|
+
.max(1);
|
|
531
|
+
headers = (1..=width)
|
|
532
|
+
.map(|index| {
|
|
533
|
+
if width == 1 {
|
|
534
|
+
"value".to_string()
|
|
535
|
+
} else {
|
|
536
|
+
format!("value{index}")
|
|
537
|
+
}
|
|
538
|
+
})
|
|
539
|
+
.collect();
|
|
540
|
+
}
|
|
541
|
+
headers
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
fn db_allele(value: &str) -> String {
|
|
545
|
+
if value == "-" || value == "0" {
|
|
546
|
+
String::new()
|
|
547
|
+
} else {
|
|
548
|
+
value.to_ascii_uppercase()
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
fn transpose_join(rows: &[Vec<String>]) -> Vec<String> {
|
|
553
|
+
let width = rows.iter().map(Vec::len).max().unwrap_or(0);
|
|
554
|
+
(0..width)
|
|
555
|
+
.map(|column| {
|
|
556
|
+
rows.iter()
|
|
557
|
+
.filter_map(|row| row.get(column))
|
|
558
|
+
.cloned()
|
|
559
|
+
.collect::<Vec<_>>()
|
|
560
|
+
.join(";")
|
|
561
|
+
})
|
|
562
|
+
.collect()
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
fn detect_region_columns(fields: &[&str]) -> Result<(usize, usize, usize, usize)> {
|
|
566
|
+
if fields.len() >= 4
|
|
567
|
+
&& fields[0].parse::<u64>().is_ok()
|
|
568
|
+
&& fields[2].parse::<u64>().is_ok()
|
|
569
|
+
&& fields[3].parse::<u64>().is_ok()
|
|
570
|
+
{
|
|
571
|
+
Ok((1, 2, 3, 4)) // UCSC bin, chrom, chromStart, chromEnd
|
|
572
|
+
} else if fields[1].parse::<u64>().is_ok() && fields[2].parse::<u64>().is_ok() {
|
|
573
|
+
Ok((0, 1, 2, 3))
|
|
574
|
+
} else {
|
|
575
|
+
bail!("unable to detect region database coordinate columns")
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
fn is_gff3(fields: &[&str]) -> bool {
|
|
580
|
+
fields.len() >= 9
|
|
581
|
+
&& fields[3].parse::<u64>().is_ok()
|
|
582
|
+
&& fields[4].parse::<u64>().is_ok()
|
|
583
|
+
&& matches!(fields[6], "+" | "-" | "." | "?")
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
fn gff3_id(attributes: &str) -> String {
|
|
587
|
+
attributes
|
|
588
|
+
.split(';')
|
|
589
|
+
.find_map(|field| field.trim().strip_prefix("ID="))
|
|
590
|
+
.unwrap_or("NA")
|
|
591
|
+
.to_string()
|
|
592
|
+
}
|