1#![allow(clippy::unwrap_used)]
2
3#[global_allocator]
4static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
5
6mod cli;
7mod pretty;
8mod print_info_schema;
9mod rule_trace_aggregates;
10mod solve;
11mod test_solve;
12use clap::{CommandFactory, Parser};
13use clap_complete::generate;
14use cli::{Cli, GlobalArgs};
15use pretty::run_pretty_command;
16use print_info_schema::run_print_info_schema_command;
17use rule_trace_aggregates::RuleTraceAggregatesHandle;
18use solve::run_solve_command;
19use std::fs::File;
20use std::io;
21use std::process::exit;
22use std::sync::Arc;
23use test_solve::run_test_solve_command;
24
25use conjure_cp_rules as _;
26
27use git_version::git_version;
28use tracing_subscriber::filter::{FilterFn, LevelFilter};
29use tracing_subscriber::layer::SubscriberExt as _;
30use tracing_subscriber::util::SubscriberInitExt as _;
31use tracing_subscriber::{EnvFilter, Layer, fmt};
32
33use conjure_cp_lsp::server;
34
35struct LoggingState {
36 rule_trace_aggregates: Option<RuleTraceAggregatesHandle>,
37}
38
39impl LoggingState {
40 fn flush(&self) {
41 if let Some(handle) = &self.rule_trace_aggregates {
42 handle.flush();
43 }
44 }
45}
46
47pub fn main() {
48 match run() {
50 Ok(_) => {
51 exit(0);
52 }
53 Err(e) => {
54 eprintln!("{e:?}");
55 exit(2);
56 }
57 }
58}
59
60pub fn run() -> anyhow::Result<()> {
61 let cli = Cli::parse();
62
63 if cli.version {
64 println!("Version: {}", git_version!());
65 return Ok(());
66 }
67
68 let logging_state = setup_logging(&cli.global_args)?;
69 let result = run_subcommand(cli);
70 logging_state.flush();
71 result
72}
73
74fn setup_logging(global_args: &GlobalArgs) -> anyhow::Result<LoggingState> {
75 let default_stderr_level = if global_args.verbose {
78 LevelFilter::DEBUG
79 } else {
80 LevelFilter::WARN
81 };
82
83 let env_filter = EnvFilter::builder()
84 .with_default_directive(default_stderr_level.into())
85 .from_env_lossy();
86
87 let stderr_layer = if global_args.verbose {
88 Layer::boxed(
89 tracing_subscriber::fmt::layer()
90 .pretty()
91 .with_writer(Arc::new(std::io::stderr()))
92 .with_ansi(true)
93 .with_filter(env_filter),
94 )
95 } else {
96 Layer::boxed(
97 tracing_subscriber::fmt::layer()
98 .compact()
99 .with_writer(Arc::new(std::io::stderr()))
100 .with_ansi(true)
101 .with_filter(env_filter),
102 )
103 };
104
105 let rule_trace_layer = global_args.rule_trace.clone().map(|x| {
106 let file = File::create(x).expect("Unable to create rule trace file");
107 fmt::layer()
108 .with_writer(file)
109 .with_level(false)
110 .without_time()
111 .with_target(false)
112 .with_filter(EnvFilter::new("rule_engine_rule_trace=trace"))
113 .with_filter(FilterFn::new(|meta| {
114 meta.target() == "rule_engine_rule_trace"
115 }))
116 });
117
118 let rule_trace_verbose_layer = global_args.rule_trace_verbose.clone().map(|x| {
119 let file = File::create(x).expect("Unable to create verbose rule trace file");
120 fmt::layer()
121 .with_writer(file)
122 .with_level(false)
123 .without_time()
124 .with_target(false)
125 .compact()
126 .with_ansi(false)
127 .with_filter(EnvFilter::new("rule_engine_rule_trace_verbose=trace"))
128 .with_filter(FilterFn::new(|meta| {
129 meta.target() == "rule_engine_rule_trace_verbose"
130 }))
131 });
132
133 let rule_trace_aggregates_handle = global_args
134 .rule_trace_aggregates
135 .clone()
136 .map(RuleTraceAggregatesHandle::new)
137 .transpose()?;
138
139 let rule_trace_aggregates_layer = rule_trace_aggregates_handle.as_ref().map(|handle| {
140 handle
141 .layer()
142 .with_filter(EnvFilter::new("rule_engine_rule_trace_aggregates=trace"))
143 .with_filter(FilterFn::new(|meta| {
144 meta.target() == "rule_engine_rule_trace_aggregates"
145 }))
146 });
147
148 let (json_layer, file_layer) = if global_args.log {
149 let mut log_path = global_args
150 .logfile
151 .clone()
152 .unwrap_or_else(|| std::path::PathBuf::from("conjure_oxide.log"));
153 let mut log_json = global_args
154 .logfile_json
155 .clone()
156 .unwrap_or_else(|| std::path::PathBuf::from("conjure_oxide_log.json"));
157
158 log_path.set_extension("log");
159 log_json.set_extension("json");
160
161 let json_log_file = File::options()
162 .truncate(true)
163 .write(true)
164 .create(true)
165 .append(false)
166 .open(log_json)?;
167
168 let log_file = File::options()
169 .truncate(true)
170 .write(true)
171 .create(true)
172 .append(false)
173 .open(log_path)?;
174
175 let json_layer = tracing_subscriber::fmt::layer()
176 .json()
177 .with_writer(Arc::new(json_log_file))
178 .with_filter(LevelFilter::TRACE);
179
180 let file_layer = tracing_subscriber::fmt::layer()
181 .compact()
182 .with_ansi(false)
183 .with_writer(Arc::new(log_file))
184 .with_filter(LevelFilter::TRACE);
185
186 (Some(json_layer), Some(file_layer))
187 } else {
188 (None, None)
189 };
190
191 tracing_subscriber::registry()
192 .with(stderr_layer)
193 .with(rule_trace_layer)
194 .with(rule_trace_verbose_layer)
195 .with(rule_trace_aggregates_layer)
196 .with(json_layer)
197 .with(file_layer)
198 .init();
199
200 Ok(LoggingState {
201 rule_trace_aggregates: rule_trace_aggregates_handle,
202 })
203}
204
205fn run_completion_command(completion_args: cli::CompletionArgs) -> anyhow::Result<()> {
206 let mut cmd = Cli::command();
207 let shell = completion_args.shell;
208 let name = cmd.get_name().to_string();
209
210 eprintln!("Generating completion for {shell}...");
211
212 generate(shell, &mut cmd, name, &mut io::stdout());
213 Ok(())
214}
215
216fn run_lsp_server() -> anyhow::Result<()> {
217 server::main();
218 Ok(())
219}
220
221fn run_subcommand(cli: Cli) -> anyhow::Result<()> {
223 let global_args = cli.global_args;
224 match cli.subcommand {
225 cli::Command::Solve(solve_args) => run_solve_command(global_args, solve_args),
226 cli::Command::TestSolve(local_args) => run_test_solve_command(global_args, local_args),
227 cli::Command::PrintJsonSchema => run_print_info_schema_command(),
228 cli::Command::Completion(completion_args) => run_completion_command(completion_args),
229 cli::Command::Pretty(pretty_args) => run_pretty_command(global_args, pretty_args),
230 cli::Command::ServerLSP => run_lsp_server(),
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use conjure_cp::parse::conjure_json::{get_example_model, get_example_model_by_path};
237
238 #[test]
239 fn test_get_example_model_success() {
240 let filename = "input";
241 get_example_model(filename).unwrap();
242 }
243
244 #[test]
245 fn test_get_example_model_by_filepath() {
246 let filepath = "../../tests-integration/tests/integration/xyz/input.essence";
247 get_example_model_by_path(filepath).unwrap();
248 }
249
250 #[test]
251 fn test_get_example_model_fail_empty_filename() {
252 let filename = "";
253 get_example_model(filename).unwrap_err();
254 }
255
256 #[test]
257 fn test_get_example_model_fail_empty_filepath() {
258 let filepath = "";
259 get_example_model_by_path(filepath).unwrap_err();
260 }
261}