Skip to content
Closed
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
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ make lint # golangci-lint, config in .golangci.yml — must be cle
make fmt # gofumpt -w .
make cover # coverage.out + per-function summary
make regress REF=master # generated output, working tree vs a git ref
make e2e # generated clients against a mock and FastAPI (testdata/e2e)
go test ./internal/parser -run TestName
```

Expand All @@ -32,7 +33,8 @@ go.yaml.in/yaml/v3. No cgo.
| `internal/parser` | kin-openapi document → `codegen` structs: models, operations, parameters |
| `internal/codegen` | the `Codegen*` structs templates render; fields mirror the Java originals |
| `internal/gen` | the pipeline: load spec → parse → render models, apis, supporting files |
| `internal/generator` | `CodegenConfig` and the language configs, `typescript/` and `dart/` |
| `internal/generator` | `CodegenConfig` and the language configs, `typescript/`, `dart/`, `golang/` |
| `internal/e2e` | end-to-end tests: mock server, runners, scenarios in `testdata/e2e` |
| `internal/template` | the Mustache engine: partials, lambdas, struct-to-map via json |
| `internal/config` | generator options and their config-file keys |
| `internal/update` | release check, verified download, self-update |
Expand Down
16 changes: 15 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help test cover cover-html lint fmt vet build install clean tidy update samples-dart-fetch regress all
.PHONY: help test cover cover-html lint fmt vet build install clean tidy update samples-dart-fetch samples-go-fetch regress e2e e2e-update all

GOFUMPT ?= go run mvdan.cc/gofumpt@v0.12.0
GOLANGCI ?= go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.0
Expand All @@ -19,6 +19,14 @@ test:
go vet ./...
go test -race ./...

## e2e: generated clients against a validating mock and the FastAPI backend (needs uv), testdata/e2e
e2e:
E2E=1 go test -count=1 -v ./internal/e2e

## e2e-update: e2e, rewriting the snapshot of the backend's spec
e2e-update:
E2E=1 E2E_UPDATE=1 go test -count=1 -v ./internal/e2e

## cover: Run tests with coverage
cover:
go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
Expand Down Expand Up @@ -72,6 +80,12 @@ samples-dart-fetch: build
@rm -rf samples/dart-fetch/petstore-expanded
$(OUTPUT_DIR)/openapi-generator generate -c samples/dart-fetch/config.yaml

## samples-go-fetch: Regenerate the go-fetch petstore sample
samples-go-fetch: build
@echo "Generating go-fetch petstore sample..."
@rm -rf samples/go-fetch/petstore-expanded
$(OUTPUT_DIR)/openapi-generator generate -c samples/go-fetch/config.yaml

## regress: Diff generated output against a git ref (default HEAD): make regress REF=master
regress:
./scripts/regress.sh $(REF)
Expand Down
27 changes: 16 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ minimal dependencies.
- Fast generation times without JVM overhead
- Load specs from local files or remote URLs
- Command-line interface compatible with the original OpenAPI Generator
- Supports OpenAPI 3.x and Swagger 2.0 specifications (auto-converts 2.0 -> 3.x)
- Supports OpenAPI 3.x and Swagger 2.0 specifications (auto-converts 2.0 -> 3.x);
a 3.1 `null` member of `anyOf`/`oneOf`, a `type` list with `null`, a `const`
and a `contentMediaType` binary string read as their 3.0 forms, as the
upstream normalizer's defaults do

## Installation

Expand Down Expand Up @@ -47,6 +50,7 @@ go install github.com/xseman/openapi-generator/cmd/openapi-generator@latest

- [typescript-fetch](./templates/typescript-fetch/README.md)
- [dart-fetch](./samples/dart-fetch/README.md)
- [go-fetch](./templates/go-fetch/README.md)

### Server

Expand Down Expand Up @@ -75,16 +79,16 @@ openapi-generator validate -i openapi.yaml --recommend

### `generate`

| Option | Short | Description |
| --------------------------- | ----- | ------------------------------------------------ |
| `--input-spec` | `-i` | Location of the OpenAPI spec (file or URL) |
| `--generator-name` | `-g` | Generator to use: typescript-fetch, dart-fetch |
| `--output` | `-o` | Output directory |
| `--config` | `-c` | Configuration file (JSON/YAML) |
| `--template-dir` | `-t` | Custom template directory |
| `--additional-properties` | `-p` | Key=value pairs for generator options |
| `--skip-validate-spec` | | Skip OpenAPI spec validation |
| `--verbose` | `-v` | Enable verbose output |
| Option | Short | Description |
| ------------------------- | ----- | ---------------------------------------------------- |
| `--input-spec` | `-i` | Location of the OpenAPI spec (file or URL) |
| `--generator-name` | `-g` | typescript-fetch, dart-fetch, go-fetch |
| `--output` | `-o` | Output directory |
| `--config` | `-c` | Configuration file (JSON/YAML) |
| `--template-dir` | `-t` | Custom template directory |
| `--additional-properties` | `-p` | `key=value` generator options, `a=1,b=2` or repeated |
| `--skip-validate-spec` | | Skip OpenAPI spec validation |
| `--verbose` | `-v` | Enable verbose output |

### `validate`

Expand Down Expand Up @@ -163,6 +167,7 @@ sequenceDiagram

CLI->>Parser: Load Spec (YAML/JSON)
Parser-->>Parser: Convert Swagger 2.0 to OpenAPI 3.x
Parser-->>Parser: Normalize 3.1 null and binary schemas
Parser-->>CLI: Parsed Spec

CLI->>Config: Merge CLI + File Config
Expand Down
3 changes: 3 additions & 0 deletions cmd/openapi-generator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ var listCmd = &cobra.Command{
fmt.Println("CLIENT generators:")
fmt.Println(" - typescript-fetch")
fmt.Println(" - dart-fetch")
fmt.Println(" - go-fetch")
fmt.Println()
},
}
Expand All @@ -123,6 +124,8 @@ var configHelpCmd = &cobra.Command{
gen.PrintTypeScriptFetchConfigHelp()
case "dart-fetch":
gen.PrintDartFetchConfigHelp()
case "go-fetch":
gen.PrintGoFetchConfigHelp()
default:
fmt.Printf("Unknown generator: %s\n", args[0])
}
Expand Down
14 changes: 8 additions & 6 deletions internal/codegen/parameter.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,14 @@ type CodegenParameter struct {
IsCollectionFormatMulti bool `json:"isCollectionFormatMulti"`

// Nested
ComposedModels []string `json:"composedModels"` // member models of a union/intersection type, for imports
Items *CodegenProperty `json:"items"`
AdditionalProperties *CodegenProperty `json:"additionalProperties"`
Vars []*CodegenProperty `json:"vars"`
RequiredVars []*CodegenProperty `json:"requiredVars"`
MostInnerItems *CodegenProperty `json:"mostInnerItems"`
ComposedModels []string `json:"composedModels"` // member models of a union/intersection type, for imports
Items *CodegenProperty `json:"items"`
AdditionalProperties *CodegenProperty `json:"additionalProperties"`
Vars []*CodegenProperty `json:"vars"`
RequiredVars []*CodegenProperty `json:"requiredVars"`
MostInnerItems *CodegenProperty `json:"mostInnerItems"`
ComposedSchemas *CodegenComposedSchemas `json:"composedSchemas"`
Ref string `json:"ref"` // the $ref the parameter's schema is, as the spec writes it

// Documentation
Description string `json:"description"`
Expand Down
3 changes: 3 additions & 0 deletions internal/codegen/property.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ type CodegenProperty struct {
IsInnerEnum bool `json:"isInnerEnum"` // Inline enum
IsEnumRef bool `json:"isEnumRef"` // Reference to enum model

// Ref is the $ref the property is typed from, as the spec writes it.
Ref string `json:"ref"`

// Enum
Enum []string `json:"enum"`
EnumName string `json:"enumName"`
Expand Down
18 changes: 18 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,24 @@ func NewDartFetchConfig() *DartFetchConfig {
}
}

// GoFetchConfig holds configuration specific to the go-fetch generator.
//
// The go-fetch generator produces a Go client that imports only the
// standard library, in a single package.
type GoFetchConfig struct {
// PackageName is the Go package name of the generated files.
PackageName string `json:"packageName,omitempty"`
// WithGoMod writes a go.mod, making the output a module of its own.
WithGoMod bool `json:"withGoMod,omitempty"`
// ModuleName is the module path in go.mod (default: PackageName).
ModuleName string `json:"moduleName,omitempty"`
}

// NewGoFetchConfig creates a new GoFetchConfig with default values.
func NewGoFetchConfig() *GoFetchConfig {
return &GoFetchConfig{PackageName: "openapi"}
}

// FileNamingType represents the file naming convention.
type FileNamingType string

Expand Down
178 changes: 178 additions & 0 deletions internal/e2e/dart_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package e2e

import (
"fmt"
"maps"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"testing"

"github.com/xseman/openapi-generator/internal/gen"
)

// dartVersion reads "Dart SDK version: 3.13.7 ..." as 3 and 13.
var dartVersion = regexp.MustCompile(`version: (\d+)\.(\d+)`)

// runDart generates a dart-fetch package per suite, depends on them from a
// package holding testdata/e2e/runners/dart and runs the suites on the VM.
// E2E_DART names the dart binary; the clients need Dart 3.13 or later.
func runDart(t *testing.T, suites []suite) map[string]map[int]outcome {
t.Helper()

dart := os.Getenv("E2E_DART")
if dart == "" {
dart = "dart"
}

ver, err := exec.Command(dart, "--version").CombinedOutput()
if m := dartVersion.FindStringSubmatch(string(ver)); err != nil || m == nil || m[1] == "3" && atoi(m[2]) < 13 {
t.Skipf("dart-fetch clients need Dart 3.13 or later (set E2E_DART): %s %v", strings.TrimSpace(string(ver)), err)
}

dir := t.TempDir()

runner, err := os.ReadFile("../../testdata/e2e/runners/dart/runner.dart")
if err != nil {
t.Fatal(err)
}

write(t, filepath.Join(dir, "runner.dart"), string(runner))

for i, s := range suites {
err := gen.Generate(gen.Options{
InputSpec: s.Spec,
OutputDir: filepath.Join(dir, "c"+strconv.Itoa(i)),
GeneratorName: "dart-fetch",
TemplateDir: "../../templates/dart-fetch",
AdditionalProperties: []string{"pubName=c" + strconv.Itoa(i)},
SkipValidation: true,
})
if err != nil {
t.Fatalf("generate %s: %v", s.Name, err)
}
}

// A client that does not compile would stop the whole program: it
// leaves the run, and its cases fail with its first error.
broken := dartErrors(t, dart, dir)
failed := map[string]map[int]outcome{}

var imports, clients, deps strings.Builder

for i, s := range suites {
pkg := "c" + strconv.Itoa(i)

if msg, ok := broken[pkg]; ok {
failed[s.Name] = map[int]outcome{}
for j := range s.Cases {
failed[s.Name][j] = outcome{Suite: s.Name, Case: j, Fail: "the client does not compile: " + msg}
}

continue
}

fmt.Fprintf(&deps, " %s:\n path: %s\n", pkg, pkg)
fmt.Fprintf(&imports, "import 'package:%s/%s.dart' as %s;\n", pkg, pkg, pkg)
fmt.Fprintf(&clients, " %q: Client(\n (base, a) => %s.Configuration(basePath: base, username: a.username, password: a.password, "+
"apiKey: (name) => a.apiKey ?? '', accessToken: () => a.bearer ?? ''),\n (cfg) => [%s],\n %q),\n",
s.Name, pkg, dartAPIs(t, filepath.Join(dir, pkg), pkg), pkg)
}

var running []suite

for _, s := range suites {
if _, ok := failed[s.Name]; !ok {
running = append(running, s)
}
}

if len(running) == 0 {
return failed
}

write(t, filepath.Join(dir, "pubspec.yaml"), "name: e2e_runner\npublish_to: none\nenvironment:\n sdk: '>=3.13.0 <4.0.0'\ndependencies:\n"+deps.String())
write(t, filepath.Join(dir, "main.dart"), "import 'runner.dart';\n"+imports.String()+
"\nvoid main(List<String> args) => run(args[0], {\n"+clients.String()+"});\n")

get := exec.Command(dart, "pub", "get", "--offline")
get.Dir = dir

if out, err := get.CombinedOutput(); err != nil {
t.Fatalf("dart pub get: %v\n%s", err, out)
}

writeInput(t, dir, running)

cmd := exec.Command(dart, "run", "main.dart", filepath.Join(dir, "input.json"))
cmd.Dir = dir

out := execRunner(t, cmd)
maps.Copy(out, failed)

return out
}

// dartErrors analyzes the generated packages under dir and returns the
// first compile error of each package that has one.
func dartErrors(t *testing.T, dart, dir string) map[string]string {
t.Helper()

cmd := exec.Command(dart, "analyze", "--format=machine", ".")
cmd.Dir = dir

out, _ := cmd.CombinedOutput()

errs := map[string]string{}

for _, line := range strings.Split(string(out), "\n") {
f := strings.Split(line, "|")
if len(f) < 8 || f[0] != "ERROR" {
continue
}

rel, err := filepath.Rel(dir, f[3])
if err != nil {
continue
}

pkg, _, _ := strings.Cut(rel, string(filepath.Separator))
if _, seen := errs[pkg]; !seen {
errs[pkg] = fmt.Sprintf("%s:%s: %s", rel, f[4], f[7])
}
}

return errs
}

// dartAPIs lists the constructors of a package's API classes for cfg.
func dartAPIs(t *testing.T, dir, pkg string) string {
t.Helper()

files, _ := filepath.Glob(filepath.Join(dir, "lib", "src", "apis", "*.dart"))
class := regexp.MustCompile(`(?m)^class (\w+) extends BaseApi`)

var calls []string

for _, f := range files {
data, err := os.ReadFile(f)
if err != nil {
t.Fatal(err)
}

for _, m := range class.FindAllStringSubmatch(string(data), -1) {
calls = append(calls, pkg+"."+m[1]+"(cfg as "+pkg+".Configuration)")
}
}

return strings.Join(calls, ", ")
}

func atoi(s string) int {
n, _ := strconv.Atoi(s)

return n
}
Loading
Loading