Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 36 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

# mochi_codegen

Code generation (TypeScript types, GraphQL SDL, Gleam types) and CLI for mochi GraphQL.
Code generation (TypeScript types, GraphQL SDL, Gleam types + resolver stubs) and CLI for mochi GraphQL.

## Installation

Expand All @@ -16,10 +16,43 @@ mochi_codegen = { git = "https://github.com/qwexvf/mochi_codegen", ref = "main"
## CLI

```sh
gleam run -m mochi_codegen/cli -- init # create mochi.config.json
gleam run -m mochi_codegen/cli -- init # create mochi.config.yaml
gleam run -m mochi_codegen/cli -- generate # generate from config
```

## Config (`mochi.config.yaml`)

```yaml
schema: "graphql/*.graphql"

# Optional: generate resolver boilerplate from .gql client operation files
operations_input: "src/graphql/**/*.gql"

output:
typescript: "src/generated/types.ts" # TypeScript type definitions
gleam_types: "src/api/domain/" # Gleam domain types (dir = one file per schema)
resolvers: "src/api/schema/" # Gleam resolver stubs (preserved on regen)
operations: "src/api/schema/" # Gleam operation resolvers (from .gql files)
sdl: null # Normalised SDL (omit to skip)

gleam:
types_module_prefix: "api/domain"
resolvers_module_prefix: "api/schema"
type_suffix: "_types"
resolver_suffix: "_resolvers"
generate_docs: true
```

Output paths ending in `/` produce one file per schema file. Paths without `/` merge all schemas into a single file.

### Write policies

| Output | Policy |
|--------|--------|
| `typescript`, `sdl` | Always overwrite |
| `gleam_types` | Overwrite only when content changed |
| `resolvers`, `operations` | Never overwrite existing functions — only appends new stubs |

## Programmatic Usage

```gleam
Expand All @@ -30,21 +63,9 @@ let sdl = mochi_codegen.to_sdl(schema)
let html = mochi_codegen.graphiql("/graphql")
```

## Config (`mochi.config.json`)

```json
{
"schema": "schema.graphql",
"output": {
"typescript": "src/generated/types.ts",
"sdl": null
}
}
```

## License

Apache-2.0

---
Built with the help of [Claude Code](https://claude.ai/code).
Built with the help of [Claude Code](https://claude.ai/code).
2 changes: 1 addition & 1 deletion gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ licences = ["Apache-2.0"]
repository = { type = "github", user = "qwexvf", repo = "mochi" }

[dependencies]
gleam_stdlib = ">= 1.0.0 and < 2.0.0"
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
gleam_json = ">= 3.0.0 and < 4.0.0"
simplifile = ">= 2.0.0 and < 3.0.0"
mochi = { git = "https://github.com/qwexvf/mochi", ref = "main" }
Expand Down
4 changes: 2 additions & 2 deletions manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ packages = [
{ name = "gleeunit", version = "1.10.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "254B697FE72EEAD7BF82E941723918E421317813AC49923EE76A18C788C61E72" },
{ name = "mochi", version = "0.3.1", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], source = "git", repo = "https://github.com/qwexvf/mochi", commit = "19f87a112fda422b571705b8a71c3d90d285d8e5" },
{ name = "simplifile", version = "2.4.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "7C18AFA4FED0B4CE1FA5B0B4BAC1FA1744427054EA993565F6F3F82E5453170D" },
{ name = "taffy", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "simplifile"], source = "git", repo = "https://github.com/qwexvf/taffy", commit = "b468f9eeb2c73ed2d259eeee96a747d2d73208fe" },
{ name = "taffy", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "simplifile"], source = "git", repo = "https://github.com/qwexvf/taffy", commit = "ce8539513a410002b91e7feca9949c2f7df4cc95" },
]

[requirements]
gleam_json = { version = ">= 3.0.0 and < 4.0.0" }
gleam_stdlib = { version = ">= 1.0.0 and < 2.0.0" }
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
mochi = { git = "https://github.com/qwexvf/mochi", ref = "main" }
simplifile = { version = ">= 2.0.0 and < 3.0.0" }
Expand Down
53 changes: 53 additions & 0 deletions src/mochi_codegen/cli.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import gleam/list
import gleam/option.{None, Some}
import gleam/result
import gleam/string
import mochi/parser as mochi_parser
import mochi/schema.{type Schema}
import mochi/sdl_ast.{type SDLDocument, SDLDocument}
import mochi/sdl_parser
import mochi_codegen/config
import mochi_codegen/gleam as gleam_gen
import mochi_codegen/operation_gen
import mochi_codegen/sdl
import mochi_codegen/typescript
import simplifile
Expand Down Expand Up @@ -140,6 +142,7 @@ fn run_generate(args: List(String)) -> Result(String, CliError) {
gleam_config,
conf.gleam.type_suffix,
conf.gleam.resolver_suffix,
conf.operations_input,
))

case messages {
Expand All @@ -163,6 +166,7 @@ fn run_direct(args: List(String)) -> Result(String, CliError) {
typescript: option.from_result(cli_config.typescript_output),
gleam_types: option.from_result(cli_config.gleam_output),
resolvers: option.from_result(cli_config.resolvers_output),
operations: None,
sdl: option.from_result(cli_config.sdl_output),
)

Expand All @@ -172,6 +176,7 @@ fn run_direct(args: List(String)) -> Result(String, CliError) {
gleam_config,
"_types",
"_resolvers",
None,
))

case messages {
Expand All @@ -192,6 +197,7 @@ fn generate_from_paths(
gleam_config: gleam_gen.GleamGenConfig,
type_suffix: String,
resolver_suffix: String,
operations_input: option.Option(String),
) -> Result(List(String), CliError) {
use merged <- result.try(read_and_merge_schemas(paths))

Expand Down Expand Up @@ -286,6 +292,40 @@ fn generate_from_paths(
}
})

// Operations — read .gql operation files, generate resolver boilerplate
use messages <- result.try(case operations_input, output.operations {
Some(input_glob), Some(out_path) -> {
use op_paths <- result.try(expand_globs([input_glob]))
list.try_fold(op_paths, messages, fn(msgs, op_path) {
use content <- result.try(
simplifile.read(op_path)
|> result.map_error(fn(_) {
FileReadError(op_path, "File system error")
}),
)
use ops_doc <- result.try(
mochi_parser.parse(content)
|> result.map_error(fn(e) { ParseError(format_op_parse_error(e)) }),
)
let generated = operation_gen.generate(ops_doc, merged)
let filename = schema_stem(op_path) <> resolver_suffix <> ".gleam"
let dest = out_path <> filename
use _ <- result.try(ensure_dir(out_path))
use written <- result.try(write_with_policy(
dest,
generated,
MergeNewFunctions,
))
let msg = case written {
True -> "Generated operations: " <> dest
False -> "Generated operations (up to date): " <> dest
}
Ok([msg, ..msgs])
})
}
_, _ -> Ok(messages)
})

// SDL — always single file (merging makes sense here)
use messages <- result.try(case output.sdl {
None -> Ok(messages)
Expand Down Expand Up @@ -888,6 +928,19 @@ fn format_error(err: CliError) -> String {
}
}

fn format_op_parse_error(err: mochi_parser.ParseError) -> String {
case err {
mochi_parser.UnexpectedToken(expected, _, pos) ->
"Unexpected token at line "
<> int.to_string(pos.line)
<> ", expected "
<> expected
mochi_parser.UnexpectedEOF(expected) ->
"Unexpected end of file, expected " <> expected
mochi_parser.LexError(_) -> "Lexer error"
}
}

fn format_parse_error(err: sdl_parser.SDLParseError) -> String {
case err {
sdl_parser.SDLLexError(_) -> "Lexer error"
Expand Down
26 changes: 24 additions & 2 deletions src/mochi_codegen/config.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ pub type Config {
Config(
/// Glob pattern(s) or explicit path(s) to GraphQL schema files.
schema: List(String),
/// Glob pattern(s) for GraphQL operation files (.gql) to generate resolvers from.
operations_input: Option(String),
/// Output configuration
output: OutputConfig,
/// Gleam-specific codegen options
Expand All @@ -56,6 +58,8 @@ pub type OutputConfig {
gleam_types: Option(String),
/// Gleam resolver stubs (file or directory)
resolvers: Option(String),
/// Gleam operation-based resolver boilerplate (directory only)
operations: Option(String),
/// Normalised SDL output (file only)
sdl: Option(String),
)
Expand Down Expand Up @@ -91,10 +95,12 @@ pub fn is_dir_output(path: String) -> Bool {
pub fn default() -> Config {
Config(
schema: ["schema.graphql"],
operations_input: None,
output: OutputConfig(
typescript: Some("src/generated/types.ts"),
gleam_types: Some("src/generated/"),
resolvers: Some("src/generated/"),
operations: None,
sdl: None,
),
gleam: GleamConfig(
Expand All @@ -118,11 +124,15 @@ pub fn to_yaml(config: Config) -> String {
<> "\n"
}

let operations_input_yaml =
opt_yaml_field("operations_input", config.operations_input)

let output_yaml =
"output:\n"
<> opt_yaml_field(" typescript", config.output.typescript)
<> opt_yaml_field(" gleam_types", config.output.gleam_types)
<> opt_yaml_field(" resolvers", config.output.resolvers)
<> opt_yaml_field(" operations", config.output.operations)
<> opt_yaml_field(" sdl", config.output.sdl)

let resolver_imports_yaml = case config.gleam.resolver_imports {
Expand Down Expand Up @@ -152,7 +162,13 @@ pub fn to_yaml(config: Config) -> String {
<> bool_to_yaml(config.gleam.generate_docs)
<> "\n"

schema_yaml <> "\n" <> output_yaml <> "\n" <> gleam_yaml
schema_yaml
<> "\n"
<> operations_input_yaml
<> "\n"
<> output_yaml
<> "\n"
<> gleam_yaml
}

fn opt_yaml_field(key: String, value: Option(String)) -> String {
Expand Down Expand Up @@ -184,7 +200,12 @@ fn decode_config(doc: taffy.Value) -> Result(Config, String) {
use schema <- result.try(decode_schema(doc))
use output <- result.try(decode_output(doc))
use gleam <- result.try(decode_gleam(doc))
Ok(Config(schema:, output:, gleam:))
Ok(Config(
schema:,
operations_input: opt_string(doc, "operations_input"),
output:,
gleam:,
))
}

fn decode_schema(doc: taffy.Value) -> Result(List(String), String) {
Expand Down Expand Up @@ -220,6 +241,7 @@ fn decode_output(doc: taffy.Value) -> Result(OutputConfig, String) {
typescript: opt_string(output, "typescript"),
gleam_types: opt_string(output, "gleam_types"),
resolvers: opt_string(output, "resolvers"),
operations: opt_string(output, "operations"),
sdl: opt_string(output, "sdl"),
))
}
Expand Down
Loading
Loading