Rust 读取示例

下面示例面向第三方工具开发者:快速把 `.gen` 或 `.gjson` 读成一个轻量的序列对象。生产环境可以在这些代码基础上补充错误类型、schema 迁移和字段校验。

rusqlite serde_json 1-based

Cargo

依赖配置

[dependencies]
anyhow = "1"
rusqlite = { version = "0.32", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

bundled 会编译内置 SQLite,无需目标机预装 SQLite,便于跨平台分发 CLI 工具。版本参考:rusqlite 0.32serde 1serde_json 1anyhow 1

SQLite

读取 .gen

use anyhow::Result;
use rusqlite::{Connection, OptionalExtension};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::File;
use std::io::Read;
use std::path::Path;

#[derive(Debug, Serialize, Deserialize)]
struct FeatureSegment {
    start: i64,
    end: i64,
    strand: Option<String>,
    name: Option<String>,
    color: Option<String>,
}

#[derive(Debug)]
struct Feature {
    id: String,
    name: String,
    feature_type: String,
    start: i64,
    end: i64,
    strand: String,
    color: Option<String>,
    label: Option<String>,
    note: Option<String>,
    frame: Option<i64>,
    visible: bool,
    segments: Vec<FeatureSegment>,
    qualifiers: HashMap<String, Vec<String>>,
}

#[derive(Debug)]
struct GeneProject {
    metadata: HashMap<String, String>,
    sequence: String,
    features: Vec<Feature>,
}

fn validate_sqlite_header(path: &Path) -> Result<()> {
    let mut file = File::open(path)?;
    let mut header = [0_u8; 16];
    file.read_exact(&mut header)?;
    anyhow::ensure!(&header == b"SQLite format 3\0", "not a .gen SQLite file");
    Ok(())
}

fn read_metadata(conn: &Connection) -> Result<HashMap<String, String>> {
    let mut stmt = conn.prepare("SELECT key, value FROM project_meta")?;
    let rows = stmt.query_map([], |row| {
        Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
    })?;

    let mut metadata = HashMap::new();
    for row in rows {
        let (key, value) = row?;
        metadata.insert(key, value);
    }
    Ok(metadata)
}

fn read_sequence(conn: &Connection) -> Result<String> {
    let mut stmt = conn.prepare(
        "SELECT bases FROM sequence_blocks ORDER BY block_index"
    )?;
    let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;

    let mut sequence = String::new();
    for row in rows {
        sequence.push_str(&row?);
    }
    Ok(sequence)
}

fn parse_json_or_default<T>(raw: Option<String>) -> Result<T>
where
    T: serde::de::DeserializeOwned + Default,
{
    match raw {
        Some(value) if !value.trim().is_empty() => Ok(serde_json::from_str(&value)?),
        _ => Ok(T::default()),
    }
}

fn read_features(conn: &Connection) -> Result<Vec<Feature>> {
    let mut stmt = conn.prepare(
        "SELECT id, name, type, start_pos, end_pos, strand, color, label, note,
                frame, visible, segments, qualifiers
         FROM features
         ORDER BY start_pos"
    )?;

    let rows = stmt.query_map([], |row| {
        let segments_raw: Option<String> = row.get(11)?;
        let qualifiers_raw: Option<String> = row.get(12)?;
        let segments = parse_json_or_default(segments_raw)
            .map_err(|e| rusqlite::Error::ToSqlConversionFailure(e.into()))?;
        let qualifiers = parse_json_or_default(qualifiers_raw)
            .map_err(|e| rusqlite::Error::ToSqlConversionFailure(e.into()))?;

        Ok(Feature {
            id: row.get(0)?,
            name: row.get(1)?,
            feature_type: row.get(2)?,
            start: row.get(3)?,
            end: row.get(4)?,
            strand: row.get(5)?,
            color: row.get(6)?,
            label: row.get(7)?,
            note: row.get(8)?,
            frame: row.get(9)?,
            visible: row.get::<_, i64>(10)? != 0,
            segments,
            qualifiers,
        })
    })?;

    let mut features = Vec::new();
    for row in rows {
        features.push(row?);
    }
    Ok(features)
}

fn read_gen(path: impl AsRef<Path>) -> Result<GeneProject> {
    let path = path.as_ref();
    validate_sqlite_header(path)?;
    let conn = Connection::open(path)?;

    Ok(GeneProject {
        metadata: read_metadata(&conn)?,
        sequence: read_sequence(&conn)?,
        features: read_features(&conn)?,
    })
}

fn main() -> Result<()> {
    let project = read_gen("example.gen")?;
    println!("name: {:?}", project.metadata.get("name"));
    println!("length: {}", project.sequence.len());
    println!("features: {}", project.features.len());
    Ok(())
}

如果需要读取 primers、base_color_ranges、alignment_entries,可按同样方式查询对应表。坐标已经是 1-based inclusive,无需转换。

预期输出(对一个含若干 feature 的 example.gen):

name: Some("pUC19")
length: 2686
features: 12

JSON

读取 .gjson

use anyhow::Result;
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;

#[derive(Debug, Deserialize, Default)]
struct GjsonFeatureSegment {
    start: i64,
    end: i64,
    strand: Option<String>,
    name: Option<String>,
    color: Option<String>,
}

#[derive(Debug, Deserialize)]
struct GjsonFeature {
    id: String,
    name: String,
    #[serde(rename = "type")]
    feature_type: String,
    start: i64,
    end: i64,
    strand: String,
    color: Option<String>,
    label: Option<String>,
    note: Option<String>,
    frame: Option<i64>,
    #[serde(default = "default_true")]
    visible: bool,
    #[serde(default)]
    segments: Vec<GjsonFeatureSegment>,
    #[serde(default)]
    qualifiers: HashMap<String, Vec<String>>,
}

#[derive(Debug, Deserialize)]
struct GjsonFile {
    version: String,
    sequence: String,
    #[serde(default)]
    custom_name: String,
    #[serde(default)]
    description: String,
    #[serde(default)]
    isCircular: bool,
    isDoubleStranded: Option<bool>,
    accession: Option<String>,
    organism: Option<String>,
    date: Option<String>,
    moleculeType: Option<String>,
    division: Option<String>,
    gbVersion: Option<String>,
    keywords: Option<String>,
    source: Option<String>,
    comments: Option<String>,
    #[serde(default)]
    features: Vec<GjsonFeature>,
}

fn default_true() -> bool {
    true
}

fn read_gjson(path: &str) -> Result<GjsonFile> {
    let text = fs::read_to_string(path)?;
    let file: GjsonFile = serde_json::from_str(&text)?;
    anyhow::ensure!(!file.version.is_empty(), "missing gjson version");
    anyhow::ensure!(!file.sequence.is_empty(), "missing sequence");
    Ok(file)
}

fn main() -> Result<()> {
    let file = read_gjson("example.gjson")?;
    println!("version: {}", file.version);
    println!("length: {}", file.sequence.len());
    println!("features: {}", file.features.len());
    Ok(())
}

`.gjson` 中的 length 只是写出时的辅助值,读取程序应以 sequence.len() 为准。

预期输出(对一个有效 example.gjson):

version: 1.9
length: 8
features: 1