1use std::collections::HashSet;
2
3use thiserror::Error;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct RuntimeLanguageVersion {
8 language: RuntimeLanguage,
9 version: String,
10}
11
12impl RuntimeLanguageVersion {
13 pub fn parse(spec: &str) -> Result<Self, RuntimeSpecError> {
27 let Some((language_text, version_text)) = spec.split_once(':') else {
28 return Err(RuntimeSpecError::InvalidFormat {
29 spec: spec.to_owned(),
30 });
31 };
32 let language = RuntimeLanguage::parse(language_text.trim())?;
33 let version = version_text.trim();
34 if version.is_empty() {
35 return Err(RuntimeSpecError::InvalidFormat {
36 spec: spec.to_owned(),
37 });
38 }
39
40 if !language.supports_version(version) {
41 return Err(RuntimeSpecError::UnsupportedVersion {
42 language,
43 version: version.to_owned(),
44 });
45 }
46
47 Ok(Self {
48 language,
49 version: version.to_owned(),
50 })
51 }
52
53 #[must_use]
59 pub const fn language(&self) -> RuntimeLanguage {
60 self.language
61 }
62
63 #[must_use]
69 pub fn version(&self) -> &str {
70 &self.version
71 }
72
73 #[must_use]
79 pub fn environment_variable(&self) -> RuntimeEnvironmentVariable {
80 RuntimeEnvironmentVariable::new(self.language.environment_variable(), self.version.clone())
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct RuntimeEnvironmentVariable {
87 name: &'static str,
88 value: String,
89}
90
91impl RuntimeEnvironmentVariable {
92 #[must_use]
103 pub fn new(name: &'static str, value: String) -> Self {
104 Self { name, value }
105 }
106
107 #[must_use]
113 pub const fn name(&self) -> &'static str {
114 self.name
115 }
116
117 #[must_use]
123 pub fn value(&self) -> &str {
124 &self.value
125 }
126
127 #[must_use]
133 pub fn docker_assignment(&self) -> String {
134 format!("{}={}", self.name, self.value)
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
140pub enum RuntimeLanguage {
141 Python,
143 Node,
145 Rust,
147 Go,
149 Swift,
151 Ruby,
153 Php,
155 Java,
157}
158
159impl RuntimeLanguage {
160 pub fn parse(language: &str) -> Result<Self, RuntimeSpecError> {
174 match language.to_ascii_lowercase().as_str() {
175 "python" | "python3" | "py" => Ok(Self::Python),
176 "node" | "nodejs" | "javascript" | "js" => Ok(Self::Node),
177 "rust" | "rustlang" => Ok(Self::Rust),
178 "go" | "golang" => Ok(Self::Go),
179 "swift" => Ok(Self::Swift),
180 "ruby" | "rb" => Ok(Self::Ruby),
181 "php" => Ok(Self::Php),
182 "java" | "jdk" => Ok(Self::Java),
183 _ => Err(RuntimeSpecError::UnsupportedLanguage {
184 language: language.to_owned(),
185 }),
186 }
187 }
188
189 #[must_use]
195 pub const fn environment_variable(self) -> &'static str {
196 match self {
197 Self::Python => "CODEX_ENV_PYTHON_VERSION",
198 Self::Node => "CODEX_ENV_NODE_VERSION",
199 Self::Rust => "CODEX_ENV_RUST_VERSION",
200 Self::Go => "CODEX_ENV_GO_VERSION",
201 Self::Swift => "CODEX_ENV_SWIFT_VERSION",
202 Self::Ruby => "CODEX_ENV_RUBY_VERSION",
203 Self::Php => "CODEX_ENV_PHP_VERSION",
204 Self::Java => "CODEX_ENV_JAVA_VERSION",
205 }
206 }
207
208 #[must_use]
214 pub const fn name(self) -> &'static str {
215 match self {
216 Self::Python => "python",
217 Self::Node => "node",
218 Self::Rust => "rust",
219 Self::Go => "go",
220 Self::Swift => "swift",
221 Self::Ruby => "ruby",
222 Self::Php => "php",
223 Self::Java => "java",
224 }
225 }
226
227 #[must_use]
233 pub const fn supported_versions(self) -> &'static [&'static str] {
234 match self {
235 Self::Python => &["3.10", "3.11.12", "3.12", "3.13", "3.14.0"],
236 Self::Node => &["18", "20", "22"],
237 Self::Rust => &[
238 "1.83.0", "1.84.1", "1.85.1", "1.86.0", "1.87.0", "1.88.0", "1.89.0", "1.90",
239 "1.91.1", "1.92.0", "1.93.0", "1.94.0", "1.95.0",
240 ],
241 Self::Go => &["1.22.12", "1.23.8", "1.24.3", "1.25.1"],
242 Self::Swift => &["5.10", "6.1", "6.2"],
243 Self::Ruby => &["3.2.3", "3.3.8", "3.4.4"],
244 Self::Php => &["8.4", "8.3", "8.2"],
245 Self::Java => &["25", "24", "23", "22", "21", "17", "11"],
246 }
247 }
248
249 fn supports_version(self, version: &str) -> bool {
250 self.supported_versions().contains(&version)
251 }
252}
253
254pub fn parse_runtime_specs(
268 specs: &[String],
269) -> Result<Vec<RuntimeLanguageVersion>, RuntimeSpecError> {
270 let mut languages = HashSet::with_capacity(specs.len());
271 let mut runtimes = Vec::with_capacity(specs.len());
272
273 for spec in specs {
274 let runtime = RuntimeLanguageVersion::parse(spec)?;
275 if !languages.insert(runtime.language()) {
276 return Err(RuntimeSpecError::DuplicateLanguage {
277 language: runtime.language(),
278 });
279 }
280 runtimes.push(runtime);
281 }
282
283 Ok(runtimes)
284}
285
286#[derive(Debug, Error, PartialEq, Eq)]
288pub enum RuntimeSpecError {
289 #[error("invalid runtime spec '{spec}', expected language:version")]
291 InvalidFormat {
292 spec: String,
294 },
295
296 #[error("unsupported runtime language '{language}'")]
298 UnsupportedLanguage {
299 language: String,
301 },
302
303 #[error(
305 "unsupported {language} runtime version '{version}', supported versions: {supported_versions}",
306 language = .language.name(),
307 supported_versions = .language.supported_versions().join(", ")
308 )]
309 UnsupportedVersion {
310 language: RuntimeLanguage,
312 version: String,
314 },
315
316 #[error("runtime language '{language}' was configured more than once", language = .language.name())]
318 DuplicateLanguage {
319 language: RuntimeLanguage,
321 },
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[test]
329 fn parse_runtime_spec_maps_golang_to_codex_env_go_version() {
330 let runtime =
331 RuntimeLanguageVersion::parse("golang:1.25.1").expect("runtime spec should parse");
332
333 assert_eq!(runtime.language(), RuntimeLanguage::Go);
334 assert_eq!(runtime.version(), "1.25.1");
335 assert_eq!(
336 runtime.environment_variable(),
337 RuntimeEnvironmentVariable::new("CODEX_ENV_GO_VERSION", "1.25.1".to_owned())
338 );
339 }
340
341 #[test]
342 fn parse_runtime_specs_rejects_unsupported_versions() {
343 let error = RuntimeLanguageVersion::parse("go:1.99.0")
344 .expect_err("unsupported version should fail");
345
346 assert!(matches!(
347 error,
348 RuntimeSpecError::UnsupportedVersion {
349 language: RuntimeLanguage::Go,
350 version
351 } if version == "1.99.0"
352 ));
353 }
354
355 #[test]
356 fn parse_runtime_specs_rejects_duplicate_languages() {
357 let error = parse_runtime_specs(&["node:20".to_owned(), "nodejs:22".to_owned()])
358 .expect_err("duplicate language should fail");
359
360 assert!(matches!(
361 error,
362 RuntimeSpecError::DuplicateLanguage {
363 language: RuntimeLanguage::Node
364 }
365 ));
366 }
367}