diff --git a/cmd/describe.go b/cmd/describe.go index b7be8514e..8eab87169 100644 --- a/cmd/describe.go +++ b/cmd/describe.go @@ -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. diff --git a/pkg/pack/ast/definitions.go b/pkg/pack/ast/definitions.go index 38abbb3d0..41916ba96 100644 --- a/pkg/pack/ast/definitions.go +++ b/pkg/pack/ast/definitions.go @@ -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. diff --git a/pkg/pack/encoding/decode.go b/pkg/pack/encoding/decode.go new file mode 100644 index 000000000..5cba0748c --- /dev/null +++ b/pkg/pack/encoding/decode.go @@ -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 +} diff --git a/pkg/pack/encoding/decode_test.go b/pkg/pack/encoding/decode_test.go new file mode 100644 index 000000000..53b1dac94 --- /dev/null +++ b/pkg/pack/encoding/decode_test.go @@ -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) +} diff --git a/pkg/pack/package.go b/pkg/pack/package.go index 4d3eb4e63..8cfe939ce 100644 --- a/pkg/pack/package.go +++ b/pkg/pack/package.go @@ -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. }