-
Notifications
You must be signed in to change notification settings - Fork 18
Auto-generate source code from json schema defintions and go generate instructions #21
Changes from all commits
f98e313
410759c
b408bdc
dffe1f5
fbf985b
b41e86c
fc4449e
661c748
40d857d
305a730
9ee089f
3f266e6
0cb308a
94ddcf5
a6e6c25
118d279
7bef7ba
c98e18c
fba70c0
90b9ca1
bcb8428
efb1540
ea0db53
1bd4ce6
7a8439f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
$schema: http://json-schema.org/draft-04/schema# | ||
title: Worker Config | ||
description: |- | ||
Schema to represent the global config object used for configuring taskcluster-worker | ||
type: object | ||
properties: | ||
workerId: | ||
title: WorkerID | ||
description: |- | ||
blah blah | ||
type: string | ||
additionalProperties: false | ||
required: | ||
- engines | ||
- plugins | ||
- workerId | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note, I wanted to create a yml file for the global config (everything except plugins and engines) but haven't yet written anything to use it. Probably we could use |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
// go-composite-schema is responsible for auto-generating plugin and engine code based | ||
// on static yml json schema files | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"go/build" | ||
"io/ioutil" | ||
"log" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
|
||
"github.com/docopt/docopt-go" | ||
"github.com/ghodss/yaml" | ||
"github.com/kr/text" | ||
"github.com/taskcluster/jsonschema2go" | ||
"github.com/taskcluster/taskcluster-base-go/jsontest" | ||
"golang.org/x/tools/imports" | ||
) | ||
|
||
var ( | ||
version = "go-composite-schema 1.0.0" | ||
usage = ` | ||
go-composite-schema | ||
go-composite-schema is a tool for generating go source code for a function to | ||
return a CompositeSchema based on a static json schema definition stored in a | ||
yaml/json file in a package directory of a go project, together with some | ||
parameters included in a "go:generate" command. See | ||
https://godoc.org/github.com/taskcluster/taskcluster-worker/runtime#CompositeSchema | ||
for more information. | ||
|
||
Although the go-composite-schema command line tool can be used wherever you | ||
require a CompositeSchema, it is currently most appliable to taskcluster-worker | ||
engines and plugins. | ||
|
||
Typically, you should include a code generation comment in your source code for | ||
each CompositeSchema returning function you wish to create: | ||
|
||
//go:generate go-composite-schema [--required] PROPERTY INPUT-FILE OUTPUT-FILE | ||
|
||
We currently use CompositeSchemas for defining both config structures and | ||
payload structures, for both engines and plugins. Therefore typically there | ||
could be one or two "go:generate" lines required per plugin and probably two | ||
per engine. This is because some plugins might not have custom config nor even | ||
custom payload, but engines are likely to require both. | ||
|
||
Please note, it is recommended to set environment variable GOPATH in order for | ||
go-composite-schema to correctly determine the correct package name. | ||
|
||
|
||
Usage: | ||
go-composite-schema [--required] PROPERTY INPUT-FILE OUTPUT-FILE | ||
go-composite-schema -h|--help | ||
go-composite-schema --version | ||
|
||
Options: | ||
-h --help Display this help text. | ||
--version Display the version (` + version + `). | ||
` | ||
) | ||
|
||
func main() { | ||
// Clear all logging fields, such as timestamps etc... | ||
log.SetFlags(0) | ||
log.SetPrefix("go-composite-schema: ") | ||
|
||
// Parse the docopt string and exit on any error or help message. | ||
args, err := docopt.Parse(usage, nil, true, version, false, true) | ||
if err != nil { | ||
log.Fatalf("ERROR: Cannot parse arguments: %s\n", err) | ||
} | ||
|
||
// assuming non-nil, and always type bool | ||
req := args["--required"].(bool) | ||
|
||
// assuming non-nil, and always type string | ||
schemaProperty := args["PROPERTY"].(string) | ||
inputFile := args["INPUT-FILE"].(string) | ||
outputFile := args["OUTPUT-FILE"].(string) | ||
|
||
// Get working directory | ||
currentFolder, err := os.Getwd() | ||
if err != nil { | ||
log.Fatalf("Unable to obtain current working directory: %s", err) | ||
} | ||
|
||
// Read current package | ||
pkg, err := build.ImportDir(currentFolder, build.AllowBinary) | ||
if err != nil { | ||
log.Fatalf("ERROR: Failed to determine go package inside directory '%s' - is your GOPATH set correctly ('%s')? Error: %s", currentFolder, os.Getenv("GOPATH"), err) | ||
} | ||
|
||
// Generate go types... | ||
ymlFile := filepath.Join(currentFolder, inputFile) | ||
if _, err := os.Stat(ymlFile); err == nil { | ||
log.Printf("Found yaml file '%v'", ymlFile) | ||
} else { | ||
log.Fatalf("ERROR: could not read file '%v'", ymlFile) | ||
} | ||
url := "file://" + ymlFile | ||
goFile := filepath.Join(currentFolder, outputFile) | ||
log.Printf("Generating '%v'...", goFile) | ||
job := &jsonschema2go.Job{ | ||
Package: pkg.Name, | ||
URLs: []string{url}, | ||
ExportTypes: false, | ||
} | ||
result, err := job.Execute() | ||
if err != nil { | ||
log.Fatalf("ERROR: Problem assembling content for file '%v': %s", goFile, err) | ||
} | ||
generatedCode := append(result.SourceCode, []byte("\n"+generateFunctions(ymlFile, result.SchemaSet.SubSchema(url).TypeName, schemaProperty, req))...) | ||
sourceCode, err := imports.Process( | ||
goFile, | ||
[]byte(generatedCode), | ||
&imports.Options{ | ||
AllErrors: true, | ||
Comments: true, | ||
TabIndent: true, | ||
TabWidth: 0, | ||
Fragment: false, | ||
}, | ||
) | ||
if err != nil { | ||
log.Fatalf("ERROR: Could not format generated source code for file '%v': %s\nCode:\n%v", goFile, err, string(generatedCode)) | ||
} | ||
ioutil.WriteFile(goFile, []byte(sourceCode), 0644) | ||
if err != nil { | ||
log.Fatalf("ERROR: Could not write generated source code to file '%v': %s", goFile, err) | ||
} | ||
} | ||
|
||
func generateFunctions(ymlFile, goType, schemaProperty string, req bool) string { | ||
data, err := ioutil.ReadFile(ymlFile) | ||
if err != nil { | ||
log.Fatalf("ERROR: Problem reading from file '%v' - %s", ymlFile, err) | ||
} | ||
// json is valid YAML, so we can safely convert, even if it is already json | ||
rawJson, err := yaml.YAMLToJSON(data) | ||
if err != nil { | ||
log.Fatalf("ERROR: Problem converting file '%v' to json format - %s", ymlFile, err) | ||
} | ||
rawJson, err = jsontest.FormatJson(rawJson) | ||
if err != nil { | ||
log.Fatalf("ERROR: Problem pretty printing json in '%v' - %s", ymlFile, err) | ||
} | ||
result := "var " + goType + "Schema = func() runtime.CompositeSchema {\n" | ||
result += "\tschema, err := runtime.NewCompositeSchema(\n" | ||
result += "\t\t\"" + schemaProperty + "\",\n" | ||
result += "\t\t`\n" | ||
// the following strings.Replace function call safely escapes backticks (`) in rawJson | ||
result += strings.Replace(text.Indent(fmt.Sprintf("%v", string(rawJson)), "\t\t")+"\n", "`", "` + \"`\" + `", -1) | ||
result += "\t\t`,\n" | ||
if req { | ||
result += "\t\ttrue,\n" | ||
} | ||
result += "\t\tfunc() interface{} {\n" | ||
result += "\t\t\treturn &" + goType + "{}\n" | ||
result += "\t\t},\n" | ||
result += "\t)\n" | ||
result += "\tif err != nil {\n" | ||
result += "\t\tpanic(err)\n" | ||
result += "\t}\n" | ||
result += "\treturn schema\n" | ||
result += "}()\n" | ||
return result | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
$schema: http://json-schema.org/draft-04/schema# | ||
title: Config | ||
description: |- | ||
Config applicable to docker engine | ||
type: object | ||
properties: | ||
rootVolume: | ||
title: Root Volume | ||
description: |- | ||
Root Volume blah blah | ||
type: string | ||
additionalProperties: false | ||
required: | ||
- rootVolume |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
//go:generate go-composite-schema --required config config-schema.yml generated_configschema.go | ||
|
||
// comments | ||
package docker |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
// This source code file is AUTO-GENERATED by github.com/taskcluster/jsonschema2go | ||
|
||
package docker | ||
|
||
import "github.com/taskcluster/taskcluster-worker/runtime" | ||
|
||
type ( | ||
// Config applicable to docker engine | ||
config struct { | ||
|
||
// Root Volume blah blah | ||
RootVolume string `json:"rootVolume"` | ||
} | ||
) | ||
|
||
var configSchema = func() runtime.CompositeSchema { | ||
schema, err := runtime.NewCompositeSchema( | ||
"config", | ||
` | ||
{ | ||
"$schema": "http://json-schema.org/draft-04/schema#", | ||
"additionalProperties": false, | ||
"description": "Config applicable to docker engine", | ||
"properties": { | ||
"rootVolume": { | ||
"description": "Root Volume blah blah", | ||
"title": "Root Volume", | ||
"type": "string" | ||
} | ||
}, | ||
"required": [ | ||
"rootVolume" | ||
], | ||
"title": "Config", | ||
"type": "object" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure what go does here... But a stable JSON result wouldn't very useful... Maybe avoid whitespace.. CompositeSchema does a simple string comparison if two entries specifies the same schema... Maybe one day we can improve that... but stable json string wouldn't hurt either... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No worries - I already have written something for that: https://github.com/taskcluster/taskcluster-base-go/blob/fa319fce2375b4a38488af1521317f4a1b15d3fa/jsontest/jsontest.go#L5-L18 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. awesome... we should copy that into the compositeSchema stuff... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jonasfj Thinking about this some more, we don't really have a place to put general purpose go packages. Should we keep taskcluster-base-go for general purpose packages? For example I'm using jsontest in several projects (generic-worker, taskcluster-client-go, taskcluster-worker, ...). I could stick it in the client, but it is not client-specific. We could rename taskcluster-base-go to just 'go' or 'go-libs' - then we'd have e.g.:
I think it would be overkill to place each package in its own repository, so a single repository for general utility packages seems like a reasonable compromise. If we did want each top-level package in its own repo, we should probably create a github org called taskcluster-go and then have e.g.:
But that feels like overkill. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
But honestly, I don't see the need for us to have a collection of go libraries...
I think (1) and (2) will cover all the small stuff. (3) makes sense for things that are reusable outside the taskcluster project... |
||
} | ||
`, | ||
true, | ||
func() interface{} { | ||
return &config{} | ||
}, | ||
) | ||
if err != nil { | ||
panic(err) | ||
} | ||
return schema | ||
}() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
// This source code file is AUTO-GENERATED by github.com/taskcluster/jsonschema2go | ||
|
||
package mockengine | ||
|
||
import "github.com/taskcluster/taskcluster-worker/runtime" | ||
|
||
type ( | ||
payload struct { | ||
Argument string `json:"argument"` | ||
|
||
Delay int `json:"delay"` | ||
|
||
// Possible values: | ||
// * "true" | ||
// * "false" | ||
// * "set-volume" | ||
// * "get-volume" | ||
// * "ping-proxy" | ||
// * "write-log" | ||
// * "write-error-log" | ||
Function string `json:"function"` | ||
} | ||
) | ||
|
||
var payloadSchema = func() runtime.CompositeSchema { | ||
schema, err := runtime.NewCompositeSchema( | ||
"start", | ||
` | ||
{ | ||
"$schema": "http://json-schema.org/draft-04/schema#", | ||
"additionalProperties": false, | ||
"properties": { | ||
"argument": { | ||
"type": "string" | ||
}, | ||
"delay": { | ||
"type": "integer" | ||
}, | ||
"function": { | ||
"enum": [ | ||
"true", | ||
"false", | ||
"set-volume", | ||
"get-volume", | ||
"ping-proxy", | ||
"write-log", | ||
"write-error-log" | ||
], | ||
"type": "string" | ||
} | ||
}, | ||
"required": [ | ||
"delay", | ||
"function", | ||
"argument" | ||
], | ||
"title": "Payload", | ||
"type": "object" | ||
} | ||
`, | ||
true, | ||
func() interface{} { | ||
return &payload{} | ||
}, | ||
) | ||
if err != nil { | ||
panic(err) | ||
} | ||
return schema | ||
}() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jonasfj can we move
go-import-subtree
into taskcluster org?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure... but we'll need to update references too... can we do it separately...
Note: added you can garndt as collaborators for now if you have any hacks for it...
I'm very much trying to keep
go-import-subtree
super generic and not taskcluster specific, which I why I wasn't exactly sure where to put it...