Skip to main content

conjure_oxide/
main.rs

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    // exit with 2 instead of 1 on failure,like grep
48    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    // It consists of composable layers, each of which logs to a different place in a different
70    // format.
71    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
234/// Runs the selected subcommand
235fn 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}