1use std::fmt;
4use thiserror::Error;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
10pub enum VersionError {
11 #[error("unsupported Pine version {0}")]
13 Unsupported(u8),
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
32pub enum PineVersion {
33 V3,
34 V4,
35 V5,
36 #[default]
37 V6,
38}
39
40impl PineVersion {
41 pub const LATEST: PineVersion = PineVersion::V6;
43
44 pub fn number(self) -> u8 {
46 match self {
47 PineVersion::V3 => 3,
48 PineVersion::V4 => 4,
49 PineVersion::V5 => 5,
50 PineVersion::V6 => 6,
51 }
52 }
53
54 pub fn from_number(n: u8) -> Option<Self> {
56 match n {
57 3 => Some(PineVersion::V3),
58 4 => Some(PineVersion::V4),
59 5 => Some(PineVersion::V5),
60 6 => Some(PineVersion::V6),
61 _ => None,
62 }
63 }
64
65 pub fn detect(source: &str) -> Result<Option<Self>, VersionError> {
73 let number = source.lines().find_map(|line| {
74 let rest = line.trim().strip_prefix("//")?;
77 let rest = rest.trim_start().strip_prefix("@version")?;
78 let rest = rest.trim_start().strip_prefix('=')?;
79 let digits: String = rest
80 .trim_start()
81 .chars()
82 .take_while(char::is_ascii_digit)
83 .collect();
84 digits.parse::<u8>().ok()
85 });
86
87 match number {
88 None => Ok(None),
89 Some(number) => Self::from_number(number)
90 .map(Some)
91 .ok_or(VersionError::Unsupported(number)),
92 }
93 }
94}
95
96impl fmt::Display for PineVersion {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 write!(f, "v{}", self.number())
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::{PineVersion, VersionError};
105
106 #[test]
107 fn detects_the_version_annotation() {
108 assert_eq!(
109 PineVersion::detect("//@version=5\nx = 1\n"),
110 Ok(Some(PineVersion::V5))
111 );
112 assert_eq!(
114 PineVersion::detect("// a comment\n// @version = 4\n"),
115 Ok(Some(PineVersion::V4))
116 );
117 }
118
119 #[test]
120 fn distinguishes_missing_from_unsupported() {
121 assert_eq!(PineVersion::detect("x = 1\n"), Ok(None));
122 assert_eq!(
123 PineVersion::detect("//@version=2\n"),
124 Err(VersionError::Unsupported(2))
125 );
126 }
127
128 #[test]
129 fn versions_order_oldest_to_newest() {
130 assert!(PineVersion::V4 < PineVersion::V5);
131 assert!(PineVersion::V5 < PineVersion::V6);
132 assert_eq!(PineVersion::default(), PineVersion::LATEST);
133 }
134}