Skip to content

Instantly share code, notes, and snippets.

@sdboyer
Last active January 4, 2022 03:41
Show Gist options
  • Save sdboyer/9184e1bce43911ca31a6c33358d2a6a1 to your computer and use it in GitHub Desktop.
Save sdboyer/9184e1bce43911ca31a6c33358d2a6a1 to your computer and use it in GitHub Desktop.
Thema input kernel handwavy code
package kernel
import (
"cuelang.org/go/cue"
"cuelang.org/go/encoding/gocode/gocodec"
"github.com/grafana/thema"
)
// An InputKernel accepts all the valid inputs for a given lineage, converges
// them onto a single statically-chosen schema version, and emits the result in
// a native Go type.
//
// It's a one-stop shop for encapsulating version juggling of inputs at the
// boundary of your program.
type InputKernel struct {
tf TypeFactory
df Decoder
lin thema.Lineage
to thema.SyntacticVersion
ctx *cue.Context
// TODO add something for interrupting/mediating translation vis-a-vis accumulated lacunae
}
// NewInputKernel constructs an input kernel.
//
// InputKernels accepts as input data, in whatever format (e.g. JSON, YAML)
// supported by its Decoder, validates the data, translates it to a single target version, then
func NewInputKernel(lin thema.Lineage, tf TypeFactory, df Decoder, to thema.SyntacticVersion) (*InputKernel, error) {
// can't even
if lin == nil {
panic("must provide a non-nil lineage")
}
// The concurrency warnings in the docs on Codec are concerning - don't use
// the Runtime concurrently for any other operations, but concurrent use of
// only the codec is fine? ugh, that wouldn't be easy to coordinate under the
// best of circumstances. And there's really nothing we can do in a
// situation like this. So...guess we're YOLOing it for now?
codec := gocodec.New((*cue.Runtime)(lin.RawValue().Context()), nil)
sch, err := thema.Pick(lin, to)
if err != nil {
return nil, err
}
// Verify that the input Go type is valid with respect to the indicated
// schema. Effect is that the caller cannot get an InputKernel without a
// valid Go type to write to.
//
// TODO verify this is actually how we check this
if err = codec.Validate(sch.RawValue(), tf()); err != nil {
return nil, err
}
return &InputKernel{
tf: tf,
df: df,
lin: lin,
to: to,
}, nil
}
// A TypeFactory must emit a pointer to the Go type that a kernel will
// ultimately produce as output.
//
// TODO the function accomplished by this should be trivial to achieve with generics...?
type TypeFactory func() interface{}
// A Decoder takes some input data and decodes it into a cue.Value.
type Decoder func(*cue.Context, interface{}) (cue.Value, error)
// Converge runs data through the kernel: validate, translate to a fixed
// version, return transformed instance along with any emitted lacunae.
//
// Valid types for the input are governed by the Decoder func with which the
// kernel was constructed.
//
// Type safety of the return value is guaranteed by checks performed in
// NewInputKernel() - if error is non-nil, the first return value is guaranteed
// to be an instance of the type returned from the TypeFactory with which the
// kernel was constructed.
func (k *InputKernel) Converge(data interface{}) (interface{}, thema.TranslationLacunae, error) {
ctx := k.lin.RawValue().Context()
// Decode the input data into a cue.Value
v, err := k.df(ctx, data)
if err != nil {
return nil, nil, err
}
// Validate that the data constitutes an instance of at least one of the schemas in the lineage
inst, err := thema.SearchAndValidate(k.lin, v)
if err != nil {
return nil, nil, err
}
transval, lac := thema.Translate(inst, k.to)
ret := k.tf()
transval.Decode(ret)
return ret, lac, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment