1use crate::version::Version;
3use std::borrow::Cow;
4use std::iter::Peekable;
5use std::str::Chars;
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
12pub enum VersionConstraint {
13 LessThan,
15 LessThanEqual,
17 Equal,
19 NotEqual,
21 GreaterThan,
23 GreaterThanEqual,
25}
26
27impl std::str::FromStr for VersionConstraint {
28 type Err = String;
29
30 fn from_str(s: &str) -> Result<Self, Self::Err> {
31 match s {
32 ">=" => Ok(VersionConstraint::GreaterThanEqual),
33 "<=" => Ok(VersionConstraint::LessThanEqual),
34 "==" => Ok(VersionConstraint::Equal),
35 "!=" => Ok(VersionConstraint::NotEqual),
36 ">" => Ok(VersionConstraint::GreaterThan),
37 "<" => Ok(VersionConstraint::LessThan),
38 _ => Err(format!("Invalid version constraint: {s}")),
39 }
40 }
41}
42
43impl std::fmt::Display for VersionConstraint {
44 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
45 match self {
46 VersionConstraint::GreaterThanEqual => f.write_str(">="),
47 VersionConstraint::LessThanEqual => f.write_str("<="),
48 VersionConstraint::Equal => f.write_str("=="),
49 VersionConstraint::NotEqual => f.write_str("!="),
50 VersionConstraint::GreaterThan => f.write_str(">"),
51 VersionConstraint::LessThan => f.write_str("<"),
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
59#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
60#[repr(u16)]
61#[allow(missing_docs)]
62pub(crate) enum SyntaxKind {
63 IDENT = 0, COMMA, L_PARENS, R_PARENS, L_ANGLE, R_ANGLE, EQUAL, NOT, WHITESPACE, NEWLINE, ERROR, ROOT, RELATION, VERSION, CONSTRAINT, }
81
82impl From<SyntaxKind> for rowan::SyntaxKind {
84 fn from(kind: SyntaxKind) -> Self {
85 Self(kind as u16)
86 }
87}
88
89pub(crate) struct Lexer<'a> {
91 input: Peekable<Chars<'a>>,
92}
93
94impl<'a> Lexer<'a> {
95 pub fn new(input: &'a str) -> Self {
97 Lexer {
98 input: input.chars().peekable(),
99 }
100 }
101
102 fn is_whitespace(c: char) -> bool {
103 c == ' ' || c == '\t' || c == '\r'
104 }
105
106 fn is_valid_ident_char(c: char) -> bool {
107 c.is_ascii_alphanumeric() || c == '-' || c == '.'
108 }
109
110 fn read_while<F>(&mut self, predicate: F) -> String
111 where
112 F: Fn(char) -> bool,
113 {
114 let mut result = String::new();
115 while let Some(&c) = self.input.peek() {
116 if predicate(c) {
117 result.push(c);
118 self.input.next();
119 } else {
120 break;
121 }
122 }
123 result
124 }
125
126 fn next_token(&mut self) -> Option<(SyntaxKind, String)> {
127 if let Some(&c) = self.input.peek() {
128 match c {
129 ',' => {
130 self.input.next();
131 Some((SyntaxKind::COMMA, ",".to_owned()))
132 }
133 '(' => {
134 self.input.next();
135 Some((SyntaxKind::L_PARENS, "(".to_owned()))
136 }
137 ')' => {
138 self.input.next();
139 Some((SyntaxKind::R_PARENS, ")".to_owned()))
140 }
141 '<' => {
142 self.input.next();
143 Some((SyntaxKind::L_ANGLE, "<".to_owned()))
144 }
145 '>' => {
146 self.input.next();
147 Some((SyntaxKind::R_ANGLE, ">".to_owned()))
148 }
149 '=' => {
150 self.input.next();
151 Some((SyntaxKind::EQUAL, "=".to_owned()))
152 }
153 '!' => {
154 self.input.next();
155 Some((SyntaxKind::NOT, "!".to_owned()))
156 }
157 '\n' => {
158 self.input.next();
159 Some((SyntaxKind::NEWLINE, "\n".to_owned()))
160 }
161 _ if Self::is_whitespace(c) => {
162 let whitespace = self.read_while(Self::is_whitespace);
163 Some((SyntaxKind::WHITESPACE, whitespace))
164 }
165 _ if Self::is_valid_ident_char(c) => {
167 let key = self.read_while(Self::is_valid_ident_char);
168 Some((SyntaxKind::IDENT, key))
169 }
170 _ => {
171 self.input.next();
172 Some((SyntaxKind::ERROR, c.to_string()))
173 }
174 }
175 } else {
176 None
177 }
178 }
179}
180
181impl Iterator for Lexer<'_> {
182 type Item = (SyntaxKind, String);
183
184 fn next(&mut self) -> Option<Self::Item> {
185 self.next_token()
186 }
187}
188
189pub(crate) fn lex(input: &str) -> Vec<(SyntaxKind, String)> {
190 let mut lexer = Lexer::new(input);
191 lexer.by_ref().collect::<Vec<_>>()
192}
193
194pub trait VersionLookup {
196 fn lookup_version<'a>(&'a self, package: &'_ str) -> Option<std::borrow::Cow<'a, Version>>;
198}
199
200impl VersionLookup for std::collections::HashMap<String, Version> {
201 fn lookup_version<'a>(&'a self, package: &str) -> Option<Cow<'a, Version>> {
202 self.get(package).map(Cow::Borrowed)
203 }
204}
205
206impl<F> VersionLookup for F
207where
208 F: Fn(&str) -> Option<Version>,
209{
210 fn lookup_version<'a>(&'a self, name: &str) -> Option<Cow<'a, Version>> {
211 self(name).map(Cow::Owned)
212 }
213}
214
215impl VersionLookup for (String, Version) {
216 fn lookup_version<'a>(&'a self, name: &str) -> Option<Cow<'a, Version>> {
217 if name == self.0 {
218 Some(Cow::Borrowed(&self.1))
219 } else {
220 None
221 }
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::VersionConstraint;
228
229 #[test]
230 fn parse_r_operators() {
231 assert_eq!("<".parse(), Ok(VersionConstraint::LessThan));
232 assert_eq!("<=".parse(), Ok(VersionConstraint::LessThanEqual));
233 assert_eq!("==".parse(), Ok(VersionConstraint::Equal));
234 assert_eq!("!=".parse(), Ok(VersionConstraint::NotEqual));
235 assert_eq!(">".parse(), Ok(VersionConstraint::GreaterThan));
236 assert_eq!(">=".parse(), Ok(VersionConstraint::GreaterThanEqual));
237 }
238
239 #[test]
240 fn debian_operators_are_rejected() {
241 assert!("<<".parse::<VersionConstraint>().is_err());
242 assert!(">>".parse::<VersionConstraint>().is_err());
243 assert!("=".parse::<VersionConstraint>().is_err());
244 }
245
246 #[test]
247 fn display_uses_r_operators() {
248 assert_eq!(VersionConstraint::LessThan.to_string(), "<");
249 assert_eq!(VersionConstraint::LessThanEqual.to_string(), "<=");
250 assert_eq!(VersionConstraint::Equal.to_string(), "==");
251 assert_eq!(VersionConstraint::NotEqual.to_string(), "!=");
252 assert_eq!(VersionConstraint::GreaterThan.to_string(), ">");
253 assert_eq!(VersionConstraint::GreaterThanEqual.to_string(), ">=");
254 }
255
256 #[test]
257 fn invalid_operator() {
258 assert!("~=".parse::<VersionConstraint>().is_err());
259 }
260}