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, LogDetail, LogFormat};
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 tracing_subscriber::filter::{FilterFn, LevelFilter};
28use tracing_subscriber::layer::SubscriberExt as _;
29use tracing_subscriber::util::SubscriberInitExt as _;
30use tracing_subscriber::{EnvFilter, Layer, fmt};
31
32use conjure_cp_lsp::server;
33
34struct LoggingState {
35 rule_trace_aggregates: Option<RuleTraceAggregatesHandle>,
36}
37
38impl LoggingState {
39 fn flush(&self) {
40 if let Some(handle) = &self.rule_trace_aggregates {
41 handle.flush();
42 }
43 }
44}
45
46pub fn main() {
47 match run() {
49 Ok(_) => {
50 exit(0);
51 }
52 Err(e) => {
53 eprintln!("{e:?}");
54 exit(2);
55 }
56 }
57}
58
59pub fn run() -> anyhow::Result<()> {
60 let cli = Cli::parse();
61
62 let logging_state = setup_logging(&cli.global_args)?;
63 let result = run_subcommand(cli);
64 logging_state.flush();
65 result
66}
67
68fn setup_logging(global_args: &GlobalArgs) -> anyhow::Result<LoggingState> {
69 let default_stderr_level = match (global_args.quiet, global_args.verbose) {
72 (true, _) => LevelFilter::OFF,
73 (false, 0) => LevelFilter::WARN,
74 (false, 1) => LevelFilter::INFO,
75 (false, 2) => LevelFilter::DEBUG,
76 (false, _) => LevelFilter::TRACE,
77 };
78
79 let env_filter = if global_args.quiet || global_args.verbose > 0 {
80 EnvFilter::new(default_stderr_level.to_string())
81 } else {
82 EnvFilter::builder()
83 .with_default_directive(default_stderr_level.into())
84 .from_env_lossy()
85 };
86
87 let stderr_layer = if global_args.verbose > 0 {
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 .with_filter(general_log_filter()),
95 )
96 } else {
97 Layer::boxed(
98 tracing_subscriber::fmt::layer()
99 .compact()
100 .with_writer(Arc::new(std::io::stderr()))
101 .with_ansi(true)
102 .with_filter(env_filter)
103 .with_filter(general_log_filter()),
104 )
105 };
106
107 let rule_trace_layer = global_args.rule_trace.clone().map(|x| {
108 let file = File::create(x).expect("Unable to create rule trace file");
109 fmt::layer()
110 .with_writer(file)
111 .with_level(false)
112 .without_time()
113 .with_target(false)
114 .with_filter(EnvFilter::new("rule_engine_rule_trace=trace"))
115 .with_filter(FilterFn::new(|meta| {
116 meta.target() == "rule_engine_rule_trace"
117 }))
118 });
119
120 let rule_attempt_trace_layer = global_args.rule_attempt_trace.clone().map(|x| {
121 let file = File::create(x).expect("Unable to create rule attempt trace file");
122 fmt::layer()
123 .with_writer(file)
124 .with_level(false)
125 .without_time()
126 .with_target(false)
127 .compact()
128 .with_ansi(false)
129 .with_filter(EnvFilter::new("rule_engine_rule_attempt_trace=trace"))
130 .with_filter(FilterFn::new(|meta| {
131 meta.target() == "rule_engine_rule_attempt_trace"
132 }))
133 });
134
135 let rule_trace_aggregates_handle = global_args
136 .rule_trace_aggregates
137 .clone()
138 .map(RuleTraceAggregatesHandle::new)
139 .transpose()?;
140
141 let rule_trace_aggregates_layer = rule_trace_aggregates_handle.as_ref().map(|handle| {
142 handle
143 .layer()
144 .with_filter(EnvFilter::new("rule_engine_rule_trace_aggregates=trace"))
145 .with_filter(FilterFn::new(|meta| {
146 meta.target() == "rule_engine_rule_trace_aggregates"
147 }))
148 });
149
150 let log_format = global_args.log_format.unwrap_or_default();
151 let log_detail = global_args.log_detail.unwrap_or_default();
152 let text_file = match (log_format, global_args.log_file.as_ref()) {
153 (LogFormat::Text, Some(path)) => Some(
154 File::options()
155 .truncate(true)
156 .write(true)
157 .create(true)
158 .append(false)
159 .open(path)?,
160 ),
161 _ => None,
162 };
163
164 let file_level = match log_detail {
165 LogDetail::Stages => LevelFilter::INFO,
166 LogDetail::Applications => LevelFilter::DEBUG,
167 LogDetail::Attempts => LevelFilter::TRACE,
168 };
169 let text_file_layer = text_file.map(|file| {
170 tracing_subscriber::fmt::layer()
171 .compact()
172 .with_ansi(false)
173 .with_writer(Arc::new(file))
174 .with_filter(file_level)
175 .with_filter(general_log_filter())
176 });
177 let json_file = match (log_format, global_args.log_file.as_ref()) {
178 (LogFormat::Json, Some(path)) => Some(
179 File::options()
180 .truncate(true)
181 .write(true)
182 .create(true)
183 .append(false)
184 .open(path)?,
185 ),
186 _ => None,
187 };
188 let json_file_layer = json_file.map(|file| {
189 tracing_subscriber::fmt::layer()
190 .json()
191 .with_writer(Arc::new(file))
192 .with_filter(file_level)
193 .with_filter(general_log_filter())
194 });
195
196 tracing_subscriber::registry()
197 .with(stderr_layer)
198 .with(rule_trace_layer)
199 .with(rule_attempt_trace_layer)
200 .with(rule_trace_aggregates_layer)
201 .with(text_file_layer)
202 .with(json_file_layer)
203 .init();
204
205 Ok(LoggingState {
206 rule_trace_aggregates: rule_trace_aggregates_handle,
207 })
208}
209
210fn general_log_filter() -> FilterFn<fn(&tracing::Metadata<'_>) -> bool> {
211 fn is_general_log(meta: &tracing::Metadata<'_>) -> bool {
212 !meta.target().starts_with("rule_engine_rule_")
213 }
214
215 FilterFn::new(is_general_log)
216}
217
218fn run_completion_command(completion_args: cli::CompletionArgs) -> anyhow::Result<()> {
219 let mut cmd = Cli::command();
220 let shell = completion_args.shell;
221 let name = cmd.get_name().to_string();
222
223 eprintln!("Generating completion for {shell}...");
224
225 generate(shell, &mut cmd, name, &mut io::stdout());
226 Ok(())
227}
228
229fn run_lsp_server() -> anyhow::Result<()> {
230 server::main();
231 Ok(())
232}
233
234fn run_subcommand(cli: Cli) -> anyhow::Result<()> {
236 let global_args = cli.global_args;
237 match cli.subcommand {
238 cli::Command::Solve(solve_args) => run_solve_command(global_args, solve_args),
239 cli::Command::TestSolve(local_args) => run_test_solve_command(global_args, local_args),
240 cli::Command::PrintJsonSchema => run_print_info_schema_command(),
241 cli::Command::Completion(completion_args) => run_completion_command(completion_args),
242 cli::Command::Pretty(pretty_args) => run_pretty_command(global_args, pretty_args),
243 cli::Command::ServerLSP => run_lsp_server(),
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use conjure_cp::parse::conjure_json::{get_example_model, get_example_model_by_path};
250
251 #[test]
252 fn test_get_example_model_success() {
253 let filename = "input";
254 get_example_model(filename).unwrap();
255 }
256
257 #[test]
258 fn test_get_example_model_by_filepath() {
259 let filepath = "../../test-suite/tests/integration/basic/misc/xyz/input.essence";
260 get_example_model_by_path(filepath).unwrap();
261 }
262
263 #[test]
264 fn test_get_example_model_fail_empty_filename() {
265 let filename = "";
266 get_example_model(filename).unwrap_err();
267 }
268
269 #[test]
270 fn test_get_example_model_fail_empty_filepath() {
271 let filepath = "";
272 get_example_model_by_path(filepath).unwrap_err();
273 }
274}