Skip to content

Commit

Permalink
Merge branch 'mwlazlo-master'
Browse files Browse the repository at this point in the history
  • Loading branch information
a-h committed Feb 4, 2019
2 parents f3043e0 + 485ac91 commit 8a14529
Show file tree
Hide file tree
Showing 40 changed files with 8,859 additions and 1,582 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
/schema-generate
.idea
test/*_gen
2 changes: 0 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
language: go

go_import_path: github.com/a-h/generate

script:
- make codecheck
- make test
19 changes: 15 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
PKG := github.com/a-h/generate
PKG := .
CMD := $(PKG)/cmd/schema-generate
BIN := schema-generate

Expand All @@ -8,22 +8,33 @@ BIN := schema-generate

all: clean $(BIN)

$(BIN):
$(BIN): generator.go jsonschema.go cmd/schema-generate/main.go
@echo "+ Building $@"
CGO_ENABLED="0" go build -v -o $@ $(CMD)

clean:
@echo "+ Cleaning $(PKG)"
go clean -i $(PKG)/...
rm -f $(BIN)
rm -rf test/*_gen

# Test

# generate sources
JSON := $(wildcard test/*.json)
GENERATED_SOURCE := $(patsubst %.json,%_gen/generated.go,$(JSON))
test/%_gen/generated.go: test/%.json
@echo "\n+ Generating code for $@"
@D=$(shell echo $^ | sed 's/.json/_gen/'); \
[ ! -d $$D ] && mkdir -p $$D || true
./schema-generate -o $@ -p $(shell echo $^ | sed 's/test\///; s/.json//') $^

.PHONY: test codecheck fmt lint vet

test:
@echo "+ Executing tests for $(PKG)"
test: $(BIN) $(GENERATED_SOURCE)
@echo "\n+ Executing tests for $(PKG)"
go test -v -race -cover $(PKG)/...


codecheck: fmt lint vet

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# generate

Generates Go (golang) Structs from JSON schema.
Generates Go (golang) Structs and Validation code from JSON schema.

# Requirements

Expand Down
143 changes: 11 additions & 132 deletions cmd/schema-generate/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,19 @@
package main

import (
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"sort"
"strings"

"github.com/a-h/generate"
"github.com/a-h/generate/jsonschema"
)

var (
o = flag.String("o", "", "The output file for the schema.")
p = flag.String("p", "main", "The package that the structs are created in.")
i = flag.String("i", "", "A single file path (used for backwards compatibility).")
nsk = flag.Bool("nsk", false, "Allow input files with no $schema key.")
o = flag.String("o", "", "The output file for the schema.")
p = flag.String("p", "main", "The package that the structs are created in.")
i = flag.String("i", "", "A single file path (used for backwards compatibility).")
schemaKeyRequiredFlag = flag.Bool("schemaKeyRequired", false, "Allow input files with no $schema key.")
)

func main() {
Expand All @@ -43,42 +38,18 @@ func main() {
os.Exit(1)
}

schemas := make([]*jsonschema.Schema, len(inputFiles))
for i, file := range inputFiles {
b, err := ioutil.ReadFile(file)
if err != nil {
fmt.Fprintln(os.Stderr, "Failed to read the input file with error ", err)
return
}

schemas[i], err = jsonschema.Parse(string(b), *nsk)
if err != nil {
if jsonError, ok := err.(*json.SyntaxError); ok {
line, character, lcErr := lineAndCharacter(b, int(jsonError.Offset))
fmt.Fprintf(os.Stderr, "Cannot parse JSON schema due to a syntax error at %s line %d, character %d: %v\n", file, line, character, jsonError.Error())
if lcErr != nil {
fmt.Fprintf(os.Stderr, "Couldn't find the line and character position of the error due to error %v\n", lcErr)
}
return
}
if jsonError, ok := err.(*json.UnmarshalTypeError); ok {
line, character, lcErr := lineAndCharacter(b, int(jsonError.Offset))
fmt.Fprintf(os.Stderr, "The JSON type '%v' cannot be converted into the Go '%v' type on struct '%s', field '%v'. See input file %s line %d, character %d\n", jsonError.Value, jsonError.Type.Name(), jsonError.Struct, jsonError.Field, file, line, character)
if lcErr != nil {
fmt.Fprintf(os.Stderr, "Couldn't find the line and character position of the error due to error %v\n", lcErr)
}
return
}
fmt.Fprintf(os.Stderr, "Failed to parse the input JSON schema file %s with error %v\n", file, err)
return
}
schemas, err := generate.ReadInputFiles(inputFiles, *schemaKeyRequiredFlag)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}

g := generate.New(schemas...)

structs, aliases, err := g.CreateTypes()
err = g.CreateTypes()
if err != nil {
fmt.Fprintln(os.Stderr, "Failure generating structs: ", err)
os.Exit(1)
}

var w io.Writer = os.Stdout
Expand All @@ -92,97 +63,5 @@ func main() {
}
}

output(w, structs, aliases)
}

func lineAndCharacter(bytes []byte, offset int) (line int, character int, err error) {
lf := byte(0x0A)

if offset > len(bytes) {
return 0, 0, fmt.Errorf("couldn't find offset %d in %d bytes", offset, len(bytes))
}

// Humans tend to count from 1.
line = 1

for i, b := range bytes {
if b == lf {
line++
character = 0
}
character++
if i == offset {
return line, character, nil
}
}

return 0, 0, fmt.Errorf("couldn't find offset %d in %d bytes", offset, len(bytes))
}

func getOrderedFieldNames(m map[string]generate.Field) []string {
keys := make([]string, len(m))
idx := 0
for k := range m {
keys[idx] = k
idx++
}
sort.Strings(keys)
return keys
}

func getOrderedStructNames(m map[string]generate.Struct) []string {
keys := make([]string, len(m))
idx := 0
for k := range m {
keys[idx] = k
idx++
}
sort.Strings(keys)
return keys
}

func output(w io.Writer, structs map[string]generate.Struct, aliases map[string]generate.Field) {
fmt.Fprintln(w, "// Code generated by schema-generate. DO NOT EDIT.")
fmt.Fprintln(w)
fmt.Fprintf(w, "package %v\n", *p)

for _, k := range getOrderedFieldNames(aliases) {
a := aliases[k]

fmt.Fprintln(w, "")
fmt.Fprintf(w, "// %s\n", a.Name)
fmt.Fprintf(w, "type %s %s\n", a.Name, a.Type)
}

for _, k := range getOrderedStructNames(structs) {
s := structs[k]

fmt.Fprintln(w, "")
outputNameAndDescriptionComment(s.Name, s.Description, w)
fmt.Fprintf(w, "type %s struct {\n", s.Name)

for _, fieldKey := range getOrderedFieldNames(s.Fields) {
f := s.Fields[fieldKey]

// Only apply omitempty if the field is not required.
omitempty := ",omitempty"
if f.Required {
omitempty = ""
}

fmt.Fprintf(w, " %s %s `json:\"%s%s\"`\n", f.Name, f.Type, f.JSONName, omitempty)
}

fmt.Fprintln(w, "}")
}
}

func outputNameAndDescriptionComment(name, description string, w io.Writer) {
if strings.Index(description, "\n") == -1 {
fmt.Fprintf(w, "// %s %s\n", name, description)
return
}

dl := strings.Split(description, "\n")
fmt.Fprintf(w, "// %s %s\n", name, strings.Join(dl, "\n// "))
generate.Output(w, g, *p)
}
116 changes: 0 additions & 116 deletions cmd/schema-generate/main_test.go
Original file line number Diff line number Diff line change
@@ -1,117 +1 @@
package main

import (
"bytes"
"io"
"io/ioutil"
"reflect"
"strings"
"testing"

"github.com/a-h/generate"
)

func TestThatFieldNamesAreOrdered(t *testing.T) {
m := map[string]generate.Field{
"z": {},
"b": {},
}

actual := getOrderedFieldNames(m)
expected := []string{"b", "z"}

if !reflect.DeepEqual(actual, expected) {
t.Errorf("expected %s and actual %s should match in order", strings.Join(expected, ", "), strings.Join(actual, ","))
}
}

func TestThatStructNamesAreOrdered(t *testing.T) {
m := map[string]generate.Struct{
"c": {},
"b": {},
"a": {},
}

actual := getOrderedStructNames(m)
expected := []string{"a", "b", "c"}

if !reflect.DeepEqual(actual, expected) {
t.Errorf("expected %s and actual %s should match in order", strings.Join(expected, ", "), strings.Join(actual, ","))
}
}

func TestThatThePackageCanBeSet(t *testing.T) {
pkg := "testpackage"
p = &pkg

r, w := io.Pipe()

go output(w, make(map[string]generate.Struct), make(map[string]generate.Field))

lr := io.LimitedReader{R: r, N: 72}
bs, _ := ioutil.ReadAll(&lr)
output := bytes.NewBuffer(bs).String()

want := `// Code generated by schema-generate. DO NOT EDIT.
package testpackage
`
if output != want {
t.Error("Unexpected package declaration: ", output)
}
}

func TestLineAndCharacterFromOffset(t *testing.T) {
tests := []struct {
In []byte
Offset int
ExpectedLine int
ExpectedCharacter int
ExpectedError bool
}{
{
In: []byte("Line 1\nLine 2"),
Offset: 6,
ExpectedLine: 2,
ExpectedCharacter: 1,
},
{
In: []byte("Line 1\r\nLine 2"),
Offset: 7,
ExpectedLine: 2,
ExpectedCharacter: 1,
},
{
In: []byte("Line 1\nLine 2"),
Offset: 0,
ExpectedLine: 1,
ExpectedCharacter: 1,
},
{
In: []byte("Line 1\nLine 2"),
Offset: 200,
ExpectedLine: 0,
ExpectedCharacter: 0,
ExpectedError: true,
},
{
In: []byte("Line 1\nLine 2"),
Offset: -1,
ExpectedLine: 0,
ExpectedCharacter: 0,
ExpectedError: true,
},
}

for _, test := range tests {
actualLine, actualCharacter, err := lineAndCharacter(test.In, test.Offset)
if err != nil && !test.ExpectedError {
t.Errorf("Unexpected error for input %s at offset %d: %v", test.In, test.Offset, err)
continue
}

if actualLine != test.ExpectedLine || actualCharacter != test.ExpectedCharacter {
t.Errorf("For '%s' at offset %d, expected %d:%d, but got %d:%d", test.In, test.Offset, test.ExpectedLine, test.ExpectedCharacter, actualLine, actualCharacter)
}
}
}
Loading

0 comments on commit 8a14529

Please sign in to comment.