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:
joeduffy 2017-01-14 07:40:13 -08:00
parent cc16e85266
commit d334ea322b
5 changed files with 218 additions and 12 deletions

View file

@ -11,7 +11,7 @@ import (
"github.com/spf13/cobra"
"github.com/marapongo/mu/pkg/encoding"
"github.com/marapongo/mu/pkg/pack"
decode "github.com/marapongo/mu/pkg/pack/encoding"
)
func newDescribeCmd() *cobra.Command {
@ -25,11 +25,9 @@ func newDescribeCmd() *cobra.Command {
Run: func(cmd *cobra.Command, args []string) {
// Enumerate the list of packages, deserialize them, and print information.
for _, arg := range args {
fmt.Printf("%v\n", arg)
// Lookup the marshaler for this format.
ext := filepath.Ext(arg)
marshaler, has := encoding.Marshalers[ext]
m, has := encoding.Marshalers[ext]
if !has {
fmt.Fprintf(os.Stderr, "error: no marshaler found for file format '%v'\n", ext)
return
@ -44,14 +42,40 @@ func newDescribeCmd() *cobra.Command {
}
// Unmarshal the contents into a fresh package.
var pkg pack.Package
if err := marshaler.Unmarshal(b, &pkg); err != nil {
pkg, err := decode.Decode(m, b)
if err != nil {
fmt.Fprintf(os.Stderr, "error: a problem occurred when unmarshaling file '%v'\n", arg)
fmt.Fprintf(os.Stderr, " %v\n", err)
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 printIL.
// TODO: respect printSymbols.

View file

@ -22,7 +22,6 @@ type definition struct {
Description *string `json:"description,omitempty"`
}
func (node *definition) nd() {}
func (node *definition) definition() {}
func (node *definition) GetName() *Identifier { return node.Name }
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.
type Module struct {
definition
Members *ModuleMembers `json:"members"`
}
// Modules is a map of ModuleToken to Module.
@ -72,7 +72,7 @@ type Class struct {
Abstract *bool `json:"abstract,omitempty"`
Record *bool `json:"record,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.

115
pkg/pack/encoding/decode.go Normal file
View 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
}

View 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)
}

View file

@ -10,7 +10,7 @@ import (
// Metadata is an informational section describing a package.
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.
Author string `json:"author,omitempty"` // an optional author.
Website string `json:"website,omitempty"` // an optional website for additional info.
@ -21,6 +21,6 @@ type Metadata struct {
type Package struct {
Metadata
Dependencies []symbols.ModuleToken `json:"dependencies,omitempty"` // all of the module dependencies.
Modules []ast.Modules `json:"modules,omitempty"` // a collection of top-level modules.
Dependencies *[]symbols.ModuleToken `json:"dependencies,omitempty"` // all of the module dependencies.
Modules *[]ast.Modules `json:"modules,omitempty"` // a collection of top-level modules.
}