demystify_web/wrap/
mod.rs

1use anyhow::Context;
2use axum::{Json, extract::Multipart};
3use axum_session::{Session, SessionNullPool};
4use once_cell::sync::Lazy;
5use serde::Deserialize;
6use serde_json::Value;
7
8use std::{fs::File, io::Write, path::PathBuf, sync::Arc};
9
10use anyhow::anyhow;
11
12use crate::util::{self, get_solver_global, set_solver_global};
13
14use demystify::problem::{self, planner::PuzzlePlanner, solver::PuzzleSolver};
15
16macro_rules! include_model_file {
17    ($path:expr) => {
18        include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", $path))
19    };
20}
21
22static EXAMPLES: Lazy<[(&str, &str, &str); 4]> = Lazy::new(|| {
23    [
24        (
25            "Sudoku",
26            include_model_file!("examples/eprime/sudoku.eprime"),
27            include_model_file!("examples/eprime/sudoku/puzzlingexample.param"),
28        ),
29        (
30            "MiracleSudoku",
31            include_model_file!("examples/eprime/miracle.eprime"),
32            include_model_file!("examples/eprime/miracle/original.param"),
33        ),
34        (
35            "StarBattle",
36            include_model_file!("examples/eprime/star-battle.eprime"),
37            include_model_file!("examples/eprime/star-battle/FATAtalkexample.param"),
38        ),
39        (
40            "Binairo",
41            include_model_file!("examples/eprime/binairo.essence"),
42            include_model_file!("examples/eprime/binairo/diiscu.param"),
43        ),
44    ]
45});
46
47pub async fn dump_full_solve(
48    session: Session<SessionNullPool>,
49) -> Result<Json<Value>, util::AppError> {
50    let solver = get_solver_global(&session)?;
51
52    let mut solver = solver.lock().unwrap();
53
54    let solve = solver.quick_solve();
55
56    Ok(Json(serde_json::value::to_value(solve).unwrap()))
57}
58
59pub async fn best_next_step(session: Session<SessionNullPool>) -> Result<String, util::AppError> {
60    let solver = get_solver_global(&session)?;
61
62    let mut solver = solver.lock().unwrap();
63
64    let (solve, lits) = solver.quick_solve_html_step();
65
66    solver.mark_lits_as_deduced(&lits);
67
68    if solve.is_empty() {
69        Ok("Please upload a puzzle or select an example to begin.".to_string())
70    } else {
71        Ok(solve)
72    }
73}
74
75pub async fn get_difficulties(session: Session<SessionNullPool>) -> Result<String, util::AppError> {
76    let solver = get_solver_global(&session)?;
77
78    let mut solver = solver.lock().unwrap();
79
80    let solve = solver.quick_generate_html_difficulties();
81
82    Ok(solve)
83}
84
85pub async fn refresh(session: Session<SessionNullPool>) -> Result<String, util::AppError> {
86    let solver = get_solver_global(&session)?;
87
88    let mut solver = solver.lock().unwrap();
89
90    let (solve, _) = solver.quick_display_html_step(None);
91
92    Ok(solve)
93}
94
95pub async fn click_literal(
96    headers: axum::http::header::HeaderMap,
97    session: Session<SessionNullPool>,
98) -> Result<String, util::AppError> {
99    let solver = get_solver_global(&session)?;
100
101    let mut solver = solver.lock().unwrap();
102
103    let cell = headers
104        .get("hx-trigger")
105        .context("Missing header: 'hx-trigger'")?;
106    let cell = cell.to_str()?;
107    let cell: Result<Vec<_>, _> = cell.split('_').skip(1).map(str::parse).collect();
108    let cell = cell?;
109
110    session.set("click_cell", &cell);
111
112    let (html, lits) = solver.quick_solve_html_step_for_literal(cell);
113
114    let lidx_lits: Vec<_> = lits.iter().map(|x| x.lidx()).collect();
115    session.set("lidx_lits", &lidx_lits);
116
117    Ok(html)
118}
119
120pub async fn upload_files(
121    session: Session<SessionNullPool>,
122    mut multipart: Multipart,
123) -> Result<String, util::AppError> {
124    let temp_dir = tempfile::tempdir().context("Failed to create temporary directory")?;
125
126    let mut model: Option<PathBuf> = None;
127    let mut param: Option<PathBuf> = None;
128
129    while let Some(field) = multipart
130        .next_field()
131        .await
132        .context("Failed to parse multipart upload")?
133    {
134        if field.name().unwrap() != "model" && field.name().unwrap() != "parameter" {
135            return Err(anyhow!(
136                "Form malformed -- should contain 'model' and 'parameter', but it contains '{}'",
137                field.name().unwrap()
138            )
139            .into());
140        }
141
142        // Grab the name
143        let form_file_name = field.file_name().context("No filename")?;
144
145        eprintln!("Got file '{form_file_name}'!");
146
147        let file_name = if form_file_name.ends_with(".param") || form_file_name.ends_with(".json") {
148            if param.is_some() {
149                return Err(anyhow!("Cannot upload two param files (.param or .json)").into());
150            }
151
152            if form_file_name.ends_with(".param") {
153                param = Some("upload.param".into());
154                "upload.param"
155            } else {
156                param = Some("upload.json".into());
157                "upload.json"
158            }
159        } else if form_file_name.ends_with(".eprime") || form_file_name.ends_with(".essence") {
160            if model.is_some() {
161                return Err(anyhow!("Can only upload one .eprime or .essence file").into());
162            }
163            if form_file_name.ends_with(".eprime") {
164                model = Some("upload.eprime".into());
165                "upload.eprime"
166            } else {
167                model = Some("upload.essence".into());
168                "upload.essence"
169            }
170        } else {
171            return Err(anyhow!(
172                "Only expecting .param, .json, .eprime or .essence uploads, not '{}'",
173                form_file_name
174            )
175            .into());
176        };
177
178        // Create a path for the soon-to-be file
179        let file_path = temp_dir.path().join(file_name);
180
181        // Unwrap the incoming bytes
182        let data = field.bytes().await.context("Failed to read file bytes")?;
183
184        // Open a handle to the file
185        let mut file_handle = File::create(file_path).context("Failed to open file for writing")?;
186
187        // Write the incoming data to the handle
188        file_handle
189            .write_all(&data)
190            .context("Failed to write data!")?;
191    }
192
193    if model.is_none() {
194        return Ok(r###"
195            <div class="alert alert-danger">
196                <h4>Upload Error</h4>
197                <p>Please upload a model file (.eprime or .essence)</p>
198            </div>
199        "###
200        .to_string());
201    }
202
203    if param.is_none() {
204        return Ok(r###"
205            <div class="alert alert-danger">
206                <h4>Upload Error</h4>
207                <p>Please upload a parameter file (.param or .json)</p>
208            </div>
209        "###
210        .to_string());
211    }
212
213    match load_model(&session, temp_dir, model, param) {
214        Ok(_) => refresh(session).await,
215        Err(e) => Ok(format!(
216            r###"
217            <div class="alert alert-danger">
218                <h4>Failed to upload puzzle</h4>
219                <pre class="text-danger">{e:#}</pre>
220                <p>Please check your files and try again.</p>
221            </div>
222            "###
223        )),
224    }
225}
226
227#[derive(Deserialize)]
228pub struct ExampleParams {
229    example_name: String,
230}
231
232#[derive(Deserialize)]
233pub struct SubmitExampleParams {
234    param_content: String,
235    example_name: String,
236}
237
238pub async fn load_example(
239    _session: Session<SessionNullPool>,
240    form: axum::extract::Form<ExampleParams>,
241) -> Result<String, util::AppError> {
242    let example_name = form.example_name.clone();
243
244    let param_content = EXAMPLES
245        .iter()
246        .find(|(name, _, _)| *name == example_name)
247        .map(|(_, _, content)| *content)
248        .context(format!("Example '{example_name}' not found"))?;
249
250    Ok(format!(
251        r###"
252        <h5>Edit Parameters for {example_name}</h5>
253        <form id="paramForm" hx-post="/submitExample" hx-target="#mainSpace">
254            <input type="hidden" name="example_name" value="{example_name}">
255            <textarea name="param_content" class="form-control" rows="15" style="font-family: monospace;">{param_content}</textarea>
256            <button type="submit" class="btn btn-primary mt-2" hx-indicator="#indicator">
257                Submit Parameters
258            </button>
259        </form>
260    "###
261    ))
262}
263
264pub async fn get_example_names() -> String {
265    let options = EXAMPLES
266        .iter()
267        .map(|(name, _, _)| format!("<option value=\"{name}\">{name}</option>"))
268        .collect::<Vec<_>>()
269        .join("");
270
271    r###"<form id="exampleForm" hx-post="/loadExample" hx-target="#exampleParams">
272                        <select name="example_name" class="form-select" required>
273    "###
274    .to_owned()
275        + &options
276        + r###" 
277                        </select>
278                        <button type="submit" class="btn btn-primary mt-3" hx-indicator="#indicator">
279                            Load Example
280                        </button>
281                    </form>
282    "###
283}
284
285pub async fn submit_example(
286    session: Session<SessionNullPool>,
287    form: axum::extract::Form<SubmitExampleParams>,
288) -> Result<String, util::AppError> {
289    let example_name = form.example_name.clone();
290    let param_content = form.param_content.clone();
291
292    let model_content = EXAMPLES
293        .iter()
294        .find(|(name, _, _)| *name == example_name)
295        .map(|(_, content, _)| *content)
296        .context(format!("Example '{example_name}' not found"))?;
297
298    let temp_dir = tempfile::tempdir().context("Failed to create temporary directory")?;
299
300    let model_dest = temp_dir.path().join("upload.eprime");
301    std::fs::write(&model_dest, model_content).context("Failed to write model file")?;
302
303    let param_dest = temp_dir.path().join("upload.param");
304    std::fs::write(&param_dest, param_content).context("Failed to write param file")?;
305
306    match load_model(
307        &session,
308        temp_dir,
309        Some("upload.eprime".into()),
310        Some("upload.param".into()),
311    ) {
312        Ok(_) => refresh(session).await,
313        Err(e) => Ok(format!(
314            r###"
315            <div class="alert alert-danger">
316                <h4>Failed to load puzzle</h4>
317                <pre class="text-danger">{e:#}</pre>
318                <p>Please check your parameter file and try again.</p>
319            </div>
320            "###
321        )),
322    }
323}
324
325fn load_model(
326    session: &Session<SessionNullPool>,
327    temp_dir: tempfile::TempDir,
328    model: Option<PathBuf>,
329    param: Option<PathBuf>,
330) -> anyhow::Result<()> {
331    let puzzle = problem::parse::parse_essence(
332        &temp_dir.path().join(model.unwrap()),
333        &temp_dir.path().join(param.unwrap()),
334    )?;
335    let puzzle = Arc::new(puzzle);
336    let puz = PuzzleSolver::new(puzzle)?;
337    let plan = PuzzlePlanner::new(puz);
338    set_solver_global(session, plan);
339    Ok(())
340}