///|
fn print_usage() -> Unit {
println(
(
#|MoonRule - explainable rules for JSON data
#|
#|Usage:
#| moonrule eval
#| moonrule eval-file
#| moonrule explain
#| moonrule explain-file
#| moonrule analyze
#| moonrule analyze-rules
#| moonrule lint
#| moonrule lint-rules
#| moonrule test-rules
#| moonrule check
#| moonrule check-batch
#| moonrule functions
#|
#|Examples:
#| moonrule eval "user.age >= 18" "{\"user\":{\"age\":21}}"
#| moonrule check examples/access-rules.json examples/user.json
#|,
),
)
}
///|
fn command_index(args : Array[String]) -> Int? {
for index, value in args {
if value == "eval" ||
value == "eval-file" ||
value == "explain" ||
value == "explain-file" ||
value == "analyze" ||
value == "analyze-rules" ||
value == "lint" ||
value == "lint-rules" ||
value == "test-rules" ||
value == "check" ||
value == "check-batch" ||
value == "functions" {
return Some(index)
}
}
None
}
///|
fn print_diagnostic(diagnostic : @lib.Diagnostic, source : String) -> Unit {
println(diagnostic.render(source))
}
///|
fn run_eval(expression : String, json_text : String, explain~ : Bool) -> Int {
let context = @json.parse(json_text) catch {
error => {
println("invalid JSON input: \{error}")
return 2
}
}
let program = match @lib.compile(expression) {
Err(diagnostic) => {
print_diagnostic(diagnostic, expression)
return 2
}
Ok(program) => program
}
if explain {
match @lib.explain(program, context) {
Err(diagnostic) => {
print_diagnostic(diagnostic, expression)
2
}
Ok(trace) => {
println(trace.to_json_string())
0
}
}
} else {
match @lib.evaluate(program, context) {
Err(diagnostic) => {
print_diagnostic(diagnostic, expression)
2
}
Ok(value) => {
println(value.stringify(indent=2))
0
}
}
}
}
///|
fn run_eval_file(expression : String, path : String, explain~ : Bool) -> Int {
let json_text = @fs.read_file_to_string(path) catch {
_ => {
println("unable to read data file: \{path}")
return 2
}
}
run_eval(expression, json_text, explain~)
}
///|
fn load_rules(path : String) -> Result[@lib.RuleSet, Int] {
let rules_text = @fs.read_file_to_string(path) catch {
_ => {
println("unable to read rules file: \{path}")
return Err(2)
}
}
let rules_json = @json.parse(rules_text) catch {
error => {
println("invalid rules JSON: \{error}")
return Err(2)
}
}
match @lib.RuleSet::from_json(rules_json) {
Err(diagnostics) => {
for diagnostic in diagnostics {
print_diagnostic(diagnostic, rules_text)
}
Err(2)
}
Ok(rules) => Ok(rules)
}
}
///|
fn run_check(rules_path : String, data_path : String) -> Int {
let rules = match load_rules(rules_path) {
Err(code) => return code
Ok(rules) => rules
}
let data_text = @fs.read_file_to_string(data_path) catch {
_ => {
println("unable to read data file: \{data_path}")
return 2
}
}
let data_json = @json.parse(data_text) catch {
error => {
println("invalid data JSON: \{error}")
return 2
}
}
let report = rules.evaluate(data_json)
println(report.to_json_string())
if report.passed {
0
} else {
1
}
}
///|
fn run_check_batch(rules_path : String, data_path : String) -> Int {
let rules = match load_rules(rules_path) {
Err(code) => return code
Ok(rules) => rules
}
let data_text = @fs.read_file_to_string(data_path) catch {
_ => {
println("unable to read data file: \{data_path}")
return 2
}
}
let data_json = @json.parse(data_text) catch {
error => {
println("invalid data JSON: \{error}")
return 2
}
}
let contexts : Array[Json] = @json.from_json(data_json) catch {
error => {
println("batch input must be a JSON array: \{error}")
return 2
}
}
let report = rules.evaluate_batch(contexts)
println(report.to_json_string())
if report.passed {
0
} else {
1
}
}
///|
fn run_analyze(expression : String) -> Int {
match @lib.compile(expression) {
Err(diagnostic) => {
print_diagnostic(diagnostic, expression)
2
}
Ok(program) => {
let analysis = @lib.analyze(program)
println(analysis.to_json_string())
if analysis.warning_count() == 0 {
0
} else {
1
}
}
}
}
///|
fn run_analyze_rules(rules_path : String) -> Int {
match load_rules(rules_path) {
Err(code) => code
Ok(rules) => {
let analysis = rules.analyze()
println(analysis.to_json_string())
if analysis.warning_count == 0 {
0
} else {
1
}
}
}
}
///|
fn run_lint(expression : String) -> Int {
match @lib.compile(expression) {
Err(diagnostic) => {
print_diagnostic(diagnostic, expression)
2
}
Ok(program) =>
match @lib.analyze_with_policy(program, @lib.AnalysisPolicy::default()) {
Err(diagnostic) => {
print_diagnostic(diagnostic, expression)
2
}
Ok(report) => {
println(report.to_json_string())
if report.passed {
0
} else {
1
}
}
}
}
}
///|
fn run_lint_rules(rules_path : String) -> Int {
match load_rules(rules_path) {
Err(code) => code
Ok(rules) =>
match rules.analyze_with_policy(@lib.AnalysisPolicy::default()) {
Err(diagnostic) => {
print_diagnostic(diagnostic, "")
2
}
Ok(report) => {
println(report.to_json_string())
if report.passed {
0
} else {
1
}
}
}
}
}
///|
fn load_test_suite(path : String) -> Result[@lib.RuleTestSuite, Int] {
let suite_text = @fs.read_file_to_string(path) catch {
_ => {
println("unable to read rule test suite: \{path}")
return Err(2)
}
}
let suite_json = @json.parse(suite_text) catch {
error => {
println("invalid rule test suite JSON: \{error}")
return Err(2)
}
}
match @lib.RuleTestSuite::from_json(suite_json) {
Err(diagnostics) => {
for diagnostic in diagnostics {
print_diagnostic(diagnostic, suite_text)
}
Err(2)
}
Ok(suite) => Ok(suite)
}
}
///|
fn run_test_rules(rules_path : String, cases_path : String) -> Int {
let rules = match load_rules(rules_path) {
Err(code) => return code
Ok(rules) => rules
}
let suite = match load_test_suite(cases_path) {
Err(code) => return code
Ok(suite) => suite
}
let report = rules.run_test_suite(suite)
println(report.to_json_string())
if report.passed {
0
} else {
1
}
}
///|
fn main {
let args = @sys.get_cli_args()
guard command_index(args) is Some(index) else {
print_usage()
return
}
let remaining = args.length() - index - 1
let exit_code = match args[index] {
"eval" if remaining == 2 =>
run_eval(args[index + 1], args[index + 2], explain=false)
"eval-file" if remaining == 2 =>
run_eval_file(args[index + 1], args[index + 2], explain=false)
"explain" if remaining == 2 =>
run_eval(args[index + 1], args[index + 2], explain=true)
"explain-file" if remaining == 2 =>
run_eval_file(args[index + 1], args[index + 2], explain=true)
"analyze" if remaining == 1 => run_analyze(args[index + 1])
"analyze-rules" if remaining == 1 => run_analyze_rules(args[index + 1])
"lint" if remaining == 1 => run_lint(args[index + 1])
"lint-rules" if remaining == 1 => run_lint_rules(args[index + 1])
"test-rules" if remaining == 2 =>
run_test_rules(args[index + 1], args[index + 2])
"check" if remaining == 2 => run_check(args[index + 1], args[index + 2])
"check-batch" if remaining == 2 =>
run_check_batch(args[index + 1], args[index + 2])
"functions" if remaining == 0 => {
println(@lib.builtin_functions_json())
0
}
_ => {
print_usage()
2
}
}
@sys.exit(exit_code)
}