diff --git a/examples/proxy.yaml b/examples/proxy.yaml new file mode 100755 index 0000000..5842b34 --- /dev/null +++ b/examples/proxy.yaml @@ -0,0 +1,22 @@ +#!/usr/bin/env sim +openapi: 3.0.0 +info: + title: Proxy API + version: 1.0.0 +servers: + - url: http://localhost:5050 +paths: + /proxy: + get: + x-sim-script: | + hello = http({"url": "http://localhost:8080/hello"}) + response = { + "status": hello["status"], + "headers": { + "Proxy": "true" + }, + "body": hello.body + } + responses: + '200': + description: OK diff --git a/internal/body.go b/internal/body.go new file mode 100644 index 0000000..b3f0b55 --- /dev/null +++ b/internal/body.go @@ -0,0 +1,56 @@ +package internal + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + + "github.com/kitproj/sim/internal/types" +) + +func getBody(r types.Request) (*bytes.Buffer, error) { + w := &bytes.Buffer{} + var err error + switch value := r.GetBody(); body := value.(type) { + case nil: + case string: + _, err = w.Write([]byte(body)) + case []byte: + _, err = w.Write(body) + default: + err = json.NewEncoder(w).Encode(body) + } + return w, err +} + +func readBody(r *http.Response) (any, error) { + cty := r.Header.Get("Content-Type") + switch cty { + case "": + return nil, nil + case "application/json": + out := map[string]any{} + err := json.NewDecoder(r.Body).Decode(&out) + return out, err + default: + out := &bytes.Buffer{} + _, err := io.Copy(out, r.Body) + return out.String(), err + } +} + +func writeBody(w io.Writer, value any) error { + switch body := value.(type) { + case nil: + return nil + case string: + _, err := w.Write([]byte(body)) + return err + case []byte: + _, err := w.Write(body) + return err + default: + return json.NewEncoder(w).Encode(body) + } +} diff --git a/internal/console.go b/internal/console.go new file mode 100644 index 0000000..06472fb --- /dev/null +++ b/internal/console.go @@ -0,0 +1,11 @@ +package internal + +import ( + "log" +) + +var console = map[string]any{ + "log": func(args ...any) { + log.Println(append([]any{"console:"}, args...)...) + }, +} diff --git a/internal/http_service.go b/internal/http_service.go new file mode 100644 index 0000000..b953aaa --- /dev/null +++ b/internal/http_service.go @@ -0,0 +1,53 @@ +package internal + +import ( + "fmt" + "io" + "log" + "net/http" + + "github.com/kitproj/sim/internal/types" +) + +func httpService(r types.Request) map[string]any { + w, err := getBody(r) + if err != nil { + panic(fmt.Errorf("failed to make HTTP request body: %w", err)) + } + log.Printf("HTTP %s %s", r.GetMethod(), r.GetURL()) + resp, err := http.DefaultClient.Do(&http.Request{ + Method: r.GetMethod(), + URL: r.GetURL(), + Header: httpHeaders(r.GetHeaders()), + Body: io.NopCloser(w), + }) + log.Printf("HTTP %s %s %d", r.GetMethod(), r.GetURL(), resp.StatusCode) + if err != nil { + panic(fmt.Errorf("failed to make HTTP request: %w", err)) + } + body, err := readBody(resp) + if err != nil { + panic(fmt.Errorf("failed to read HTTP response body: %w", err)) + } + return Response{ + "status": resp.StatusCode, + "headers": reverseHttpHeaders(resp.Header), + "body": body, + } +} + +func httpHeaders(in map[string]string) http.Header { + out := http.Header{} + for k, v := range in { + out.Set(k, v) + } + return out +} + +func reverseHttpHeaders(in http.Header) map[string]string { + out := map[string]string{} + for k, v := range in { + out[k] = v[0] + } + return out +} diff --git a/internal/random_uuid.go b/internal/random_uuid.go new file mode 100644 index 0000000..3732f7f --- /dev/null +++ b/internal/random_uuid.go @@ -0,0 +1,11 @@ +package internal + +import "github.com/google/uuid" + +func randomUUID() string { + random, err := uuid.NewRandom() + if err != nil { + panic(err) + } + return random.String() +} diff --git a/types.go b/internal/response.go similarity index 80% rename from types.go rename to internal/response.go index 4366883..b214253 100644 --- a/types.go +++ b/internal/response.go @@ -1,16 +1,11 @@ -package main +package internal -import ( - "fmt" -) - -type Request map[string]any +import "fmt" type Response map[string]any func (r Response) GetStatus() int { - v, ok := r["status"].(int64) - if ok { + if v, ok := r["status"].(int64); ok { return int(v) } if r.GetBody() != nil { diff --git a/sim.go b/internal/sim.go similarity index 89% rename from sim.go rename to internal/sim.go index de4ad38..a41bad5 100644 --- a/sim.go +++ b/internal/sim.go @@ -1,4 +1,4 @@ -package main +package internal import ( "encoding/json" @@ -12,7 +12,6 @@ import ( "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/routers" "github.com/getkin/kin-openapi/routers/gorillamux" - "github.com/google/uuid" "github.com/kitproj/sim/internal/db" ) @@ -23,7 +22,16 @@ type Sim struct { routers map[*openapi3.T]routers.Router } -func (s *Sim) add(path string) error { +func NewSim() *Sim { + return &Sim{ + servers: make(map[int]*http.Server), + specs: make(map[string]*openapi3.T), + vms: make(map[*openapi3.T]*goja.Runtime), + routers: make(map[*openapi3.T]routers.Router), + } +} + +func (s *Sim) Add(path string) error { spec, err := openapi3.NewLoader().LoadFromFile(path) if err != nil { return err @@ -53,22 +61,20 @@ func (s *Sim) add(path string) error { return err } s.routers[spec] = router - vm := goja.New() vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true)) - var randomUUID = func() string { - random, err := uuid.NewRandom() - if err != nil { - panic(err) - } - return random.String() - } if err := vm.Set("randomUUID", randomUUID); err != nil { return err } + if err := vm.Set("http", httpService); err != nil { + return err + } if err := vm.Set("db", db.Instance); err != nil { return err } + if err := vm.Set("console", console); err != nil { + return err + } script, ok := spec.Extensions["x-sim-script"] if ok { log.Printf("Found x-sim-script: %v", script) @@ -101,8 +107,6 @@ func (s *Sim) add(path string) error { } func (s *Sim) Handle(w http.ResponseWriter, r *http.Request) { - mu.Lock() - defer mu.Unlock() log.Printf("Request: %s %s", r.Method, r.URL.Path) log.Printf("Request URL: %v", r.Host) spec, route, pathParams, err := s.find(r) @@ -112,20 +116,6 @@ func (s *Sim) Handle(w http.ResponseWriter, r *http.Request) { } op := route.Operation log.Printf("Found operation: %v", op.OperationID) - var writeBody = func(value any) error { - switch body := value.(type) { - case nil: - return nil - case string: - _, err := w.Write([]byte(body)) - return err - case []byte: - _, err := w.Write(body) - return err - default: - return json.NewEncoder(w).Encode(body) - } - } script, ok := op.Extensions["x-sim-script"] if ok { log.Printf("Found x-sim-script: %v", script) @@ -149,7 +139,6 @@ func (s *Sim) Handle(w http.ResponseWriter, r *http.Request) { } vm := s.vms[spec] log.Printf("globals: %v", vm.GlobalObject().Keys()) - if err := vm.Set("request", request); err != nil { http.Error(w, fmt.Sprintf("failed to set request: %v", err), http.StatusInternalServerError) return @@ -172,7 +161,7 @@ func (s *Sim) Handle(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") } w.WriteHeader(response.GetStatus()) - if err := writeBody(response.GetBody()); err != nil { + if err := writeBody(w, response.GetBody()); err != nil { http.Error(w, fmt.Sprintf("failed to encode response: %v", err), http.StatusInternalServerError) return } @@ -184,7 +173,7 @@ func (s *Sim) Handle(w http.ResponseWriter, r *http.Request) { for mediaType, value := range resp.Value.Content { w.Header().Set("Content-Type", mediaType) w.WriteHeader(status) - if err := writeBody(value.Example); err != nil { + if err := writeBody(w, value.Example); err != nil { http.Error(w, fmt.Sprintf("failed to encode response: %v", err), http.StatusInternalServerError) } return diff --git a/internal/types/request.go b/internal/types/request.go new file mode 100644 index 0000000..1327a9e --- /dev/null +++ b/internal/types/request.go @@ -0,0 +1,43 @@ +package types + +import ( + "fmt" + "net/url" +) + +type Request map[string]any + +func (r Request) GetMethod() string { + if v, ok := r["method"].(string); ok { + return v + } + return "GET" +} + +func (r Request) GetURL() *url.URL { + v, ok := r["url"].(string) + if !ok { + panic(fmt.Errorf("url absent or not a string")) + } + parsed, err := url.Parse(v) + if err != nil { + panic(fmt.Errorf("invalid url %q: %w", v, err)) + } + return parsed +} + +func (r Request) GetHeaders() map[string]string { + out := map[string]string{} + headers, ok := r["headers"].(map[string]any) + if !ok { + return nil + } + for k, v := range headers { + out[k] = fmt.Sprint(v) + } + return out +} + +func (r Request) GetBody() any { + return r["body"] +} diff --git a/main.go b/main.go index 265a932..8737960 100644 --- a/main.go +++ b/main.go @@ -3,17 +3,14 @@ package main import ( "context" "log" - "net/http" "os" "os/signal" "path/filepath" - "sync" "syscall" - "github.com/dop251/goja" + "github.com/kitproj/sim/internal" + "github.com/fsnotify/fsnotify" - "github.com/getkin/kin-openapi/openapi3" - "github.com/getkin/kin-openapi/routers" ) func init() { @@ -30,12 +27,7 @@ func main() { defer stop() // Find OpenAPI spec files in directory - sim := &Sim{ - servers: make(map[int]*http.Server), - specs: make(map[string]*openapi3.T), - vms: make(map[*openapi3.T]*goja.Runtime), - routers: make(map[*openapi3.T]routers.Router), - } + sim := internal.NewSim() watcher, err := fsnotify.NewWatcher() if err != nil { @@ -61,12 +53,12 @@ func main() { if filepath.Ext(file.Name()) != ".yaml" { continue } - if err := sim.add(filepath.Join(path, file.Name())); err != nil { + if err := sim.Add(filepath.Join(path, file.Name())); err != nil { log.Fatalf("Error adding spec: %s\n", err) } } } else { - if err := sim.add(path); err != nil { + if err := sim.Add(path); err != nil { log.Fatalf("Error adding spec: %s\n", err) } } @@ -81,12 +73,10 @@ func main() { case event := <-watcher.Events: log.Println("event:", event) if filepath.Ext(event.Name) == ".yaml" && (event.Has(fsnotify.Write) || event.Has(fsnotify.Create)) { - if err := sim.add(event.Name); err != nil { + if err := sim.Add(event.Name); err != nil { log.Printf("Error adding spec: %s\n", err) } } } } } - -var mu = &sync.Mutex{} diff --git a/main_test.go b/main_test.go index aa00767..7896843 100644 --- a/main_test.go +++ b/main_test.go @@ -61,4 +61,10 @@ func TestSim(t *testing.T) { assert.Equal(t, 204, resp.StatusCode) }) }) + t.Run("proxy", func(t *testing.T) { + resp, err := http.Get("http://localhost:5050/proxy") + assert.NoError(t, err) + assert.Equal(t, 200, resp.StatusCode) + assert.Equal(t, "true", resp.Header.Get("Proxy")) + }) }