///|
/// `sfv-tool` — the RFC 9651 Structured Field Values command-line checker.
///
/// Usage:
/// sfv-tool parse --type - INPUT
/// sfv-tool validate --type
- INPUT
/// sfv-tool canonicalize --type
- INPUT
/// sfv-tool roundtrip --type
- INPUT
/// sfv-tool conformance
///
/// INPUT is read from the single positional argument. Errors are printed to
/// stderr with the error kind and the byte offset, and the exit code is
/// non-zero. Byte sequences are rendered as hex; dates keep their raw
/// seconds value.
fn main {
let args = @env.args()
let rest = strip_program_name(args)
if rest.is_empty() {
print_usage()
return
}
let command = rest[0]
let tail = rest[1:].to_owned()
match command {
"conformance" => run_conformance_cmd(tail)
"parse" => run_field_cmd(tail, "parse")
"validate" => run_field_cmd(tail, "validate")
"canonicalize" => run_field_cmd(tail, "canonicalize")
"roundtrip" => run_field_cmd(tail, "roundtrip")
"-h" | "--help" => print_usage()
"help" => print_usage()
"-V" | "--version" => print_version()
_ => {
println("sfv-tool: unknown command: \{command}")
print_usage()
cli_exit(2)
}
}
}
///|
/// The CLI version, kept in sync with `moon.mod`'s `version`.
const CLI_VERSION = "0.1.0"
///|
fn print_version() -> Unit {
println("moon-sfv \{CLI_VERSION}")
println(
"RFC 9651 Structured Field Values parser, serializer, and conformance toolkit",
)
}
///|
fn strip_program_name(args : Array[String]) -> Array[String] {
if args.is_empty() {
return []
}
let first = args[0]
let known = [
"parse", "validate", "canonicalize", "roundtrip", "conformance", "-h", "--help",
"help", "-V", "--version",
]
for k in known {
if first == k {
return args
}
}
// First argument is the executable path; drop it.
args[1:].to_owned()
}
///|
fn print_usage() -> Unit {
println("sfv-tool - RFC 9651 Structured Field Values checker")
println("")
println("Usage:")
println(" sfv-tool parse --type
- INPUT")
println(" sfv-tool validate --type
- INPUT")
println(" sfv-tool canonicalize --type
- INPUT")
println(" sfv-tool roundtrip --type
- INPUT")
println(" sfv-tool conformance")
println(" sfv-tool --version")
println("")
println(" --type is required for field commands. INPUT is a single")
println(" positional argument (reading from stdin is not supported yet).")
}
///|
/// Runs the `conformance` command.
fn run_conformance_cmd(_args : Array[String]) -> Unit {
let stats = @lib.run_conformance()
println("=== httpwg conformance report ===")
println("required_valid_total: \{stats.required_valid_total}")
println("required_valid_passed: \{stats.required_valid_passed}")
println("required_invalid_total: \{stats.required_invalid_total}")
println("required_invalid_passed: \{stats.required_invalid_passed}")
println("canonical_total: \{stats.canonical_total}")
println("canonical_passed: \{stats.canonical_passed}")
println("optional_total: \{stats.optional_total}")
println("optional_passed: \{stats.optional_passed}")
println("optional_failed: \{stats.optional_failed}")
println("expected_total: \{stats.expected_total}")
println("expected_passed: \{stats.expected_passed}")
println("failures: \{stats.failures.length()}")
for f in stats.failures {
println(" - \{f}")
}
if stats.required_valid_passed == stats.required_valid_total &&
stats.required_invalid_passed == stats.required_invalid_total &&
stats.canonical_passed == stats.canonical_total {
println("RESULT: PASS")
} else {
println("RESULT: FAIL")
cli_exit(1)
}
}
///|
/// Runs `parse`, `validate`, `canonicalize`, or `roundtrip`.
fn run_field_cmd(args : Array[String], command : String) -> Unit {
let mut field_type : @lib.FieldType? = None
let mut input : String? = None
let mut i = 0
while i < args.length() {
let a = args[i]
if a == "--type" {
if i + 1 >= args.length() {
println("sfv-tool: --type requires a value (item|list|dictionary)")
cli_exit(2)
return
}
let name = args[i + 1]
field_type = @lib.FieldType::from_wire_name(name)
if field_type is None {
println("sfv-tool: unknown field type: \{name}")
cli_exit(2)
return
}
i = i + 2
} else if a == "-h" || a == "--help" {
print_usage()
return
} else {
input = Some(a)
i = i + 1
}
}
let ft = match field_type {
None => {
println("sfv-tool: missing --type (item|list|dictionary)")
cli_exit(2)
return
}
Some(v) => v
}
let text = match input {
None => {
println(
"sfv-tool: missing INPUT (reading from stdin is not supported yet)",
)
cli_exit(2)
return
}
Some(v) => v
}
dispatch_field_command(command, ft, text)
}
///|
fn dispatch_field_command(
command : String,
ft : @lib.FieldType,
input : String,
) -> Unit {
match command {
"validate" =>
match parse_field(ft, input) {
Err(e) => {
println("INVALID: \{e.to_string()}")
cli_exit(1)
}
Ok(_) => println("VALID")
}
"parse" =>
match parse_field(ft, input) {
Err(e) => {
println("ERROR: \{e.to_string()}")
cli_exit(1)
}
Ok(rendered) => println(rendered)
}
"canonicalize" =>
match @lib.canonicalize(input, ft) {
Err(e) => {
println("ERROR: \{e.to_string()}")
cli_exit(1)
}
Ok(sf) =>
if sf.is_omitted() {
println("(omitted)")
} else {
println(sf.to_string())
}
}
"roundtrip" =>
match @lib.canonicalize(input, ft) {
Err(e) => {
println("ERROR: \{e.to_string()}")
cli_exit(1)
}
Ok(sf) => {
if sf.is_omitted() {
println("(omitted)")
return
}
let wire = sf.to_string()
// Re-parse and re-serialize: must be stable.
match @lib.canonicalize(wire, ft) {
Err(e) => {
println("ROUNDTRIP FAILED: \{e.to_string()}")
cli_exit(1)
}
Ok(sf2) => {
if sf2.is_omitted() || sf2.to_string() != wire {
println("ROUNDTRIP FAILED: not stable")
cli_exit(1)
}
println(wire)
}
}
}
}
_ => {
println("sfv-tool: unknown command \{command}")
cli_exit(2)
}
}
}
///|
/// Parses a field and renders the parsed structure for the `parse` command.
fn parse_field(
ft : @lib.FieldType,
input : String,
) -> Result[String, @lib.SfError] {
match ft {
@lib.Item =>
match @lib.parse_item(input) {
Err(e) => Err(e)
Ok(v) => Ok(render_item(v))
}
@lib.List =>
match @lib.parse_list(input) {
Err(e) => Err(e)
Ok(v) => Ok(render_list(v))
}
@lib.Dictionary =>
match @lib.parse_dictionary(input) {
Err(e) => Err(e)
Ok(v) => Ok(render_dict(v))
}
}
}
///|
/// Renders a Byte Sequence as a hex string (no raw binary in output).
fn render_bytes(b : Bytes) -> String {
let buf = @buffer.Buffer()
buf.write_string_utf8("0x")
for i in 0..> 4) & 0x0F))
buf.write_char_utf8(nibble_hex(byte.to_int() & 0x0F))
}
@utf8.decode_lossy(buf.to_bytes())
}
///|
/// Maps a nibble (0-15) to its lowercase hex character.
fn nibble_hex(n : Int) -> Char {
let code = if n < 10 { 0x30 + n } else { 0x61 + n - 10 }
let ch = code.to_char()
match ch {
Some(c) => c
None => '\u{FFFD}'
}
}
///|
fn render_bare(b : @lib.BareItem) -> String {
match b {
@lib.Integer(v) => v.to_string()
@lib.Decimal(d) =>
match d.to_canonical_string() {
Err(_) => "?"
Ok(s) => s
}
@lib.StringItem(s) => "\"" + escape_json(s) + "\""
@lib.Token(s) => s
@lib.ByteSequence(bs) => render_bytes(bs)
@lib.Boolean(v) => if v { "true" } else { "false" }
@lib.Date(v) => "@" + v.to_string()
@lib.DisplayString(s) => "%\"" + escape_json(s) + "\""
}
}
///|
fn escape_json(s : String) -> String {
let bytes = @utf8.encode(s, bom=false)
let buf = @buffer.Buffer()
for i in 0.. String {
if p.len() == 0 {
return "[]"
}
let mut out = "["
for i in 0.. ()
Some(pe) => {
if i > 0 {
out = out + ", "
}
out = out + "\"" + escape_json(pe.key) + "\": " + render_bare(pe.value)
}
}
}
out + "]"
}
///|
fn render_item(it : @lib.Item) -> String {
"{\"bare\": " +
render_bare(it.bare) +
", \"parameters\": " +
render_params(it.parameters) +
"}"
}
///|
fn render_member(m : @lib.ListMember) -> String {
match m {
@lib.ItemMember(it) => render_item(it)
@lib.InnerListMember(il) => {
let mut out = "{\"inner_list\": ["
for i in 0.. 0 {
out = out + ", "
}
out = out + render_item(il.items[i])
}
out + "], \"parameters\": " + render_params(il.parameters) + "}"
}
}
}
///|
fn render_list(l : @lib.SfList) -> String {
let mut out = "["
for i in 0.. 0 {
out = out + ", "
}
match l.get_by_index(i) {
None => ()
Some(m) => out = out + render_member(m)
}
}
out + "]"
}
///|
fn render_dict(d : @lib.SfDictionary) -> String {
let mut out = "{"
for i in 0.. 0 {
out = out + ", "
}
match d.get_by_index(i) {
None => ()
Some(e) =>
out = out + "\"" + escape_json(e.key) + "\": " + render_member(e.value)
}
}
out + "}"
}