Quick start
From an empty directory to a project that builds. There are four commands, and two of them are the interesting ones: check the spec, then generate from it.
1. Write a spec #
Make a directory and a .api file. A spec has three parts: a group of
settings, the types the routes trade in, and the service block that names the routes.
syntax = "v1"
type (
LoginReq {
Username string `json:"username"`
Password string `json:"password"`
}
LoginResp {
Token string `json:"token"`
}
Profile {
Id int64 `path:"id"`
Name string `json:"name"`
}
)
@server (
prefix: /api/v1
group: user
)
service user-api {
@doc "log a user in"
@handler Login
post /login (LoginReq) returns (LoginResp)
@handler GetProfile
get /profile/:id returns (Profile)
}
The @server block applies to the service under it: every route in
user-api is served under /api/v1 and belongs to the
user group. Each route names its handler and, in parentheses, the type it
takes and returns. A get route with no request type simply leaves the
parentheses out.
2. Check it #
The check reads the file the same way generation will, and stops on anything it cannot account for:
$ mctl api validate user.api mctl: user.api is valid
Forget a @handler and it says so, with the line number:
$ mctl api validate broken.api error [syntax] line 16: route get /orders/:id has no handler; name one with `@handler` mctl: broken.api: 1 error(s), 0 warning(s)
mctl api format rewrites the file in the one layout the tool considers
canonical — comments and struct tags untouched, indentation and blank lines fixed.
Pass --write to do it in place, or leave it off to get the text on stdout:
$ mctl api format user.api --write mctl: formatted user.api
3. Generate #
One command writes the whole project into the current directory:
$ mctl gen api user.api mctl: wrote moon.mod mctl: wrote moon.pkg mctl: wrote user-api.mbt mctl: wrote etc/user-api.yaml mctl: wrote internal/config/moon.pkg mctl: wrote internal/config/config.mbt mctl: wrote internal/svc/moon.pkg mctl: wrote internal/svc/servicecontext.mbt mctl: wrote internal/types/moon.pkg mctl: wrote internal/types/types.mbt mctl: wrote internal/handler/moon.pkg mctl: wrote internal/handler/routes_test.mbt mctl: wrote internal/handler/routes.mbt mctl: wrote internal/handler/user/moon.pkg mctl: wrote internal/handler/user/loginhandler.mbt mctl: wrote internal/handler/user/getprofilehandler.mbt mctl: wrote internal/logic/user/moon.pkg mctl: wrote internal/logic/user/loginlogic.mbt mctl: wrote internal/logic/user/getprofilelogic.mbt mctl: wrote 19 file(s) from user.api
Into the current directory by default, under --dir out when you name one.
--flat skips the tree and writes the routes and types as a single
.mbt file.
What was written #
Some of these files belong to the spec and are rewritten every time; the rest are written once and then belong to you.
| File | On the next run | What it holds |
|---|---|---|
moon.mod, moon.pkg | once | the module and the root package |
user-api.mbt | once | the entry point: read the config, build the context, serve HTTP |
etc/user-api.yaml | once | host and port |
internal/config/config.mbt | once | the configuration record |
internal/svc/servicecontext.mbt | once | everything the handlers share |
internal/types/types.mbt | every run | the spec's types |
internal/handler/routes.mbt | every run | the route table, prefix applied |
internal/handler/moon.pkg | every run | the imports the handler tree needs |
internal/handler/user/*handler.mbt | once | the HTTP edge of one route |
internal/logic/user/*logic.mbt | once | where the work goes |
internal/handler/routes_test.mbt | once | a test that calls every route |
Run the same command again and it says which of the two it did:
$ mctl gen api user.api mctl: wrote internal/types/types.mbt mctl: wrote internal/handler/moon.pkg mctl: wrote internal/handler/routes.mbt mctl: wrote 3 file(s) from user.api; kept the 16 already there
Three files, not nineteen. A comment you add to a logic file is still there after the run — regeneration rewrites what the spec decides and reads everything else.
4. Fill in the logic #
Each route got a handler and a logic file. The logic file is the one to open:
///|
/// `post /api/v1/login` — log a user in. This is where the service does its work.
pub fn loginlogic(
_svc : @svc.ServiceContext,
_ctx : @moonapi.Context,
) -> @moonasgi.Response {
@moonapi.text(200, "TODO: Login")
}
Replace the body. The signature stays as it is — the handler above it is generated and calls this function, so the two agree on the shape without either being edited.
5. Build it #
The project that came out compiles as it stands, for all four targets:
$ moon check --target all Finished. moon: ran 339 tasks, now up to date
moon run . starts the service on the host and port in
etc/user-api.yaml — 0.0.0.0:12000 until you change it — and the
entry point prints the address it is serving on. Building a native binary links through
a C toolchain, so that step needs one installed; moon check does not.
Where next #
The spec language has more in it than one example shows: tags that become validation, nested and embedded types, several groups in one service. The clients page takes the same spec and generates the other side of the wire.