1#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
6
7use std::fmt;
8use std::fs;
9use std::path::{Path, PathBuf};
10
11use presolve_compiler::platform::WorkspaceConfiguration;
12
13use crate::decode_cli_workspace_configuration_bytes_v1;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum CliExitCodeV1 {
17 Success = 0,
18 CompilationFailure = 1,
19 ConfigurationError = 2,
20 WorkspaceError = 3,
21 CompilerInternalError = 4,
22 CacheError = 5,
23 ToolingError = 6,
24 UnexpectedPlatformError = 7,
25}
26impl CliExitCodeV1 {
27 #[must_use]
28 pub const fn as_i32(self) -> i32 {
29 self as i32
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum CliCommandV1 {
35 Create,
36 Migrate,
37 Dev,
38 Build,
39 Watch,
40 Check,
41 Clean,
42 Explain,
43 Inspect,
44 Graph,
45 Trace,
46 Profile,
47 Benchmark,
48 Doctor,
49 Cache,
50 Workspace,
51 Version,
52}
53impl CliCommandV1 {
54 #[must_use]
55 pub const fn as_str(self) -> &'static str {
56 match self {
57 Self::Create => "create",
58 Self::Migrate => "migrate",
59 Self::Dev => "dev",
60 Self::Build => "build",
61 Self::Watch => "watch",
62 Self::Check => "check",
63 Self::Clean => "clean",
64 Self::Explain => "explain",
65 Self::Inspect => "inspect",
66 Self::Graph => "graph",
67 Self::Trace => "trace",
68 Self::Profile => "profile",
69 Self::Benchmark => "benchmark",
70 Self::Doctor => "doctor",
71 Self::Cache => "cache",
72 Self::Workspace => "workspace",
73 Self::Version => "version",
74 }
75 }
76}
77
78#[must_use]
79pub fn parse_cli_command_v1(value: &str) -> Option<CliCommandV1> {
80 match value {
81 "create" => Some(CliCommandV1::Create),
82 "migrate" => Some(CliCommandV1::Migrate),
83 "dev" => Some(CliCommandV1::Dev),
84 "build" => Some(CliCommandV1::Build),
85 "watch" => Some(CliCommandV1::Watch),
86 "check" => Some(CliCommandV1::Check),
87 "clean" => Some(CliCommandV1::Clean),
88 "explain" => Some(CliCommandV1::Explain),
89 "inspect" => Some(CliCommandV1::Inspect),
90 "graph" => Some(CliCommandV1::Graph),
91 "trace" => Some(CliCommandV1::Trace),
92 "profile" => Some(CliCommandV1::Profile),
93 "benchmark" => Some(CliCommandV1::Benchmark),
94 "doctor" => Some(CliCommandV1::Doctor),
95 "cache" => Some(CliCommandV1::Cache),
96 "workspace" => Some(CliCommandV1::Workspace),
97 "version" => Some(CliCommandV1::Version),
98 _ => None,
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct CliProjectEnvelopeV1 {
104 pub project_root: PathBuf,
105 pub configuration_path: PathBuf,
106 pub configuration: WorkspaceConfiguration,
107}
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct CliProjectEnvelopeErrorV1 {
110 pub code: &'static str,
111 pub message: String,
112}
113impl fmt::Display for CliProjectEnvelopeErrorV1 {
114 fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
115 write!(output, "{}: {}", self.code, self.message)
116 }
117}
118impl std::error::Error for CliProjectEnvelopeErrorV1 {}
119
120pub fn load_explicit_project_envelope_v1(
124 project_root: &Path,
125 configuration_path: &Path,
126) -> Result<CliProjectEnvelopeV1, CliProjectEnvelopeErrorV1> {
127 let bytes = fs::read(configuration_path).map_err(|error| CliProjectEnvelopeErrorV1 {
128 code: "L9P001_CONFIGURATION_READ_FAILED",
129 message: format!("failed to read {}: {error}", configuration_path.display()),
130 })?;
131 let configuration = decode_cli_workspace_configuration_bytes_v1(&bytes).map_err(|error| {
132 CliProjectEnvelopeErrorV1 {
133 code: "L9P002_CONFIGURATION_DECODE_FAILED",
134 message: error.to_string(),
135 }
136 })?;
137 Ok(CliProjectEnvelopeV1 {
138 project_root: project_root.into(),
139 configuration_path: configuration_path.into(),
140 configuration,
141 })
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 #[test]
148 fn l9b_command_names_and_exit_codes_are_stable() {
149 assert_eq!(parse_cli_command_v1("version"), Some(CliCommandV1::Version));
150 assert_eq!(parse_cli_command_v1("unknown"), None);
151 assert_eq!(CliExitCodeV1::ConfigurationError.as_i32(), 2);
152 assert_eq!(CliCommandV1::Workspace.as_str(), "workspace");
153 }
154 #[test]
155 fn l9b_loader_reads_only_the_explicit_configuration_document() {
156 let root = std::env::temp_dir().join("presolve-l9b-envelope");
157 let _ = fs::remove_dir_all(&root);
158 fs::create_dir_all(&root).unwrap();
159 let config = root.join("chosen.json");
160 fs::write(
161 &config,
162 include_bytes!("../fixtures/configuration/minimum-cli-v1.json"),
163 )
164 .unwrap();
165 let envelope = load_explicit_project_envelope_v1(&root, &config).unwrap();
166 assert_eq!(envelope.configuration, WorkspaceConfiguration::default());
167 assert_eq!(envelope.configuration_path, config);
168 fs::remove_dir_all(root).unwrap();
169 }
170}