moonctl

moonctl (mctl) — a spec-driven code generator for MoonBit (← goctl): parse a .api service spec and emit compilable moonapi scaffolding (routes + handler stubs).

goctl
codegen
scaffold
api
moonapi
moonbit
cli
Download zip
Author
Version
0.7.0
License
Apache-2.0
Last updated
11 hours ago
Downloads
40

#moonctl (mctl)

A spec-driven code generator for MoonBit — ← goctl.

Check and Test License mooncakes

moonctl parses a .api service spec and emits compilable moonapi scaffolding — the role goctl plays for Go. It's pure logic (parse → generate text), so it has no runtime dependencies and runs on every backend.

#From spec to code

Given this .api spec:

service greet { get /ping ping "health check" get /users/:id get_user post /users create_user "create a user" }

generate(parse(spec)) emits:

// Code generated by moonctl. DO NOT EDIT.

///|
/// Build the greet application with its routes wired to handlers.
pub fn build_app() -> @moonapi.App {
let app = @moonapi.App::new()
app.get("/ping", ctx => ping(ctx), summary="health check")
app.get("/users/:id", ctx => get_user(ctx))
app.post("/users", ctx => create_user(ctx), summary="create a user")
app
}

///|
/// `get /ping`.
pub fn ping(_ctx : @moonapi.Context) -> @moonasgi.Response {
@moonapi.text(200, "TODO: ping")
}
// ... one stub per handler

Each route registers its handler through an arrow closure. moonapi's ApiHandler is a raising fn-type, and under moonc 0.10.5 a pure named function no longer coerces to one; a closure infers the raise effect, so both a stub that never raises and a filled-in handler that raises an HttpException register unchanged.

The generated output is verified to compile against real moonapi + moonasgi — a code generator whose output doesn't build isn't a code generator.

#Route groups

An @server( … ) block annotates the service block under it, the way goctl groups handlers:

@server ( group: user prefix: /api/v1 jwt: Auth middleware: Log,Trace ) service greet { @doc "log a user in" @handler login post /login (LoginReq) returns (LoginResp) }

Every route in the block carries the group. Its path is emitted with the prefix applied, the OpenAPI document tags the operation user, and the scaffold marks the block with what the spec asked for — a group's jwt and middleware have no moonapi call to emit them into, so they are stated rather than dropped:

pub fn build_app() -> @moonapi.App {
let app = @moonapi.App::new()
// @server group: user, prefix: /api/v1, jwt: Auth, middleware: Log,Trace
app.post("/api/v1/login", ctx => login(ctx), summary="log a user in")
app
}

///|
/// `post /api/v1/login` of group user.
pub fn login(_ctx : @moonapi.Context) -> @moonasgi.Response {
@moonapi.text(200, "TODO: login")
}

group, prefix, jwt, middleware, maxBytes, timeout and signature are read into a Group; any other annotation the block carried is kept in Group::extra, for a template or plugin to use.

#Struct tags

A field's back-tick tag says where its value comes from and what it has to look like, the way goctl borrowed it from Go:

type ListReq { Region string `path:"region"` Page int `form:"page,default=1,range=[1:100]"` Size int `form:"size,optional"` Trace string `header:"X-Trace"` Sort string `json:"sort,options=asc|desc"` }

json: reads out of the request body, form: off the query string, path: out of a URL segment and header: off a request header — so the OpenAPI document gives page as a query parameter, region as a path one under the name its tag chose, and keeps only the body-bound fields as schema properties. The options after the wire name are read too: optional (and Go's omitempty) keeps a field out of required, and default=, options= and range= become a default, an enum and minimum/maximum in the document — plus the code that enforces them, emitted beside the struct:

pub fn ListReq::with_defaults(self : ListReq) -> ListReq { … } // fills in every default= pub fn ListReq::check(self : ListReq) -> String? { … } // the first field that fails

A block can inline another by naming it on a line of its own (Base), and a nested Inner { … } becomes a type of its own named OuterInner — MoonBit has neither embedding nor anonymous structs, so an inlined block's fields are written out where they belong. A field is spelled snake_case in every generated MoonBit name, because UserName is not a MoonBit struct field; json_name is the wire side of the same field.

#The project tree

mctl gen api writes what goctl writes — a layered project, not a file:

$ mctl gen api greet.api --dir out --style go_zero out/moon.mod.json out/internal/types/types.mbt out/moon.pkg.json out/internal/handler/routes.mbt out/greet.mbt out/internal/handler/user/login_handler.mbt out/etc/greet.yaml out/internal/logic/user/login_logic.mbt out/internal/config/config.mbt out/internal/middleware/log_middleware.mbt out/internal/svc/service_context.mbt

A grouped route's handler and logic go under the group's own directory, an ungrouped one's sit beside the routes, and every directory gets the moon.pkg.json that makes it a MoonBit package. --flat writes the single <file>.mbt earlier versions did.

Run it again and your code survives. Only the two files moonctl owns — internal/types/types.mbt and internal/handler/routes.mbt, both of which must follow the spec — are rewritten. Everything else is written once and then left alone, because that is where the handler and logic bodies you filled in live. A route added to the spec since the last run still gets its stubs; the ones already on disk are not touched.

The same rule is available to a library caller, which is where it is decided:

let tree = @moonctl.generate_tree(spec, style~, dir~) // -> [TreeFile { path, content, regen }]
let write = @moonctl.tree_plan(tree, p => on_disk(p)) // -> the files to actually write

#Naming style

--style is goctl's naming template, not a list of cases: <before>GO<through>ZERO<after>, where the casing of the GO marker spells the first word, ZERO's spells every later one, and what stands between the markers stands between the words.

templatewelcome_to_go_zero becomes
gozero (default)welcometogozero
goZerowelcomeToGoZero
go_zerowelcome_to_go_zero
Go#zeroWelcome#to#go#zero

Words split on _ and before each capital. A template missing a marker (go, zero) or casing one of them any other way (gOZero, goZEro) is refused with a StyleError rather than silently taken for something else. The style names every file of the tree and the handler, logic and middleware entry points inside them; the names the spec itself chose — its type blocks and their fields — keep their spelling, since a MoonBit type name has to keep its capital.

#Multi-file specs

A spec can import others, singly or in a group, and each path is read relative to the file that wrote it:

import "user.api" import ( "shared/base.api" "../common.api" )

The imported types, routes and @server groups are merged in ahead of the importing file's own. A file is read once however many times it is imported, so a cycle terminates, and an import naming a file that is not there is reported against the file that asked for it rather than silently generating half a program.

Reading files is the caller's job — the library stays pure and all-backend — so it comes in two halves: deps says which files a source needs, and parse_all merges the ones you collected.

let spec = @moonctl.parse_all(source, files, from="spec/greet.api")

#When a spec is wrong

parse raises a SpecError naming the line it could not read — a misspelt verb or annotation, a block left open, a field with no type, a route no @handler names:

@moonctl.parse("service greet {\n gett /ping ping\n}")
// SpecError — line 2: unknown verb "gett"

Without that, a one-character typo generates a program that is quietly missing a route. parse_lenient skips those lines instead and returns whatever the spec does describe, for a spec that is still being written.

#Usage

As a library:

let spec = @moonctl.parse(source) // -> Spec { service, routes, types, info, groups, imports }
let code = @moonctl.generate(spec) // -> compilable moonapi scaffold (String)
let tree = @moonctl.generate_tree(spec) // -> the layered project, file by file

As the mctl command-line tool (native binary):

$ mctl gen api greet.api # -> a moonapi project tree (etc/, internal/…) $ mctl gen api greet.api --style go_zero --dir out $ mctl gen api greet.api --flat # -> greet.mbt (one file, as before) $ mctl gen proto greet.proto # -> greet_grpc.mbt (moonrpc service stub) $ mctl gen model user.api # -> user_model.mbt (moonorm model + migration) $ mctl gen crud schema.sql # -> schema_model.mbt (moonorm model + typed CRUD) $ mctl gen doc greet.api # -> greet_openapi.json + greet_swagger.html $ mctl model datasource sqlite:shop.db # -> datasource_model.mbt (read a live DB's schema) $ mctl api new blog # scaffold a moonapi service project under blog/ $ mctl rpc new echo # scaffold a moonrpc service project under echo/ $ mctl model new account # scaffold a moonorm data-layer project under account/ $ mctl docker greet # -> Dockerfile + .dockerignore $ mctl kube greet # -> deploy/deployment.yaml + deploy/service.yaml $ mctl plugin ./my-plugin greet.api # run an external plugin over the parsed spec

generate works as a synonym for gen, and rpc for proto.

#Cross-repo codegen

Like goctl, mctl is the code generator for the whole stack, not just the web tier. Two more front-ends feed the sibling libraries:

  • .protomoonrpc: parse_proto reads a proto3 file (service / rpc / message, including stream, repeated, and map<K, V>); generate_grpc emits a struct per message, a @moonrpc.Method descriptor per RPC (its gRPC :path is /package.Service/Method), a <Service>Server handler-registration struct (one synchronous-core handler per RPC, returning its reply or a gRPC @moonrpc.Status), and a <service>_methods() listing.

  • .api typemoonorm: generate_model turns each storable type block into a model struct, a <table>_model : @moonorm.Model[T] (declared columns — an id column is the primary key — plus the from_row decoder and to_columns projection that MoonBit can't synthesise for want of reflection), a <table>_table : @moonorm.Table descriptor, and a <table>_up / <table>_down migration pair (create the table idempotently, drop it — the explicit stand-in for an Alembic revision). A block with a non-storable field (a slice, map, or nested message) still gets its struct and a plain Table descriptor. The output compiles against moonorm + moondb, so a consumer imports both.

Both outputs are verified to compile against the real moonrpc / moonorm in a scratch consumer project (moon check → rc 0), the same guarantee the moonapi scaffold carries.

let stub = @moonctl.generate_grpc(@moonctl.parse_proto(proto_src)) // -> moonrpc stub
let model = @moonctl.generate_model(@moonctl.parse(api_src)) // -> moonorm model + migration

#SQL DDL → CRUD

goctl model mysql ddl reads a .sql schema and generates a model with typed CRUD; parse_ddl + generate_crud are the MoonBit version. parse_ddl reads CREATE TABLE statements — column types, column- and table-level primary keys, NOT NULL, and DEFAULT clauses — and generate_crud emits, per table, the record struct, a @moonorm.Model, an up/down migration pair, and typed CRUD: <table>_insert and <table>_all always, plus <table>_find_by_id, <table>_update, and <table>_delete_by_id when the table has a single-column primary key. SQL types map onto the @moondb.Value set — INTEGER/BIGINT Int/Int64, TEXTString, REALDouble, BLOBBytes, BOOLEANBool. Every generated statement is parameterised, so a CRUD call binds its id and columns rather than splicing them.

let code = @moonctl.generate_crud_from_ddl(sql_src) // -> moonorm model + CRUD (String)

The output is verified to compile against the real moonorm + moondb in a scratch consumer (moon check → rc 0).

#Live datasource reflection

goctl model … datasource connects to a running database, reads its schema, and generates models. mctl model datasource <dsn> is the MoonBit version. A SQLite DSN reads sqlite_master + PRAGMA table_info through the FFI driver; a PostgreSQL DSN reads information_schema over the async wire protocol. Both produce the same neutral ReflectedColumn list, which folds into the same generate_crud pipeline the .sql front end uses, so a live schema and a .sql file yield an identical model.

$ mctl model datasource sqlite:shop.db $ mctl model datasource postgres://user:pass@localhost:5432/shop

DSN parsing (parse_dsn) and the reflected-column → model fold (tables_from_reflection / generate_crud_from_reflection) are pure, all-backend core in the library; the schema reading lives in the native reflect sub-package, so the library itself stays dependency-free. The pipeline is verified end to end against a real in-memory SQLite database (create tables → reflect → generate → the generated models compile against moonorm + moondb).

#Project scaffolds

Like goctl's api new / rpc new / docker / kube, mctl scaffolds whole nested project trees, not just single files:

$ mctl api new blog # blog/{moon.mod.json, blog.api, src/{moon.pkg.json, app.mbt}, README.md} $ mctl rpc new echo # echo/{moon.mod.json, echo.proto, src/{moon.pkg.json, service.mbt}, README.md} $ mctl model new account # account/{moon.mod.json, schema.sql, src/{moon.pkg.json, model.mbt}, README.md} $ mctl docker greet # Dockerfile (two-stage native build) + .dockerignore $ mctl kube greet # deploy/deployment.yaml + deploy/service.yaml

The api, rpc and model skeletons emit their source through the same built-in generators the gen commands use, seeded from a sample spec, so a fresh scaffold compiles against the published libraries out of the box (moon check → rc 0, no warnings). Each returns its files as GenFile { path, content } — the same shape the plugin protocol uses — which the mctl binary writes out, creating nested directories as it goes:

let files = @moonctl.scaffold_api("blog") // -> Array[GenFile] to write

#Plugins

Like goctl api plugin, mctl plugin <exe> <file.api> drives an out-of-tree generator off the same parsed spec the built-in generators use, and speaks goctl's protocol: mctl runs <exe> with whatever arguments the invocation carried and writes it one JSON object on stdin — {Api, ApiFilePath, Style, Dir}, the parsed spec plus the file it came from, the --style template and the output directory, which is what tells a plugin where to write and how to spell what it writes. The plugin prints the files it wants written — a JSON {"files": [{"path", "content"},…]} reply — on its stdout, which mctl writes to disk. The wire shapes are plugin_request (request) and parse_gen_files (reply); a plugin written in MoonBit reads its input with plugin_from_json (or spec_from_json, which unwraps the envelope for a plugin that wants nothing but the spec) and writes its output with gen_files_to_json. cmd/demo-plugin is a working example that emits a route listing.

let (bin, argv) = @moonctl.plugin_argv("my-plugin -flag value") // what to run
let req = @moonctl.plugin_request(spec, api_file_path~, style~, dir~) // its stdin
let files = @moonctl.parse_gen_files(@json.parse(out)) // the plugin's stdout -> files to write

#OpenAPI / Swagger

generate_doc emits an OpenAPI document straight from a .api spec — routes become paths (a :id segment turns into {id}, with a typed path parameter), and type blocks become component schemas. It speaks every mainstream dialect, the same three moonapi's runtime emitter does, so a generated service and a hand-built app document the same way.

let spec = @moonctl.parse(api_src)
let json = @moonctl.generate_doc(spec, version=OpenApi31) // or Swagger20 / OpenApi30
let html = @moonctl.swagger_ui_stub(spec_url="/openapi.json")

#Template engine

moonctl ships a runtime template engine — the faithful equivalent of goctl's text/template. Because MoonBit has no reflection, template data is an explicit tagged Value (Str/Int/Bool/List/Dict/…), the way encoding/json models dynamic data. It supports {{.Field}} interpolation, {{if}}/{{else if}}/{{else}}/{{end}}, {{range}} (with $index, $value binding) and {{with}}, $variable assignment, | pipelines with parenthesised sub-pipelines, {{- -}} whitespace trimming, {{/* comments */}}, and a function library (upper, lower, title, trim, trimPrefix/trimSuffix, hasPrefix/hasSuffix, contains, replace, eq/ne/lt/le/gt/ge, and/or/not, len, index, default, printf/print/println, plus your own via func).

let out = @moonctl.render(
"{{range .routes}}{{.verb}} {{.path}} -> {{.handler}}\n{{end}}",
@moonctl.spec_to_value(spec),
)
// or drive codegen from a caller-supplied template:
let code = @moonctl.generate(spec, template=my_template) // == generate_with

#Status & roadmap (transliterating goctl)

Shipped: the .api parser (service blocks, @server( … ) route groups, verb path handler "summary" routes, typed type blocks → request/response schemas, // comments, and a SpecError naming the line a spec goes wrong on), the moonapi generator (verified to compile against real moonapi), the runtime template engine (goctl's text/template equivalent), the cross-repo generators — .protomoonrpc service stubs, .api typemoonorm models (struct + Model + Table + up/down migration), SQL DDL → moonorm models with typed CRUD, and .api → OpenAPI (Swagger 2.0 / 3.0 / 3.1) + a Swagger UI stub — with the generated model and CRUD verified to compile against the real moonorm + moondb and the generated document verified to parse — the plugin system (an external generator driven off the parsed spec over stdio JSON, with a working demo plugin), live-datasource schema reflection (model datasource — read a running SQLite or PostgreSQL schema and generate models, verified end to end against a real SQLite database), the nested project scaffolds (api/rpc/model new, docker, kube), and the mctl gen api/proto/model/crud/doc + mctl model datasource + mctl plugin CLI binary (native), the layered project tree gen api writes (etc/, internal/config, svc, types, handler, logic, middleware) with goctl's regeneration rule — only the routes and the types are rewritten, so a second run keeps every handler body you wrote — goctl's --style naming templates, and multi-file specs (import, single and grouped, resolved against the importing file). Generated moonapi routing registers each handler through a raising arrow closure, so a scaffold compiles under moonc 0.10.5 (where a pure named handler no longer coerces to moonapi's raising ApiHandler). All verified across every backend (0 warnings under --deny-warn). Next, feature-by-feature: a Redis-cached CRUD variant.

#License

Apache-2.0.

FuncFn

type FuncFn = (Array[Value]) -> Value raise TemplateError

A pipeline function: it receives the already-evaluated argument list (with the piped-in value, if any, appended last, mirroring Go) and returns a Value.

DataSourceError

pub suberror DataSourceError {
BadDsn(String)
}

Raised when a DSN string cannot be parsed into a DataSource.

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.

StyleError

pub suberror StyleError {
BadStyle(String)
}

A --style template that is not <before>GO<through>ZERO<after>: either a marker is missing (go, zero), they are the wrong way round, or one is cased neither go/GO/Go nor zero/ZERO/Zero (gOZero, goZEro).

TemplateError

pub suberror TemplateError {
ParseError(String)
ExecError(String)
}

A template parse or execution failure, carrying a human-readable message.

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:).

Case

type Case

The case a marker asks its words to be spelled in.

DataSource

pub(all) enum DataSource {
Sqlite(String)
Postgres(PgTarget)
}

A parsed datasource DSN: which backend to read, and how to reach it.

DdlColumn

pub(all) struct DdlColumn {
name : String
type_ : String
primary_key : Bool
nullable : Bool
default_ : String?
}

One parsed column of a CREATE TABLE statement: the SQL column name, the MoonBit type_ its SQL type maps to (Int/Int64/String/Double/Bool/ Bytes), whether it is a primary key, whether it accepts NULL, and its literal DEFAULT clause if any (kept as source text — it is emitted into DDL comments, never bound).

DdlTable

pub(all) struct DdlTable {
name : String
columns : Array[DdlColumn]
}

A parsed CREATE TABLE: the SQL table name and its ordered columns.

DocVersion

pub(all) enum DocVersion {
Swagger20
OpenApi30
OpenApi31
} derive(Eq)

Target OpenAPI / Swagger document version. mctl emits every mainstream version from one .api spec — the same shape moonapi's runtime emitter takes, so a generated service and a hand-built moonapi app document the same way.

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.

GenFile

pub(all) struct GenFile {
path : String
content : String
}

One file a plugin asks mctl to write: a destination path (relative to the output directory) and its full content.

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.

PgTarget

pub(all) struct PgTarget {
host : String
port : Int
user : String
password : String
database : String
}

A PostgreSQL connection target parsed from a postgres:// DSN.

Plugin

pub(all) struct Plugin {
api : Spec
api_file_path : String
style : String
dir : String
}

What a plugin is handed on stdin (← goctl's plugin.Plugin): the parsed spec under api, the .api file it was read from, the --style naming template, and the directory the generated tree goes under.

Proto

pub(all) struct Proto {
package_ : String
services : Array[ProtoService]
messages : Array[ProtoMessage]
enums : Array[ProtoEnum]
}

A parsed .proto file: its package (empty when none) and the services and messages it declares.

ProtoEnum

pub(all) struct ProtoEnum {
name : String
values : Array[ProtoEnumValue]
}

A protobuf enum declaration, generated into a MoonBit enum plus an integer mapping (proto3 enums are int-backed, and the value numbered 0 is the default).

ProtoEnumValue

pub(all) struct ProtoEnumValue {
name : String
number : Int
}

One value of a protobuf enum: its name and its integer number.

ProtoField

pub(all) struct ProtoField {
name : String
type_ : String
number : Int
}

One field of a protobuf message: its name, the (already MoonBit-mapped) type, and its wire number (the = N). repeated becomes Array[T] and map<K, V> becomes Map[K, V].

ProtoMessage

pub(all) struct ProtoMessage {
name : String
fields : Array[ProtoField]
oneofs : Array[ProtoOneof]
reserved_numbers : Array[Int]
reserved_names : Array[String]
}

A protobuf message declaration, generated into a MoonBit struct. oneof groups are lifted out of fields into oneofs; reserved field numbers and names are recorded so a reused one can be caught.

ProtoOneof

pub(all) struct ProtoOneof {
name : String
variants : Array[ProtoField]
}

A protobuf oneof group: its name and the members (exactly one may be set). Generated into a MoonBit enum — the faithful equivalent of the "exactly one" invariant, which flattening to optional fields would lose.

ProtoService

pub(all) struct ProtoService {
name : String
rpcs : Array[Rpc]
}

A protobuf service declaration and its RPC methods.

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 (( )).

ReflectedColumn

pub(all) struct ReflectedColumn {
table : String
name : String
sql_type : String
primary_key : Bool
nullable : Bool
}

One column of a reflected database schema: the table it belongs to, its name, its raw SQL type text (INTEGER, character varying, VARCHAR(255), …), and whether it is a primary key / accepts NULL. This is the neutral shape a live reader (SQLite PRAGMA table_info, PostgreSQL information_schema.columns) produces and tables_from_reflection folds into DdlTables.

Regen

pub(all) enum Regen {
Always
Once
} derive(Eq)

Whether a regeneration may replace a file that is already on disk. Always is for what moonctl owns — the routes and the types, which must follow the spec — and Once for what it only seeds: handlers, logic, configuration, manifests.

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").

Rpc

pub(all) struct Rpc {
name : String
request : String
response : String
client_streaming : Bool
server_streaming : Bool
}

One rpc method of a service: its name, request and response message names, and whether either side is a stream (client/server/bidi streaming).

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.

Style

pub struct Style {
before : String
through : String
after : String
head : Case
tail : Case
}

A --style naming template (← goctl's --style), ready to spell names with. Build one with Style::parse, apply it with Style::format.

Style::format

fn Style::format(self : Style, name : String) -> String

Spell name in this style. name is split into words on _ and before each capital, the first word takes the GO marker's case and the rest take ZERO's, and they are joined with whatever stood between the markers.

Style::gozero

fn Style::gozero() -> Style

goctl's default style, gozero: every word lower-cased and run together.

Style::parse

fn Style::parse(s : String) -> Style raise StyleError

Read a --style template. It must contain GO and then ZERO — in any case, with anything before, between and after them:

Style::parse("gozero").format("welcome_to_go_zero") // welcometogozero Style::parse("goZero").format("welcome_to_go_zero") // welcomeToGoZero Style::parse("go_zero").format("welcome_to_go_zero") // welcome_to_go_zero Style::parse("Go#zero").format("welcome_to_go_zero") // Welcome#to#go#zero

The GO marker's own casing (go, GO or Go) says how the first word is spelled, ZERO's says how every later word is, and what stands between the two markers is what stands between the words. A template missing a marker (go, zero), holding them in the wrong order, or casing one of them any other way (gOZero, goZEro) raises StyleError.

Template

pub struct Template {
renderer : (Value) -> String raise TemplateError
funcs : Map[String, (Array[Value]) -> Value raise TemplateError]
}

A parsed, reusable template — moonctl's text/template equivalent. Build it with Template::new, register extra functions with func, parse a source with parse, then render it against a Value any number of times. The parsed node tree stays private, captured inside renderer.

Template::func

fn Template::func(self : Template, name : String, f : (Array[Value]) -> Value raise TemplateError) -> Template

Register (or override) a pipeline function, returning self for chaining — the analogue of Go's Template.Funcs. Register before parse.

Template::new

fn Template::new() -> Template

A fresh template preloaded with the default function library. Rendering before parse yields the empty string.

Template::parse

fn Template::parse(self : Template, source : String) -> Template raise TemplateError

Parse source into this template's node tree, returning self for chaining. Raises ParseError on a syntax error (unclosed action, dangling {{end}}, …).

Template::render

fn Template::render(self : Template, data : Value) -> String raise TemplateError

Render the template against data, producing the output string. Raises ExecError on an execution fault (undefined variable/function, bad range target, …).

TreeFile

pub(all) struct TreeFile {
path : String
content : String
regen : Regen
}

One file of a generated project tree: where it goes, what is in it, and whether regenerating the tree is allowed to overwrite it.

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.

Value

pub(all) enum Value {
Null
Bool(Bool)
Int(Int64)
Float(Double)
Str(String)
List(Array[Value])
Dict(Map[String, Value])
}

Dynamic template data. .Field lookups resolve against Dict; range iterates List (index/element) or Dict (sorted key/value). This is the explicit stand-in for the arbitrary Go values goctl reflects over.

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.

gen_files_to_json

fn gen_files_to_json(files : Array[GenFile]) -> Json

Serialise a list of generated files into the plugin-reply JSON — the writer a MoonBit plugin uses on its stdout. Emits the {"files": [...]} envelope.

generate

fn generate(spec : Spec, template? : String) -> String raise TemplateError

Generate compilable moonapi scaffolding from a spec: a MoonBit struct for every type block, a build_app that wires every route to its handler, plus a stub for each handler. The routing part depends only on Lfan-ke/moonapi and Lfan-ke/moonasgi; the emitted schemas are dependency-free.

Pass template to render the spec through the runtime template engine (see generate_with) instead of the built-in generator; omit it for the default scaffold.

generate_agent

fn generate_agent(spec : Spec) -> String

Generate a moonkoog agent scaffold from spec. Every route becomes a Tool whose descriptor advertises the operation (a single string input parameter to fill out) and whose execute_raw is a stub, and build_agent assembles them into an AIAgent with the service's name in its system prompt. The output compiles against Lfan-ke/moonkoog; fill in each execute_raw to make it run.

generate_crud

fn generate_crud(tables : Array[DdlTable]) -> String

Generate a moonorm data-access layer from parsed DDL: for every table, the record struct, its @moonorm.Model (columns + from_row + to_columns), a @moonorm.Table descriptor, an up/down migration pair, and typed CRUD (insert/all, plus find_by_id/update/delete_by_id when the table has a single-column primary key). The output compiles against Lfan-ke/moonorm + Lfan-ke/moondb; a consuming package imports both. This is the .sql-schema counterpart of goctl's model mysql ddl.

generate_crud_from_ddl

fn generate_crud_from_ddl(source : String) -> String

Parse a SQL DDL script and generate its moonorm data-access layer in one step.

generate_crud_from_reflection

fn generate_crud_from_reflection(cols : Array[ReflectedColumn]) -> String

Generate a moonorm data-access layer (models + typed CRUD) straight from a live schema's reflected columns — the in-memory counterpart of generate_crud_from_ddl. The reflect sub-package produces the ReflectedColumns from a real connection.

generate_doc

fn generate_doc(spec : Spec, version? : DocVersion, title? : String, api_version? : String) -> String

Generate an OpenAPI / Swagger document from spec, stringified with two-space indentation. version picks the dialect (Swagger 2.0 / OpenAPI 3.0 / 3.1).

generate_gql

fn generate_gql(spec : Spec) -> String

Generate a moongql code-first schema and resolver stubs from spec's type blocks. Each type becomes an object type with its fields mapped to GraphQL types; a Query root gains a <type>(id: ID!): <Type> lookup per type; and every field gets a resolver stub. The output compiles against Lfan-ke/moongql; fill in the stub bodies to make it run.

generate_grpc

fn generate_grpc(proto : Proto) -> String

Generate a moonrpc service stub from a parsed .proto: a MoonBit struct per message, a @moonrpc.Method descriptor per RPC (its gRPC :path is /package.Service/Method), a <Service>Server handler-registration struct (one synchronous-core handler field per RPC, each returning its reply or a gRPC @moonrpc.Status), and a <service>_methods() listing. The output compiles against Lfan-ke/moonrpc.

generate_model

fn generate_model(spec : Spec) -> String

Generate a moonorm data layer for every type block in spec. A block whose fields are all storable yields: the model struct; a <table>_model : @moonorm.Model[T] built with @moonorm.Model::new — declared columns (an id column is the primary key), a from_row decoder, and the to_columns projection an INSERT binds; a <table>_table : @moonorm.Table descriptor; and a <table>_up / <table>_down migration pair (create the table idempotently / drop it). This is the explicit MoonBit stand-in for a SQLAlchemy declarative class plus an Alembic revision, which reflection would otherwise synthesise. A block with a non-storable field (a slice, map, or nested message) still gets its struct and a plain @moonorm.Table descriptor.

The output compiles against Lfan-ke/moonorm + Lfan-ke/moondb; a consuming package imports both.

generate_tree

fn generate_tree(spec : Spec, style? : Style, dir? : String) -> Array[TreeFile]

Generate the layered project tree goctl writes, in MoonBit: the service entry point and its moon.mod.json, etc/<service>.yaml, internal/config, internal/svc, internal/types, internal/handler (the generated routes plus a stub per handler, under internal/handler/<group>/ for a grouped route), internal/logic, internal/middleware for every middleware an @server block named, and a moon.pkg.json for each of those packages.

style (default gozero) names the files and the handler, logic and middleware entry points inside them. Names the spec itself chose — the type blocks and their fields — are left as written, since a MoonBit type name has to keep its capital. dir puts the whole tree under a directory.

Each file says whether a regeneration may overwrite it; hand the result to tree_plan to apply that.

generate_with

fn generate_with(spec : Spec, template_source : String) -> String raise TemplateError

Generate code from a Spec with a caller-supplied template instead of the built-in generator — the runtime-template path goctl exposes for custom scaffolds. Equivalent to render(template_source, spec_to_value(spec)).

openapi_document

fn openapi_document(spec : Spec, version? : DocVersion, title? : String, api_version? : String) -> Json

Build the OpenAPI / Swagger document for spec as a Json value. Routes fold into paths → HTTP method → operation (with operationId, any :name path parameters, and a 200 response); every type block becomes a component schema (definitions in 2.0, components/schemas in 3.x). version selects the document dialect; title/api_version fill the info block.

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_ddl

fn parse_ddl(source : String) -> Array[DdlTable]

Parse a SQL DDL script into its CREATE TABLE definitions. Recognises CREATETABLE [IF NOT EXISTS] name ( … ) (quoted or bare name), skipping any other statement. Column types, primary keys (column- or table-level), NOT NULL and DEFAULT clauses are captured; everything needed to emit a moonorm model.

parse_dsn

fn parse_dsn(dsn : String) -> DataSource raise DataSourceError

Parse a datasource DSN into a DataSource. Recognised forms:

  • SQLite: sqlite:PATH, sqlite://PATH, sqlite3:PATH, file:PATH, the literal :memory:, or a bare path ending in .db / .sqlite / .sqlite3.
  • PostgreSQL: postgres://[user[:password]@]host[:port][/database][?…] (and the postgresql:// spelling). The port defaults to 5432, the user to postgres; the database name is required.

Raises BadDsn on an unrecognised scheme or a PostgreSQL URL missing its host or database.

parse_gen_files

fn parse_gen_files(j : Json) -> Array[GenFile]

Parse a plugin's stdout into the files to write. Two shapes are accepted: a bare array [{path, content}, …], or an object {"files": [ … ]} (goctl-style envelope). An element missing path or content is skipped. The list preserves the plugin's order.

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.

parse_proto

fn parse_proto(source : String) -> Proto

Parse a minimal .proto (proto3). Recognises the package declaration, service blocks with their rpc methods (including stream), message blocks with scalar/repeated/map fields, and top-level enum declarations. syntax, import, and option are skipped.

syntax = "proto3"; package greet; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }

plugin_argv

fn plugin_argv(invocation : String) -> (String, Array[String])

Split a plugin invocation into the program to run and the arguments to run it with, the way goctl reads its -plugin value: the first whitespace-separated word is the executable, the rest are its argv. A quoted word keeps its spaces.

plugin_from_json

fn plugin_from_json(j : Json) -> Plugin

Read the request a plugin was handed on stdin. Missing members degrade to empty rather than raising, so a hand-written request still parses.

plugin_request

fn plugin_request(spec : Spec, api_file_path? : String, style? : String, dir? : String) -> String

The plugin request as a two-space-indented JSON string, ready to write to the plugin's stdin: goctl's four keys — the spec under Api, plus the file it was read from, the --style template and the output directory, which is everything a plugin needs to decide where its output goes and what to call it.

proto_reserved_conflicts

fn proto_reserved_conflicts(m : ProtoMessage) -> Array[String]

The names of m's fields (oneof members included) that reuse a reserved field number or name — protobuf forbids reusing either, and this surfaces an accidental reuse. Empty when the message is clean.

render

fn render(source : String, data : Value) -> String raise TemplateError

Parse and render source against data in one call, with the default function library.

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.

scaffold_api

fn scaffold_api(name : String) -> Array[GenFile]

Scaffold a runnable moonapi service project under <name>/: a moon.mod.json (depending on the published moonapi + moonasgi), a sample <name>.api spec, its generated routes+handlers in src/app.mbt (through the same generate the gen api command uses, so it compiles), and a README. This is goctl's goctl api new.

scaffold_docker

fn scaffold_docker(name : String, port? : Int) -> Array[GenFile]

Scaffold container files for a native mctl-generated service: a Dockerfile (a MoonBit build stage that produces the native binary, then a slim runtime stage that runs it) and a .dockerignore. name is the binary/image name, port the port the service listens on. goctl's goctl docker.

scaffold_kube

fn scaffold_kube(name : String, port? : Int, replicas? : Int) -> Array[GenFile]

Scaffold a Kubernetes deployment for the service under deploy/: a Deployment (replicas pods of the <name> image, a container port, and a liveness probe on /ping) and a Service exposing it. goctl's goctl kube deploy.

scaffold_model

fn scaffold_model(name : String) -> Array[GenFile]

Scaffold a moonorm data-layer project under <name>/: a moon.mod.json (depending on the published moonorm + moondb), a sample schema.sql, its generated models + CRUD in src/model.mbt (through the same generate_crud_from_ddl as gen crud), and a README. goctl's goctl model … new.

scaffold_rpc

fn scaffold_rpc(name : String) -> Array[GenFile]

Scaffold a moonrpc service project under <name>/: a moon.mod.json (depending on the published moonrpc), a sample <name>.proto, its generated service stub in src/service.mbt (through the same generate_grpc as gen proto), and a README. goctl's goctl rpc new.

spec_from_json

fn spec_from_json(j : Json) -> Spec

Reconstruct a Spec from the request JSON — the reader a plugin written in MoonBit uses on its stdin. The whole request ({Api, ApiFilePath, Style, Dir}) and a bare spec are both accepted, so a plugin that wants nothing but the routes need not unwrap the envelope. Missing or mistyped members degrade to empty rather than raising, so a partial request still parses.

spec_to_json

fn spec_to_json(spec : Spec) -> Json

Serialise a Spec into the plugin request JSON. The shape mirrors the Spec itself — service, routes (verb/path/handler/summary, plus the group the route was declared under), types (name plus fields of name/type) and groups — so a plugin in any language parses it with an ordinary JSON reader. type_ is written as type, the name a caller expects.

spec_to_value

fn spec_to_value(spec : Spec) -> Value

Expose a parsed .api Spec as template Value data: a Dict with service (string), routes (list of {verb, path, handler, summary,hasSummary, group, hasGroup}), types (list of {name, fields:[{name,type}]}), groups (list of the @server blocks) and hasTypes (bool). This is what the template-driven generator ranges over.

swagger_ui_stub

fn swagger_ui_stub(spec_url? : String, title? : String) -> String

A self-contained Swagger UI page for the document served at spec_url. The same stub moonapi ships, so a generated service and a live app render alike.

tables_from_reflection

fn tables_from_reflection(cols : Array[ReflectedColumn]) -> Array[DdlTable]

Fold reflected columns into DdlTables, grouping by table in first-seen order and mapping each raw SQL type onto its MoonBit scalar (the leading type keyword drives the mapping, so character varying and VARCHAR(255) both land on String). The result feeds generate_crud, so a live schema and a .sql file reach the same model generator.

tree_plan

fn tree_plan(tree : Array[TreeFile], exists : (String) -> Bool) -> Array[GenFile]

The files of tree a run should actually write, given a way to ask whether a path is already on disk. A file moonctl owns (Always — the routes and the types) is always written; anything else is written only when it is not there yet, so a second run refreshes what follows the spec and leaves every handler and logic body the author has since filled in exactly as it is.