///|
fn fixture_action(value : Json, path : String) -> Action raise SieveError {
let obj = json_object(value, path)
json_keys(obj, ["kind", "target"], path)
let kind = json_string(json_required(obj, "kind", path), path + ".kind")
match kind {
"keep" | "discard" => {
if obj.contains("target") {
fail("json.action", "keep/discard cannot have a target", origin())
}
if kind == "keep" {
Keep
} else {
Discard
}
}
"fileinto" | "redirect" => {
let target = json_string(
json_required(obj, "target", path),
path + ".target",
)
safe_destination(target, kind, origin())
if kind == "fileinto" {
FileInto(target)
} else {
Redirect(target)
}
}
_ => {
fail("json.action", "unsupported expected action", origin())
Keep
}
}
}
///|
pub fn parse_replay_json(
text : String,
limits? : Limits = Limits::default(),
) -> Array[ReplayCase] raise SieveError {
let array = json_array(parse_json_bounded(text, limits), "$")
if array.length() > 1000 {
fail("replay.count", "fixture array exceeds 1000 cases", origin())
}
let cases : Array[ReplayCase] = []
for i = 0; i < array.length(); i = i + 1 {
let path = "$[\{i}]"
let obj = json_object(array[i], path)
json_keys(obj, ["id", "message", "expected", "expected_error"], path)
let id = json_string(json_required(obj, "id", path), path + ".id")
let message = Message::from_json(
json_required(obj, "message", path),
limits~,
)
if obj.contains("expected") && obj.contains("expected_error") {
fail(
"json.expectation",
"expected and expected_error are mutually exclusive",
origin(),
)
}
let expectation = match (obj.get("expected"), obj.get("expected_error")) {
(Some(value), None) => {
let values = json_array(value, path + ".expected")
if values.length() > limits.actions {
fail("limit.actions", "too many expected actions", origin())
}
let actions : Array[Action] = []
for value in values {
actions.push(fixture_action(value, path + ".expected[]"))
}
Deliver(actions)
}
(None, Some(value)) =>
FailWith(json_string(value, path + ".expected_error"))
_ => Observe
}
cases.push({ id, message, expectation })
}
cases
}
///|
pub fn PolicyChange::as_json(
self : PolicyChange,
redact_destinations? : Bool = false,
) -> Json {
Json::object({
"id": Json::string(self.id),
"changed": Json::boolean(self.changed),
"removed": Json::array(
self.removed.map(action => action_json(action, redact_destinations)),
),
"added": Json::array(
self.added.map(action => action_json(action, redact_destinations)),
),
"delivery_lost": Json::boolean(self.delivery_lost),
"new_redirect": Json::boolean(self.new_redirect),
"before_error": match self.before_error {
Some(s) => Json::string(s)
None => Json::null()
},
"after_error": match self.after_error {
Some(s) => Json::string(s)
None => Json::null()
},
})
}
///|
fn dispatch_request(text : String) -> (Int, String) raise SieveError {
let obj = json_object(
parse_json_bounded(text, { ..Limits::default(), source_chars: 4194304 }),
"$",
)
json_keys(
obj,
["command", "source", "message", "fixtures", "before", "io_error"],
"$",
)
if obj.contains("io_error") {
fail(
"cli.io",
json_string(json_required(obj, "io_error", "$"), "$.io_error"),
origin(),
)
}
let command = json_string(json_required(obj, "command", "$"), "$.command")
if command == "help" {
return (
0, "MoonSieve 离线规则工具\n用法:moon run cmd/main --target js -- <命令> <文件...>\n lint SCRIPT\n format SCRIPT\n run SCRIPT MESSAGE.json\n replay SCRIPT CASES.json\n diff BEFORE.sieve AFTER.sieve CASES.json\n输出不执行任何实际邮件操作。退出码:0 成功,1 回放不符/规则变化,2 输入或执行错误。",
)
}
let source = json_string(json_required(obj, "source", "$"), "$.source")
match command {
"format" => (0, format_script(source))
"lint" => (0, compile(source).audit().as_json().stringify(indent=2))
"run" => {
let message = json_string(json_required(obj, "message", "$"), "$.message")
(0, run_json(source, message).stringify(indent=2))
}
"replay" => {
let fixtures = json_string(
json_required(obj, "fixtures", "$"),
"$.fixtures",
)
let report = compile(source).replay(parse_replay_json(fixtures))
let unexpected_errors = report.results.any(item => {
item.error is Some(_) && item.expectation_met != Some(true)
})
let code = if unexpected_errors {
2
} else if report.failed_expectations > 0 {
1
} else {
0
}
(code, report.as_json().stringify(indent=2))
}
"diff" => {
let old_source = json_string(
json_required(obj, "before", "$"),
"$.before",
)
let fixtures = json_string(
json_required(obj, "fixtures", "$"),
"$.fixtures",
)
let cases = parse_replay_json(fixtures)
let before = compile(old_source).replay(cases)
let after = compile(source).replay(cases)
let changes = compare_replays(before, after)
let code = if before.execution_errors > 0 || after.execution_errors > 0 {
2
} else if changes.any(change => change.changed) {
1
} else {
0
}
(
code,
Json::array(changes.map(change => change.as_json())).stringify(indent=2),
)
}
_ => {
fail("cli.command", "unknown command; use help", origin())
(2, "")
}
}
}
///|
pub fn handle_request(text : String) -> (Int, String) {
dispatch_request(text) catch {
SieveError(d) =>
(2, Json::object({ "error": d.as_json() }).stringify(indent=2))
}
}