diff --git a/.gitignore b/.gitignore index 314503f..ba5f7b9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .chotki_cmd_log.txt +.idea \ No newline at end of file diff --git a/repl/README.md b/repl/README.md index 9566a90..3cf3413 100644 --- a/repl/README.md +++ b/repl/README.md @@ -22,6 +22,12 @@ returns an id and/or an error message. ## Backup/restore +## Http/swagger +1. `servehttp 8001` + serve http server on defined port with handlers to manipulate opened chotki instance +2. `swagger` + serve swagger on http://127.0.0.1:8000/, you can select ports in the top to work with different servers + ## Classes and objects 1. `class {_ref:Parent,Name:S,Girl:T}` diff --git a/repl/commands.go b/repl/commands.go index 1c3e101..cb1fd48 100644 --- a/repl/commands.go +++ b/repl/commands.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "net/http" "os" "path/filepath" "time" @@ -18,6 +19,25 @@ func replicaDirName(rno uint64) string { return fmt.Sprintf("cho%x", rno) } +func (repl *REPL) idFromNameOrText(a *rdx.RDX) (id rdx.ID, err error) { + switch a.RdxType { + case rdx.Reference: + id = rdx.IDFromText(a.Text) + case rdx.Term: + var names rdx.MapTR + names, err = repl.Host.MapTRField(chotki.IdNames) + if oid, ok := names[a.String()]; !ok { + err = fmt.Errorf("No such name") + return + } else { + id = oid + } + default: + err = fmt.Errorf("Wrong type") + } + return +} + var HelpCreate = errors.New("create zone/1 {Name:\"Name\",Description:\"long text\"}") func (repl *REPL) CommandCreate(arg *rdx.RDX) (id rdx.ID, err error) { @@ -181,10 +201,10 @@ func (repl *REPL) CommandClass(arg *rdx.RDX) (id rdx.ID, err error) { key := fields[i] val := fields[i+1] if string(key.Text) == "_ref" { - if val.RdxType != rdx.Reference || parent != rdx.ID0 { + if val.RdxType != rdx.Reference && val.RdxType != rdx.Term || parent != rdx.ID0 { return } - parent = rdx.IDFromText(val.Text) + parent, err = repl.idFromNameOrText(&val) continue } if key.RdxType != rdx.Term || val.RdxType != rdx.Term { @@ -233,7 +253,10 @@ func (repl *REPL) CommandNew(arg *rdx.RDX) (id rdx.ID, err error) { } else if arg.RdxType == rdx.Mapping { pairs := arg.Nested if len(pairs) >= 2 && pairs[0].String() == "_ref" { - tid = rdx.IDFromText(pairs[1].Text) + if pairs[1].RdxType != rdx.Reference && pairs[1].RdxType != rdx.Term { + return + } + tid, err = repl.idFromNameOrText(&pairs[1]) pairs = pairs[2:] } var fields chotki.Fields @@ -290,10 +313,11 @@ func (repl *REPL) CommandEdit(arg *rdx.RDX) (id rdx.ID, err error) { return } if arg.Nested[0].String() == "_id" { - if arg.Nested[1].RdxType != rdx.Reference { + if arg.Nested[1].RdxType != rdx.Reference && arg.Nested[1].RdxType != rdx.Term { return } - oid := rdx.IDFromText(arg.Nested[1].Text) + var oid rdx.ID + oid, err = repl.idFromNameOrText(&arg.Nested[1]) return repl.Host.EditObjectRDX(context.Background(), oid, arg.Nested[2:]) } else { // todo return @@ -308,10 +332,11 @@ func (repl *REPL) CommandAdd(arg *rdx.RDX) (id rdx.ID, err error) { if arg.RdxType == rdx.Mapping { pairs := arg.Nested for i := 0; i+1 < len(pairs) && err == nil; i += 2 { - if pairs[i].RdxType != rdx.Reference || pairs[i+1].RdxType != rdx.Integer { + if pairs[i].RdxType != rdx.Reference && pairs[i].RdxType != rdx.Term || pairs[i+1].RdxType != rdx.Integer { return rdx.BadId, HelpAdd } - fid := rdx.IDFromText(pairs[i].Text) + var fid rdx.ID + fid, err = repl.idFromNameOrText(&pairs[i]) var add uint64 _, err = fmt.Sscanf(string(pairs[i+1].Text), "%d", &add) if fid.Off() == 0 || err != nil { @@ -333,7 +358,7 @@ var HelpInc = errors.New( func (repl *REPL) CommandInc(arg *rdx.RDX) (id rdx.ID, err error) { id = rdx.BadId err = HelpInc - if arg.RdxType == rdx.Reference { + if arg.RdxType == rdx.Reference || arg.RdxType == rdx.Term { fid := rdx.IDFromText(arg.Text) if id.Off() == 0 { return @@ -403,10 +428,11 @@ var HelpCat = errors.New( func (repl *REPL) CommandCat(arg *rdx.RDX) (id rdx.ID, err error) { id = rdx.BadId err = HelpCat - if arg == nil || arg.RdxType != rdx.Reference { + if arg == nil || arg.RdxType != rdx.Reference && arg.RdxType != rdx.Term { return } - oid := rdx.IDFromText(arg.Text) + var oid rdx.ID + oid, err = repl.idFromNameOrText(arg) var txt string txt, err = repl.Host.ObjectString(oid) if err != nil { @@ -790,3 +816,49 @@ func (repl *REPL) CommandCompile(arg *rdx.RDX) (id rdx.ID, err error) { } return } + +func (repl *REPL) CommandSwagger(arg *rdx.RDX) (id rdx.ID, err error) { + mux := http.NewServeMux() + fs := http.FileServer(http.Dir("./swagger")) + + mux.Handle("/", fs) + mux.HandleFunc("/swagger.yaml", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "./swagger/swagger.yaml") + }) + + go func() { + err := http.ListenAndServe("127.0.0.1:8000", mux) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "failed to serve: %s\n", err.Error()) + } + }() + + return +} + +var HelpServeHttp = errors.New("servehttp 8001") + +func (repl *REPL) CommandServeHttp(arg *rdx.RDX) (id rdx.ID, err error) { + if arg == nil || arg.RdxType != rdx.Integer { + return rdx.BadId, HelpServeHttp + } + + mux := http.NewServeMux() + mux.HandleFunc("/listen", AddCorsHeaders(ListenHandler(repl))) + mux.HandleFunc("/connect", AddCorsHeaders(ConnectHandler(repl))) + mux.HandleFunc("/class", AddCorsHeaders(ClassHandler(repl))) + mux.HandleFunc("/name", AddCorsHeaders(NameHandler(repl))) + mux.HandleFunc("/new", AddCorsHeaders(NewHandler(repl))) + mux.HandleFunc("/edit", AddCorsHeaders(EditHandler(repl))) + mux.HandleFunc("/cat", AddCorsHeaders(CatHandler(repl))) + mux.HandleFunc("/list", AddCorsHeaders(ListHandler(repl))) + + go func() { + err := http.ListenAndServe("127.0.0.1:"+arg.String(), mux) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "failed to serve: %s\n", err.Error()) + } + }() + + return +} diff --git a/repl/handlers.go b/repl/handlers.go new file mode 100644 index 0000000..30f62c3 --- /dev/null +++ b/repl/handlers.go @@ -0,0 +1,260 @@ +package main + +import ( + "fmt" + "io" + "net/http" + "strings" + + "github.com/drpcorg/chotki/rdx" +) + +func AddCorsHeaders(f func(w http.ResponseWriter, req *http.Request)) func(w http.ResponseWriter, req *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Connection", "keep-alive") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "*") + w.Header().Set("Access-Control-Max-Age", "86400") + f(w, req) + } +} + +func ListenHandler(repl *REPL) func(w http.ResponseWriter, req *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + switch method := req.Method; method { + case "OPTIONS": + w.Header().Set("Access-Control-Allow-Methods", "POST") + w.WriteHeader(http.StatusNoContent) + case "POST": + body, err := io.ReadAll(req.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + arg, err := rdx.ParseRDX(body) + if arg == nil || arg.RdxType != rdx.String { + http.Error(w, fmt.Sprintf("Argument must be string"), http.StatusUnprocessableEntity) + return + } + _, err = repl.CommandListen(arg) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusOK) + default: + http.Error(w, fmt.Sprintf("Unsupported method %s", req.Method), http.StatusMethodNotAllowed) + } + } +} + +func ConnectHandler(repl *REPL) func(w http.ResponseWriter, req *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + switch method := req.Method; method { + case "OPTIONS": + w.Header().Set("Access-Control-Allow-Methods", "POST") + w.WriteHeader(http.StatusNoContent) + case "POST": + body, err := io.ReadAll(req.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + arg, err := rdx.ParseRDX(body) + if arg == nil || arg.RdxType != rdx.String { + http.Error(w, fmt.Sprintf("Argument must be string"), http.StatusUnprocessableEntity) + return + } + _, err = repl.CommandConnect(arg) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusOK) + default: + http.Error(w, fmt.Sprintf("Unsupported method %s", req.Method), http.StatusMethodNotAllowed) + } + } +} + +func ClassHandler(repl *REPL) func(w http.ResponseWriter, req *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + switch method := req.Method; method { + case "OPTIONS": + w.Header().Set("Access-Control-Allow-Methods", "POST") + w.WriteHeader(http.StatusNoContent) + case "POST": + body, err := io.ReadAll(req.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + arg, err := rdx.ParseRDX(body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if arg == nil || arg.RdxType != rdx.Mapping { + http.Error(w, fmt.Sprintf("Argument must be mapping"), http.StatusUnprocessableEntity) + return + } + id, err := repl.CommandClass(arg) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusOK) + w.Write([]byte(id.String())) + default: + http.Error(w, fmt.Sprintf("Unsupported method %s", req.Method), http.StatusMethodNotAllowed) + } + } +} + +func NameHandler(repl *REPL) func(w http.ResponseWriter, req *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + switch method := req.Method; method { + case "OPTIONS": + w.Header().Set("Access-Control-Allow-Methods", "PUT") + w.WriteHeader(http.StatusNoContent) + case "PUT": + body, err := io.ReadAll(req.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + arg, err := rdx.ParseRDX(body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if arg == nil || arg.RdxType != rdx.Mapping { + http.Error(w, fmt.Sprintf("Argument must be mapping"), http.StatusUnprocessableEntity) + return + } + id, err := repl.CommandName(arg) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusOK) + w.Write([]byte(id.String())) + default: + http.Error(w, fmt.Sprintf("Unsupported method %s", req.Method), http.StatusMethodNotAllowed) + } + } +} + +func NewHandler(repl *REPL) func(w http.ResponseWriter, req *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + switch method := req.Method; method { + case "OPTIONS": + w.Header().Set("Access-Control-Allow-Methods", "POST") + w.WriteHeader(http.StatusNoContent) + case "POST": + body, err := io.ReadAll(req.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + arg, err := rdx.ParseRDX(body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if arg == nil || arg.RdxType != rdx.Mapping { + http.Error(w, fmt.Sprintf("Argument must be mapping"), http.StatusUnprocessableEntity) + return + } + id, err := repl.CommandNew(arg) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusOK) + w.Write([]byte(id.String())) + default: + http.Error(w, fmt.Sprintf("Unsupported method %s", req.Method), http.StatusMethodNotAllowed) + } + } +} + +func EditHandler(repl *REPL) func(w http.ResponseWriter, req *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + switch method := req.Method; method { + case "OPTIONS": + w.Header().Set("Access-Control-Allow-Methods", "PUT") + w.WriteHeader(http.StatusNoContent) + case "PUT": + body, err := io.ReadAll(req.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + arg, err := rdx.ParseRDX(body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if arg == nil || arg.RdxType != rdx.Mapping { + http.Error(w, fmt.Sprintf("Argument must be eulerian"), http.StatusUnprocessableEntity) + return + } + id, err := repl.CommandEdit(arg) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusOK) + w.Write([]byte(id.String())) + default: + http.Error(w, fmt.Sprintf("Unsupported method %s", req.Method), http.StatusMethodNotAllowed) + } + } +} + +func CatHandler(repl *REPL) func(w http.ResponseWriter, req *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + switch method := req.Method; method { + case "OPTIONS": + w.Header().Set("Access-Control-Allow-Methods", "GET") + w.WriteHeader(http.StatusNoContent) + case "GET": + id := req.URL.Query().Get("id") + oid := rdx.IDFromText([]byte(id)) + + txt, err := repl.Host.ObjectString(oid) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusOK) + w.Write([]byte(txt)) + default: + http.Error(w, fmt.Sprintf("Unsupported method %s", req.Method), http.StatusMethodNotAllowed) + } + } +} + +func ListHandler(repl *REPL) func(w http.ResponseWriter, req *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + switch method := req.Method; method { + case "OPTIONS": + w.Header().Set("Access-Control-Allow-Methods", "GET") + w.WriteHeader(http.StatusNoContent) + case "GET": + id := req.URL.Query().Get("id") + oid := rdx.IDFromText([]byte(id)) + + strs, err := repl.ListObject(oid) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusOK) + w.Write([]byte(strings.Join(strs, ""))) + default: + http.Error(w, fmt.Sprintf("Unsupported method %s", req.Method), http.StatusMethodNotAllowed) + } + } +} diff --git a/repl/repl.go b/repl/repl.go index 0b3556f..eae5f00 100644 --- a/repl/repl.go +++ b/repl/repl.go @@ -58,6 +58,9 @@ var completer = readline.NewPrefixCompleter( readline.PcItem("ponc"), readline.PcItem("tinc"), readline.PcItem("sinc"), + + readline.PcItem("servehttp"), + readline.PcItem("swagger"), ) func filterInput(r rune) (rune, bool) { @@ -127,6 +130,10 @@ func (repl *REPL) REPL(line string) (id rdx.ID, err error) { id, err = repl.CommandOpen(arg) case "opendir": id, err = repl.CommandOpenDir(arg) + case "swagger": + id, err = repl.CommandSwagger(arg) + case "servehttp": + id, err = repl.CommandServeHttp(arg) case "checkpoint", "cp": id, err = repl.CommandCheckpoint(arg) case "close": diff --git a/swagger/index.html b/swagger/index.html new file mode 100644 index 0000000..663495c --- /dev/null +++ b/swagger/index.html @@ -0,0 +1,22 @@ + + + + + + + SwaggerUI + + + +
+ + + + diff --git a/swagger/swagger.yaml b/swagger/swagger.yaml new file mode 100644 index 0000000..988ddcd --- /dev/null +++ b/swagger/swagger.yaml @@ -0,0 +1,207 @@ +openapi: 3.0.3 +info: + title: Swagger Chotki - OpenAPI 3.0 + description: |- + This is a chotki example backend server based on the OpenAPI 3.0 specification. You can find out more about + Swagger at [https://swagger.io](https://swagger.io). + termsOfService: http://swagger.io/terms/ + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.0 +servers: + - url: http://127.0.0.1:8001 + - url: http://127.0.0.1:8002 + - url: http://127.0.0.1:8003 + - url: http://127.0.0.1:8004 + - url: http://127.0.0.1:8005 + - url: http://127.0.0.1:8006 + - url: http://127.0.0.1:8007 + - url: http://127.0.0.1:8008 + - url: http://127.0.0.1:8009 + - url: http://127.0.0.1:8010 +paths: + /connect: + post: + tags: + - commands + summary: Connect a listening replica by TCP route + operationId: connect + requestBody: + description: TCP route to connect + content: + plain/text: + example: 'tcp://127.0.0.1:8100' + responses: + '200': + description: Successful operation + '400': + description: Something went wrong + '404': + description: Key was not found + '422': + description: Validation exception + /listen: + post: + tags: + - commands + summary: Listen for connections to synchronize + operationId: listen + requestBody: + description: Start listening for synchronization connections + content: + plain/text: + example: 'tcp://127.0.0.1:8100' + responses: + '200': + description: Successful operation + '400': + description: Something went wrong + '404': + description: Key was not found + '422': + description: Validation exception + /class: + post: + tags: + - commands + summary: Create class + operationId: class + requestBody: + description: Class creation details. Use _ref for inheritance. + content: + plain/text: + example: '{_ref:Parent,Name:S,Girl:T}' + responses: + '200': + description: Successful operation + content: + plain/text: + example: 'b0b-1' + '400': + description: Something went wrong + '404': + description: Key was not found + '422': + description: Validation exception + /name: + put: + tags: + - commands + summary: Create or update a name + operationId: name + requestBody: + description: '{name:id}' + content: + plain/text: + example: '{Child:b0b-9}' + responses: + '200': + description: Successful operation + content: + plain/text: + example: 'b0b-1' + '400': + description: Something went wrong + '404': + description: Key was not found + '422': + description: Validation exception + /new: + post: + tags: + - commands + summary: Create a new object + operationId: new + requestBody: + description: Object creation details using class in _ref + content: + plain/text: + example: '{_ref:Child,Name:"Alice"}' + responses: + '200': + description: Successful operation + content: + plain/text: + example: 'b0b-1' + '400': + description: Something went wrong + '404': + description: Key was not found + '422': + description: Validation exception + /edit: + put: + tags: + - commands + summary: Edit an existing object + operationId: edit + requestBody: + description: Object edit details + content: + plain/text: + example: '{_ref:b0b-a,Girl:true}' + responses: + '200': + description: 'Successful operation' + content: + plain/text: + example: 'b0b-1' + '400': + description: Something went wrong + '404': + description: Key was not found + '422': + description: Validation exception + /cat: + get: + tags: + - commands + summary: Get an object partially + operationId: cat + parameters: + - in: query + name: id + required: true + schema: + type: string + description: ID of the object to retrieve + responses: + '200': + description: Successful operation + content: + plain/text: + example: '{Name: "Bob", Girl: False}' + '400': + description: Something went wrong + '404': + description: Key was not found + '422': + description: Validation exception + /list: + get: + tags: + - commands + summary: Get an object completely + operationId: list + parameters: + - in: query + name: id + required: true + schema: + type: string + description: ID of the object to retrieve + responses: + '200': + description: Successful operation + content: + plain/text: + example: '{Name: "Bob", Girl: False}' + '400': + description: Something went wrong + '404': + description: Key was not found + '422': + description: Validation exception