From 8d2c421b4caefefe04e3ccc042eea3c2efff738d Mon Sep 17 00:00:00 2001 From: Brandur Date: Fri, 23 Jun 2017 14:31:59 -0700 Subject: [PATCH] Move StubServer to its own file --- main.go | 149 +++---------------------------------------------- main_test.go | 73 +----------------------- server.go | 149 +++++++++++++++++++++++++++++++++++++++++++++++++ server_test.go | 74 ++++++++++++++++++++++++ 4 files changed, 231 insertions(+), 214 deletions(-) create mode 100644 server.go create mode 100644 server_test.go diff --git a/main.go b/main.go index 35f3a3b7..7b3c0299 100644 --- a/main.go +++ b/main.go @@ -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"` } @@ -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 diff --git a/main_test.go b/main_test.go index f3baf25d..221e9231 100644 --- a/main_test.go +++ b/main_test.go @@ -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\w+)\z`, - compilePath(OpenAPIPath("/v1/charges/{id}")).String()) -} +import () diff --git a/server.go b/server.go new file mode 100644 index 00000000..4faa0589 --- /dev/null +++ b/server.go @@ -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) + } +} diff --git a/server_test.go b/server_test.go new file mode 100644 index 00000000..f3baf25d --- /dev/null +++ b/server_test.go @@ -0,0 +1,74 @@ +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\w+)\z`, + compilePath(OpenAPIPath("/v1/charges/{id}")).String()) +}