use std::fmt;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum VersionError {
#[error("unsupported Pine version {0}")]
Unsupported(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum PineVersion {
V3,
V4,
V5,
#[default]
V6,
}
impl PineVersion {
pub const LATEST: PineVersion = PineVersion::V6;
pub fn number(self) -> u8 {
match self {
PineVersion::V3 => 3,
PineVersion::V4 => 4,
PineVersion::V5 => 5,
PineVersion::V6 => 6,
}
}
pub fn from_number(n: u8) -> Option<Self> {
match n {
3 => Some(PineVersion::V3),
4 => Some(PineVersion::V4),
5 => Some(PineVersion::V5),
6 => Some(PineVersion::V6),
_ => None,
}
}
pub fn detect(source: &str) -> Result<Option<Self>, VersionError> {
let number = source.lines().find_map(|line| {
let rest = line.trim().strip_prefix("//")?;
let rest = rest.trim_start().strip_prefix("@version")?;
let rest = rest.trim_start().strip_prefix('=')?;
let digits: String = rest
.trim_start()
.chars()
.take_while(char::is_ascii_digit)
.collect();
digits.parse::<u8>().ok()
});
match number {
None => Ok(None),
Some(number) => Self::from_number(number)
.map(Some)
.ok_or(VersionError::Unsupported(number)),
}
}
}
impl fmt::Display for PineVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "v{}", self.number())
}
}
#[cfg(test)]
mod tests {
use super::{PineVersion, VersionError};
#[test]
fn detects_the_version_annotation() {
assert_eq!(
PineVersion::detect("//@version=5\nx = 1\n"),
Ok(Some(PineVersion::V5))
);
assert_eq!(
PineVersion::detect("// a comment\n// @version = 4\n"),
Ok(Some(PineVersion::V4))
);
}
#[test]
fn distinguishes_missing_from_unsupported() {
assert_eq!(PineVersion::detect("x = 1\n"), Ok(None));
assert_eq!(
PineVersion::detect("//@version=2\n"),
Err(VersionError::Unsupported(2))
);
}
#[test]
fn versions_order_oldest_to_newest() {
assert!(PineVersion::V4 < PineVersion::V5);
assert!(PineVersion::V5 < PineVersion::V6);
assert_eq!(PineVersion::default(), PineVersion::LATEST);
}
}