Sign in

    moonbitstack/moonctl/api does not have a README file

    SpecError

    pub suberror SpecError {
    Syntax(line~ : Int, msg~ : String)
    Missing(file~ : String, path~ : String)
    }

    A spec the parser could not read: Syntax is the 1-based line and what went wrong there, Missing an import whose path was not among the files handed to parse_all.

    Bind

    pub(all) enum Bind {
    Body
    Query
    Path
    Header
    } derive(Eq)

    Where a field's value is bound from (← goctl's struct tags): the request body (json:, and an untagged field), the query string (form:), a URL segment (path:), or a request header (header:).

    Bind::equal

    fn Bind::equal(Bind, Bind) -> Bool

    Bind::not_equal

    fn Bind::not_equal(x : Bind, y : Bind) -> Bool

    Field

    pub(all) struct Field {
    name : String
    type_ : String
    tag : String
    }

    One field of a type block: its name, the (already MoonBit-mapped) type, and the raw back-tick struct tag (json:"…"/path/form/header, empty when absent) that carries the wire name and binding location.

    Field::bind

    fn Field::bind(self : Field) -> Bind

    Where this field's value is bound from: form: off the query string, path: out of a URL segment, header: off a request header, and everything else — an untagged field included — out of the request body.

    Field::default_

    fn Field::default_(self : Field) -> String?

    The default= this field's tag declared, written the way the spec wrote it, or None when it declared none.

    Field::json_name

    fn Field::json_name(self : Field) -> String

    The name this field is carried under on the wire: the one its binding tag gives (json:"user_name" → user_name, form:"page" → page, path:"region" → region), with the options after it stripped, falling back to the field's own name when it carries no tag.

    Field::mbt_name

    fn Field::mbt_name(self : Field) -> String

    The MoonBit spelling of this field: snake_case, because a MoonBit struct field has to start lower-case — UserName is not one. Every generator that emits MoonBit names the field with this, so the struct, the row decoder and the column projection all agree; json_name is the wire side of the same field.

    Field::optional

    fn Field::optional(self : Field) -> Bool

    Whether the tag marked this field optional (or Go's omitempty): it may be absent, which is what keeps it out of a schema's required list.

    Field::options

    fn Field::options(self : Field) -> Array[String]

    The values an options=a|b|c tag allows this field to take, empty when the tag declared no such list.

    Field::range

    fn Field::range(self : Field) -> Range?

    The range=[lo:hi] bounds this field's tag declared, or None when it declared none. Either bound may be empty, which leaves that end open.

    Group

    pub(all) struct Group {
    name : String
    prefix : String
    jwt : String
    middleware : Array[String]
    max_bytes : Int64
    timeout : String
    signature : Bool
    extra : Array[(String, String)]
    }

    The @server( … ) annotations governing a block of routes (← goctl's route group): the name under which its routes are grouped, the path prefix already folded into their paths, the jwt claim type, the middleware chain, the max_bytes request cap (0 when unset), the request timeout, and whether the group's requests are signed. extra keeps every other annotation the block carried, in order — goctl lets a spec invent its own, and dropping them would lose what the author wrote.

    Range

    pub(all) struct Range {
    lo : String
    hi : String
    lo_inc : Bool
    hi_inc : Bool
    } derive(Eq)

    A range= constraint, e.g. range=[1:120] or range=(0:]. Each bound is written as the spec wrote it and is empty when that end is open; lo_inc / hi_inc say whether the bracket was inclusive ([ ]) or exclusive (( )).

    Range::equal

    fn Range::equal(Range, Range) -> Bool

    Range::not_equal

    fn Range::not_equal(x : Range, y : Range) -> Bool

    Route

    pub(all) struct Route {
    verb : String
    path : String
    handler : String
    summary : String
    req : String
    resp : String
    group : Group?
    }

    One route in a service spec: HTTP verb (lower-cased), path pattern (with its group's prefix applied), handler name, an optional summary, and the @server group it was declared under. A modern goctl route (verb /path (Req) returns(Resp)) also carries its request/response type names (its @doc fills summary); both are empty for the legacy inline form (verb /path handler"summary").

    Spec

    pub(all) struct Spec {
    service : String
    routes : Array[Route]
    types : Array[TypeDef]
    info : Array[(String, String)]
    groups : Array[Group]
    imports : Array[String]
    }

    A parsed .api service specification. groups lists the @server( … ) blocks in the order they were declared; each route also points at the one it belongs to. imports holds the import paths exactly as the file wrote them — relative to the file itself, so resolving one needs the path it was read from; deps and parse_all do that.

    TypeDef

    pub(all) struct TypeDef {
    name : String
    fields : Array[Field]
    embeds : Array[String]
    }

    A named message schema declared by a type Name { field: Type … } block, generated into a MoonBit struct (goctl's request/response types). embeds names the blocks it inlined — a bare Base line — whose fields belong to it as if they had been written out; an anonymous nested block instead becomes a TypeDef of its own, named after the two blocks, with a field pointing at it.

    deps

    fn deps(source : String, from? : String) -> Array[String]

    The files source imports, each resolved against from — the path source itself was read from. A caller that can read files walks a multi-file spec with this: read a file, follow its deps, and hand everything it collected to parse_all.

    find_type

    fn find_type(types : Array[TypeDef], name : String) -> TypeDef?

    The TypeDef named name, or None when the spec declared no such block.

    flat_fields

    fn flat_fields(t : TypeDef, types : Array[TypeDef]) -> Array[Field]

    Every field a type block carries with its inline embeds folded in first, which is both the shape an embedded Go struct serialises to and the only one MoonBit can spell — a MoonBit struct has no embedding. An embed naming a block that is not in types, and one that leads back to a block already folded in, contribute nothing.

    index_of

    fn index_of(s : String, ch : UInt16, from : Int) -> Int

    Index of the first ch in s at or after from, or -1 if absent.

    parse

    fn parse(source : String) -> Spec raise SpecError

    Read a goctl-style .api description into a Spec. Grammar (one statement per line):

    syntax = "v1" info ( title: "greet" version: "v2" ) type LoginReq { name: string } @server ( group: user prefix: /api/v1 middleware: Log ) service greet { @doc "health check" @handler ping get /ping @handler login post /login (LoginReq) returns (LoginResp) get /legacy legacy_handler "the inline form" }

    A type block declares a message schema (its fields become a MoonBit struct); field types use goctl's Go spellings (string, int64, []T, map[K]V, …). An @server( … ) block annotates every route of the service block that follows it: the routes carry its prefix in their paths and its Group on their group. Verbs are lower-cased, blank lines and // comments are dropped.

    Any other line raises a SpecError naming it — a misspelt verb or a missing brace would otherwise generate a quietly truncated program. Use parse_lenient for a best-effort read of a spec that is still being written.

    parse_all

    fn parse_all(source : String, files : Map[String, String], from? : String) -> Spec raise SpecError

    Parse source — the spec at from — together with everything it imports, taken from files (a map from resolved path to source, the shape deps resolves to). What the imports describe is merged in first, in the order they were declared, then the importing file's own: types, routes and @server groups all end up in one Spec. A file is scanned once however many times it is imported, so a cycle terminates.

    The service name is the last one declared, so the importing file's own service block wins and a spec that does nothing but import still takes the name from what it imported.

    An import naming a file that is not in files raises Missing; a file that does not parse raises its first complaint the way parse does, with the file it came from named alongside the line.

    parse_lenient

    fn parse_lenient(source : String) -> Spec

    parse without the diagnostics: a line the parser cannot read is skipped and whatever the spec does describe is returned. For a caller that generates from a half-written spec on every keystroke; anything that reports to a user should call parse.

    resolve_import

    fn resolve_import(from : String, path : String) -> String

    An import path resolved against the file that declared it: taken relative to that file's directory, with ./.. folded away. An already-absolute path is left where it points.

    to_i64

    fn to_i64(s : String) -> Int64?

    s read as a non-negative decimal, or None when it is not one.

    tokens

    fn tokens(s : String) -> Array[String]

    Whitespace-split a line into tokens, keeping a "quoted string" as one token (with the quotes stripped).