Skip to content

Commit

Permalink
Move StubServer to its own file
Browse files Browse the repository at this point in the history
  • Loading branch information
brandur committed Jun 23, 2017
1 parent 9b192eb commit 8d2c421
Show file tree
Hide file tree
Showing 4 changed files with 231 additions and 214 deletions.
149 changes: 7 additions & 142 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ import (
"log"
"net"
"net/http"
"regexp"
"strconv"
"strings"
"time"
)

const defaultPort = 6065

// verbose tracks whether the program is operating in verbose mode
var verbose bool

// ---

type Fixtures struct {
Resources map[ResourceID]interface{} `json:"resources"`
}
Expand Down Expand Up @@ -64,147 +68,8 @@ type OpenAPIStatusCode string

type ResourceID string

type StubServerRoute struct {
pattern *regexp.Regexp
method *OpenAPIMethod
}

type StubServer struct {
fixtures *Fixtures
routes map[HTTPVerb][]StubServerRoute
spec *OpenAPISpec
}

func (s *StubServer) routeRequest(r *http.Request) *OpenAPIMethod {
verbRoutes := s.routes[HTTPVerb(r.Method)]
for _, route := range verbRoutes {
if route.pattern.MatchString(r.URL.Path) {
return route.method
}
}
return nil
}

func (s *StubServer) handleRequest(w http.ResponseWriter, r *http.Request) {
log.Printf("Request: %v %v", r.Method, r.URL.Path)
start := time.Now()

method := s.routeRequest(r)
if method == nil {
writeResponse(w, start, http.StatusNotFound, nil)
return
}

response, ok := method.Responses["200"]
if !ok {
log.Printf("Couldn't find 200 response in spec")
writeResponse(w, start, http.StatusInternalServerError, nil)
return
}

if verbose {
log.Printf("Response schema: %+v", response.Schema)
}

generator := DataGenerator{s.spec.Definitions, s.fixtures}
data, err := generator.Generate(response.Schema, r.URL.Path)
if err != nil {
log.Printf("Couldn't generate response: %v", err)
writeResponse(w, start, http.StatusInternalServerError, nil)
return
}
writeResponse(w, start, http.StatusOK, data)
}

func (s *StubServer) initializeRouter() {
var numEndpoints int
var numPaths int

s.routes = make(map[HTTPVerb][]StubServerRoute)

for path, verbs := range s.spec.Paths {
numPaths++

pathPattern := compilePath(path)

if verbose {
log.Printf("Compiled path: %v", pathPattern.String())
}

for verb, method := range verbs {
numEndpoints++

route := StubServerRoute{
pattern: pathPattern,
method: method,
}

// net/http will always give us verbs in uppercase, so build our
// routing table this way too
verb = HTTPVerb(strings.ToUpper(string(verb)))

s.routes[verb] = append(s.routes[verb], route)
}
}

log.Printf("Routing to %v path(s) and %v endpoint(s)",
numPaths, numEndpoints)
}

// ---

var pathParameterPattern = regexp.MustCompile(`\{(\w+)\}`)

func compilePath(path OpenAPIPath) *regexp.Regexp {
pattern := `\A`
parts := strings.Split(string(path), "/")

for _, part := range parts {
if part == "" {
continue
}

submatches := pathParameterPattern.FindAllStringSubmatch(part, -1)
if submatches == nil {
pattern += `/` + part
} else {
pattern += `/(?P<` + submatches[0][1] + `>\w+)`
}
}

return regexp.MustCompile(pattern + `\z`)
}

func writeResponse(w http.ResponseWriter, start time.Time, status int, data interface{}) {
if data == nil {
data = []byte(http.StatusText(status))
}

encodedData, err := json.Marshal(&data)
if err != nil {
log.Printf("Error serializing response: %v", err)
writeResponse(w, start, http.StatusInternalServerError, nil)
return
}

w.WriteHeader(status)
_, err = w.Write(encodedData)
if err != nil {
log.Printf("Error writing to client: %v", err)
}
log.Printf("Response: elapsed=%v status=%v", time.Now().Sub(start), status)
if verbose {
log.Printf("Response body: %v", encodedData)
}
}

// ---

const defaultPort = 6065

// verbose tracks whether the program is operating in verbose mode
var verbose bool

func main() {
var port int
var unix string
Expand Down
73 changes: 1 addition & 72 deletions main_test.go
Original file line number Diff line number Diff line change
@@ -1,74 +1,3 @@
package main

import (
"net/http"
"net/url"
"testing"

assert "github.com/stretchr/testify/require"
)

var chargeAllMethod *OpenAPIMethod
var chargeCreateMethod *OpenAPIMethod
var chargeDeleteMethod *OpenAPIMethod
var chargeGetMethod *OpenAPIMethod
var testSpec *OpenAPISpec
var testFixtures *Fixtures

func init() {
chargeAllMethod = &OpenAPIMethod{}
chargeCreateMethod = &OpenAPIMethod{}
chargeDeleteMethod = &OpenAPIMethod{}
chargeGetMethod = &OpenAPIMethod{}

testFixtures =
&Fixtures{
Resources: map[ResourceID]interface{}{
ResourceID("charge"): map[string]interface{}{"id": "ch_123"},
},
}

testSpec = &OpenAPISpec{
Definitions: map[string]*JSONSchema{
"charge": {XResourceID: "charge"},
},
Paths: map[OpenAPIPath]map[HTTPVerb]*OpenAPIMethod{
OpenAPIPath("/v1/charges"): {
"get": chargeAllMethod,
"post": chargeCreateMethod,
},
OpenAPIPath("/v1/charges/{id}"): {
"get": chargeGetMethod,
"delete": chargeDeleteMethod,
},
},
}
}

// ---

func TestStubServerRouteRequest(t *testing.T) {
server := &StubServer{spec: testSpec}
server.initializeRouter()

assert.Equal(t, chargeAllMethod, server.routeRequest(
&http.Request{Method: "GET", URL: &url.URL{Path: "/v1/charges"}}))
assert.Equal(t, chargeCreateMethod, server.routeRequest(
&http.Request{Method: "POST", URL: &url.URL{Path: "/v1/charges"}}))
assert.Equal(t, chargeGetMethod, server.routeRequest(
&http.Request{Method: "GET", URL: &url.URL{Path: "/v1/charges/ch_123"}}))
assert.Equal(t, chargeDeleteMethod, server.routeRequest(
&http.Request{Method: "DELETE", URL: &url.URL{Path: "/v1/charges/ch_123"}}))

assert.Equal(t, (*OpenAPIMethod)(nil), server.routeRequest(
&http.Request{Method: "GET", URL: &url.URL{Path: "/v1/doesnt-exist"}}))
}

// ---

func TestCompilePath(t *testing.T) {
assert.Equal(t, `\A/v1/charges\z`,
compilePath(OpenAPIPath("/v1/charges")).String())
assert.Equal(t, `\A/v1/charges/(?P<id>\w+)\z`,
compilePath(OpenAPIPath("/v1/charges/{id}")).String())
}
import ()
149 changes: 149 additions & 0 deletions server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package main

import (
"encoding/json"
"log"
"net/http"
"regexp"
"strings"
"time"
)

// StubServer handles incoming HTTP requests and responds to them appropriately
// based off the set of OpenAPI routes that it's been configured with.
type StubServer struct {
fixtures *Fixtures
routes map[HTTPVerb][]stubServerRoute
spec *OpenAPISpec
}

// stubServerRoute is a single route in a StubServer's routing table. It has a
// pattern to match an incoming path and a description of the method that would
// be executed in the event of a match.
type stubServerRoute struct {
pattern *regexp.Regexp
method *OpenAPIMethod
}

func (s *StubServer) routeRequest(r *http.Request) *OpenAPIMethod {
verbRoutes := s.routes[HTTPVerb(r.Method)]
for _, route := range verbRoutes {
if route.pattern.MatchString(r.URL.Path) {
return route.method
}
}
return nil
}

func (s *StubServer) handleRequest(w http.ResponseWriter, r *http.Request) {
log.Printf("Request: %v %v", r.Method, r.URL.Path)
start := time.Now()

method := s.routeRequest(r)
if method == nil {
writeResponse(w, start, http.StatusNotFound, nil)
return
}

response, ok := method.Responses["200"]
if !ok {
log.Printf("Couldn't find 200 response in spec")
writeResponse(w, start, http.StatusInternalServerError, nil)
return
}

if verbose {
log.Printf("Response schema: %+v", response.Schema)
}

generator := DataGenerator{s.spec.Definitions, s.fixtures}
data, err := generator.Generate(response.Schema, r.URL.Path)
if err != nil {
log.Printf("Couldn't generate response: %v", err)
writeResponse(w, start, http.StatusInternalServerError, nil)
return
}
writeResponse(w, start, http.StatusOK, data)
}

func (s *StubServer) initializeRouter() {
var numEndpoints int
var numPaths int

s.routes = make(map[HTTPVerb][]stubServerRoute)

for path, verbs := range s.spec.Paths {
numPaths++

pathPattern := compilePath(path)

if verbose {
log.Printf("Compiled path: %v", pathPattern.String())
}

for verb, method := range verbs {
numEndpoints++

route := stubServerRoute{
pattern: pathPattern,
method: method,
}

// net/http will always give us verbs in uppercase, so build our
// routing table this way too
verb = HTTPVerb(strings.ToUpper(string(verb)))

s.routes[verb] = append(s.routes[verb], route)
}
}

log.Printf("Routing to %v path(s) and %v endpoint(s)",
numPaths, numEndpoints)
}

// ---

var pathParameterPattern = regexp.MustCompile(`\{(\w+)\}`)

func compilePath(path OpenAPIPath) *regexp.Regexp {
pattern := `\A`
parts := strings.Split(string(path), "/")

for _, part := range parts {
if part == "" {
continue
}

submatches := pathParameterPattern.FindAllStringSubmatch(part, -1)
if submatches == nil {
pattern += `/` + part
} else {
pattern += `/(?P<` + submatches[0][1] + `>\w+)`
}
}

return regexp.MustCompile(pattern + `\z`)
}

func writeResponse(w http.ResponseWriter, start time.Time, status int, data interface{}) {
if data == nil {
data = []byte(http.StatusText(status))
}

encodedData, err := json.Marshal(&data)
if err != nil {
log.Printf("Error serializing response: %v", err)
writeResponse(w, start, http.StatusInternalServerError, nil)
return
}

w.WriteHeader(status)
_, err = w.Write(encodedData)
if err != nil {
log.Printf("Error writing to client: %v", err)
}
log.Printf("Response: elapsed=%v status=%v", time.Now().Sub(start), status)
if verbose {
log.Printf("Response body: %v", encodedData)
}
}
Loading

0 comments on commit 8d2c421

Please sign in to comment.