1
use std::fs::File;
2
use std::io::Write;
3
use std::path::PathBuf;
4
use std::process::exit;
5
use std::sync::Arc;
6

            
7
use anyhow::Result as AnyhowResult;
8
use anyhow::{anyhow, bail};
9
use clap::{arg, command, Parser};
10
use conjure_oxide::defaults::get_default_rule_sets;
11
use schemars::schema_for;
12
use serde_json::json;
13
use serde_json::to_string_pretty;
14

            
15
use conjure_core::context::Context;
16
use conjure_oxide::find_conjure::conjure_executable;
17
use conjure_oxide::model_from_json;
18
use conjure_oxide::rule_engine::{
19
    get_rule_priorities, get_rules_vec, resolve_rule_sets, rewrite_model,
20
};
21

            
22
use conjure_oxide::utils::conjure::{get_minion_solutions, minion_solutions_to_json};
23
use conjure_oxide::SolverFamily;
24
use tracing_subscriber::filter::LevelFilter;
25
use tracing_subscriber::layer::SubscriberExt as _;
26
use tracing_subscriber::util::SubscriberInitExt as _;
27
use tracing_subscriber::{EnvFilter, Layer};
28

            
29
#[derive(Parser)]
30
#[command(author, version, about, long_about = None)]
31
struct Cli {
32
    #[arg(value_name = "INPUT_ESSENCE", help = "The input Essence file")]
33
    input_file: PathBuf,
34

            
35
    #[arg(
36
        long,
37
        value_name = "EXTRA_RULE_SETS",
38
        help = "Names of extra rule sets to enable"
39
    )]
40
    extra_rule_sets: Vec<String>,
41

            
42
    #[arg(
43
        long,
44
        value_enum,
45
        value_name = "SOLVER",
46
        short = 's',
47
        help = "Solver family use (Minion by default)"
48
    )]
49
    solver: Option<SolverFamily>, // ToDo this should probably set the solver adapter
50

            
51
    // TODO: subcommands instead of these being a flag.
52
    #[arg(
53
        long,
54
        default_value_t = false,
55
        help = "Print the schema for the info JSON and exit"
56
    )]
57
    print_info_schema: bool,
58

            
59
    #[arg(long, help = "Save execution info as JSON to the given file-path.")]
60
    info_json_path: Option<PathBuf>,
61

            
62
    #[arg(
63
        long,
64
        short = 'o',
65
        help = "Save solutions to a JSON file (prints to stdin by default)"
66
    )]
67
    output: Option<PathBuf>,
68

            
69
    #[arg(long, short = 'v', help = "Log verbosely to sterr")]
70
    verbose: bool,
71
}
72

            
73
#[allow(clippy::unwrap_used)]
74
pub fn main() -> AnyhowResult<()> {
75
    let cli = Cli::parse();
76

            
77
    #[allow(clippy::unwrap_used)]
78
    if cli.print_info_schema {
79
        let schema = schema_for!(Context);
80
        println!("{}", serde_json::to_string_pretty(&schema).unwrap());
81
        return Ok(());
82
    }
83

            
84
    let target_family = cli.solver.unwrap_or(SolverFamily::Minion);
85
    let mut extra_rule_sets: Vec<String> = get_default_rule_sets();
86
    extra_rule_sets.extend(cli.extra_rule_sets);
87

            
88
    let out_file: Option<File> = match &cli.output {
89
        None => None,
90
        Some(pth) => Some(
91
            File::options()
92
                .create(true)
93
                .truncate(true)
94
                .write(true)
95
                .open(pth)?,
96
        ),
97
    };
98

            
99
    // Logging:
100
    //
101
    // Using `tracing` framework, but this automatically reads stuff from `log`.
102
    //
103
    // A Subscriber is responsible for logging.
104
    //
105
    // It consists of composable layers, each of which logs to a different place in a different
106
    // format.
107
    let json_log_file = File::options()
108
        .create(true)
109
        .append(true)
110
        .open("conjure_oxide_log.json")?;
111

            
112
    let log_file = File::options()
113
        .create(true)
114
        .append(true)
115
        .open("conjure_oxide.log")?;
116

            
117
    // get log level from env-var RUST_LOG
118

            
119
    let json_layer = tracing_subscriber::fmt::layer()
120
        .json()
121
        .with_writer(Arc::new(json_log_file))
122
        .with_filter(LevelFilter::TRACE);
123

            
124
    let file_layer = tracing_subscriber::fmt::layer()
125
        .compact()
126
        .with_ansi(false)
127
        .with_writer(Arc::new(log_file))
128
        .with_filter(LevelFilter::TRACE);
129

            
130
    let default_stderr_level = if cli.verbose {
131
        LevelFilter::DEBUG
132
    } else {
133
        LevelFilter::WARN
134
    };
135

            
136
    let env_filter = EnvFilter::builder()
137
        .with_default_directive(default_stderr_level.into())
138
        .from_env_lossy();
139

            
140
    let stderr_layer = if cli.verbose {
141
        Layer::boxed(
142
            tracing_subscriber::fmt::layer()
143
                .pretty()
144
                .with_writer(Arc::new(std::io::stderr()))
145
                .with_ansi(true)
146
                .with_filter(env_filter),
147
        )
148
    } else {
149
        Layer::boxed(
150
            tracing_subscriber::fmt::layer()
151
                .compact()
152
                .with_writer(Arc::new(std::io::stderr()))
153
                .with_ansi(true)
154
                .with_filter(env_filter),
155
        )
156
    };
157

            
158
    // load the loggers
159
    tracing_subscriber::registry()
160
        .with(json_layer)
161
        .with(stderr_layer)
162
        .with(file_layer)
163
        .init();
164

            
165
    if target_family != SolverFamily::Minion {
166
        log::error!("Only the Minion solver is currently supported!");
167
        exit(1);
168
    }
169

            
170
    let rule_sets = match resolve_rule_sets(target_family, &extra_rule_sets) {
171
        Ok(rs) => rs,
172
        Err(e) => {
173
            log::error!("Error resolving rule sets: {}", e);
174
            exit(1);
175
        }
176
    };
177

            
178
    let pretty_rule_sets = rule_sets
179
        .iter()
180
        .map(|rule_set| rule_set.name)
181
        .collect::<Vec<_>>()
182
        .join(", ");
183

            
184
    println!("Enabled rule sets: [{}]", pretty_rule_sets);
185
    log::info!(
186
        target: "file",
187
        "Rule sets: {}",
188
        pretty_rule_sets
189
    );
190

            
191
    let rule_priorities = get_rule_priorities(&rule_sets)?;
192
    let rules_vec = get_rules_vec(&rule_priorities);
193

            
194
    log::info!(target: "file",
195
         "Rules and priorities: {}",
196
         rules_vec.iter()
197
            .map(|rule| format!("{}: {}", rule.name, rule_priorities.get(rule).unwrap_or(&0)))
198
            .collect::<Vec<_>>()
199
            .join(", "));
200

            
201
    log::info!(target: "file", "Input file: {}", cli.input_file.display());
202
    let input_file: &str = cli.input_file.to_str().ok_or(anyhow!(
203
        "Given input_file could not be converted to a string"
204
    ))?;
205

            
206
    /******************************************************/
207
    /*        Parse essence to json using Conjure         */
208
    /******************************************************/
209

            
210
    conjure_executable()
211
        .map_err(|e| anyhow!("Could not find correct conjure executable: {}", e))?;
212

            
213
    let mut cmd = std::process::Command::new("conjure");
214
    let output = cmd
215
        .arg("pretty")
216
        .arg("--output-format=astjson")
217
        .arg(input_file)
218
        .output()?;
219

            
220
    let conjure_stderr = String::from_utf8(output.stderr)?;
221
    if !conjure_stderr.is_empty() {
222
        bail!(conjure_stderr);
223
    }
224

            
225
    let astjson = String::from_utf8(output.stdout)?;
226

            
227
    let context = Context::new_ptr(
228
        target_family,
229
        extra_rule_sets.clone(),
230
        rules_vec.clone(),
231
        rule_sets.clone(),
232
    );
233

            
234
    context.write().unwrap().file_name = Some(cli.input_file.to_str().expect("").into());
235

            
236
    if cfg!(feature = "extra-rule-checks") {
237
        log::info!("extra-rule-checks: enabled");
238
    } else {
239
        log::info!("extra-rule-checks: disabled");
240
    }
241

            
242
    let mut model = model_from_json(&astjson, context.clone())?;
243

            
244
    log::info!(target: "file", "Initial model: {}", json!(model));
245

            
246
    log::info!(target: "file", "Rewriting model...");
247
    model = rewrite_model(&model, &rule_sets)?;
248
    let constraints_string = format!("{:?}", model.constraints);
249
    tracing::info!(%constraints_string, model=%json!(model),"Rewritten model");
250

            
251
    let solutions = get_minion_solutions(model)?; // ToDo we need to properly set the solver adaptor here, not hard code minion
252
    log::info!(target: "file", "Solutions: {}", minion_solutions_to_json(&solutions));
253

            
254
    let solutions_json = minion_solutions_to_json(&solutions);
255
    let solutions_str = to_string_pretty(&solutions_json)?;
256
    match out_file {
257
        None => {
258
            println!("Solutions:");
259
            println!("{}", solutions_str);
260
        }
261
        Some(mut outf) => {
262
            outf.write_all(solutions_str.as_bytes())?;
263
            println!(
264
                "Solutions saved to {:?}",
265
                &cli.output.unwrap().canonicalize()?
266
            )
267
        }
268
    }
269

            
270
    if let Some(path) = cli.info_json_path {
271
        #[allow(clippy::unwrap_used)]
272
        let context_obj = context.read().unwrap().clone();
273
        let generated_json = &serde_json::to_value(context_obj)?;
274
        let pretty_json = serde_json::to_string_pretty(&generated_json)?;
275
        File::create(path)?.write_all(pretty_json.as_bytes())?;
276
    }
277
    Ok(())
278
}
279

            
280
#[cfg(test)]
281
mod tests {
282
    use conjure_oxide::{get_example_model, get_example_model_by_path};
283

            
284
    #[test]
285
1
    fn test_get_example_model_success() {
286
1
        let filename = "input";
287
1
        get_example_model(filename).unwrap();
288
1
    }
289

            
290
    #[test]
291
1
    fn test_get_example_model_by_filepath() {
292
1
        let filepath = "tests/integration/xyz/input.essence";
293
1
        get_example_model_by_path(filepath).unwrap();
294
1
    }
295

            
296
    #[test]
297
1
    fn test_get_example_model_fail_empty_filename() {
298
1
        let filename = "";
299
1
        get_example_model(filename).unwrap_err();
300
1
    }
301

            
302
    #[test]
303
1
    fn test_get_example_model_fail_empty_filepath() {
304
1
        let filepath = "";
305
1
        get_example_model_by_path(filepath).unwrap_err();
306
1
    }
307
}