///|
priv struct FileCliResponse {
roster : Json
error : Json
} derive(ToJson)
///|
fn file_cli_error(message : String) -> String {
let absent : String? = None
let response : FileCliResponse = {
roster: @json.to_json(absent),
error: @json.to_json(message),
}
@json.to_json(response).stringify(indent=2)
}
///|
priv struct FileOptions {
path : String?
check : Bool
csv : Bool
fair_limit : Int?
}
///|
fn parse_file_options(args : Array[String]) -> Result[FileOptions, String] {
let mut path : String? = None
let mut use_stdin = false
let mut check = false
let mut csv = false
let mut fair_limit : Int? = None
let mut index = 1
while index < args.length() {
match args[index] {
"--file" => {
if path is Some(_) ||
index + 1 >= args.length() ||
args[index + 1].has_prefix("--") {
return Err("--file requires exactly one path")
}
path = Some(args[index + 1])
index = index + 2
}
"--stdin" => {
if use_stdin {
return Err("--stdin may be given only once")
}
use_stdin = true
index = index + 1
}
"--check" => {
if check {
return Err("--check may be given only once")
}
check = true
index = index + 1
}
"--csv" => {
if csv {
return Err("--csv may be given only once")
}
csv = true
index = index + 1
}
"--fair-node-limit" => {
if fair_limit is Some(_) || index + 1 >= args.length() {
return Err("--fair-node-limit requires exactly one positive integer")
}
let limit = @string.parse_int(args[index + 1]) catch {
_ =>
return Err(
"--fair-node-limit requires exactly one positive integer",
)
}
if limit <= 0 {
return Err("--fair-node-limit requires exactly one positive integer")
}
fair_limit = Some(limit)
index = index + 2
}
other => return Err("unknown option: \{other}")
}
}
if (path is Some(_) && use_stdin) || (path is None && !use_stdin) {
return Err("exactly one of --file PATH and --stdin is required")
}
if (check && (csv || fair_limit is Some(_))) || (csv && fair_limit is Some(_)) {
return Err("--check, --csv, and --fair-node-limit cannot be combined")
}
Ok({ path, check, csv, fair_limit, })
}
///|
fn render_coverage(text : String, options : FileOptions) -> String {
let request = match @moonplan.decode_coverage_request(text) {
Err(error) => return file_cli_error(error.message())
Ok(request) => request
}
match options.fair_limit {
Some(limit) =>
match request.solve_fairest(limit) {
Err(error) => file_cli_error(error.message())
Ok(outcome) => @moonplan.encode_fair_coverage_outcome(outcome, 2)
}
None => {
let analysis = match
@moonplan.analyze_coverage_with_policy(
request.workers,
request.requirements,
request.policy,
) {
Err(error) => return file_cli_error(error.message())
Ok(analysis) => analysis
}
if options.check {
return @moonplan.encode_coverage_analysis(analysis, 2)
}
if !analysis.is_feasible() {
return file_cli_error(analysis.summary())
}
match request.solve() {
Err(error) => file_cli_error(error.message())
Ok(roster) => {
if options.csv {
return @moonplan.encode_coverage_roster_csv(roster)
}
let absent : String? = None
let response : FileCliResponse = {
roster: @json.to_json(roster),
error: @json.to_json(absent),
}
@json.to_json(response).stringify(indent=2)
}
}
}
}
}
///|
async fn main {
let args = @env.args()
let options = match parse_file_options(args) {
Err(message) => {
println(file_cli_error(message))
return
}
Ok(options) => options
}
let text = try {
match options.path {
Some(file) => @fs.read_file(file).text()
None => @stdio.stdin.read_all().text()
}
} catch {
error => {
println(file_cli_error("cannot read coverage input: \{error}"))
return
}
}
println(render_coverage(text, options))
}