Skip to content

Commit

Permalink
feat(apply): Apply command, and --apply flag for save
Browse files Browse the repository at this point in the history
  • Loading branch information
dustmop committed Jan 15, 2021
1 parent fac37da commit c01a4bf
Show file tree
Hide file tree
Showing 20 changed files with 627 additions and 36 deletions.
3 changes: 3 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ func NewServerRoutes(s Server) *http.ServeMux {
sqlh := NewSQLHandlers(s.Instance, cfg.API.ReadOnly)
m.Handle("/sql", s.middleware(sqlh.QueryHandler("/sql")))

tfh := NewTransformHandlers(s.Instance)
m.Handle("/apply", s.middleware(tfh.ApplyHandler("/apply")))

if !cfg.API.DisableWebui {
m.Handle("/webui", s.middleware(WebuiHandler))
}
Expand Down
1 change: 1 addition & 0 deletions api/datasets.go
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,7 @@ func (h *DatasetHandlers) saveHandler(w http.ResponseWriter, r *http.Request) {
p := &lib.SaveParams{
Ref: ref.AliasString(),
Dataset: ds,
Apply: r.FormValue("apply") == "true",
Private: r.FormValue("private") == "true",
Force: r.FormValue("force") == "true",
ShouldRender: !(r.FormValue("no_render") == "true"),
Expand Down
38 changes: 38 additions & 0 deletions api/transform.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package api

import (
"encoding/json"
"net/http"

"github.com/qri-io/qri/api/util"
"github.com/qri-io/qri/lib"
)

// TransformHandlers connects HTTP requests to the TransformMethods subsystem
type TransformHandlers struct {
*lib.TransformMethods
}

// NewTransformHandlers constructs a TrasnformHandlers struct
func NewTransformHandlers(inst *lib.Instance) TransformHandlers {
return TransformHandlers{TransformMethods: lib.NewTransformMethods(inst)}
}

// ApplyHandler is an HTTP handler function for executing a transform script
func (h TransformHandlers) ApplyHandler(prefix string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
p := lib.ApplyParams{}
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
util.WriteErrResponse(w, http.StatusBadRequest, err)
return
}

res := lib.ApplyResult{}
if err := h.TransformMethods.Apply(&p, &res); err != nil {
util.WriteErrResponse(w, http.StatusBadRequest, err)
return
}

util.WriteResponse(w, res)
}
}
7 changes: 5 additions & 2 deletions base/body.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package base
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"

Expand All @@ -11,6 +12,9 @@ import (
"github.com/qri-io/qfs"
)

// ErrNoBodyToInline is an error returned when a dataset has no body for inlining
var ErrNoBodyToInline = errors.New("no body to inline")

// ReadBody grabs some or all of a dataset's body, writing an output in the desired format
func ReadBody(ds *dataset.Dataset, format dataset.DataFormat, fcfg dataset.FormatConfig, limit, offset int, all bool) (data []byte, err error) {
if ds == nil {
Expand Down Expand Up @@ -78,8 +82,7 @@ func ReadEntries(reader dsio.EntryReader) (interface{}, error) {
func InlineJSONBody(ds *dataset.Dataset) error {
file := ds.BodyFile()
if file == nil {
log.Error("no body file")
return fmt.Errorf("no response body file")
return ErrNoBodyToInline
}

if ds.Structure.Format == dataset.JSONDataFormat.String() {
Expand Down
4 changes: 4 additions & 0 deletions base/transform_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ func TransformApply(
head *dataset.Dataset
)

if target.Transform == nil || target.Transform.ScriptFile() == nil {
return errors.New("apply requires a transform with script file")
}

if ds.Name != "" {
head, err = loader(ctx, fmt.Sprintf("%s/%s", pro.Peername, ds.Name))
if errors.Is(err, dsref.ErrRefNotFound) || errors.Is(err, dsref.ErrNoHistory) {
Expand Down
113 changes: 113 additions & 0 deletions cmd/apply.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package cmd

import (
"encoding/json"
"errors"
"path/filepath"
"strings"

"github.com/qri-io/dataset"
"github.com/qri-io/ioes"
"github.com/qri-io/qri/lib"
"github.com/qri-io/qri/repo"
"github.com/spf13/cobra"
)

// NewApplyCommand creates a new `qri apply` cobra command for applying transformations
func NewApplyCommand(f Factory, ioStreams ioes.IOStreams) *cobra.Command {
o := &ApplyOptions{IOStreams: ioStreams}
cmd := &cobra.Command{
Use: "apply",
Short: "apply a transform to a dataset",
Long: `Apply runs a transform script. The result of the transform is displayed after
the command completes.
Nothing is saved in the user's repository.`,
Example: ` # Apply a transform and display the output:
$ qri apply --script transform.star`,
Annotations: map[string]string{
"group": "dataset",
},
RunE: func(cmd *cobra.Command, args []string) error {
if err := o.Complete(f, args); err != nil {
return err
}
return o.Run()
},
}

cmd.Flags().StringVar(&o.FilePath, "file", "", "path of transform script file")
cmd.MarkFlagRequired("file")
cmd.Flags().StringSliceVar(&o.Secrets, "secrets", nil, "transform secrets as comma separated key,value,key,value,... sequence")

return cmd
}

// ApplyOptions encapsulates state for the apply command
type ApplyOptions struct {
ioes.IOStreams

Refs *RefSelect
FilePath string
Secrets []string

TransformMethods *lib.TransformMethods
}

// Complete adds any missing configuration that can only be added just before calling Run
func (o *ApplyOptions) Complete(f Factory, args []string) (err error) {
if o.TransformMethods, err = f.TransformMethods(); err != nil {
return err
}
if o.Refs, err = GetCurrentRefSelect(f, args, -1, nil); err != nil {
// This error will be handled during validation
if err != repo.ErrEmptyRef {
return err
}
err = nil
}
o.FilePath, err = filepath.Abs(o.FilePath)
if err != nil {
return err
}
return nil
}

// Run executes the apply command
func (o *ApplyOptions) Run() error {
printRefSelect(o.ErrOut, o.Refs)

var err error

if !strings.HasSuffix(o.FilePath, ".star") {
return errors.New("only transform scripts are supported by --file")
}

tf := dataset.Transform{
ScriptPath: o.FilePath,
}

if len(o.Secrets) > 0 {
tf.Secrets, err = parseSecrets(o.Secrets...)
if err != nil {
return err
}
}

params := lib.ApplyParams{
Refstr: o.Refs.Ref(),
Transform: &tf,
ScriptOutput: o.Out,
}
res := lib.ApplyResult{}
if err = o.TransformMethods.Apply(&params, &res); err != nil {
return err
}

data, err := json.MarshalIndent(res.Data, "", " ")
if err != nil {
return err
}
printSuccess(o.Out, string(data))
return nil
}
84 changes: 84 additions & 0 deletions cmd/apply_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package cmd

import (
"strings"
"testing"

"github.com/google/go-cmp/cmp"
)

func TestTransformApply(t *testing.T) {
run := NewTestRunner(t, "test_peer_transform_apply", "qri_test_transform_apply")
defer run.Delete()

// Apply a transform which makes a body
output := run.MustExec(t, "qri apply --file testdata/movies/tf_one_movie.star")
expectContains := ` "body": [
[
"Spectre",
148
]
],`
if !strings.Contains(output, expectContains) {
t.Errorf("contents mismatch, want: %s, got: %s", expectContains, output)
}

// Save a first version with a normal body
run.MustExec(t, "qri save --body testdata/movies/body_ten.csv me/movies")

// Apply a transform which sets a meta on the existing dataset
output = run.MustExec(t, "qri apply --file testdata/movies/tf_set_meta.star me/movies")
expectContains = `"title": "Did Set Title"`
if !strings.Contains(output, expectContains) {
t.Errorf("contents mismatch, want: %s, got: %s", expectContains, output)
}
}

func TestApplyRefNotFound(t *testing.T) {
run := NewTestRunner(t, "test_peer_transform_apply", "qri_test_transform_apply")
defer run.Delete()

// Error to apply a transform using a dataset ref that doesn't exist.
err := run.ExecCommand("qri apply --file testdata/movies/tf_one_movie.star me/not_found")
if err == nil {
t.Errorf("error expected, did not get one")
}
expectErr := `reference not found`
if diff := cmp.Diff(expectErr, err.Error()); diff != "" {
t.Errorf("result mismatch (-want +got):%s\n", diff)
}
}

func TestApplyMetaOnly(t *testing.T) {
run := NewTestRunner(t, "test_peer_apply_meta_only", "qri_test_apply_meta_only")
defer run.Delete()

// Apply a transform which sets a meta to the existing dataset
output := run.MustExec(t, "qri apply --file testdata/movies/tf_set_meta.star")
expectContains := `"title": "Did Set Title"`
if !strings.Contains(output, expectContains) {
t.Errorf("contents mismatch, want: %s, got: %s", expectContains, output)
}
}

func TestApplyModifyBody(t *testing.T) {
run := NewTestRunner(t, "test_peer_apply_mod_body", "qri_test_apply_mod_body")
defer run.Delete()

// Save two versions, the second of which uses get_body in a transformation
run.MustExec(t, "qri save --body=testdata/movies/body_two.json me/test_ds")
output := run.MustExec(t, "qri apply --file=testdata/movies/tf_add_one.star me/test_ds")
expectContains := `"body": [
[
"Avatar",
179
],
[
"Pirates of the Caribbean: At World's End",
170
]
],`
if !strings.Contains(output, expectContains) {
t.Errorf("contents mismatch, want: %s, got: %s", expectContains, output)
}
}
14 changes: 7 additions & 7 deletions cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ func TestSaveThenOverrideTransform(t *testing.T) {

// Save a version, then save another with a transform
run.MustExec(t, "qri save --file=testdata/movies/ds_ten.yaml me/test_ds")
run.MustExec(t, "qri save --file=testdata/movies/tf.star me/test_ds")
run.MustExec(t, "qri save --apply --file=testdata/movies/tf.star me/test_ds")

// Read head from the dataset that was saved, as json string.
dsPath := run.GetPathForDataset(t, 0)
Expand Down Expand Up @@ -343,7 +343,7 @@ func TestSaveThenOverrideMetaAndTransformAndViz(t *testing.T) {

// Save a version, then save another with three components at once
run.MustExec(t, "qri save --file=testdata/movies/ds_ten.yaml me/test_ds")
run.MustExec(t, "qri save --file=testdata/movies/meta_override.yaml --file=testdata/movies/tf.star --file=testdata/template.html me/test_ds")
run.MustExec(t, "qri save --apply --file=testdata/movies/meta_override.yaml --file=testdata/movies/tf.star --file=testdata/template.html me/test_ds")

// Read head from the dataset that was saved, as json string.
dsPath := run.GetPathForDataset(t, 0)
Expand Down Expand Up @@ -404,14 +404,14 @@ func TestSaveTransformWithoutChanges(t *testing.T) {
defer run.Delete()

// Save a version, then another with no changes
run.MustExec(t, "qri save --file=testdata/movies/tf_123.star me/test_ds")
run.MustExec(t, "qri save --apply --file=testdata/movies/tf_123.star me/test_ds")

errOut := run.GetCommandErrOutput()
if !strings.Contains(errOut, "setting body") {
t.Errorf("expected ErrOutput to contain print statement from transform script. errOutput:\n%s", errOut)
}

err := run.ExecCommand("qri save --file=testdata/movies/tf_123.star me/test_ds")
err := run.ExecCommand("qri save --apply --file=testdata/movies/tf_123.star me/test_ds")
expect := `error saving: no changes`
if err == nil {
t.Fatalf("expected error: did not get one")
Expand All @@ -432,7 +432,7 @@ func TestTransformUsingGetBodyAndSetBody(t *testing.T) {

// Save two versions, the second of which uses get_body in a transformation
run.MustExec(t, "qri save --body=testdata/movies/body_two.json me/test_ds")
run.MustExec(t, "qri save --file=testdata/movies/tf_add_one.star me/test_ds")
run.MustExec(t, "qri save --apply --file=testdata/movies/tf_add_one.star me/test_ds")

// Read body from the dataset that was created with the transform
dsPath := run.GetPathForDataset(t, 0)
Expand All @@ -455,10 +455,10 @@ func TestSaveTransformModifiedButSameBody(t *testing.T) {
defer run.Delete()

// Save a version
run.MustExec(t, "qri save --file=testdata/movies/tf_123.star me/test_ds")
run.MustExec(t, "qri save --apply --file=testdata/movies/tf_123.star me/test_ds")

// Save another version with a modified transform that produces the same body
err := run.ExecCommand("qri save --file=testdata/movies/tf_modified.star me/test_ds")
err := run.ExecCommand("qri save --apply --file=testdata/movies/tf_modified.star me/test_ds")

if err != nil {
t.Errorf("unexpected error: %q", err)
Expand Down
1 change: 1 addition & 0 deletions cmd/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type Factory interface {
SQLMethods() (*lib.SQLMethods, error)
FSIMethods() (*lib.FSIMethods, error)
RenderMethods() (*lib.RenderMethods, error)
TransformMethods() (*lib.TransformMethods, error)
}

// StandardRepoPath returns qri paths based on the QRI_PATH environment
Expand Down
5 changes: 5 additions & 0 deletions cmd/factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,8 @@ func (t TestFactory) SQLMethods() (*lib.SQLMethods, error) {
func (t TestFactory) RenderMethods() (*lib.RenderMethods, error) {
return lib.NewRenderMethods(t.inst), nil
}

// TransformMethods generates a lib.TransformMethods from internal state
func (t TestFactory) TransformMethods() (*lib.TransformMethods, error) {
return lib.NewTransformMethods(t.inst), nil
}
9 changes: 9 additions & 0 deletions cmd/qri.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ https://github.com/qri-io/qri/issues`,
cmd.PersistentFlags().BoolVarP(&opt.LogAll, "log-all", "", false, "log all activity")

cmd.AddCommand(
NewApplyCommand(opt, ioStreams),
NewAutocompleteCommand(opt, ioStreams),
NewCheckoutCommand(opt, ioStreams),
NewConfigCommand(opt, ioStreams),
Expand Down Expand Up @@ -238,6 +239,14 @@ func (o *QriOptions) DatasetMethods() (*lib.DatasetMethods, error) {
return lib.NewDatasetMethods(o.inst), nil
}

// TransformMethods generates a lib.TransformMethods from internal state
func (o *QriOptions) TransformMethods() (*lib.TransformMethods, error) {
if err := o.Init(); err != nil {
return nil, err
}
return lib.NewTransformMethods(o.inst), nil
}

// RemoteMethods generates a lib.RemoteMethods from internal state
func (o *QriOptions) RemoteMethods() (*lib.RemoteMethods, error) {
if err := o.Init(); err != nil {
Expand Down

0 comments on commit c01a4bf

Please sign in to comment.