Skip to content

Commit

Permalink
feat(lib): Dispatch methods call, used by FSI
Browse files Browse the repository at this point in the history
Dispatch is a new mechanism for handling calls invoked on
the lib.Instance. Calls are sent to dispatch as a method name
and input parameters. The actual implementation is looked up and
then invokved using reflection. This allows the http api to
layer itself directly on top of these same methods, which also
lets us replace the old rpc style with the http api. It also
nicely sets us up to introduce multi-tenancy and multi-processing,
by having a single place that handles incoming requests.

This mechanmism is introduced here, and only used for FSI now.
Dispatch can be introduced gradually, and does not require
changing the whole world at once. This PR should serve as a guide
for how to do the same refactoring for other method groups. Note
that each FSI method in FSIMethods is now a very thin call to
Dispatch, and then a type coercion afterwards to get the correct
return value. Actual implementations live at the bottom of the
same source file, and each take a Scope, which is a new structure
to control access to the otherwise global resources in Instance.
  • Loading branch information
dustmop committed Mar 2, 2021
1 parent 15944db commit afaf06d
Show file tree
Hide file tree
Showing 27 changed files with 868 additions and 484 deletions.
210 changes: 104 additions & 106 deletions api/fsi.go
Original file line number Diff line number Diff line change
@@ -1,33 +1,27 @@
package api

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"

"github.com/qri-io/dataset"
"github.com/qri-io/qri/api/util"
"github.com/qri-io/qri/fsi"
"github.com/qri-io/qri/dsref"
"github.com/qri-io/qri/lib"
"github.com/qri-io/qri/profile"
"github.com/qri-io/qri/repo"
reporef "github.com/qri-io/qri/repo/ref"
)

// FSIHandlers connects HTTP requests to the FSI subsystem
type FSIHandlers struct {
lib.FSIMethods
inst *lib.Instance
dsm *lib.DatasetMethods
ReadOnly bool
}

// NewFSIHandlers creates handlers that talk to qri's filesystem integration
func NewFSIHandlers(inst *lib.Instance, readOnly bool) FSIHandlers {
return FSIHandlers{
FSIMethods: *lib.NewFSIMethods(inst),
dsm: lib.NewDatasetMethods(inst),
ReadOnly: readOnly,
inst: inst,
dsm: lib.NewDatasetMethods(inst),
ReadOnly: readOnly,
}
}

Expand All @@ -42,7 +36,7 @@ func (h *FSIHandlers) StatusHandler(routePrefix string) http.HandlerFunc {
}

switch r.Method {
case http.MethodGet:
case http.MethodGet, http.MethodPost:
handleStatus(w, r)
default:
util.NotFoundHandler(w, r)
Expand All @@ -52,20 +46,26 @@ func (h *FSIHandlers) StatusHandler(routePrefix string) http.HandlerFunc {

func (h *FSIHandlers) statusHandler(routePrefix string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ref, err := lib.DsRefFromPath(r.URL.Path[len(routePrefix):])
method := "fsi.status"
p := h.inst.NewInputParam(method)

// TODO(dustmop): Add this to UnmarshalParams for methods that can
// receive a refstr in the URL, or annotate the param struct with
// a tag and marshal the url to that field
err := addDsRefFromURL(r, routePrefix)
if err != nil {
util.WriteErrResponse(w, http.StatusBadRequest, fmt.Errorf("bad reference: %s", err.Error()))
util.WriteErrResponse(w, http.StatusBadRequest, err)
return
}

res := []lib.StatusItem{}
alias := ref.Alias()
err = h.StatusForAlias(&alias, &res)
if err == fsi.ErrNoLink {
util.WriteErrResponse(w, http.StatusBadRequest, fmt.Errorf("no working directory: %s", alias))
if err := UnmarshalParams(r, p); err != nil {
util.WriteErrResponse(w, http.StatusBadRequest, err)
return
} else if err != nil {
util.WriteErrResponse(w, http.StatusInternalServerError, fmt.Errorf("error getting status: %s", err.Error()))
}

res, err := h.inst.Dispatch(r.Context(), method, p)
if err != nil {
util.RespondWithError(w, err)
return
}
util.WriteResponse(w, res)
Expand Down Expand Up @@ -94,24 +94,21 @@ func (h *FSIHandlers) WhatChangedHandler(routePrefix string) http.HandlerFunc {

func (h *FSIHandlers) whatChangedHandler(routePrefix string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ref, err := lib.DsRefFromPath(r.URL.Path[len(routePrefix):])
if err != nil {
util.WriteErrResponse(w, http.StatusBadRequest, fmt.Errorf("bad reference: %s", err.Error()))
method := "fsi.whatchanged"
p := h.inst.NewInputParam(method)

if err := UnmarshalParams(r, p); err != nil {
util.WriteErrResponse(w, http.StatusBadRequest, err)
return
}

res := []lib.StatusItem{}
refStr := ref.String()
err = h.WhatChanged(&refStr, &res)
res, err := h.inst.Dispatch(r.Context(), method, p)
if err != nil {
if err == repo.ErrNoHistory {
util.WriteErrResponse(w, http.StatusUnprocessableEntity, err)
return
}
util.WriteErrResponse(w, http.StatusInternalServerError, fmt.Errorf("error getting status: %s", err.Error()))
util.RespondWithError(w, err)
return
}
util.WriteResponse(w, res)
return
}
}

Expand All @@ -136,51 +133,20 @@ func (h *FSIHandlers) InitHandler(routePrefix string) http.HandlerFunc {

func (h *FSIHandlers) initHandler(routePrefix string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
p := &lib.InitDatasetParams{
TargetDir: r.FormValue("targetdir"),
Name: r.FormValue("name"),
Format: r.FormValue("format"),
BodyPath: r.FormValue("bodypath"),
}
method := "fsi.init"
p := h.inst.NewInputParam(method)

var name string
if err := h.InitDataset(r.Context(), p, &name); err != nil {
if err := UnmarshalParams(r, p); err != nil {
util.WriteErrResponse(w, http.StatusBadRequest, err)
return
}

// Get code taken
// taken from ./root.go
gp := lib.GetParams{
Refstr: name,
}

res, err := h.dsm.Get(r.Context(), &gp)
res, err := h.inst.Dispatch(r.Context(), method, p)
if err != nil {
if err == repo.ErrNotFound {
util.NotFoundHandler(w, r)
return
}
util.WriteErrResponse(w, http.StatusInternalServerError, err)
return
}
if res.Dataset == nil || res.Dataset.IsEmpty() {
util.WriteErrResponse(w, http.StatusNotFound, errors.New("cannot find peer dataset"))
util.RespondWithError(w, err)
return
}

// TODO (b5) - why is this necessary?
ref := reporef.DatasetRef{
Peername: res.Dataset.Peername,
ProfileID: profile.IDB58DecodeOrEmpty(res.Dataset.ProfileID),
Name: res.Dataset.Name,
Path: res.Dataset.Path,
FSIPath: res.FSIPath,
Published: res.Published,
Dataset: res.Dataset,
}

util.WriteResponse(w, ref)
util.WriteResponse(w, res)
return
}
}
Expand All @@ -205,30 +171,30 @@ func (h *FSIHandlers) WriteHandler(routePrefix string) http.HandlerFunc {

func (h *FSIHandlers) writeHandler(routePrefix string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ref, err := lib.DsRefFromPath(r.URL.Path[len(routePrefix):])
method := "fsi.write"
p := h.inst.NewInputParam(method)

// TODO(dustmop): Add this to UnmarshalParams for methods that can
// receive a refstr in the URL, or annotate the param struct with
// a tag and marshal the url to that field
err := addDsRefFromURL(r, routePrefix)
if err != nil {
util.WriteErrResponse(w, http.StatusBadRequest, fmt.Errorf("bad reference: %s", err.Error()))
util.WriteErrResponse(w, http.StatusBadRequest, err)
return
}

ds := &dataset.Dataset{}
if err := json.NewDecoder(r.Body).Decode(ds); err != nil {
if err := UnmarshalParams(r, p); err != nil {
util.WriteErrResponse(w, http.StatusBadRequest, err)
return
}

p := &lib.FSIWriteParams{
Ref: ref.Alias(),
Ds: ds,
}

out := []lib.StatusItem{}
if err := h.Write(p, &out); err != nil {
util.WriteErrResponse(w, http.StatusBadRequest, err)
res, err := h.inst.Dispatch(r.Context(), method, p)
if err != nil {
util.RespondWithError(w, err)
return
}

util.WriteResponse(w, out)
util.WriteResponse(w, res)
return
}
}

Expand All @@ -252,24 +218,30 @@ func (h *FSIHandlers) CheckoutHandler(routePrefix string) http.HandlerFunc {

func (h *FSIHandlers) checkoutHandler(routePrefix string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ref, err := lib.DsRefFromPath(r.URL.Path[len(routePrefix):])
method := "fsi.checkout"
p := h.inst.NewInputParam(method)

// TODO(dustmop): Add this to UnmarshalParams for methods that can
// receive a refstr in the URL, or annotate the param struct with
// a tag and marshal the url to that field
err := addDsRefFromURL(r, routePrefix)
if err != nil {
util.WriteErrResponse(w, http.StatusBadRequest, fmt.Errorf("bad reference: %s", err.Error()))
util.WriteErrResponse(w, http.StatusBadRequest, err)
return
}

p := &lib.CheckoutParams{
Dir: r.FormValue("dir"),
Ref: ref.String(),
if err := UnmarshalParams(r, p); err != nil {
util.WriteErrResponse(w, http.StatusBadRequest, err)
return
}

var res string
if err := h.Checkout(p, &res); err != nil {
util.WriteErrResponse(w, http.StatusInternalServerError, err)
res, err := h.inst.Dispatch(r.Context(), method, p)
if err != nil {
util.RespondWithError(w, err)
return
}

util.WriteResponse(w, res)
return
}
}

Expand All @@ -293,27 +265,53 @@ func (h *FSIHandlers) RestoreHandler(routePrefix string) http.HandlerFunc {

func (h *FSIHandlers) restoreHandler(routePrefix string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ref, err := lib.DsRefFromPath(r.URL.Path[len(routePrefix):])
method := "fsi.restore"
p := h.inst.NewInputParam(method)

// TODO(dustmop): Add this to UnmarshalParams for methods that can
// receive a refstr in the URL, or annotate the param struct with
// a tag and marshal the url to that field
err := addDsRefFromURL(r, routePrefix)
if err != nil {
util.WriteErrResponse(w, http.StatusBadRequest, fmt.Errorf("bad reference: %s", err.Error()))
util.WriteErrResponse(w, http.StatusBadRequest, err)
return
}

// Add the path for the version to restore
ref.Path = r.FormValue("path")

p := &lib.RestoreParams{
Dir: r.FormValue("dir"),
Ref: ref.String(),
Component: r.FormValue("component"),
if err := UnmarshalParams(r, p); err != nil {
util.WriteErrResponse(w, http.StatusBadRequest, err)
return
}

var res string
if err := h.Restore(p, &res); err != nil {
util.WriteErrResponse(w, http.StatusInternalServerError, err)
res, err := h.inst.Dispatch(r.Context(), method, p)
if err != nil {
util.RespondWithError(w, err)
return
}

util.WriteResponse(w, res)
return
}
}

// If the route has a dataset reference in the url, parse that ref, and
// add it to the request object using the field "refstr".
func addDsRefFromURL(r *http.Request, routePrefix string) error {
// routePrefix looks like "/route/{path:.*}" and we only want "/route/"
pos := strings.LastIndex(routePrefix, "/")
if pos > 1 {
routePrefix = routePrefix[:pos+1]
}

// Parse the ref, then reencode it and attach back on the url
url := r.URL.Path[len(routePrefix):]
ref, err := lib.DsRefFromPath(url)
if err != nil {
if err == dsref.ErrEmptyRef {
return nil
}
return err
}
q := r.URL.Query()
q.Add("refstr", ref.String())
r.URL.RawQuery = q.Encode()
return nil
}
6 changes: 5 additions & 1 deletion api/fsi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,11 @@ func TestFSIWrite(t *testing.T) {
t.Errorf("expected body %s, got %s", expectBody, actualBody)
}

status, strRes := JSONAPICallWithBody("POST", "/me/write_test", &dataset.Dataset{Meta: &dataset.Meta{Title: "oh hai there"}}, fsiHandler.WriteHandler(""))
p := lib.FSIWriteParams{
Refstr: "peer/write_test",
Ds: &dataset.Dataset{Meta: &dataset.Meta{Title: "oh hai there"}},
}
status, strRes := JSONAPICallWithBody("POST", "/fsi/write/me/write_test", p, fsiHandler.WriteHandler("/fsi/write/"))

if status != http.StatusOK {
t.Errorf("status code mismatch. expected: %d, got: %d", http.StatusOK, status)
Expand Down
Binary file modified api/testdata/api.snapshot
Binary file not shown.
9 changes: 9 additions & 0 deletions api/util/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
golog "github.com/ipfs/go-log"
"github.com/qri-io/qfs"
"github.com/qri-io/qri/dsref"
"github.com/qri-io/qri/fsi"
"github.com/qri-io/qri/repo"
)

Expand Down Expand Up @@ -35,6 +36,14 @@ func RespondWithError(w http.ResponseWriter, err error) {
WriteErrResponse(w, http.StatusNotFound, err)
return
}
if errors.Is(err, repo.ErrNotFound) {
WriteErrResponse(w, http.StatusNotFound, err)
return
}
if errors.Is(err, fsi.ErrNoLink) {
WriteErrResponse(w, http.StatusBadRequest, err)
return
}
if errors.Is(err, repo.ErrNoHistory) {
WriteErrResponse(w, http.StatusUnprocessableEntity, err)
return
Expand Down
Loading

0 comments on commit afaf06d

Please sign in to comment.