use std::str::FromStr;
use context_error::*;
use mzcv::AccessionCode;
use serde::{Deserialize, Serialize};
use thin_vec::ThinVec;
use crate::{
ontology::Ontology,
parse_json::{ParseJson, use_serde},
sequence::{
AminoAcid, Modification, SequenceElement, SequencePosition, SimpleModificationInner,
},
space::{Space, UsedSpace},
};
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum PlacementRule {
AminoAcid(ThinVec<AminoAcid>, Position),
PsiModification(u32, Position),
Position(Position),
}
impl Space for PlacementRule {
fn space(&self) -> UsedSpace {
(UsedSpace::stack(1)
+ match self {
Self::AminoAcid(a, p) => a.space() + p.space(),
Self::PsiModification(i, p) => i.space() + p.space(),
Self::Position(p) => p.space(),
})
.set_total::<Self>()
}
}
impl ParseJson for PlacementRule {
fn from_json_value(value: serde_json::Value) -> Result<Self, BoxedError<'static, BasicKind>> {
if let serde_json::Value::Object(map) = &value {
if let Some(serde_json::Value::String(v)) = map.get("Terminal") {
Ok(Self::Position(Position::from_str(v).map_err(|()| {
BoxedError::new(
BasicKind::Error,
"Invalid Position",
"It should be 'Anywhere', 'AnyNTerm', 'ProteinNTerm', 'AnyCTerm', or 'ProteinCTerm'",
Context::default().lines(0, value.to_string()),
)
})?))
} else if let Some(serde_json::Value::String(v)) = map.get("Position") {
Ok(Self::Position(Position::from_str(v).map_err(|()| {
BoxedError::new(
BasicKind::Error,
"Invalid Position",
"It should be 'Anywhere', 'AnyNTerm', 'ProteinNTerm', 'AnyCTerm', or 'ProteinCTerm'",
Context::default().lines(0, value.to_string()),
)
})?))
} else if let Some(v) = map.get("PsiModification") {
<(u32, Position)>::from_json_value(v.clone())
.map(|v| Self::PsiModification(v.0, v.1))
} else if let Some(v) = map.get("AminoAcid") {
<(ThinVec<AminoAcid>, Position)>::from_json_value(v.clone())
.map(|v| Self::AminoAcid(v.0, v.1))
} else {
Err(BoxedError::new(
BasicKind::Error,
"Invalid PlacementRule",
"It should be a 'Position', 'AminoAcid', or 'PsiModification' object",
Context::default().lines(0, value.to_string()),
))
}
} else if let serde_json::Value::String(str) = &value
&& str.eq_ignore_ascii_case("Anywhere")
{
Ok(Self::Position(Position::Anywhere))
} else {
Err(BoxedError::new(
BasicKind::Error,
"Invalid PlacementRule",
"It should be an object",
Context::default().lines(0, value.to_string()),
))
}
}
}
#[derive(
Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Serialize, Deserialize,
)]
pub enum Position {
#[default]
Anywhere,
AnyNTerm,
AnyCTerm,
ProteinNTerm,
ProteinCTerm,
}
impl ParseJson for Position {
fn from_json_value(value: serde_json::Value) -> Result<Self, BoxedError<'static, BasicKind>> {
use_serde(value)
}
}
impl std::fmt::Display for Position {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", match self {
Self::Anywhere => "Anywhere",
Self::AnyNTerm => "AnyNTerm",
Self::AnyCTerm => "AnyCTerm",
Self::ProteinNTerm => "ProteinNTerm",
Self::ProteinCTerm => "ProteinCTerm",
})
}
}
impl PlacementRule {
pub fn is_possible<T>(&self, seq: &SequenceElement<T>, position: SequencePosition) -> bool {
match self {
Self::AminoAcid(aa, r_pos) => {
aa.iter().any(|a| *a == seq.aminoacid.aminoacid())
&& RulePosition::is_possible(*r_pos, position, false)
&& matches!(position, SequencePosition::Index(_, _))
}
Self::PsiModification(mod_index, r_pos) => {
seq.modifications.iter().any(|m| {
if let Modification::Simple(sim) = m {
if let SimpleModificationInner::Database { id, .. } = &**sim
&& id.ontology == Ontology::Psimod
&& let AccessionCode::Numeric(i) = id.id()
{
i == *mod_index
} else {
false
}
} else {
false
}
}) && RulePosition::is_possible(*r_pos, position, false)
}
Self::Position(r_pos) => RulePosition::is_possible(*r_pos, position, true),
}
}
pub fn is_possible_aa<P: RulePosition>(&self, aa: AminoAcid, position: P) -> bool {
match self {
Self::AminoAcid(allowed_aa, r_pos) => {
allowed_aa.contains(&aa) && RulePosition::is_possible(*r_pos, position, false)
}
Self::PsiModification(..) => false,
Self::Position(r_pos) => RulePosition::is_possible(*r_pos, position, true),
}
}
pub fn any_possible<T>(
rules: &[Self],
seq: &SequenceElement<T>,
position: SequencePosition,
) -> bool {
rules.iter().any(|r| r.is_possible(seq, position))
}
pub fn any_possible_aa<P: RulePosition + Clone>(
rules: &[Self],
aa: AminoAcid,
position: P,
) -> bool {
rules.iter().any(|r| r.is_possible_aa(aa, position.clone()))
}
pub fn is_subset(&self, other: &Self) -> bool {
match (self, other) {
(Self::Position(ps), Self::Position(po)) => ps.is_subset(*po),
(Self::AminoAcid(aas, ps), Self::AminoAcid(aao, po)) => {
ps.is_subset(*po) && aas.iter().all(|a| aao.contains(a))
}
(Self::PsiModification(is, ps), Self::PsiModification(io, po)) => {
is == io && ps.is_subset(*po)
}
_ => false,
}
}
pub fn combine_rules(&mut self, other: &Self) -> bool {
match (self, other) {
(Self::AminoAcid(new_aa, new_pos), Self::AminoAcid(aa, pos)) if pos == new_pos => {
for a in aa {
if !new_aa.contains(a) {
new_aa.push(*a);
}
}
new_aa.sort_unstable();
true
}
(Self::Position(new_pos), Self::Position(pos)) => {
if new_pos == pos {
true
} else if matches!(new_pos, Position::ProteinNTerm | Position::AnyNTerm)
&& matches!(pos, Position::ProteinNTerm | Position::AnyNTerm)
{
*new_pos = Position::AnyNTerm;
true
} else if matches!(new_pos, Position::ProteinCTerm | Position::AnyCTerm)
&& matches!(pos, Position::ProteinCTerm | Position::AnyCTerm)
{
*new_pos = Position::AnyCTerm;
true
} else {
false
}
}
(a, b) if a == b => true,
_ => false,
}
}
}
impl FromStr for PlacementRule {
type Err = BoxedError<'static, BasicKind>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some((head, tail)) = s.split_once('@') {
let aa: Vec<AminoAcid> = head
.chars()
.enumerate()
.map(|(i, c)| {
AminoAcid::try_from(c).map_err(|()| {
BoxedError::new(
BasicKind::Error,
"Invalid amino acid",
"Invalid amino acid in specified amino acids in placement rule",
Context::default().lines(0, s).add_highlight((0, i, 1)).to_owned(),
)
})
})
.collect::<Result<Vec<_>, _>>()?;
tail.parse().map_or_else(
|()| {
Err(BoxedError::new(
BasicKind::Error,
"Invalid position",
"Use any of the following for the position: Anywhere, AnyNTerm, ProteinNTerm, AnyCTerm, ProteinCTerm",
Context::default().lines(0, s).add_highlight((0, head.len() + 1, tail.len())).to_owned(),
))
},
|position| Ok(Self::AminoAcid(aa.into(), position)),
)
} else if let Ok(position) = s.parse() {
Ok(Self::Position(position))
} else {
Err(BoxedError::new(
BasicKind::Error,
"Invalid position",
"Use any of the following for the position: Anywhere, AnyNTerm, ProteinNTerm, AnyCTerm, ProteinCTerm",
Context::default().line_index(0).lines(0, s).to_owned(),
))
}
}
}
pub trait RulePosition {
fn is_possible(rule: Position, position: Self, sidechain_strict: bool) -> bool;
}
impl RulePosition for Position {
fn is_possible(rule: Position, position: Self, _sidechain_strict: bool) -> bool {
match rule {
Self::Anywhere => true,
Self::AnyNTerm => position == Self::AnyNTerm || position == Self::ProteinNTerm,
Self::ProteinNTerm => position == Self::ProteinNTerm,
Self::AnyCTerm => position == Self::AnyCTerm || position == Self::ProteinCTerm,
Self::ProteinCTerm => position == Self::ProteinCTerm,
}
}
}
impl RulePosition for SequencePosition {
fn is_possible(rule: Position, position: Self, sidechain_strict: bool) -> bool {
match rule {
Position::Anywhere => true,
Position::AnyNTerm | Position::ProteinNTerm => {
position == Self::NTerm
|| !sidechain_strict && matches!(position, Self::Index(0, _))
}
Position::AnyCTerm | Position::ProteinCTerm => {
position == Self::CTerm
|| !sidechain_strict
&& matches!(position, Self::Index(i, l) if i == l.saturating_sub(1))
}
}
}
}
impl Position {
pub const fn is_subset(self, other: Self) -> bool {
matches!(
(self, other),
(_, Self::Anywhere)
| (Self::ProteinCTerm, Self::ProteinCTerm | Self::AnyCTerm)
| (Self::AnyCTerm, Self::AnyCTerm)
| (Self::ProteinNTerm, Self::ProteinNTerm | Self::AnyNTerm)
| (Self::AnyNTerm, Self::AnyNTerm)
)
}
}
impl FromStr for Position {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"" | "anywhere" => Ok(Self::Anywhere),
"anynterm" | "anyn-term" | "any n-term" => Ok(Self::AnyNTerm),
"proteinnterm" | "proteinn-term" | "protein n-term" => Ok(Self::ProteinNTerm),
"anycterm" | "anyc-term" | "any c-term" => Ok(Self::AnyCTerm),
"proteincterm" | "proteinc-term" | "protein c-term" => Ok(Self::ProteinCTerm),
_ => Err(()),
}
}
}
#[cfg(test)]
#[expect(clippy::missing_panics_doc)]
mod tests {
use super::*;
use crate::{
ontology::STATIC_ONTOLOGIES,
sequence::{CheckedAminoAcid, RulePossible},
};
#[test]
fn multi_level_rule() {
let ontologies = &STATIC_ONTOLOGIES;
assert!(
!PlacementRule::PsiModification(30, Position::Anywhere).is_possible(
&SequenceElement::new(CheckedAminoAcid::Alanine, None),
SequencePosition::Index(0, 5),
),
"Multi level mod cannot be placed if the dependent mod is not present"
);
let mut seq = SequenceElement::new(CheckedAminoAcid::Alanine, None);
seq.modifications.push(
ontologies
.psimod()
.get_by_index(&AccessionCode::Numeric(30))
.unwrap()
.into(),
);
assert!(
PlacementRule::PsiModification(30, Position::Anywhere)
.is_possible(&seq, SequencePosition::Index(0, 5),),
"Multi level mod can be placed if the dependent mod is present"
);
}
#[test]
fn place_anywhere() {
assert!(
PlacementRule::AminoAcid(vec![AminoAcid::Glutamine].into(), Position::ProteinNTerm)
.is_possible(
&SequenceElement::new(CheckedAminoAcid::Q, None),
SequencePosition::Index(0, 5),
),
"start"
);
assert!(
PlacementRule::AminoAcid(vec![AminoAcid::Glutamine].into(), Position::Anywhere)
.is_possible(
&SequenceElement::new(CheckedAminoAcid::Q, None),
SequencePosition::Index(2, 5),
),
"middle"
);
assert!(
PlacementRule::AminoAcid(vec![AminoAcid::Glutamine].into(), Position::ProteinCTerm)
.is_possible(
&SequenceElement::new(CheckedAminoAcid::Q, None),
SequencePosition::Index(4, 5),
),
"end"
);
assert_eq!(
STATIC_ONTOLOGIES
.unimod()
.get_by_index(&AccessionCode::Numeric(7))
.unwrap()
.is_possible(
&SequenceElement::new(CheckedAminoAcid::Q, None),
SequencePosition::Index(4, 5),
),
RulePossible::Symmetric(std::collections::BTreeSet::from([0])),
"unimod deamidated at end"
);
}
#[test]
fn parse_json() {
assert_eq!(
PlacementRule::from_json_value(serde_json::Value::String("Anywhere".to_string())),
Ok(PlacementRule::Position(Position::Anywhere))
);
assert_eq!(
PlacementRule::from_json_value(serde_json::Value::Object(serde_json::Map::from_iter(
[(
"Position".to_string(),
serde_json::Value::String("Anywhere".to_string())
)]
))),
Ok(PlacementRule::Position(Position::Anywhere))
);
assert_eq!(
PlacementRule::from_json_value(serde_json::Value::Object(serde_json::Map::from_iter(
[(
"Terminal".to_string(),
serde_json::Value::String("AnyNTerm".to_string())
)]
))),
Ok(PlacementRule::Position(Position::AnyNTerm))
);
assert_eq!(
PlacementRule::from_json_value(serde_json::Value::Object(serde_json::Map::from_iter(
[(
"AminoAcid".to_string(),
serde_json::Value::Array(vec![
serde_json::Value::Array(vec![serde_json::Value::String(
"Cysteine".to_string()
),]),
serde_json::Value::String("Anywhere".to_string())
])
)]
))),
Ok(PlacementRule::AminoAcid(
vec![AminoAcid::Cysteine].into(),
Position::Anywhere
))
);
}
}