Skip to content

Commit d8688bc

Browse files
authored
feat: add go type gen support (#8)
* feat: add go type gen support * fix: fuzz targets
1 parent ae38904 commit d8688bc

6 files changed

Lines changed: 1137 additions & 3 deletions

File tree

Makefile

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,10 @@ test-conformance: clone-test-suite
4747
@go tool cover -func=test_conformance.out
4848

4949
test-fuzz:
50-
@go test -fuzz=FuzzCompile -fuzztime=60s -run=^$$ .
51-
@go test -fuzz=FuzzValidate -fuzztime=60s -run=^$$ .
52-
@go test -fuzz=FuzzGenerate -fuzztime=60s -run=^$$ .
50+
@go test -fuzz='^FuzzCompile$$' -fuzztime=60s -run=^$$ .
51+
@go test -fuzz='^FuzzValidate$$' -fuzztime=60s -run=^$$ .
52+
@go test -fuzz='^FuzzGenerate$$' -fuzztime=60s -run=^$$ .
53+
@go test -fuzz='^FuzzGenerateGo$$' -fuzztime=60s -run=^$$ .
5354

5455
test-mutation: clone-test-suite
5556
@go tool github.com/go-gremlins/gremlins/cmd/gremlins unleash --config .gremlins.yaml

gentypes/main.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Command gentypes generates Go type declarations from a JSON Schema.
2+
//
3+
// Usage:
4+
//
5+
// gentypes [flags] [schema-file]
6+
//
7+
// The schema is read from the file argument, or from standard input when no
8+
// file is given. Generated Go source is written to standard output, or to
9+
// the file named by -o.
10+
//
11+
// As a go tool:
12+
//
13+
// go tool gentypes -package models -o models.go schema.json
14+
package main
15+
16+
import (
17+
"flag"
18+
"fmt"
19+
"io"
20+
"os"
21+
22+
"github.com/go-rotini/jsonschema"
23+
)
24+
25+
func main() {
26+
os.Exit(run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr))
27+
}
28+
29+
// run is the testable core of main: it parses args, generates types, and
30+
// returns the process exit code.
31+
func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
32+
flags := flag.NewFlagSet("gentypes", flag.ContinueOnError)
33+
flags.SetOutput(stderr)
34+
pkg := flags.String("package", "models", "package name for the generated file")
35+
root := flags.String("root", "", `name of the root type (default: schema "title", else "Root")`)
36+
outPath := flags.String("o", "", "write output to this file instead of stdout")
37+
flags.Usage = func() {
38+
fmt.Fprintln(stderr, "usage: gentypes [flags] [schema-file]")
39+
fmt.Fprintln(stderr, "reads a JSON Schema (from schema-file or stdin) and writes Go types")
40+
flags.PrintDefaults()
41+
}
42+
if err := flags.Parse(args); err != nil {
43+
return 2
44+
}
45+
46+
schemaJSON, err := readSchema(flags.Arg(0), stdin)
47+
if err != nil {
48+
fmt.Fprintln(stderr, "gentypes:", err)
49+
return 1
50+
}
51+
52+
opts := []jsonschema.GoOption{jsonschema.WithGoPackage(*pkg)}
53+
if *root != "" {
54+
opts = append(opts, jsonschema.WithGoRootType(*root))
55+
}
56+
src, err := jsonschema.GenerateGo(schemaJSON, opts...)
57+
if err != nil {
58+
fmt.Fprintln(stderr, "gentypes:", err)
59+
return 1
60+
}
61+
62+
if err := writeOutput(*outPath, src, stdout); err != nil {
63+
fmt.Fprintln(stderr, "gentypes:", err)
64+
return 1
65+
}
66+
return 0
67+
}
68+
69+
// readSchema reads schema bytes from path, or from stdin when path is empty.
70+
func readSchema(path string, stdin io.Reader) ([]byte, error) {
71+
if path == "" {
72+
data, err := io.ReadAll(stdin)
73+
if err != nil {
74+
return nil, fmt.Errorf("read stdin: %w", err)
75+
}
76+
return data, nil
77+
}
78+
data, err := os.ReadFile(path)
79+
if err != nil {
80+
return nil, fmt.Errorf("read schema file: %w", err)
81+
}
82+
return data, nil
83+
}
84+
85+
// writeOutput writes src to path, or to stdout when path is empty.
86+
func writeOutput(path string, src []byte, stdout io.Writer) error {
87+
if path == "" {
88+
if _, err := stdout.Write(src); err != nil {
89+
return fmt.Errorf("write stdout: %w", err)
90+
}
91+
return nil
92+
}
93+
if err := os.WriteFile(path, src, 0o600); err != nil {
94+
return fmt.Errorf("write output file: %w", err)
95+
}
96+
return nil
97+
}

gentypes/main_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"go/parser"
6+
"go/token"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
"testing"
11+
)
12+
13+
func TestRun_stdinToStdout(t *testing.T) {
14+
schema := `{"title":"Widget","type":"object","required":["id"],"properties":{"id":{"type":"string"}}}`
15+
var out, errOut bytes.Buffer
16+
code := run([]string{"-package", "widgets"}, strings.NewReader(schema), &out, &errOut)
17+
if code != 0 {
18+
t.Fatalf("run exit=%d stderr=%s", code, errOut.String())
19+
}
20+
got := out.String()
21+
for _, w := range []string{"package widgets", "type Widget struct", "Id string"} {
22+
if !strings.Contains(got, w) {
23+
t.Errorf("output missing %q\n%s", w, got)
24+
}
25+
}
26+
}
27+
28+
func TestRun_invalidSchema(t *testing.T) {
29+
var out, errOut bytes.Buffer
30+
code := run([]string{}, strings.NewReader("not json"), &out, &errOut)
31+
if code == 0 {
32+
t.Fatalf("expected non-zero exit for invalid schema; stderr=%s", errOut.String())
33+
}
34+
}
35+
36+
func TestRun_fileOutput(t *testing.T) {
37+
dir := t.TempDir()
38+
outFile := filepath.Join(dir, "models.go")
39+
schema := `{"title":"Thing","type":"object","properties":{"name":{"type":"string"}}}`
40+
var out, errOut bytes.Buffer
41+
code := run([]string{"-o", outFile}, strings.NewReader(schema), &out, &errOut)
42+
if code != 0 {
43+
t.Fatalf("run exit=%d stderr=%s", code, errOut.String())
44+
}
45+
data, err := os.ReadFile(outFile)
46+
if err != nil {
47+
t.Fatalf("read output file: %v", err)
48+
}
49+
if _, err := parser.ParseFile(token.NewFileSet(), "models.go", data, 0); err != nil {
50+
t.Fatalf("output file is not valid Go: %v\n%s", err, data)
51+
}
52+
if !strings.Contains(string(data), "type Thing struct") {
53+
t.Errorf("output file missing type Thing:\n%s", data)
54+
}
55+
}
56+
57+
func TestRun_rootFlag(t *testing.T) {
58+
schema := `{"type":"object","properties":{"x":{"type":"string"}}}`
59+
var out, errOut bytes.Buffer
60+
code := run([]string{"-root", "Custom"}, strings.NewReader(schema), &out, &errOut)
61+
if code != 0 {
62+
t.Fatalf("run exit=%d stderr=%s", code, errOut.String())
63+
}
64+
if !strings.Contains(out.String(), "type Custom struct") {
65+
t.Errorf("output missing type Custom:\n%s", out.String())
66+
}
67+
}
68+
69+
func TestRun_flagError(t *testing.T) {
70+
var out, errOut bytes.Buffer
71+
if code := run([]string{"-nonexistent"}, strings.NewReader(""), &out, &errOut); code != 2 {
72+
t.Fatalf("expected exit 2 for unknown flag, got %d", code)
73+
}
74+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212

1313
tool (
1414
github.com/go-gremlins/gremlins/cmd/gremlins
15+
github.com/go-rotini/jsonschema/gentypes
1516
github.com/golangci/golangci-lint/v2/cmd/golangci-lint
1617
github.com/google/go-licenses/v2
1718
golang.org/x/vuln/cmd/govulncheck

0 commit comments

Comments
 (0)