// Command implementations for `webfinger-tool`. Every command returns a
// single-line deterministic JSON object on stdout:
//
// {"ok":true,"command":, ...result fields...}
// {"ok":false,"command":,"error":{"code":...,"message":...}} (CLI usage)
// {"ok":false,"command":,"error":{"stage":...,"kind":..., ...}} (library error)
///|
/// The tool usage text.
fn usage_text() -> String {
"webfinger-tool — RFC 7033 WebFinger / JRD command-line toolkit (local tooling only; no network access).\n\n" +
"usage:\n" +
" webfinger-tool request --resource [--rel ]... [--origin ]\n" +
" webfinger-tool parse (--input | --input-file )\n" +
" webfinger-tool validate (--input | --input-file )\n" +
" webfinger-tool query --rel [--type ] (--input | --input-file )\n" +
" webfinger-tool canonicalize (--input | --input-file )\n" +
" webfinger-tool audit (--input | --input-file )\n" +
" webfinger-tool stats (--input | --input-file )\n" +
" webfinger-tool version\n" +
" webfinger-tool help\n\n" +
"options:\n" +
" --resource the WebFinger query target (absolute URI)\n" +
" --rel link relation filter (repeatable; URI or registered relation type)\n" +
" --origin HTTPS origin for full URL construction (e.g. https://example.com)\n" +
" --input inline JRD JSON\n" +
" --input-file read the JRD JSON from a local file (native and js targets)\n" +
" --type media type filter for the query command\n\n" +
"all output is deterministic JSON; failed commands exit with status 1."
}
///|
/// Build the standard ok envelope.
fn ok_json(command : String, fields : Map[String, Json]) -> String {
let members : Map[String, Json] = Map([])
members.set("ok", Json::boolean(true))
members.set("command", Json::string(command))
for k, v in fields {
members.set(k, v)
}
stringify_json(Json::object(members))
}
///|
/// Build a CLI-level error envelope.
fn cli_err_json(command : String, code : String, message : String) -> String {
let members : Map[String, Json] = Map([])
members.set("ok", Json::boolean(false))
members.set("command", Json::string(command))
let err : Map[String, Json] = Map([])
err.set("code", Json::string(code))
err.set("message", Json::string(message))
members.set("error", Json::object(err))
stringify_json(Json::object(members))
}
///|
/// Build a library-error envelope.
fn lib_err_json(command : String, e : WebFingerError) -> String {
let members : Map[String, Json] = Map([])
members.set("ok", Json::boolean(false))
members.set("command", Json::string(command))
let err : Map[String, Json] = Map([])
err.set("stage", Json::string(e.stage_name()))
err.set("kind", Json::string(e.kind_name()))
match e.offset() {
Some(n) => err.set("offset", Json::number(n.to_double()))
None => err.set("offset", Json::null())
}
err.set("context", Json::string(e.context()))
members.set("error", Json::object(err))
stringify_json(Json::object(members))
}
///|
/// Int as JSON number.
fn int_json(n : Int) -> Json {
Json::number(n.to_double())
}
///|
/// String array as JSON array.
fn strings_json(values : Array[String]) -> Json {
let arr : Array[Json] = []
for v in values {
arr.push(Json::string(v))
}
Json::array(arr)
}
///|
/// Resolve the JRD input text from --input or --input-file.
fn input_text(opts : CliOptions) -> Result[String, String] {
match (opts.input, opts.input_file) {
(Some(_), Some(_)) => Err("use only one of --input or --input-file")
(None, None) => Err("--input or --input-file is required")
(Some(s), None) => Ok(s)
(None, Some(path)) => {
if !cli_file_input_supported() {
return Err("--input-file is not supported on this target")
}
match cli_read_file(path) {
Some(content) => Ok(content)
None => Err("cannot read input file: \{path}")
}
}
}
}
///|
/// Parse the JRD text; return the JRD or the error envelope.
fn parse_input(
command : String,
opts : CliOptions,
) -> Result[JsonResourceDescriptor, String] {
match input_text(opts) {
Err(msg) => Err(cli_err_json(command, "MissingInput", msg))
Ok(text) =>
match parse_jrd(text) {
Ok(jrd) => Ok(jrd)
Err(e) => Err(lib_err_json(command, e))
}
}
}
///|
/// `webfinger-tool version`
fn cmd_version() -> String {
let fields : Map[String, Json] = Map([])
fields.set("version", Json::string(library_version()))
fields.set("module", Json::string("qianfuzsc/moon-webfinger"))
ok_json("version", fields)
}
///|
/// `webfinger-tool help`
fn cmd_help() -> String {
let fields : Map[String, Json] = Map([])
fields.set("usage", Json::string(usage_text()))
ok_json("help", fields)
}
///|
/// `webfinger-tool request`
fn cmd_request(opts : CliOptions) -> String {
match opts.resource {
None => cli_err_json("request", "MissingOption", "--resource is required")
Some(resource) =>
match build_request_target_with_rels(resource, opts.rels) {
Err(e) => lib_err_json("request", e)
Ok(target) => {
let fields : Map[String, Json] = Map([])
fields.set("resource", Json::string(resource))
fields.set("rels", strings_json(opts.rels))
fields.set("request_target", Json::string(target))
match opts.origin {
None => ()
Some(origin) =>
match build_webfinger_url(origin, resource, opts.rels) {
Ok(url) => fields.set("url", Json::string(url))
Err(e) => return lib_err_json("request", e)
}
}
ok_json("request", fields)
}
}
}
}
///|
/// `webfinger-tool parse`
fn cmd_parse(opts : CliOptions) -> String {
match parse_input("parse", opts) {
Err(out) => out
Ok(jrd) => {
let fields : Map[String, Json] = Map([])
fields.set("jrd", jrd_to_json(jrd))
fields.set("links", int_json(jrd.links.length()))
fields.set("aliases", int_json(jrd.aliases.length()))
ok_json("parse", fields)
}
}
}
///|
/// `webfinger-tool validate`
fn cmd_validate(opts : CliOptions) -> String {
match parse_input("validate", opts) {
Err(out) => out
Ok(jrd) => {
let issues = validate_jrd(jrd)
let arr : Array[Json] = []
let mut errors = 0
let mut warnings = 0
let mut infos = 0
for issue in issues {
match issue.severity_name() {
"Error" => errors = errors + 1
"Warning" => warnings = warnings + 1
_ => infos = infos + 1
}
let entry : Map[String, Json] = Map([])
entry.set("severity", Json::string(issue.severity_name()))
entry.set("code", Json::string(issue.code))
entry.set("message", Json::string(issue.message))
arr.push(Json::object(entry))
}
let fields : Map[String, Json] = Map([])
fields.set("valid", Json::boolean(errors == 0))
fields.set("errors", int_json(errors))
fields.set("warnings", int_json(warnings))
fields.set("infos", int_json(infos))
fields.set("issues", Json::array(arr))
ok_json("validate", fields)
}
}
}
///|
/// `webfinger-tool query`
fn cmd_query(opts : CliOptions) -> String {
match opts.media_type {
None => query_impl(opts, None)
Some(t) => query_impl(opts, Some(t))
}
}
///|
/// Query implementation shared by both media-type modes.
fn query_impl(opts : CliOptions, media_type : String?) -> String {
if opts.rels.length() == 0 {
return cli_err_json("query", "MissingOption", "--rel is required")
}
let rel = opts.rels[0]
match parse_input("query", opts) {
Err(out) => out
Ok(jrd) => {
let matches = match media_type {
None => find_links_by_rel(jrd, rel)
Some(t) => find_links_by_rel_and_type(jrd, rel, t)
}
let hrefs : Array[String] = []
let link_json : Array[Json] = []
for link in matches {
match link.href {
Some(h) => hrefs.push(h)
None => ()
}
link_json.push(link_model_to_json(link))
}
let fields : Map[String, Json] = Map([])
fields.set("rel", Json::string(rel))
match media_type {
Some(t) => fields.set("type", Json::string(t))
None => ()
}
fields.set("count", int_json(matches.length()))
match first_href(jrd, rel) {
Some(h) => fields.set("first_href", Json::string(h))
None => fields.set("first_href", Json::null())
}
fields.set("hrefs", strings_json(hrefs))
fields.set("links", Json::array(link_json))
ok_json("query", fields)
}
}
}
///|
/// `webfinger-tool canonicalize`
fn cmd_canonicalize(opts : CliOptions) -> String {
match input_text(opts) {
Err(msg) => cli_err_json("canonicalize", "MissingInput", msg)
Ok(text) =>
match canonicalize_jrd_text(text) {
Ok(output) => {
let fields : Map[String, Json] = Map([])
fields.set("output", Json::string(output))
ok_json("canonicalize", fields)
}
Err(e) => lib_err_json("canonicalize", e)
}
}
}
///|
/// `webfinger-tool audit`
fn cmd_audit(opts : CliOptions) -> String {
match parse_input("audit", opts) {
Err(out) => out
Ok(jrd) => {
let findings = audit_jrd(jrd)
let arr : Array[Json] = []
let mut warnings = 0
let mut infos = 0
for finding in findings {
match finding.severity_name() {
"Warning" => warnings = warnings + 1
_ => infos = infos + 1
}
let entry : Map[String, Json] = Map([])
entry.set("severity", Json::string(finding.severity_name()))
entry.set("code", Json::string(finding.code))
entry.set("message", Json::string(finding.message))
arr.push(Json::object(entry))
}
let fields : Map[String, Json] = Map([])
fields.set("warnings", int_json(warnings))
fields.set("infos", int_json(infos))
fields.set("findings", Json::array(arr))
ok_json("audit", fields)
}
}
}
///|
/// `webfinger-tool stats`
fn cmd_stats(opts : CliOptions) -> String {
match input_text(opts) {
Err(msg) => cli_err_json("stats", "MissingInput", msg)
Ok(text) =>
match parse_jrd(text) {
Err(e) => lib_err_json("stats", e)
Ok(jrd) => {
let mut titles = 0
let mut link_properties = 0
for link in jrd.links {
titles = titles + link.titles.length()
link_properties = link_properties + link.properties.length()
}
let fields : Map[String, Json] = Map([])
let has_subject = match jrd.subject {
Some(_) => true
None => false
}
fields.set("input_bytes", int_json(utf8_byte_length(text)))
fields.set("has_subject", Json::boolean(has_subject))
fields.set("aliases", int_json(jrd.aliases.length()))
fields.set("properties", int_json(jrd.properties.length()))
fields.set("links", int_json(jrd.links.length()))
fields.set("titles", int_json(titles))
fields.set("link_properties", int_json(link_properties))
fields.set("extensions", int_json(jrd.extensions.length()))
fields.set(
"serialized_bytes",
int_json(utf8_byte_length(serialize_jrd(jrd))),
)
ok_json("stats", fields)
}
}
}
}