use crate::error::{Primer3Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Interval {
pub start: usize,
pub length: usize,
}
impl Interval {
pub fn new(start: usize, length: usize) -> Self {
Self { start, length }
}
pub fn end(&self) -> usize {
self.start + self.length
}
}
impl From<(usize, usize)> for Interval {
fn from((start, length): (usize, usize)) -> Self {
Self { start, length }
}
}
impl From<Interval> for std::ops::Range<usize> {
fn from(interval: Interval) -> Self {
interval.start..interval.end()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OkRegionPair {
pub left: Option<Interval>,
pub right: Option<Interval>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SequenceArgs {
pub sequence_id: Option<String>,
pub sequence: String,
pub targets: Vec<Interval>,
pub excluded_regions: Vec<Interval>,
pub internal_excluded_regions: Vec<Interval>,
pub ok_regions: Vec<OkRegionPair>,
pub primer_overlap_junctions: Vec<usize>,
pub internal_overlap_junctions: Vec<usize>,
pub included_region_start: usize,
pub included_region_length: Option<usize>,
pub quality: Option<Vec<i32>>,
pub left_primer: Option<String>,
pub right_primer: Option<String>,
pub internal_oligo: Option<String>,
pub force_left_start: Option<usize>,
pub force_left_end: Option<usize>,
pub force_right_start: Option<usize>,
pub force_right_end: Option<usize>,
pub overhang_left: Option<String>,
pub overhang_right: Option<String>,
}
impl SequenceArgs {
pub fn builder() -> SequenceArgsBuilder {
SequenceArgsBuilder {
args: SequenceArgs {
sequence_id: None,
sequence: String::new(),
targets: Vec::new(),
excluded_regions: Vec::new(),
internal_excluded_regions: Vec::new(),
ok_regions: Vec::new(),
primer_overlap_junctions: Vec::new(),
internal_overlap_junctions: Vec::new(),
included_region_start: 0,
included_region_length: None,
quality: None,
left_primer: None,
right_primer: None,
internal_oligo: None,
force_left_start: None,
force_left_end: None,
force_right_start: None,
force_right_end: None,
overhang_left: None,
overhang_right: None,
},
}
}
}
#[derive(Debug, Clone)]
pub struct SequenceArgsBuilder {
args: SequenceArgs,
}
impl SequenceArgsBuilder {
pub fn sequence_id(mut self, id: impl Into<String>) -> Self {
self.args.sequence_id = Some(id.into());
self
}
pub fn sequence(mut self, seq: impl Into<String>) -> Self {
self.args.sequence = seq.into();
self
}
pub fn target(mut self, start: usize, length: usize) -> Self {
self.args.targets.push(Interval::new(start, length));
self
}
pub fn excluded_region(mut self, start: usize, length: usize) -> Self {
self.args.excluded_regions.push(Interval::new(start, length));
self
}
pub fn internal_excluded_region(mut self, start: usize, length: usize) -> Self {
self.args.internal_excluded_regions.push(Interval::new(start, length));
self
}
pub fn ok_region(
mut self,
left: Option<(usize, usize)>,
right: Option<(usize, usize)>,
) -> Self {
self.args.ok_regions.push(OkRegionPair {
left: left.map(|(s, l)| Interval::new(s, l)),
right: right.map(|(s, l)| Interval::new(s, l)),
});
self
}
pub fn included_region(mut self, start: usize, length: usize) -> Self {
self.args.included_region_start = start;
self.args.included_region_length = Some(length);
self
}
pub fn quality(mut self, quality: Vec<i32>) -> Self {
self.args.quality = Some(quality);
self
}
pub fn left_primer(mut self, seq: impl Into<String>) -> Self {
self.args.left_primer = Some(seq.into());
self
}
pub fn right_primer(mut self, seq: impl Into<String>) -> Self {
self.args.right_primer = Some(seq.into());
self
}
pub fn internal_oligo(mut self, seq: impl Into<String>) -> Self {
self.args.internal_oligo = Some(seq.into());
self
}
pub fn primer_overlap_junction(mut self, pos: usize) -> Self {
self.args.primer_overlap_junctions.push(pos);
self
}
pub fn overhang_left(mut self, seq: impl Into<String>) -> Self {
self.args.overhang_left = Some(seq.into());
self
}
pub fn overhang_right(mut self, seq: impl Into<String>) -> Self {
self.args.overhang_right = Some(seq.into());
self
}
pub fn build(self) -> Result<SequenceArgs> {
if self.args.sequence.is_empty() {
return Err(Primer3Error::InvalidSequence("template sequence is empty".into()));
}
let seq_len = self.args.sequence.len();
if let Some(ref quality) = self.args.quality {
if quality.len() != seq_len {
return Err(Primer3Error::InvalidSetting(format!(
"quality length ({}) does not match sequence length ({})",
quality.len(),
seq_len,
)));
}
}
if let Some(inc_len) = self.args.included_region_length {
if self.args.included_region_start + inc_len > seq_len {
return Err(Primer3Error::InvalidSetting(format!(
"included_region (start={}, length={}) exceeds sequence length ({})",
self.args.included_region_start, inc_len, seq_len,
)));
}
}
for target in &self.args.targets {
if target.end() > seq_len {
return Err(Primer3Error::InvalidSetting(format!(
"target (start={}, length={}) exceeds sequence length ({})",
target.start, target.length, seq_len,
)));
}
}
for excluded in &self.args.excluded_regions {
if excluded.end() > seq_len {
return Err(Primer3Error::InvalidSetting(format!(
"excluded_region (start={}, length={}) exceeds sequence length ({})",
excluded.start, excluded.length, seq_len,
)));
}
}
for excluded in &self.args.internal_excluded_regions {
if excluded.end() > seq_len {
return Err(Primer3Error::InvalidSetting(format!(
"internal_excluded_region (start={}, length={}) exceeds sequence length ({})",
excluded.start, excluded.length, seq_len,
)));
}
}
Ok(self.args)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_interval_new_and_end() {
let iv = Interval::new(10, 20);
assert_eq!(iv.start, 10);
assert_eq!(iv.length, 20);
assert_eq!(iv.end(), 30);
}
#[test]
fn test_builder_minimal() {
let args = SequenceArgs::builder().sequence("ATCGATCG").build().unwrap();
assert_eq!(args.sequence, "ATCGATCG");
assert!(args.targets.is_empty());
}
#[test]
fn test_builder_empty_sequence_fails() {
let result = SequenceArgs::builder().sequence("").build();
assert!(result.is_err());
}
#[test]
fn test_builder_target_in_bounds() {
let args =
SequenceArgs::builder().sequence("ATCGATCGATCGATCG").target(4, 8).build().unwrap();
assert_eq!(args.targets.len(), 1);
assert_eq!(args.targets[0].start, 4);
assert_eq!(args.targets[0].length, 8);
}
#[test]
fn test_builder_target_out_of_bounds_fails() {
let result = SequenceArgs::builder().sequence("ATCG").target(2, 5).build();
assert!(result.is_err());
}
#[test]
fn test_builder_excluded_region_out_of_bounds_fails() {
let result = SequenceArgs::builder().sequence("ATCGATCG").excluded_region(5, 10).build();
assert!(result.is_err());
}
#[test]
fn test_builder_internal_excluded_out_of_bounds_fails() {
let result =
SequenceArgs::builder().sequence("ATCGATCG").internal_excluded_region(5, 10).build();
assert!(result.is_err());
}
#[test]
fn test_builder_included_region_out_of_bounds_fails() {
let result = SequenceArgs::builder().sequence("ATCGATCG").included_region(4, 10).build();
assert!(result.is_err());
}
#[test]
fn test_builder_quality_length_mismatch_fails() {
let result = SequenceArgs::builder().sequence("ATCG").quality(vec![40, 40, 40]).build();
assert!(result.is_err());
}
#[test]
fn test_builder_quality_correct_length() {
let args =
SequenceArgs::builder().sequence("ATCG").quality(vec![40, 40, 40, 40]).build().unwrap();
assert_eq!(args.quality.as_ref().unwrap().len(), 4);
}
#[test]
fn test_builder_multiple_targets() {
let args = SequenceArgs::builder()
.sequence("ATCGATCGATCGATCGATCG")
.target(2, 3)
.target(10, 3)
.build()
.unwrap();
assert_eq!(args.targets.len(), 2);
}
#[test]
fn test_builder_with_primers() {
let args = SequenceArgs::builder()
.sequence("ATCGATCGATCG")
.left_primer("ATCG")
.right_primer("CGAT")
.build()
.unwrap();
assert_eq!(args.left_primer.as_deref(), Some("ATCG"));
assert_eq!(args.right_primer.as_deref(), Some("CGAT"));
}
}