Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

handle json null for QMP API generation #152

Merged
merged 7 commits into from
Oct 10, 2017
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
11 changes: 7 additions & 4 deletions internal/qmp-gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ func Generate() error {

// Write out the concatenated spec.
var spec bytes.Buffer

// First add a comment with the best guess of version
spec.WriteString("\n##QEMU SPECIFICATION VERSION: ")
spec.WriteString(tryGetVersionFromSpecPath(*inputSpec))
spec.WriteString("\n\n")

for _, def := range defs {
spec.WriteString(def.Docstring)
spec.WriteByte('\n')
Expand All @@ -68,9 +74,6 @@ func Generate() error {
ioutil.WriteFile(*outputGo, bs, 0640)
return err
}
if err = ioutil.WriteFile(*outputGo, formatted, 0640); err != nil {
return err
}

return nil
return ioutil.WriteFile(*outputGo, formatted, 0640)
}
60 changes: 43 additions & 17 deletions internal/qmp-gen/templates/alternate
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,15 @@ type {{ $basename }} interface {
{{- range $suffix, $type := .Options }}
{{ $subname := printf "%s%s" $basename $suffix.Go }}

// {{ $subname }} is an implementation of {{ $basename }}
type {{ $subname }} {{ $type.Go }}
{{- if $type.NullType }}
// {{ $subname }} is a JSON null type, so it must
// also implement the isNullable interface.
type {{ $subname }} struct {}
func ({{ $subname }}) isNull() bool { return true }
{{- else }}
// {{ $subname }} is an implementation of {{ $basename }}
type {{ $subname }} {{ $type.Go }}
{{- end }}

{{- if eq (typeOf (index API $type)) "flatunion" }}
{{ $u := index API $type }}
Expand All @@ -38,6 +45,23 @@ type {{ $basename }} interface {
{{- end }}

func decode{{ $basename }}(bs json.RawMessage) ({{ $basename }}, error) {
{{/*
Unfortunately, we have to range through the types three times in order
for us to do the type discovery via unmarshalling in the correct order
*/}}
{{- range $suffix, $type := .Options }}
{{- if $type.NullType }}
// Always try unmarshalling for nil first if it's an option
// because other types could unmarshal successfully in the case
// where a Null json type was provided.
var {{ $suffix }} *int
if err := json.Unmarshal([]byte(bs), &{{ $suffix }}); err == nil {
if {{ $suffix }} == nil {
return {{ printf "%s%s" $basename $suffix.Go }}{}, nil
}
}
{{- end }}
{{- end }}
{{- range $suffix, $type := .Options }}
{{- if $type.SimpleType }}
var {{ $suffix }} {{ printf "%s%s" $basename $suffix.Go }}
Expand All @@ -47,22 +71,24 @@ func decode{{ $basename }}(bs json.RawMessage) ({{ $basename }}, error) {
{{- end }}
{{- end }}
{{- range $suffix, $type := .Options }}
{{- if not $type.SimpleType }}
{{ $subtype := index API $type }}
{{- if eq (typeOf $subtype) "flatunion" }}
if {{ $suffix }}, err := decode{{ $type.Go }}([]byte(bs)); err == nil {
switch impl := {{ $suffix }}.(type) {
{{- range $suffix, $type := $subtype.Options }}
case {{ $subtype.Name.Go }}{{ $suffix.Go }}:
return impl, nil
{{- end }}
{{- if not $type.NullType }}
{{- if not $type.SimpleType }}
{{ $subtype := index API $type }}
{{- if eq (typeOf $subtype) "flatunion" }}
if {{ $suffix }}, err := decode{{ $type.Go }}([]byte(bs)); err == nil {
switch impl := {{ $suffix }}.(type) {
{{- range $suffix, $type := $subtype.Options }}
case {{ $subtype.Name.Go }}{{ $suffix.Go }}:
return impl, nil
{{- end }}
}
}
}
{{- else }}
var {{ $suffix }} {{ printf "%s%s" $basename $suffix.Go }}
if err := json.Unmarshal([]byte(bs), &{{ $suffix }}); err == nil {
return {{ $suffix }}, nil
}
{{- else }}
var {{ $suffix }} {{ printf "%s%s" $basename $suffix.Go }}
if err := json.Unmarshal([]byte(bs), &{{ $suffix }}); err == nil {
return {{ $suffix }}, nil
}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
Expand Down
6 changes: 6 additions & 0 deletions internal/qmp-gen/templates/main
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ import (
"fmt"
)

// IsNullable is implemented by any
// JSON null type
type IsNullable interface {
isNull() bool
}

{{ range . }}
{{ render . }}
{{ end }}
95 changes: 84 additions & 11 deletions internal/qmp-gen/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,40 @@ type definition struct {
JSON []byte
}

// definitions that are safe to ignore when a docstring is missing.
func ignoreWithoutDocstring(buf []byte) bool {
switch {
case bytes.HasPrefix(buf, []byte("{ 'pragma'")):
return true
case bytes.HasPrefix(buf, []byte("{ 'include'")):
return true
}

return false
}

func importDefinitions(path string, jsonBuf []byte) ([]definition, error) {
v := struct {
I string `json:"include"`
}{}

if err := json.Unmarshal(jsonBuf, &v); err != nil {
return nil, fmt.Errorf("failed to unmarshal include %q json: %s", path, err)
}

incPath, err := resolvePath(path, v.I)
if err != nil {
return nil, fmt.Errorf("failed to resolve include %q relative to %q: %s", v.I, path, err)
}

subdefs, err := readDefinitions(incPath)
if err != nil {
return nil, fmt.Errorf("failed to parse included file %q: %s", incPath, err)
}

return subdefs, nil
}

// readDefinitions reads the definitions from a QAPI spec file.
//
// Includes are processed, so the returned definitions is the full API
Expand All @@ -51,32 +85,49 @@ func readDefinitions(path string) ([]definition, error) {
// input so that we can process it with less gymnastics.
bs = bytes.Replace(bs, []byte("}\n##"), []byte("}\n\n##"), -1)

// As mentioned directly above, most of the spec includes two newlines
// between definitions. This normalizes stacked include statements.
bs = bytes.Replace(bs, []byte("}\n{ 'include'"), []byte("}\n\n{ 'include'"), -1)

for _, part := range bytes.Split(bs, []byte("\n\n")) {
if len(part) < 1 {
continue
}

fs := bytes.SplitN(part, []byte("\n{"), 2)
switch len(fs) {
default:
return nil, fmt.Errorf("unexpected part of spec file %q: %s", path, string(part))
case 1:
if len(fs) == 1 && part[0] == '{' && !bytes.HasPrefix(part, []byte("{ 'pragma'")) {
if len(fs) == 1 && part[0] == '{' && !ignoreWithoutDocstring(part) {
return nil, fmt.Errorf("found type definition without a docstring in %q: %s", path, string(part))
}

// handle 'include' when no docstring is present
if bytes.HasPrefix(fs[0], []byte("{ 'include'")) {
js := pyToJSON(fs[0])

subdefs, err := importDefinitions(path, js)
if err != nil {
return nil, err

}

ret = append(ret, subdefs...)
}

// This part looks like a non-docstring comment, just skip it.
case 2:
docstring := string(fs[0])
js := pyToJSON(append([]byte{'{'}, fs[1]...))

v := struct {
I string `json:"include"`
}{}
if err = json.Unmarshal(js, &v); err == nil && v.I != "" {
incPath, err := resolvePath(path, v.I)
if err != nil {
return nil, fmt.Errorf("failed to resolve include %q relative to %q: %s", v.I, path, err)
}
subdefs, err := readDefinitions(incPath)
if bytes.HasPrefix(fs[0], []byte("{ 'include'")) {
subdefs, err := importDefinitions(path, js)
if err != nil {
return nil, fmt.Errorf("failed to parse included file %q: %s", incPath, err)
return nil, err

}

ret = append(ret, subdefs...)
} else {
ret = append(ret, definition{docstring, js})
Expand Down Expand Up @@ -424,6 +475,13 @@ func (n name) SimpleType() bool {
return ok
}

func (n name) NullType() bool {
if n.SimpleType() {
return false
}
return strings.EqualFold(string(n), "Null")
}

func (n name) InterfaceType(api map[name]interface{}) bool {
if n.SimpleType() {
return false
Expand Down Expand Up @@ -460,6 +518,7 @@ var upperWords = map[string]bool{
"fd": true,
"ftp": true,
"ftps": true,
"guid": true,
"http": true,
"https": true,
"id": true,
Expand All @@ -476,11 +535,13 @@ var upperWords = map[string]bool{
"qmp": true,
"ram": true,
"sparc": true,
"ssh": true,
"tcp": true,
"tls": true,
"tpm": true,
"ttl": true,
"udp": true,
"uri": true,
"uuid": true,
"vm": true,
"vmdk": true,
Expand Down Expand Up @@ -527,6 +588,18 @@ func pyToJSON(py []byte) []byte {
return ret
}

func tryGetVersionFromSpecPath(specPath string) string {
retVersion := "UNKNOWN"
verPath, err := resolvePath(specPath, "VERSION")
if err == nil {
verBuf, err := getQAPI(verPath)
if err == nil {
return string(verBuf)
}
}
return retVersion
}

func resolvePath(orig, new string) (string, error) {
u, err := url.Parse(orig)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion qemu/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func (d *Domain) Commands() ([]string, error) {
if err != nil {
return nil, err
}

// flatten response
cmds := make([]string, 0, len(commands))
for _, c := range commands {
Expand Down
Loading