1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
//! # umm
//!
//! A scriptable build tool/grader/test runner for Java projects that don't use
//! package managers.

#![warn(missing_docs)]
#![warn(clippy::missing_docs_in_private_items)]
#![feature(iterator_try_collect)]
#![feature(stmt_expr_attributes)]
#![feature(let_chains)]
#![feature(iter_collect_into)]

/// A module defining a bunch of constant values to be used throughout
pub mod constants;
/// For all things related to grading
pub mod grade;
/// For all things related to project health
pub mod health;
/// For discovering Java projects, analyzing them, and generating/executing
/// build tasks
pub mod java;
/// For all parsers used
pub mod parsers;
/// Utility functions for convenience
pub mod util;
/// For structs and enums related to VSCode Tasks
pub mod vscode;

use anyhow::{
    Context,
    Result,
};
use constants::{
    BUILD_DIR,
    COURSE,
    LIB_DIR,
    POSTGREST_CLIENT,
    ROOT_DIR,
    RUNTIME,
    SCRIPT_AST,
    TERM,
};
use grade::*;
use java::{
    File,
    FileType,
    Parser,
    Project,
};
use rhai::{
    Engine,
    EvalAltResult,
};
use umm_derive::generate_rhai_variant;
use util::{
    use_active_retrieval,
    use_heuristic_retrieval,
};

/// Defined for convenience
type Dict = std::collections::HashMap<String, String>;

/// Creates and returns a new `Engine` with all the types and functions
/// registered
pub fn create_engine() -> Engine {
    let mut engine = Engine::new();
    engine
        .register_type_with_name::<FileType>("JavaFileType")
        .build_type::<DocsGrader>()
        .build_type::<ByUnitTestGrader>()
        .build_type::<UnitTestGrader>()
        .build_type::<ByHiddenTestGrader>()
        .build_type::<DiffGrader>()
        .build_type::<Grade>()
        .build_type::<GradeResult>()
        .build_type::<Parser>()
        .build_type::<File>()
        .build_type::<Query>()
        .build_type::<QueryGrader>()
        .build_type::<Project>()
        .register_fn("clean", clean_script)
        .register_fn("show_results", show_result_script)
        .register_fn("generate_single_feedback", generate_single_feedback_script)
        .register_fn("generate_feedback", generate_feedback_script)
        .register_fn("use_active_retrieval", use_active_retrieval)
        .register_fn("use_heuristic_retrieval", use_heuristic_retrieval);
    engine
}

/// Prints the result of grading
pub fn grade(name_or_path: &str) -> Result<()> {
    let engine = create_engine();

    // println!("{}", engine.gen_fn_signatures(false).join("\n"));
    let script = match std::fs::read_to_string(name_or_path) {
        Ok(s) => s,
        Err(_) => {
            let assignment_name = name_or_path.to_string().replace(['\"', '\\'], "");
            let rt = RUNTIME.handle().clone();

            let resp = rt.block_on(async {
                POSTGREST_CLIENT
                    .from("grading_scripts")
                    .eq("course", COURSE)
                    .eq("term", TERM)
                    .eq("assignment", &assignment_name)
                    .select("url")
                    .single()
                    .execute()
                    .await?
                    .text()
                    .await
                    .context(format!(
                        "Could not get grading script for {assignment_name}"
                    ))
            });
            let resp: serde_json::Value = serde_json::from_str(resp?.as_str())?;
            let resp = resp.as_object().unwrap();

            if let Some(message) = resp.get("message") {
                anyhow::bail!("Error for {assignment_name}: {message}");
            }

            let script_url = resp.get("url").unwrap().as_str().unwrap();

            reqwest::blocking::get(script_url)
                .context(format!("Cannot get url: {script_url}"))?
                .text()
                .context(format!(
                    "Could not parse the response from {script_url} to text."
                ))?
        }
    };
    let ast = engine.compile(script)?;
    {
        let ast = std::sync::Arc::clone(&SCRIPT_AST);
        let mut ast = ast.lock().unwrap();
        *ast = ast.clone();
    }

    // Run the script
    engine.run_ast(&ast)?;

    Ok(())
}

#[generate_rhai_variant(Fallible)]
/// Deletes all java compiler artefacts
pub fn clean() -> Result<()> {
    if BUILD_DIR.as_path().exists() {
        std::fs::remove_dir_all(BUILD_DIR.as_path())
            .with_context(|| format!("Could not delete {}", BUILD_DIR.display()))?;
    }
    if LIB_DIR.as_path().exists() {
        std::fs::remove_dir_all(LIB_DIR.as_path())
            .with_context(|| format!("Could not delete {}", LIB_DIR.display()))?;
    }
    if ROOT_DIR.join(".vscode/settings.json").as_path().exists() {
        std::fs::remove_file(ROOT_DIR.join(".vscode/settings.json").as_path()).with_context(
            || {
                format!(
                    "Could not delete {}",
                    ROOT_DIR.join(".vscode/settings.json").display()
                )
            },
        )?;
    }
    if ROOT_DIR.join(".vscode/tasks.json").as_path().exists() {
        std::fs::remove_file(ROOT_DIR.join(".vscode/tasks.json").as_path()).with_context(|| {
            format!(
                "Could not delete {}",
                ROOT_DIR.join(".vscode/tasks.json").display()
            )
        })?;
    }

    Ok(())
}

// TODO: replace std::Command with cmd_lib
// TODO: Lazily load all constants from rhai scripts instead
// TODO: Fix java mod impls
// TODO: update classpath when discovering project
// TODO: fix grading api
// TODO: add rhai scripting for grading
// TODO: find a way to generate a rhai wrapper for all methods
// TODO: add rhai scripting for project init
// TODO: update tabled to 0.6
// TODO: make reedline shell optional behind a feature
// TODO: Download jars only if required OR remove jar requirement altogether.