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.
data/src/model.rs ADDED
@@ -0,0 +1,123 @@
1
+ use anyhow::{Result, bail};
2
+ use serde::{Deserialize, Serialize};
3
+
4
+ /// Canonical internal representation. `start == end` represents an insertion
5
+ /// between bases; all other records use a zero-based half-open interval.
6
+ #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
7
+ pub struct Variant {
8
+ pub chrom: String,
9
+ pub output_chrom: String,
10
+ pub start: u64,
11
+ pub end: u64,
12
+ pub reference: String,
13
+ pub alternate: String,
14
+ pub extra: Vec<String>,
15
+ pub source_line: usize,
16
+ pub source_record: Option<usize>,
17
+ pub allele_index: usize,
18
+ }
19
+
20
+ impl Variant {
21
+ pub fn new(
22
+ chrom: impl Into<String>,
23
+ start: u64,
24
+ end: u64,
25
+ reference: impl Into<String>,
26
+ alternate: impl Into<String>,
27
+ ) -> Result<Self> {
28
+ if end < start {
29
+ bail!("variant end ({end}) precedes start ({start})");
30
+ }
31
+ let chrom = normalize_chrom(&chrom.into());
32
+ Ok(Self {
33
+ output_chrom: chrom.clone(),
34
+ chrom,
35
+ start,
36
+ end,
37
+ reference: reference.into().to_ascii_uppercase(),
38
+ alternate: alternate.into().to_ascii_uppercase(),
39
+ extra: Vec::new(),
40
+ source_line: 0,
41
+ source_record: None,
42
+ allele_index: 0,
43
+ })
44
+ }
45
+
46
+ pub fn key(&self) -> VariantKey<'_> {
47
+ VariantKey {
48
+ chrom: &self.chrom,
49
+ start: self.start,
50
+ end: self.end,
51
+ reference: &self.reference,
52
+ alternate: &self.alternate,
53
+ }
54
+ }
55
+
56
+ pub fn avinput_fields(&self) -> [String; 5] {
57
+ let (start, end, reference, alternate) = if self.start == self.end {
58
+ (
59
+ self.start,
60
+ self.start,
61
+ "-".to_string(),
62
+ display_allele(&self.alternate),
63
+ )
64
+ } else {
65
+ (
66
+ self.start + 1,
67
+ self.end,
68
+ display_allele(&self.reference),
69
+ display_allele(&self.alternate),
70
+ )
71
+ };
72
+ [
73
+ self.output_chrom.clone(),
74
+ start.to_string(),
75
+ end.to_string(),
76
+ reference,
77
+ alternate,
78
+ ]
79
+ }
80
+
81
+ pub fn overlaps(&self, start: u64, end: u64) -> bool {
82
+ if self.start == self.end {
83
+ self.start >= start && self.start <= end
84
+ } else {
85
+ self.start < end && start < self.end
86
+ }
87
+ }
88
+ }
89
+
90
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
91
+ pub struct VariantKey<'a> {
92
+ pub chrom: &'a str,
93
+ pub start: u64,
94
+ pub end: u64,
95
+ pub reference: &'a str,
96
+ pub alternate: &'a str,
97
+ }
98
+
99
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100
+ pub enum AnnotationKind {
101
+ Filter,
102
+ Region,
103
+ Gene,
104
+ }
105
+
106
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107
+ pub struct Annotation {
108
+ pub protocol: String,
109
+ pub kind: AnnotationKind,
110
+ pub values: Vec<String>,
111
+ }
112
+
113
+ pub fn normalize_chrom(chrom: &str) -> String {
114
+ chrom.strip_prefix("chr").unwrap_or(chrom).to_string()
115
+ }
116
+
117
+ fn display_allele(allele: &str) -> String {
118
+ if allele.is_empty() {
119
+ "-".to_string()
120
+ } else {
121
+ allele.to_string()
122
+ }
123
+ }
data/src/pipeline.rs ADDED
@@ -0,0 +1,288 @@
1
+ use crate::database::{FilterDatabase, RegionDatabase};
2
+ use crate::gene::GeneDatabase;
3
+ use crate::io::{VcfDocument, open_writer};
4
+ use crate::model::{Annotation, Variant};
5
+ use anyhow::{Context, Result, bail};
6
+ use rayon::prelude::*;
7
+ use std::collections::HashMap;
8
+ use std::io::Write;
9
+ use std::path::{Path, PathBuf};
10
+
11
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
12
+ pub enum Operation {
13
+ Gene,
14
+ Region,
15
+ Filter,
16
+ }
17
+
18
+ impl Operation {
19
+ pub fn parse(value: &str) -> Result<Self> {
20
+ match value {
21
+ "g" | "gx" | "gene" => Ok(Self::Gene),
22
+ "r" | "region" => Ok(Self::Region),
23
+ "f" | "filter" => Ok(Self::Filter),
24
+ _ => bail!("unknown operation {value:?}; use g, r, or f"),
25
+ }
26
+ }
27
+ }
28
+
29
+ #[derive(Debug, Clone)]
30
+ pub struct Protocol {
31
+ pub name: String,
32
+ pub operation: Operation,
33
+ pub database: PathBuf,
34
+ pub fasta: Option<PathBuf>,
35
+ }
36
+
37
+ #[derive(Debug)]
38
+ pub struct TableResult {
39
+ pub headers: Vec<String>,
40
+ pub rows: Vec<Vec<String>>,
41
+ }
42
+
43
+ pub fn resolve_protocols(
44
+ db_dir: &Path,
45
+ build: &str,
46
+ names: &[String],
47
+ operations: &[Operation],
48
+ ) -> Result<Vec<Protocol>> {
49
+ if names.len() != operations.len() {
50
+ bail!("protocol and operation counts differ");
51
+ }
52
+ names
53
+ .iter()
54
+ .zip(operations)
55
+ .map(|(name, operation)| {
56
+ let database = db_dir.join(format!("{build}_{name}.txt"));
57
+ if !database.exists() {
58
+ bail!("database does not exist: {}", database.display());
59
+ }
60
+ let fasta = (*operation == Operation::Gene)
61
+ .then(|| db_dir.join(format!("{build}_{name}Mrna.fa")))
62
+ .filter(|p| p.exists());
63
+ Ok(Protocol {
64
+ name: name.clone(),
65
+ operation: *operation,
66
+ database,
67
+ fasta,
68
+ })
69
+ })
70
+ .collect()
71
+ }
72
+
73
+ pub fn annotate_table(
74
+ variants: &[Variant],
75
+ protocols: &[Protocol],
76
+ nastring: &str,
77
+ ) -> Result<TableResult> {
78
+ enum Loaded {
79
+ Filter(FilterDatabase),
80
+ Region(RegionDatabase),
81
+ Gene(GeneDatabase),
82
+ }
83
+ let mut loaded = Vec::new();
84
+ let mut headers = vec!["Chr", "Start", "End", "Ref", "Alt"]
85
+ .into_iter()
86
+ .map(str::to_string)
87
+ .collect::<Vec<_>>();
88
+ for protocol in protocols {
89
+ let database = match protocol.operation {
90
+ Operation::Filter => {
91
+ let db = FilterDatabase::load_for_variants(&protocol.database, variants)?;
92
+ headers.extend(
93
+ db.headers
94
+ .iter()
95
+ .map(|header| qualified_header(header, &protocol.name)),
96
+ );
97
+ Loaded::Filter(db)
98
+ }
99
+ Operation::Region => {
100
+ let db = RegionDatabase::load(&protocol.database)?;
101
+ headers.extend(
102
+ db.headers
103
+ .iter()
104
+ .map(|header| qualified_header(header, &protocol.name)),
105
+ );
106
+ Loaded::Region(db)
107
+ }
108
+ Operation::Gene => {
109
+ headers.extend(
110
+ ["Func", "Gene", "GeneDetail", "ExonicFunc", "AAChange"]
111
+ .map(|header| format!("{header}.{}", protocol.name)),
112
+ );
113
+ Loaded::Gene(GeneDatabase::load(
114
+ &protocol.database,
115
+ protocol.fasta.as_deref(),
116
+ )?)
117
+ }
118
+ };
119
+ loaded.push(database);
120
+ }
121
+ let extra_width = variants
122
+ .iter()
123
+ .map(|variant| variant.extra.len())
124
+ .max()
125
+ .unwrap_or(0);
126
+ headers.extend((1..=extra_width).map(|index| format!("Otherinfo{index}")));
127
+ let rows = variants
128
+ .par_iter()
129
+ .map(|variant| {
130
+ let mut row = variant.avinput_fields().to_vec();
131
+ for (protocol, database) in protocols.iter().zip(&loaded) {
132
+ let (annotation, width) = match database {
133
+ Loaded::Filter(db) => (db.annotate(variant, &protocol.name), db.headers.len()),
134
+ Loaded::Region(db) => {
135
+ (db.annotate(variant, &protocol.name, 0.0), db.headers.len())
136
+ }
137
+ Loaded::Gene(db) => (Some(db.annotate(variant, &protocol.name, 2, 1000)), 5),
138
+ };
139
+ append_annotation(&mut row, annotation, width, nastring);
140
+ }
141
+ row.extend(variant.extra.clone());
142
+ while row.len() < headers.len() {
143
+ row.push(nastring.to_string());
144
+ }
145
+ row
146
+ })
147
+ .collect();
148
+ Ok(TableResult { headers, rows })
149
+ }
150
+
151
+ pub fn write_table(result: &TableResult, path: &Path, csv: bool) -> Result<()> {
152
+ let mut writer = open_writer(path)?;
153
+ let delimiter = if csv { ',' } else { '\t' };
154
+ write_record(&mut writer, &result.headers, delimiter)?;
155
+ for row in &result.rows {
156
+ write_record(&mut writer, row, delimiter)?;
157
+ }
158
+ Ok(())
159
+ }
160
+
161
+ pub fn write_annotated_vcf(
162
+ document: &VcfDocument,
163
+ result: &TableResult,
164
+ protocols: &[Protocol],
165
+ path: &Path,
166
+ nastring: &str,
167
+ ) -> Result<()> {
168
+ let mut writer = open_writer(path)?;
169
+ for header in &document.headers {
170
+ if header.starts_with("#CHROM") {
171
+ for protocol in protocols {
172
+ writeln!(
173
+ writer,
174
+ "##INFO=<ID=FA_{},Number=.,Type=String,Description=\"RustAnnovar {} annotation\">",
175
+ sanitize_info(&protocol.name),
176
+ protocol.name
177
+ )?;
178
+ }
179
+ }
180
+ writeln!(writer, "{header}")?;
181
+ }
182
+ let mut by_record: HashMap<usize, Vec<&Vec<String>>> = HashMap::new();
183
+ for (variant, row) in document.variants.iter().zip(&result.rows) {
184
+ if let Some(record) = variant.source_record {
185
+ by_record.entry(record).or_default().push(row);
186
+ }
187
+ }
188
+ let mut offsets = Vec::new();
189
+ let mut cursor = 5usize;
190
+ for protocol in protocols {
191
+ let width = match protocol.operation {
192
+ Operation::Gene => 5,
193
+ _ => result.headers[cursor..]
194
+ .iter()
195
+ .take_while(|h| h.ends_with(&format!(".{}", protocol.name)))
196
+ .count()
197
+ .max(1),
198
+ };
199
+ offsets.push((cursor, width));
200
+ cursor += width;
201
+ }
202
+ for (record_index, original) in document.records.iter().enumerate() {
203
+ let mut record = original.clone();
204
+ if let Some(rows) = by_record.get(&record_index) {
205
+ let mut additions = Vec::new();
206
+ for (protocol, (start, width)) in protocols.iter().zip(&offsets) {
207
+ let alleles = rows
208
+ .iter()
209
+ .map(|row| row[*start..*start + *width].join("|"))
210
+ .collect::<Vec<_>>()
211
+ .join(",");
212
+ if alleles != nastring {
213
+ additions.push(format!(
214
+ "FA_{}={}",
215
+ sanitize_info(&protocol.name),
216
+ escape_info(&alleles)
217
+ ));
218
+ }
219
+ }
220
+ if !additions.is_empty() {
221
+ if record[7] == "." {
222
+ record[7].clear();
223
+ } else {
224
+ record[7].push(';');
225
+ }
226
+ record[7].push_str(&additions.join(";"));
227
+ }
228
+ }
229
+ writeln!(writer, "{}", record.join("\t"))?;
230
+ }
231
+ Ok(())
232
+ }
233
+
234
+ fn append_annotation(
235
+ row: &mut Vec<String>,
236
+ annotation: Option<Annotation>,
237
+ width: usize,
238
+ nastring: &str,
239
+ ) {
240
+ if let Some(annotation) = annotation {
241
+ let before = row.len();
242
+ row.extend(annotation.values);
243
+ while row.len() < before + width {
244
+ row.push(nastring.to_string());
245
+ }
246
+ } else {
247
+ row.extend(std::iter::repeat_n(nastring.to_string(), width));
248
+ }
249
+ }
250
+
251
+ fn qualified_header(header: &str, protocol: &str) -> String {
252
+ if header == "value" {
253
+ protocol.to_string()
254
+ } else {
255
+ format!("{header}.{protocol}")
256
+ }
257
+ }
258
+
259
+ fn write_record(writer: &mut dyn Write, fields: &[String], delimiter: char) -> Result<()> {
260
+ let encoded = fields
261
+ .iter()
262
+ .map(|value| {
263
+ if delimiter == ','
264
+ && (value.contains(',') || value.contains('"') || value.contains('\n'))
265
+ {
266
+ format!("\"{}\"", value.replace('"', "\"\""))
267
+ } else {
268
+ value.clone()
269
+ }
270
+ })
271
+ .collect::<Vec<_>>();
272
+ writeln!(writer, "{}", encoded.join(&delimiter.to_string())).context("failed writing output")
273
+ }
274
+ fn sanitize_info(value: &str) -> String {
275
+ value
276
+ .chars()
277
+ .map(|c| {
278
+ if c.is_ascii_alphanumeric() || c == '_' {
279
+ c
280
+ } else {
281
+ '_'
282
+ }
283
+ })
284
+ .collect()
285
+ }
286
+ fn escape_info(value: &str) -> String {
287
+ value.replace([' ', ';', '='], "_")
288
+ }
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rust-annovar
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.beta.1
5
+ platform: ruby
6
+ authors:
7
+ - Longtao Huangfu
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-09-15 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |-
14
+ RustAnnovar is an open-beta genomic variant annotation command-line tool.
15
+ This source gem compiles the Rust executable during installation and reads
16
+ supported ANNOVAR humandb formats supplied separately by the user.
17
+ email:
18
+ - ydlongtao@gmail.com
19
+ executables:
20
+ - rust-annovar
21
+ extensions:
22
+ - ext/rust_annovar/extconf.rb
23
+ extra_rdoc_files: []
24
+ files:
25
+ - Cargo.lock
26
+ - Cargo.toml
27
+ - LICENSE-APACHE
28
+ - LICENSE-MIT
29
+ - README.md
30
+ - bin/rust-annovar
31
+ - ext/rust_annovar/extconf.rb
32
+ - lib/rust_annovar.rb
33
+ - lib/rust_annovar/version.rb
34
+ - src/database.rs
35
+ - src/gene.rs
36
+ - src/io.rs
37
+ - src/lib.rs
38
+ - src/main.rs
39
+ - src/model.rs
40
+ - src/pipeline.rs
41
+ homepage: https://github.com/ydlongtao/RustAnnovar
42
+ licenses:
43
+ - MIT
44
+ - Apache-2.0
45
+ metadata:
46
+ source_code_uri: https://github.com/ydlongtao/RustAnnovar
47
+ documentation_uri: https://github.com/ydlongtao/RustAnnovar#readme
48
+ bug_tracker_uri: https://github.com/ydlongtao/RustAnnovar/issues
49
+ post_install_message:
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '2.6'
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">"
61
+ - !ruby/object:Gem::Version
62
+ version: 1.3.1
63
+ requirements: []
64
+ rubygems_version: 3.0.3.1
65
+ signing_key:
66
+ specification_version: 4
67
+ summary: Rust implementation of core ANNOVAR-compatible workflows
68
+ test_files: []