pub mod error;
pub mod issue;
pub mod normalization;
pub mod types;
pub mod validation;
pub use error::A3Error;
pub use issue::{A3Issue, A3IssueCode};
pub use types::{
A3, A3_SCHEMA_URI, A3_VERSION, A3Index, Annotations, FlexEntry, Metadata, RegionEntry,
SiteEntry, VariantRecord,
};
pub use validation::validate;
use serde::Serialize as _;
use serde_json::Value;
pub fn a3_from_json(text: &str) -> Result<A3, A3Error> {
let raw: Value = serde_json::from_str(text)?;
validate(&raw).map_err(A3Error::Validate)
}
pub fn a3_to_json(a3: &A3, indent: Option<usize>) -> Result<String, A3Error> {
match indent {
None => serde_json::to_string(a3).map_err(A3Error::Serialize),
Some(n) => {
let indent_str = " ".repeat(n);
let formatter = serde_json::ser::PrettyFormatter::with_indent(indent_str.as_bytes());
let mut buf = Vec::new();
let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter);
a3.serialize(&mut ser).map_err(A3Error::Serialize)?;
Ok(String::from_utf8(buf).expect("serde_json always produces valid UTF-8"))
}
}
}
pub fn residue_at(a3: &A3, position: u32) -> Option<char> {
if position == 0 || position > a3.sequence().len() as u32 {
return None;
}
a3.sequence()
.as_bytes()
.get((position - 1) as usize)
.map(|&b| b as char)
}
pub fn variants_at(a3: &A3, position: u32) -> Vec<&VariantRecord> {
a3.annotations()
.variant()
.iter()
.filter(|v| v.position() == position)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
const MINIMAL_JSON: &str = r#"{
"$schema": "https://schema.rtemis.org/a3/v1/schema.json",
"a3_version": "1.0.0",
"sequence": "MAEPRQ"
}"#;
#[test]
fn round_trip_is_stable() {
let a3 = a3_from_json(MINIMAL_JSON).unwrap();
let json = a3_to_json(&a3, None).unwrap();
let again = a3_from_json(&json).unwrap();
assert_eq!(a3, again);
}
#[test]
fn canonical_form_materializes_every_default() {
let a3 = a3_from_json(MINIMAL_JSON).unwrap();
let v: Value = serde_json::from_str(&a3_to_json(&a3, None).unwrap()).unwrap();
assert!(v["annotations"]["site"].is_object());
assert!(v["annotations"]["variant"].is_array());
assert_eq!(v["metadata"]["organism"], "");
}
#[test]
fn residue_at_valid_position() {
let a3 = a3_from_json(MINIMAL_JSON).unwrap();
assert_eq!(residue_at(&a3, 1), Some('M'));
assert_eq!(residue_at(&a3, 6), Some('Q'));
}
#[test]
fn residue_at_out_of_bounds() {
let a3 = a3_from_json(MINIMAL_JSON).unwrap();
assert_eq!(residue_at(&a3, 0), None);
assert_eq!(residue_at(&a3, 99), None);
}
#[test]
fn non_json_input_is_a_parse_error_not_a_validation_error() {
let err = a3_from_json("not json at all").unwrap_err();
assert!(matches!(err, A3Error::Parse(_)));
assert!(err.issues().is_empty());
assert_eq!(err.stage(), None);
}
#[test]
fn every_shape_problem_is_collectable() {
let json = r#"{
"$schema": "https://schema.rtemis.org/a3/v1/schema.json",
"a3_version": "1.0.0",
"sequence": 42,
"annotations": {"site": {"s": {"index": "nope"}}},
"metadata": {"uniprot_id": 7}
}"#;
let err = a3_from_json(json).unwrap_err();
let codes: Vec<&str> = err.issues().iter().map(|i| i.code.code()).collect();
assert_eq!(
codes,
vec![
"A3E_SEQ_NOT_STRING",
"A3E_INDEX_NOT_ARRAY",
"A3E_METADATA_FIELD_NOT_STRING"
]
);
}
#[test]
fn variants_at_position() {
let json = r#"{
"$schema": "https://schema.rtemis.org/a3/v1/schema.json",
"a3_version": "1.0.0",
"sequence": "MAEPRQ",
"annotations": {"variant": [{"position": 5, "to": "W"}, {"position": 5, "to": "C"}]}
}"#;
let a3 = a3_from_json(json).unwrap();
assert_eq!(variants_at(&a3, 5).len(), 2);
assert_eq!(variants_at(&a3, 1).len(), 0);
}
#[test]
fn pretty_print_contains_newlines() {
let a3 = a3_from_json(MINIMAL_JSON).unwrap();
assert!(a3_to_json(&a3, Some(2)).unwrap().contains('\n'));
}
}