Skip to content

Commit

Permalink
feat: add HTTP request
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Collins <[email protected]>
  • Loading branch information
alexec committed Mar 26, 2023
1 parent b28049f commit 2366141
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 54 deletions.
22 changes: 22 additions & 0 deletions examples/proxy.yaml
Original file line number Diff line number Diff line change
@@ -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
56 changes: 56 additions & 0 deletions internal/body.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
11 changes: 11 additions & 0 deletions internal/console.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package internal

import (
"log"
)

var console = map[string]any{
"log": func(args ...any) {
log.Println(append([]any{"console:"}, args...)...)
},
}
53 changes: 53 additions & 0 deletions internal/http_service.go
Original file line number Diff line number Diff line change
@@ -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
}
11 changes: 11 additions & 0 deletions internal/random_uuid.go
Original file line number Diff line number Diff line change
@@ -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()
}
11 changes: 3 additions & 8 deletions types.go → internal/response.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
49 changes: 19 additions & 30 deletions sim.go → internal/sim.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package internal

import (
"encoding/json"
Expand All @@ -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"
)

Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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
Expand Down
43 changes: 43 additions & 0 deletions internal/types/request.go
Original file line number Diff line number Diff line change
@@ -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"]
}
Loading

0 comments on commit 2366141

Please sign in to comment.