Skip to content

Commit

Permalink
Merge pull request #187 from grafana/sdboyer/go-lenses
Browse files Browse the repository at this point in the history
thema: Initial pass at Go lenses/migrations
  • Loading branch information
sdboyer committed Jul 28, 2023
2 parents e35caab + 5d824ad commit 64e9d78
Show file tree
Hide file tree
Showing 6 changed files with 619 additions and 7 deletions.
122 changes: 120 additions & 2 deletions bind.go
@@ -1,12 +1,14 @@
package thema

import (
"bytes"
"fmt"

"cuelang.org/go/cue"
cerrors "cuelang.org/go/cue/errors"
"cuelang.org/go/cue/token"
"github.com/cockroachdb/errors"

terrors "github.com/grafana/thema/errors"
"github.com/grafana/thema/internal/compat"
)
Expand Down Expand Up @@ -39,10 +41,27 @@ type maybeLineage struct {

allv []SyntacticVersion

implens []ImperativeLens

lensmap map[lensID]ImperativeLens

// The raw input value is the root of a package instance
// rawIsPackage bool
}

// to, from
type lensID struct {
From, To SyntacticVersion
}

func lid(from, to SyntacticVersion) lensID {
return lensID{from, to}
}

func (id lensID) String() string {
return fmt.Sprintf("%s -> %s", id.From, id.To)
}

func (ml *maybeLineage) checkGoValidity(cfg *bindConfig) error {
schiter, err := ml.uni.LookupPath(cue.MakePath(cue.Str("schemas"))).List()
if err != nil {
Expand Down Expand Up @@ -167,6 +186,12 @@ func (ml *maybeLineage) checkNativeValidity(cfg *bindConfig) error {
}

func (ml *maybeLineage) checkLensesOrder() error {
// Two distinct validation paths, depending on whether the lenses were defined in
// Go or CUE.
if len(ml.implens) > 0 {
return ml.checkGoLensCompleteness()
}

lensIter, err := ml.uni.LookupPath(cue.MakePath(cue.Str("lenses"))).List()
if err != nil {
return nil // no lenses found
Expand All @@ -179,7 +204,7 @@ func (ml *maybeLineage) checkLensesOrder() error {
return err
}

if err := checkLensesOrder(previous, &curr); err != nil {
if err := doCheck(previous, &curr); err != nil {
return err
}

Expand All @@ -189,6 +214,99 @@ func (ml *maybeLineage) checkLensesOrder() error {
return nil
}

func (ml *maybeLineage) checkGoLensCompleteness() error {
// TODO(sdboyer) it'd be nice to consolidate all the errors so that the user always sees a complete set of problems
all := make(map[lensID]bool)
for _, lens := range ml.implens {
id := lid(lens.From, lens.To)
if all[id] {
return fmt.Errorf("duplicate Go migration %s", id)
}
if lens.Mapper == nil {
return fmt.Errorf("nil Go migration func for %s", id)
}
all[id] = true
}

var missing []lensID

var prior SyntacticVersion
for _, sch := range ml.schlist[1:] {
// there must always at least be a reverse lens
v := sch.Version()
revid := lid(v, prior)

if !all[revid] {
missing = append(missing, revid)
} else {
delete(all, revid)
}

if v[0] != prior[0] {
// if we crossed a major version, there must also be a forward lens
fwdid := lid(prior, v)
if !all[fwdid] {
missing = append(missing, fwdid)
} else {
delete(all, fwdid)
}
}
prior = v
}

// TODO is it worth making each sub-item into its own error type?
if len(missing) > 0 {
b := new(bytes.Buffer)

fmt.Fprintf(b, "Go migrations not provided for the following version pairs:\n")
for _, mlid := range missing {
fmt.Fprint(b, "\t", mlid, "\n")
}
return errors.Mark(errors.New(b.String()), terrors.ErrMissingLenses)
}

if len(all) > 0 {
b := new(bytes.Buffer)

fmt.Fprintf(b, "Go migrations erroneously provided for the following version pairs:\n")
// walk the slice so output is reliably ordered
for _, lens := range ml.implens {
// if it's not in the list it's because it was expected & already processed
elid := lid(lens.From, lens.To)
if _, has := all[elid]; !has {
continue
}
if !synvExists(ml.allv, lens.To) {
fmt.Fprintf(b, "\t%s (schema version %s does not exist)", elid, lens.To)
} else if !synvExists(ml.allv, lens.From) {
fmt.Fprintf(b, "\t%s (schema version %s does not exist)", elid, lens.From)
} else if elid.To == elid.From {
fmt.Fprintf(b, "\t%s (self-migrations not allowed)", elid)
} else if elid.To.Less(elid.From) {
// reverse lenses
// only possibility is non-sequential versions connected
fmt.Fprintf(b, "\t%s (%s is predecessor of %s, not %s)", elid, ml.allv[searchSynv(ml.allv, elid.From)-1], elid.From, elid.To)
} else {
// forward lenses
// either a minor lens was provided, or non-sequential versions connected
if lens.To[0] != lens.From[0] {
fmt.Fprintf(b, "\t%s (minor version upgrades are handled automatically)", elid)
} else {
fmt.Fprintf(b, "\t%s (%s is successor of %s, not %s)", elid, ml.allv[searchSynv(ml.allv, elid.From)+1], elid.From, elid.To)
}
}
}
return errors.Mark(errors.New(b.String()), terrors.ErrErroneousLenses)
}

ml.lensmap = make(map[lensID]ImperativeLens, len(ml.implens))
for _, lens := range ml.implens {
ml.lensmap[lid(lens.From, lens.To)] = lens
}

return nil
}

type lensVersionDef struct {
to SyntacticVersion
from SyntacticVersion
Expand All @@ -209,7 +327,7 @@ func newLensVersionDef(val cue.Value) (lensVersionDef, error) {
return lensVersionDef{to: to, from: from}, err
}

func checkLensesOrder(prev, curr *lensVersionDef) error {
func doCheck(prev, curr *lensVersionDef) error {
if prev == nil {
return nil
}
Expand Down
14 changes: 14 additions & 0 deletions errors/errors.go
Expand Up @@ -108,6 +108,20 @@ var (
// ErrInvalidLensesOrder indicates that lenses are in the wrong order - they must be sorted by `to`, then `from`.
ErrInvalidLensesOrder = errors.New("lenses in lineage are not ordered by version")

// ErrDuplicateLenses indicates that a lens was defined declaratively in CUE, but the same lens
// was also provided as a Go function to BindLineage.
ErrDuplicateLenses = errors.New("lens is declared in both CUE and Go")

// ErrMissingLenses indicates that the lenses provided to BindLineage in either
// CUE or Go were missing at least one of the expected lenses determined by the
// set of schemas in the lineage.
ErrMissingLenses = errors.New("not all expected lenses were provided")

// ErrErroneousLenses indicates that a lens was provided to BindLineage in either
// CUE or Go that was not one of the expected lenses determined by the set of
// schemas in the lineage.
ErrErroneousLenses = errors.New("unexpected lenses were erroneously provided")

// ErrVersionNotExist indicates that no schema exists in a lineage with a
// given version.
ErrVersionNotExist = errors.New("lineage does not contain schema with version") // ErrNoSchemaWithVersion
Expand Down

0 comments on commit 64e9d78

Please sign in to comment.