// The `gql` generator: turn a `.api` spec's `type` blocks into a moongql
// code-first schema plus resolver stubs, the GraphQL counterpart of the moonapi
// (`generate`) and moonrpc (`generate_grpc`) generators. goctl has no GraphQL
// target; this is the moon-heke suite extending mctl to scaffold the whole stack.
// The output compiles against `Lfan-ke/moongql`.
///|
/// Lowercase the first character (the inverse of `cap_first`), naming a `Query`
/// lookup field after its type.
fn lower_first(s : String) -> String {
if s.length() == 0 {
return s
}
let c = s[0].to_int()
if c >= 0x41 && c <= 0x5A {
(c + 0x20).to_byte().to_char().to_string() + s[1:].to_owned()
} else {
s
}
}
///|
/// Whether `s` begins with `prefix` (core ships no `String::starts_with`).
fn gql_starts_with(s : String, prefix : String) -> Bool {
if s.length() < prefix.length() {
return false
}
for i = 0; i < prefix.length(); i = i + 1 {
if s[i] != prefix[i] {
return false
}
}
true
}
///|
/// A non-null scalar constructor expression.
fn nn_scalar(name : String) -> String {
"@moongql.NonNull(@moongql.Scalar(\"" + name + "\"))"
}
///|
/// The moongql `GqlType` constructor expression for a field's MoonBit type (as the
/// `.api` parser emits it). MoonBit scalars map to GraphQL scalars, `Array[T]` to a
/// non-null list, a `Map[...]` to a JSON string (GraphQL has no map type), and any
/// other name to a `Named` reference. Every field is `NonNull` — the scaffold's
/// default, loosened per field as the schema is filled in.
fn gql_type_expr(mb_type : String) -> String {
if gql_starts_with(mb_type, "Array[") {
let inner = mb_type[6:mb_type.length() - 1].to_owned()
"@moongql.NonNull(@moongql.ListOf(" + gql_type_expr(inner) + "))"
} else if gql_starts_with(mb_type, "Map[") {
nn_scalar("String")
} else {
match mb_type {
"String" => nn_scalar("String")
"Int" => nn_scalar("Int")
"Int64" => nn_scalar("Int")
"UInt" => nn_scalar("Int")
"UInt64" => nn_scalar("Int")
"Bool" => nn_scalar("Boolean")
"Double" => nn_scalar("Float")
"Float" => nn_scalar("Float")
"ID" => nn_scalar("ID")
other => "@moongql.NonNull(@moongql.Named(\"" + other + "\"))"
}
}
}
///|
/// A resolver stub line: `resolvers.field("", "", fn(info) { ... })`,
/// a null placeholder for the caller to replace with real logic.
fn resolver_stub(type_name : String, field_name : String) -> String {
" resolvers.field(\"" +
type_name +
"\", \"" +
field_name +
"\", fn(info) {\n let _ = info\n Json::null()\n })\n"
}
///|
/// 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 `(id: ID!): ` 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.
pub fn generate_gql(spec : Spec) -> String {
let mut out = "// Code generated by moonctl. DO NOT EDIT.\n\n"
out = out +
"///|\n" +
"/// A moongql code-first schema built from the `.api` `type` blocks.\n" +
"pub fn build_schema() -> @moongql.Schema {\n" +
" let schema = @moongql.Schema::new()\n"
for t in spec.types {
let v = lower_first(t.name)
out = out + " let " + v + " = schema.object(\"" + t.name + "\")\n"
for f in t.fields {
out = out +
" " +
v +
".field(\"" +
f.name +
"\", " +
gql_type_expr(f.type_) +
")\n"
}
}
out = out + " let query = schema.object(\"Query\")\n"
for t in spec.types {
out = out +
" query.field_args(\"" +
lower_first(t.name) +
"\", [(\"id\", @moongql.NonNull(@moongql.Scalar(\"ID\")))], @moongql.Named(\"" +
t.name +
"\"))\n"
}
out = out + " schema\n}\n\n"
out = out +
"///|\n" +
"/// Resolver stubs for the generated schema. Replace each body with real logic.\n" +
"pub fn resolvers() -> @moongql.Resolvers {\n" +
" let resolvers = @moongql.Resolvers::new()\n"
for t in spec.types {
for f in t.fields {
out = out + resolver_stub(t.name, f.name)
}
}
for t in spec.types {
out = out + resolver_stub("Query", lower_first(t.name))
}
out = out + " resolvers\n}\n"
out
}