diff --git a/lock.go b/lock.go index 17af55193f..46dc2bff69 100644 --- a/lock.go +++ b/lock.go @@ -5,9 +5,7 @@ package dep import ( - "bytes" "encoding/hex" - "fmt" "io" "sort" @@ -24,48 +22,62 @@ type Lock struct { } type rawLock struct { - Memo string - P []lockedDep + Memo string + Projects []rawLockedProject } -type lockedDep struct { +type rawLockedProject struct { Name string - Version string Branch string Revision string + Version string Source string Packages []string } func readLock(r io.Reader) (*Lock, error) { - rl := rawLock{} - err := json.NewDecoder(r).Decode(&rl) + tree, err := toml.LoadReader(r) if err != nil { - return nil, err + return nil, errors.Wrap(err, "Unable to parse the lock as TOML") } - b, err := hex.DecodeString(rl.Memo) - if err != nil { - return nil, fmt.Errorf("invalid hash digest in lock's memo field") + mapper := &tomlMapper{Tree: tree} + + raw := rawLock{ + Memo: readKeyAsString(mapper, "memo"), + Projects: readTableAsLockedProjects(mapper, "projects"), + } + + if mapper.Error != nil { + return nil, errors.Wrap(mapper.Error, "Invalid lock structure") } + return fromRawLock(raw) +} + +func fromRawLock(raw rawLock) (*Lock, error) { + var err error l := &Lock{ - Memo: b, - P: make([]gps.LockedProject, len(rl.P)), + P: make([]gps.LockedProject, len(raw.Projects)), + } + + l.Memo, err = hex.DecodeString(raw.Memo) + if err != nil { + return nil, errors.Errorf("invalid hash digest in lock's memo field") } - for i, ld := range rl.P { + for i, ld := range raw.Projects { r := gps.Revision(ld.Revision) var v gps.Version = r if ld.Version != "" { if ld.Branch != "" { - return nil, fmt.Errorf("lock file specified both a branch (%s) and version (%s) for %s", ld.Branch, ld.Version, ld.Name) + return nil, errors.Errorf("lock file specified both a branch (%s) and version (%s) for %s", ld.Branch, ld.Version, ld.Name) } v = gps.NewVersion(ld.Version).Is(r) } else if ld.Branch != "" { v = gps.NewBranch(ld.Branch).Is(r) } else if r == "" { - return nil, fmt.Errorf("lock file has entry for %s, but specifies no branch or version", ld.Name) + return nil, errors.Errorf("lock file has entry for %s, but specifies no branch or version", ld.Name) } id := gps.ProjectIdentifier{ @@ -74,7 +86,6 @@ func readLock(r io.Reader) (*Lock, error) { } l.P[i] = gps.NewLockedProject(id, v, ld.Packages) } - return l, nil } @@ -89,15 +100,15 @@ func (l *Lock) Projects() []gps.LockedProject { // toRaw converts the manifest into a representation suitable to write to the lock file func (l *Lock) toRaw() rawLock { raw := rawLock{ - Memo: hex.EncodeToString(l.Memo), - P: make([]lockedDep, len(l.P)), + Memo: hex.EncodeToString(l.Memo), + Projects: make([]rawLockedProject, len(l.P)), } sort.Sort(SortedLockedProjects(l.P)) for k, lp := range l.P { id := lp.Ident() - ld := lockedDep{ + ld := rawLockedProject{ Name: string(id.ProjectRoot), Source: id.Source, Packages: lp.Packages(), @@ -106,7 +117,7 @@ func (l *Lock) toRaw() rawLock { v := lp.Version() ld.Revision, ld.Branch, ld.Version = getVersionInfo(v) - raw.P[k] = ld + raw.Projects[k] = ld } // TODO sort output - #15 @@ -117,12 +128,11 @@ func (l *Lock) toRaw() rawLock { func (l *Lock) MarshalTOML() (string, error) { raw := l.toRaw() - // TODO(carolynvs) Consider adding reflection-based marshal functionality to go-toml m := make(map[string]interface{}) m["memo"] = raw.Memo - p := make([]map[string]interface{}, len(raw.P)) + p := make([]map[string]interface{}, len(raw.Projects)) for i := 0; i < len(p); i++ { - srcPrj := raw.P[i] + srcPrj := raw.Projects[i] prj := make(map[string]interface{}) prj["name"] = srcPrj.Name prj["revision"] = srcPrj.Revision diff --git a/manifest.go b/manifest.go index 5bf4b7ec36..054976242c 100644 --- a/manifest.go +++ b/manifest.go @@ -5,9 +5,8 @@ package dep import ( - "encoding/json" - "fmt" "io" + "sort" "github.com/pelletier/go-toml" "github.com/pkg/errors" @@ -24,22 +23,14 @@ type Manifest struct { } type rawManifest struct { - Dependencies map[string]possibleProps - Overrides map[string]possibleProps + Dependencies []rawProject + Overrides []rawProject Ignores []string Required []string } -func newRawManifest() rawManifest { - return rawManifest{ - Dependencies: make(map[string]possibleProps), - Overrides: make(map[string]possibleProps), - Ignores: make([]string, 0), - Required: make([]string, 0), - } -} - -type possibleProps struct { +type rawProject struct { + Name string Branch string Revision string Version string @@ -47,108 +38,150 @@ type possibleProps struct { } func readManifest(r io.Reader) (*Manifest, error) { - rm := rawManifest{} - err := json.NewDecoder(r).Decode(&rm) + tree, err := toml.LoadReader(r) if err != nil { - return nil, err + return nil, errors.Wrap(err, "Unable to parse the manifest as TOML") } + + mapper := &tomlMapper{Tree: tree} + raw := rawManifest{ + Dependencies: readTableAsProjects(mapper, "dependencies"), + Overrides: readTableAsProjects(mapper, "overrides"), + Required: readKeyAsStringList(mapper, "required"), + Ignores: readKeyAsStringList(mapper, "ignores"), + } + + if mapper.Error != nil { + return nil, errors.Wrap(mapper.Error, "Invalid manifest structure") + } + return fromRawManifest(raw) +} + +func fromRawManifest(raw rawManifest) (*Manifest, error) { m := &Manifest{ - Dependencies: make(gps.ProjectConstraints, len(rm.Dependencies)), - Ovr: make(gps.ProjectConstraints, len(rm.Overrides)), - Ignores: rm.Ignores, - Required: rm.Required, + Dependencies: make(gps.ProjectConstraints, len(raw.Dependencies)), + Ovr: make(gps.ProjectConstraints, len(raw.Overrides)), + Ignores: raw.Ignores, + Required: raw.Required, } - for n, pp := range rm.Dependencies { - m.Dependencies[gps.ProjectRoot(n)], err = toProps(n, pp) + for i := 0; i < len(raw.Dependencies); i++ { + name, prj, err := toProject(raw.Dependencies[i]) if err != nil { return nil, err } + m.Dependencies[name] = prj } - for n, pp := range rm.Overrides { - m.Ovr[gps.ProjectRoot(n)], err = toProps(n, pp) + for i := 0; i < len(raw.Overrides); i++ { + name, prj, err := toProject(raw.Overrides[i]) if err != nil { return nil, err } + m.Ovr[name] = prj } return m, nil } -// toProps interprets the string representations of project information held in -// a possibleProps, converting them into a proper gps.ProjectProperties. An -// error is returned if the possibleProps contains some invalid combination - +// toProject interprets the string representations of project information held in +// a rawProject, converting them into a proper gps.ProjectProperties. An +// error is returned if the rawProject contains some invalid combination - // for example, if both a branch and version constraint are specified. -func toProps(n string, p possibleProps) (pp gps.ProjectProperties, err error) { - if p.Branch != "" { - if p.Version != "" || p.Revision != "" { - return pp, fmt.Errorf("multiple constraints specified for %s, can only specify one", n) +func toProject(raw rawProject) (n gps.ProjectRoot, pp gps.ProjectProperties, err error) { + n = gps.ProjectRoot(raw.Name) + if raw.Branch != "" { + if raw.Version != "" || raw.Revision != "" { + return n, pp, errors.Errorf("multiple constraints specified for %s, can only specify one", n) } - pp.Constraint = gps.NewBranch(p.Branch) - } else if p.Version != "" { - if p.Revision != "" { - return pp, fmt.Errorf("multiple constraints specified for %s, can only specify one", n) + pp.Constraint = gps.NewBranch(raw.Branch) + } else if raw.Version != "" { + if raw.Revision != "" { + return n, pp, errors.Errorf("multiple constraints specified for %s, can only specify one", n) } // always semver if we can - pp.Constraint, err = gps.NewSemverConstraint(p.Version) + pp.Constraint, err = gps.NewSemverConstraint(raw.Version) if err != nil { // but if not, fall back on plain versions - pp.Constraint = gps.NewVersion(p.Version) + pp.Constraint = gps.NewVersion(raw.Version) } - } else if p.Revision != "" { - pp.Constraint = gps.Revision(p.Revision) + } else if raw.Revision != "" { + pp.Constraint = gps.Revision(raw.Revision) } else { // If the user specifies nothing, it means an open constraint (accept // anything). pp.Constraint = gps.Any() } - pp.Source = p.Source - return pp, nil + pp.Source = raw.Source + return n, pp, nil } // toRaw converts the manifest into a representation suitable to write to the manifest file func (m *Manifest) toRaw() rawManifest { raw := rawManifest{ - Dependencies: make(map[string]possibleProps, len(m.Dependencies)), - Overrides: make(map[string]possibleProps, len(m.Ovr)), + Dependencies: make([]rawProject, 0, len(m.Dependencies)), + Overrides: make([]rawProject, 0, len(m.Ovr)), Ignores: m.Ignores, Required: m.Required, } - for n, pp := range m.Dependencies { - raw.Dependencies[string(n)] = toPossible(pp) + for n, prj := range m.Dependencies { + raw.Dependencies = append(raw.Dependencies, toRawProject(n, prj)) } - for n, pp := range m.Ovr { - raw.Overrides[string(n)] = toPossible(pp) + sort.Sort(sortedRawProjects(raw.Dependencies)) + + for n, prj := range m.Ovr { + raw.Overrides = append(raw.Overrides, toRawProject(n, prj)) } + sort.Sort(sortedRawProjects(raw.Overrides)) + return raw } +// TODO(carolynvs) when gps is moved, we can use the unexported gps.sortedConstraints +type sortedRawProjects []rawProject + +func (s sortedRawProjects) Len() int { return len(s) } +func (s sortedRawProjects) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s sortedRawProjects) Less(i, j int) bool { + l, r := s[i], s[j] + + if l.Name < r.Name { + return true + } + if r.Name < l.Name { + return false + } + + return l.Source < r.Source +} + func (m *Manifest) MarshalTOML() (string, error) { raw := m.toRaw() - // TODO(carolynvs) Consider adding reflection-based marshal functionality to go-toml - copyProjects := func(src map[string]possibleProps) []map[string]interface{} { - dest := make([]map[string]interface{}, 0, len(src)) - for prjName, srcPrj := range src { - prj := make(map[string]interface{}) - prj["name"] = prjName - if srcPrj.Source != "" { - prj["source"] = srcPrj.Source - } - if srcPrj.Branch != "" { - prj["branch"] = srcPrj.Branch - } - if srcPrj.Version != "" { - prj["version"] = srcPrj.Version - } - if srcPrj.Revision != "" { - prj["revision"] = srcPrj.Revision - } - dest = append(dest, prj) + mapRawProject := func(raw rawProject) map[string]interface{} { + prj := make(map[string]interface{}) + prj["name"] = raw.Name + if raw.Source != "" { + prj["source"] = raw.Source + } + if raw.Branch != "" { + prj["branch"] = raw.Branch + } + if raw.Version != "" { + prj["version"] = raw.Version + } + if raw.Revision != "" { + prj["revision"] = raw.Revision + } + return prj + } + mapRawProjects := func(src []rawProject) []map[string]interface{} { + dest := make([]map[string]interface{}, len(src)) + for i := 0; i < len(src); i++ { + dest[i] = mapRawProject(src[i]) } return dest } @@ -163,10 +196,10 @@ func (m *Manifest) MarshalTOML() (string, error) { data := make(map[string]interface{}) if len(raw.Dependencies) > 0 { - data["dependencies"] = copyProjects(raw.Dependencies) + data["dependencies"] = mapRawProjects(raw.Dependencies) } if len(raw.Overrides) > 0 { - data["overrides"] = copyProjects(raw.Overrides) + data["overrides"] = mapRawProjects(raw.Overrides) } if len(raw.Ignores) > 0 { data["ignores"] = copyProjectRefs(raw.Ignores) @@ -183,21 +216,22 @@ func (m *Manifest) MarshalTOML() (string, error) { return result, errors.Wrap(err, "Unable to marshal the lock to a TOML string") } -func toPossible(pp gps.ProjectProperties) possibleProps { - p := possibleProps{ - Source: pp.Source, +func toRawProject(name gps.ProjectRoot, project gps.ProjectProperties) rawProject { + raw := rawProject{ + Name: string(name), + Source: project.Source, } - if v, ok := pp.Constraint.(gps.Version); ok { + if v, ok := project.Constraint.(gps.Version); ok { switch v.Type() { case gps.IsRevision: - p.Revision = v.String() + raw.Revision = v.String() case gps.IsBranch: - p.Branch = v.String() + raw.Branch = v.String() case gps.IsSemver, gps.IsVersion: - p.Version = v.String() + raw.Version = v.String() } - return p + return raw } // We simply don't allow for a case where the user could directly @@ -205,11 +239,11 @@ func toPossible(pp gps.ProjectProperties) possibleProps { // the 'any' case, because that's the other possibility, and it's what // we interpret not having any constraint expressions at all to mean. // if !gps.IsAny(pp.Constraint) && !gps.IsNone(pp.Constraint) { - if !gps.IsAny(pp.Constraint) && pp.Constraint != nil { + if !gps.IsAny(project.Constraint) && project.Constraint != nil { // Has to be a semver range. - p.Version = pp.Constraint.String() + raw.Version = project.Constraint.String() } - return p + return raw } func (m *Manifest) DependencyConstraints() gps.ProjectConstraints { diff --git a/testdata/manifest/golden.toml b/testdata/manifest/golden.toml index f3c4759d04..7de52e5089 100644 --- a/testdata/manifest/golden.toml +++ b/testdata/manifest/golden.toml @@ -1,13 +1,13 @@ ignores = ["github.com/foo/bar"] -[[dependencies]] - name = "github.com/sdboyer/gps" - version = ">=0.12.0, <1.0.0" - [[dependencies]] name = "github.com/babble/brook" revision = "d05d5aca9f895d19e9265839bffeadd74a2d2ecb" +[[dependencies]] + name = "github.com/sdboyer/gps" + version = ">=0.12.0, <1.0.0" + [[overrides]] branch = "master" name = "github.com/sdboyer/gps" diff --git a/toml.go b/toml.go new file mode 100644 index 0000000000..fb5324f477 --- /dev/null +++ b/toml.go @@ -0,0 +1,177 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package dep + +import ( + "github.com/pelletier/go-toml" + "github.com/pkg/errors" +) + +type tomlMapper struct { + Tree *toml.TomlTree + Error error +} + +func readTableAsProjects(mapper *tomlMapper, table string) []rawProject { + if mapper.Error != nil { // Stop mapping if an error has already occurred + return nil + } + + query, err := mapper.Tree.Query("$." + table) + if err != nil { + mapper.Error = errors.Wrapf(err, "Unable to query for [[%s]]", table) + return nil + } + + matches := query.Values() + if len(matches) == 0 { + return nil + } + + tables, ok := matches[0].([]*toml.TomlTree) + if !ok { + mapper.Error = errors.Errorf("Invalid query result type for [[%s]], should be a TOML array of tables but got %T", table, matches[0]) + return nil + } + + subMapper := &tomlMapper{} + projects := make([]rawProject, len(tables)) + for i := 0; i < len(tables); i++ { + subMapper.Tree = tables[i] + projects[i] = mapProject(subMapper) + } + + if subMapper.Error != nil { + mapper.Error = subMapper.Error + return nil + } + return projects +} + +func readTableAsLockedProjects(mapper *tomlMapper, table string) []rawLockedProject { + if mapper.Error != nil { // Stop mapping if an error has already occurred + return nil + } + + query, err := mapper.Tree.Query("$." + table) + if err != nil { + mapper.Error = errors.Wrapf(err, "Unable to query for [[%s]]", table) + return nil + } + + matches := query.Values() + if len(matches) == 0 { + return nil + } + + tables, ok := matches[0].([]*toml.TomlTree) + if !ok { + mapper.Error = errors.Errorf("Invalid query result type for [[%s]], should be a TOML array of tables but got %T", table, matches[0]) + return nil + } + + subMapper := &tomlMapper{} + projects := make([]rawLockedProject, len(tables)) + for i := 0; i < len(tables); i++ { + subMapper.Tree = tables[i] + projects[i] = mapLockedProject(subMapper) + } + + if subMapper.Error != nil { + mapper.Error = subMapper.Error + return nil + } + return projects +} + +func mapProject(mapper *tomlMapper) rawProject { + if mapper.Error != nil { // Stop mapping if an error has already occurred + return rawProject{} + } + + prj := rawProject{ + Name: readKeyAsString(mapper, "name"), + Branch: readKeyAsString(mapper, "branch"), + Revision: readKeyAsString(mapper, "revision"), + Version: readKeyAsString(mapper, "version"), + Source: readKeyAsString(mapper, "source"), + } + + if mapper.Error != nil { + return rawProject{} + } + + return prj +} + +func mapLockedProject(mapper *tomlMapper) rawLockedProject { + if mapper.Error != nil { // Stop mapping if an error has already occurred + return rawLockedProject{} + } + + prj := rawLockedProject{ + Name: readKeyAsString(mapper, "name"), + Branch: readKeyAsString(mapper, "branch"), + Revision: readKeyAsString(mapper, "revision"), + Version: readKeyAsString(mapper, "version"), + Source: readKeyAsString(mapper, "source"), + Packages: readKeyAsStringList(mapper, "packages"), + } + + if mapper.Error != nil { + return rawLockedProject{} + } + + return prj +} + +func readKeyAsString(mapper *tomlMapper, key string) string { + if mapper.Error != nil { // Stop mapping if an error has already occurred + return "" + } + + rawValue := mapper.Tree.GetDefault(key, "") + value, ok := rawValue.(string) + if !ok { + mapper.Error = errors.Errorf("Invalid type for %s, should be a string, but it is a %T", key, rawValue) + return "" + } + + return value +} + +func readKeyAsStringList(mapper *tomlMapper, key string) []string { + if mapper.Error != nil { // Stop mapping if an error has already occurred + return nil + } + + query, err := mapper.Tree.Query("$." + key) + if err != nil { + mapper.Error = errors.Wrapf(err, "Unable to query for [%s]", key) + return nil + } + + matches := query.Values() + if len(matches) == 0 { + return nil + } + + lists, ok := matches[0].([]interface{}) + if !ok { + mapper.Error = errors.Errorf("Invalid query result type for [%s], should be a TOML list ([]interface{}) but got %T", key, matches[0]) + return nil + } + + results := make([]string, len(lists)) + for i := range lists { + ref, ok := lists[i].(string) + if !ok { + mapper.Error = errors.Errorf("Invalid query result item type for [%s], should be a TOML list of strings([]string) but got %T", key, lists[i]) + return nil + } + results[i] = ref + } + return results +}