Add custom decoding for MuPack metadata
This adds basic custom decoding for the MuPack metadata section of the incoming JSON/YAML. Because of the type discriminated union nature of the incoming payload, we cannot rely on the simple built-in JSON/YAML unmarshaling behavior. Note that for the metadata section -- what is in this checkin -- we could have, but the IL AST nodes are problematic. (To know what kind of structure to creat requires inspecting the "kind" field of the IL.) We will use a reflection-driven walk of the target structure plus a weakly typed deserialized map[string]interface{}, as is fairly customary in Go for scenarios like this (though good libaries seem to be lacking in this area...).
This commit is contained in:
parent
cc16e85266
commit
d334ea322b
|
@ -11,7 +11,7 @@ import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/marapongo/mu/pkg/encoding"
|
"github.com/marapongo/mu/pkg/encoding"
|
||||||
"github.com/marapongo/mu/pkg/pack"
|
decode "github.com/marapongo/mu/pkg/pack/encoding"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newDescribeCmd() *cobra.Command {
|
func newDescribeCmd() *cobra.Command {
|
||||||
|
@ -25,11 +25,9 @@ func newDescribeCmd() *cobra.Command {
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
// Enumerate the list of packages, deserialize them, and print information.
|
// Enumerate the list of packages, deserialize them, and print information.
|
||||||
for _, arg := range args {
|
for _, arg := range args {
|
||||||
fmt.Printf("%v\n", arg)
|
|
||||||
|
|
||||||
// Lookup the marshaler for this format.
|
// Lookup the marshaler for this format.
|
||||||
ext := filepath.Ext(arg)
|
ext := filepath.Ext(arg)
|
||||||
marshaler, has := encoding.Marshalers[ext]
|
m, has := encoding.Marshalers[ext]
|
||||||
if !has {
|
if !has {
|
||||||
fmt.Fprintf(os.Stderr, "error: no marshaler found for file format '%v'\n", ext)
|
fmt.Fprintf(os.Stderr, "error: no marshaler found for file format '%v'\n", ext)
|
||||||
return
|
return
|
||||||
|
@ -44,14 +42,40 @@ func newDescribeCmd() *cobra.Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmarshal the contents into a fresh package.
|
// Unmarshal the contents into a fresh package.
|
||||||
var pkg pack.Package
|
pkg, err := decode.Decode(m, b)
|
||||||
if err := marshaler.Unmarshal(b, &pkg); err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "error: a problem occurred when unmarshaling file '%v'\n", arg)
|
fmt.Fprintf(os.Stderr, "error: a problem occurred when unmarshaling file '%v'\n", arg)
|
||||||
fmt.Fprintf(os.Stderr, " %v\n", err)
|
fmt.Fprintf(os.Stderr, " %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: pretty-print.
|
// Pretty-print the package metadata:
|
||||||
|
fmt.Printf("Package %v\n", pkg.Name)
|
||||||
|
fmt.Printf("\tpath = %v\n", arg)
|
||||||
|
if pkg.Description != "" {
|
||||||
|
fmt.Printf("\tdescription = %v\n", pkg.Description)
|
||||||
|
}
|
||||||
|
if pkg.Author != "" {
|
||||||
|
fmt.Printf("\tauthor = %v\n", pkg.Author)
|
||||||
|
}
|
||||||
|
if pkg.Website != "" {
|
||||||
|
fmt.Printf("\twebsite = %v\n", pkg.Website)
|
||||||
|
}
|
||||||
|
if pkg.License != "" {
|
||||||
|
fmt.Printf("\tlicense = %v\n", pkg.License)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the dependencies:
|
||||||
|
fmt.Printf("\tdependencies = [")
|
||||||
|
if pkg.Dependencies != nil && len(*pkg.Dependencies) > 0 {
|
||||||
|
fmt.Printf("\n")
|
||||||
|
for _, dep := range *pkg.Dependencies {
|
||||||
|
fmt.Printf("\t\t%v", dep)
|
||||||
|
}
|
||||||
|
fmt.Printf("\t")
|
||||||
|
}
|
||||||
|
fmt.Printf("]\n")
|
||||||
|
|
||||||
// TODO: respect printExports.
|
// TODO: respect printExports.
|
||||||
// TODO: respect printIL.
|
// TODO: respect printIL.
|
||||||
// TODO: respect printSymbols.
|
// TODO: respect printSymbols.
|
||||||
|
|
|
@ -22,7 +22,6 @@ type definition struct {
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (node *definition) nd() {}
|
|
||||||
func (node *definition) definition() {}
|
func (node *definition) definition() {}
|
||||||
func (node *definition) GetName() *Identifier { return node.Name }
|
func (node *definition) GetName() *Identifier { return node.Name }
|
||||||
func (node *definition) GetDescription() *string { return node.Description }
|
func (node *definition) GetDescription() *string { return node.Description }
|
||||||
|
@ -32,6 +31,7 @@ func (node *definition) GetDescription() *string { return node.Description }
|
||||||
// Module contains members, including variables, functions, and/or classes.
|
// Module contains members, including variables, functions, and/or classes.
|
||||||
type Module struct {
|
type Module struct {
|
||||||
definition
|
definition
|
||||||
|
Members *ModuleMembers `json:"members"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modules is a map of ModuleToken to Module.
|
// Modules is a map of ModuleToken to Module.
|
||||||
|
@ -72,7 +72,7 @@ type Class struct {
|
||||||
Abstract *bool `json:"abstract,omitempty"`
|
Abstract *bool `json:"abstract,omitempty"`
|
||||||
Record *bool `json:"record,omitempty"`
|
Record *bool `json:"record,omitempty"`
|
||||||
Interface *bool `json:"interface,omitempty"`
|
Interface *bool `json:"interface,omitempty"`
|
||||||
Members []ClassMember `json:"members,omitempty"`
|
Members *[]ClassMember `json:"members,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClassMember is a Definition that belongs to a Class.
|
// ClassMember is a Definition that belongs to a Class.
|
||||||
|
|
115
pkg/pack/encoding/decode.go
Normal file
115
pkg/pack/encoding/decode.go
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
// Copyright 2016 Marapongo, Inc. All rights reserved.
|
||||||
|
|
||||||
|
// Because of the complex structure of the MuPack and MuIL metadata formats, we cannot rely on the standard JSON
|
||||||
|
// marshaling and unmarshaling routines. Instead, we will need to do it mostly "by hand". This package does that.
|
||||||
|
package encoding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/marapongo/mu/pkg/encoding"
|
||||||
|
"github.com/marapongo/mu/pkg/pack"
|
||||||
|
//"github.com/marapongo/mu/pkg/pack/ast"
|
||||||
|
//"github.com/marapongo/mu/pkg/pack/symbols"
|
||||||
|
"github.com/marapongo/mu/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type object map[string]interface{}
|
||||||
|
type array []interface{}
|
||||||
|
|
||||||
|
// Decode unmarshals the entire contents of the given byte array into a Package object.
|
||||||
|
func Decode(m encoding.Marshaler, b []byte) (*pack.Package, error) {
|
||||||
|
// First convert the whole contents of the metadata into a map. Although it would be more efficient to walk the
|
||||||
|
// token stream, token by token, this allows us to reuse existing YAML packages in addition to JSON ones.
|
||||||
|
var tree object
|
||||||
|
if err := m.Unmarshal(b, &tree); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return decodePackage(tree)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMissing(ty string, field string) error {
|
||||||
|
return fmt.Errorf("Missing required %v field `%v`", ty, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWrongType(ty string, field string, expect string, actual string) error {
|
||||||
|
msg := fmt.Sprintf("%v `%v` must be a `%v`", ty, field, expect)
|
||||||
|
if actual != "" {
|
||||||
|
msg += fmt.Sprintf(", got `%v`", actual)
|
||||||
|
}
|
||||||
|
return errors.New(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeField decodes primitive fields. For fields of complex types, we use custom deserialization.
|
||||||
|
func decodeField(tree object, ty string, field string, target interface{}, req bool) error {
|
||||||
|
vdst := reflect.ValueOf(target)
|
||||||
|
util.AssertM(vdst.Kind() == reflect.Ptr && !vdst.IsNil() && vdst.Elem().CanSet(),
|
||||||
|
"Target must be a non-nil, settable pointer")
|
||||||
|
if v, has := tree[field]; has {
|
||||||
|
// The field exists; okay, try to map it to the right type.
|
||||||
|
vsrc := reflect.ValueOf(v)
|
||||||
|
vsrcType := vsrc.Type()
|
||||||
|
vdstType := vdst.Type().Elem()
|
||||||
|
|
||||||
|
// So long as the target element is a pointer, we have a pointer to pointer; keep digging through until we
|
||||||
|
// bottom out on the non-pointer type that matches the source. This assumes the source isn't itself a pointer!
|
||||||
|
util.Assert(vsrcType.Kind() != reflect.Ptr)
|
||||||
|
for vdstType.Kind() == reflect.Ptr {
|
||||||
|
vdst = vdst.Elem()
|
||||||
|
vdstType = vdstType.Elem()
|
||||||
|
if !vdst.Elem().CanSet() {
|
||||||
|
// If the pointer is nil, initialize it so we can set it below.
|
||||||
|
util.Assert(vdst.IsNil())
|
||||||
|
vdst.Set(reflect.New(vdstType))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the source and destination types don't match, after depointerizing the type above, bail right away.
|
||||||
|
if vsrcType != vdstType {
|
||||||
|
return newWrongType(ty, field, vdstType.Name(), vsrcType.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, go ahead and copy the value from source to the target.
|
||||||
|
vdst.Elem().Set(vsrc)
|
||||||
|
} else if req {
|
||||||
|
// The field doesn't exist and yet it is required; issue an error.
|
||||||
|
return newMissing(ty, field)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeMetadata(tree object) (*pack.Metadata, error) {
|
||||||
|
var meta pack.Metadata
|
||||||
|
if err := decodeField(tree, "Metadata", "name", &meta.Name, true); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := decodeField(tree, "Metadata", "description", &meta.Description, false); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := decodeField(tree, "Metadata", "author", &meta.Author, false); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := decodeField(tree, "Metadata", "website", &meta.Website, false); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := decodeField(tree, "Metadata", "license", &meta.License, false); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &meta, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodePackage(tree object) (*pack.Package, error) {
|
||||||
|
meta, err := decodeMetadata(tree)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pack := pack.Package{Metadata: *meta}
|
||||||
|
|
||||||
|
// TODO: dependencies.
|
||||||
|
// TODO: modules.
|
||||||
|
|
||||||
|
return &pack, nil
|
||||||
|
}
|
67
pkg/pack/encoding/decode_test.go
Normal file
67
pkg/pack/encoding/decode_test.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
// Copyright 2016 Marapongo, Inc. All rights reserved.
|
||||||
|
|
||||||
|
package encoding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type bag struct {
|
||||||
|
Bool bool
|
||||||
|
BoolP *bool
|
||||||
|
String string
|
||||||
|
StringP *string
|
||||||
|
Float64 float64
|
||||||
|
Float64P *float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeField(t *testing.T) {
|
||||||
|
tree := make(object)
|
||||||
|
tree["b"] = true
|
||||||
|
tree["s"] = "hello"
|
||||||
|
tree["f"] = float64(3.14159265359)
|
||||||
|
|
||||||
|
// Try some simple primitive decodes.
|
||||||
|
var s bag
|
||||||
|
var err error
|
||||||
|
err = decodeField(tree, "bag", "b", &s.Bool, true)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, tree["b"], s.Bool)
|
||||||
|
err = decodeField(tree, "bag", "b", &s.BoolP, true)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, tree["b"], *s.BoolP)
|
||||||
|
err = decodeField(tree, "bag", "s", &s.String, true)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, tree["s"], s.String)
|
||||||
|
err = decodeField(tree, "bag", "s", &s.StringP, true)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, tree["s"], *s.StringP)
|
||||||
|
err = decodeField(tree, "bag", "f", &s.Float64, true)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, tree["f"], s.Float64)
|
||||||
|
err = decodeField(tree, "bag", "f", &s.Float64P, true)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, tree["f"], *s.Float64P)
|
||||||
|
|
||||||
|
// Ensure missing optional fields are ignored:
|
||||||
|
s.String = "x"
|
||||||
|
err = decodeField(tree, "bag", "missing", &s.String, false)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "x", s.String)
|
||||||
|
|
||||||
|
// Try some error conditions; first, wrong type:
|
||||||
|
s.String = "x"
|
||||||
|
err = decodeField(tree, "bag", "b", &s.String, true)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Equal(t, "bag `b` must be a `string`, got `bool`", err.Error())
|
||||||
|
assert.Equal(t, "x", s.String)
|
||||||
|
|
||||||
|
// Next, missing required field:
|
||||||
|
s.String = "x"
|
||||||
|
err = decodeField(tree, "bag", "missing", &s.String, true)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Equal(t, "Missing required bag field `missing`", err.Error())
|
||||||
|
assert.Equal(t, "x", s.String)
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ import (
|
||||||
|
|
||||||
// Metadata is an informational section describing a package.
|
// Metadata is an informational section describing a package.
|
||||||
type Metadata struct {
|
type Metadata struct {
|
||||||
Name Name `json:"name"` // a required fully qualified name.
|
Name string `json:"name"` // a required fully qualified name.
|
||||||
Description string `json:"description,omitempty"` // an optional informational description.
|
Description string `json:"description,omitempty"` // an optional informational description.
|
||||||
Author string `json:"author,omitempty"` // an optional author.
|
Author string `json:"author,omitempty"` // an optional author.
|
||||||
Website string `json:"website,omitempty"` // an optional website for additional info.
|
Website string `json:"website,omitempty"` // an optional website for additional info.
|
||||||
|
@ -21,6 +21,6 @@ type Metadata struct {
|
||||||
type Package struct {
|
type Package struct {
|
||||||
Metadata
|
Metadata
|
||||||
|
|
||||||
Dependencies []symbols.ModuleToken `json:"dependencies,omitempty"` // all of the module dependencies.
|
Dependencies *[]symbols.ModuleToken `json:"dependencies,omitempty"` // all of the module dependencies.
|
||||||
Modules []ast.Modules `json:"modules,omitempty"` // a collection of top-level modules.
|
Modules *[]ast.Modules `json:"modules,omitempty"` // a collection of top-level modules.
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue