Skip to main content

codex_ws/
runtime.rs

1use std::collections::HashSet;
2
3use thiserror::Error;
4
5/// One supported language runtime selected for a workspace.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct RuntimeLanguageVersion {
8    language: RuntimeLanguage,
9    version: String,
10}
11
12impl RuntimeLanguageVersion {
13    /// Parse a runtime language specification.
14    ///
15    /// # Arguments
16    ///
17    /// * `spec` - Runtime spec in `language:version` form.
18    ///
19    /// # Returns
20    ///
21    /// A validated runtime language version.
22    ///
23    /// # Errors
24    ///
25    /// Returns [`RuntimeSpecError`] when the spec format, language, or version is unsupported.
26    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    /// Return the selected runtime language.
54    ///
55    /// # Returns
56    ///
57    /// The supported runtime language.
58    #[must_use]
59    pub const fn language(&self) -> RuntimeLanguage {
60        self.language
61    }
62
63    /// Return the selected runtime version.
64    ///
65    /// # Returns
66    ///
67    /// The exact version string supported by Codex Universal.
68    #[must_use]
69    pub fn version(&self) -> &str {
70        &self.version
71    }
72
73    /// Convert this runtime spec into a Docker environment variable.
74    ///
75    /// # Returns
76    ///
77    /// The `CODEX_ENV_*` variable consumed by Codex Universal.
78    #[must_use]
79    pub fn environment_variable(&self) -> RuntimeEnvironmentVariable {
80        RuntimeEnvironmentVariable::new(self.language.environment_variable(), self.version.clone())
81    }
82}
83
84/// Docker environment variable generated from a workspace runtime spec.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct RuntimeEnvironmentVariable {
87    name: &'static str,
88    value: String,
89}
90
91impl RuntimeEnvironmentVariable {
92    /// Create a runtime environment variable.
93    ///
94    /// # Arguments
95    ///
96    /// * `name` - Environment variable name recognized by Codex Universal.
97    /// * `value` - Runtime version value.
98    ///
99    /// # Returns
100    ///
101    /// A runtime environment variable.
102    #[must_use]
103    pub fn new(name: &'static str, value: String) -> Self {
104        Self { name, value }
105    }
106
107    /// Return the environment variable name.
108    ///
109    /// # Returns
110    ///
111    /// The `CODEX_ENV_*` variable name.
112    #[must_use]
113    pub const fn name(&self) -> &'static str {
114        self.name
115    }
116
117    /// Return the environment variable value.
118    ///
119    /// # Returns
120    ///
121    /// The selected runtime version.
122    #[must_use]
123    pub fn value(&self) -> &str {
124        &self.value
125    }
126
127    /// Return the `NAME=value` form accepted by `docker run -e`.
128    ///
129    /// # Returns
130    ///
131    /// A Docker environment assignment.
132    #[must_use]
133    pub fn docker_assignment(&self) -> String {
134        format!("{}={}", self.name, self.value)
135    }
136}
137
138/// Supported Codex Universal runtime languages.
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
140pub enum RuntimeLanguage {
141    /// Python via `CODEX_ENV_PYTHON_VERSION`.
142    Python,
143    /// Node.js via `CODEX_ENV_NODE_VERSION`.
144    Node,
145    /// Rust via `CODEX_ENV_RUST_VERSION`.
146    Rust,
147    /// Go via `CODEX_ENV_GO_VERSION`.
148    Go,
149    /// Swift via `CODEX_ENV_SWIFT_VERSION`.
150    Swift,
151    /// Ruby via `CODEX_ENV_RUBY_VERSION`.
152    Ruby,
153    /// PHP via `CODEX_ENV_PHP_VERSION`.
154    Php,
155    /// Java via `CODEX_ENV_JAVA_VERSION`.
156    Java,
157}
158
159impl RuntimeLanguage {
160    /// Parse a supported runtime language or alias.
161    ///
162    /// # Arguments
163    ///
164    /// * `language` - Language name from a workspace runtime spec.
165    ///
166    /// # Returns
167    ///
168    /// A supported runtime language.
169    ///
170    /// # Errors
171    ///
172    /// Returns [`RuntimeSpecError::UnsupportedLanguage`] when the language is unknown.
173    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    /// Return the Codex Universal environment variable for this language.
190    ///
191    /// # Returns
192    ///
193    /// The `CODEX_ENV_*` variable name.
194    #[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    /// Return the canonical language name.
209    ///
210    /// # Returns
211    ///
212    /// Lowercase language name used in error messages.
213    #[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    /// Return supported versions for this language.
228    ///
229    /// # Returns
230    ///
231    /// Versions supported by the current Codex Universal support matrix.
232    #[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
254/// Validate a list of runtime language specs.
255///
256/// # Arguments
257///
258/// * `specs` - Runtime specs in `language:version` form.
259///
260/// # Returns
261///
262/// Runtime language versions in the same order as the input.
263///
264/// # Errors
265///
266/// Returns [`RuntimeSpecError`] when any spec is invalid or configures a language twice.
267pub 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/// Errors returned while parsing workspace runtime specs.
287#[derive(Debug, Error, PartialEq, Eq)]
288pub enum RuntimeSpecError {
289    /// Runtime spec did not use `language:version` form.
290    #[error("invalid runtime spec '{spec}', expected language:version")]
291    InvalidFormat {
292        /// Invalid runtime spec.
293        spec: String,
294    },
295
296    /// Runtime language is not supported by Codex Universal.
297    #[error("unsupported runtime language '{language}'")]
298    UnsupportedLanguage {
299        /// Unsupported language name.
300        language: String,
301    },
302
303    /// Runtime version is not supported for a language.
304    #[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        /// Runtime language.
311        language: RuntimeLanguage,
312        /// Unsupported version.
313        version: String,
314    },
315
316    /// A language was configured more than once.
317    #[error("runtime language '{language}' was configured more than once", language = .language.name())]
318    DuplicateLanguage {
319        /// Duplicated language.
320        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}