diff --git a/api.go b/api.go index c9106d9..aea6ba7 100644 --- a/api.go +++ b/api.go @@ -2,6 +2,8 @@ package apidemic import ( "encoding/json" + "errors" + "fmt" "net/http" "time" @@ -9,8 +11,8 @@ import ( "github.com/pmylund/go-cache" ) -//Version is the version of apidemic. Apidemic uses semver. -const Version = "0.2" +// Version is the version of apidemic. Apidemic uses semver. +const Version = "0.3" var maxItemTime = cache.DefaultExpiration @@ -19,10 +21,13 @@ var store = func() *cache.Cache { return c }() -//API is the struct for the json object that is passed to apidemic for registration. +var allowedHttpMethods = []string{"OPTIONS", "GET", "POST", "PUT", "DELETE", "HEAD"} + +// API is the struct for the json object that is passed to apidemic for registration. type API struct { - Endpoint string `json:"endpoint"` - Payload map[string]interface{} `json:"payload"` + Endpoint string `json:"endpoint"` + HTTPMethod string `json:"http_method"` + Payload map[string]interface{} `json:"payload"` } // Home renders hopme page. It renders a json response with information about the service. @@ -35,10 +40,11 @@ func Home(w http.ResponseWriter, r *http.Request) { return } -// RenderJSON helder for rndering JSON response, it marshals value into json and writes +// RenderJSON helper for rendering JSON response, it marshals value into json and writes // it into w. func RenderJSON(w http.ResponseWriter, code int, value interface{}) { w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) err := json.NewEncoder(w).Encode(value) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -46,16 +52,24 @@ func RenderJSON(w http.ResponseWriter, code int, value interface{}) { } } -//RegisterEndpoint receives API objects and registers them. The payload from the request is +// RegisterEndpoint receives API objects and registers them. The payload from the request is // transformed into a self aware Value that is capable of faking its own attribute. func RegisterEndpoint(w http.ResponseWriter, r *http.Request) { + var httpMethod string a := API{} err := json.NewDecoder(r.Body).Decode(&a) if err != nil { - RenderJSON(w, http.StatusInternalServerError, NewResponse(err.Error())) + RenderJSON(w, http.StatusBadRequest, NewResponse(err.Error())) + return + } + + if httpMethod, err = getAllowedMethod(a.HTTPMethod); err != nil { + RenderJSON(w, http.StatusBadRequest, NewResponse(err.Error())) return } - if _, ok := store.Get(a.Endpoint); ok { + + eKey := getCacheKeys(a.Endpoint, httpMethod) + if _, ok := store.Get(eKey); ok { RenderJSON(w, http.StatusOK, NewResponse("endpoint already taken")) return } @@ -65,22 +79,50 @@ func RegisterEndpoint(w http.ResponseWriter, r *http.Request) { RenderJSON(w, http.StatusInternalServerError, NewResponse(err.Error())) return } - store.Set(a.Endpoint, obj, maxItemTime) + store.Set(eKey, obj, maxItemTime) RenderJSON(w, http.StatusOK, NewResponse("cool")) } -//GetEndpoint renders registered endpoints. -func GetEndpoint(w http.ResponseWriter, r *http.Request) { +func getCacheKeys(endpoint, httpMethod string) string { + eKey := fmt.Sprintf("%s-%v-e", endpoint, httpMethod) + + return eKey +} + +func getAllowedMethod(method string) (string, error) { + if method == "" { + return "GET", nil + } + + for _, m := range allowedHttpMethods { + if method == m { + return m, nil + } + } + + return "", errors.New("HTTP method is not allowed") +} + +// DynamicEndpoint renders registered endpoints. +func DynamicEndpoint(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - endpoint := vars["endpoint"] - if eVal, ok := store.Get(endpoint); ok { - RenderJSON(w, http.StatusOK, eVal) + code := http.StatusOK + + eKey := getCacheKeys(vars["endpoint"], r.Method) + if eVal, ok := store.Get(eKey); ok { + if r.Method == "POST" { + code = http.StatusCreated + } + + RenderJSON(w, code, eVal) return } - RenderJSON(w, http.StatusNotFound, NewResponse("apidemic: "+endpoint+" is not found")) + + responseText := fmt.Sprintf("apidemic: %s has no %s endpoint", vars["endpoint"], r.Method) + RenderJSON(w, http.StatusNotFound, NewResponse(responseText)) } -//NewResponse helper for response JSON message +// NewResponse helper for response JSON message func NewResponse(message string) interface{} { return struct { Text string `json:"text"` @@ -89,11 +131,11 @@ func NewResponse(message string) interface{} { } } -//NewServer returns a new apidemic server +// NewServer returns a new apidemic server func NewServer() *mux.Router { m := mux.NewRouter() m.HandleFunc("/", Home) m.HandleFunc("/register", RegisterEndpoint).Methods("POST") - m.HandleFunc("/api/{endpoint}", GetEndpoint).Methods("GET") + m.HandleFunc("/api/{endpoint}", DynamicEndpoint).Methods("OPTIONS", "GET", "POST", "PUT", "DELETE", "HEAD") return m } diff --git a/api_test.go b/api_test.go index 7faffea..34ba906 100644 --- a/api_test.go +++ b/api_test.go @@ -8,9 +8,30 @@ import ( "net/http" "net/http/httptest" "testing" + + "github.com/stretchr/testify/assert" ) -func TestAPI(t *testing.T) { +func TestDynamicEndpointFailsWithoutRegistration(t *testing.T) { + s := NewServer() + sample, err := ioutil.ReadFile("fixtures/sample_post_request.json") + if err != nil { + t.Fatal(err) + } + var out map[string]interface{} + err = json.NewDecoder(bytes.NewReader(sample)).Decode(&out) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + req := jsonRequest("POST", "/api/test", out) + s.ServeHTTP(w, req) + + assert.Equal(t, w.Code, http.StatusNotFound) +} + +func TestDynamicEndpointWithGetRequest(t *testing.T) { s := NewServer() sample, err := ioutil.ReadFile("fixtures/sample_request.json") if err != nil { @@ -24,13 +45,36 @@ func TestAPI(t *testing.T) { w := httptest.NewRecorder() req := jsonRequest("POST", "/register", out) - s.ServeHTTP(w, req) w = httptest.NewRecorder() req = jsonRequest("GET", "/api/test", nil) + s.ServeHTTP(w, req) + assert.Equal(t, w.Code, http.StatusOK) +} + +func TestDynamicEndpointWithPostRequest(t *testing.T) { + s := NewServer() + sample, err := ioutil.ReadFile("fixtures/sample_post_request.json") + if err != nil { + t.Fatal(err) + } + var out map[string]interface{} + err = json.NewDecoder(bytes.NewReader(sample)).Decode(&out) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + req := jsonRequest("POST", "/register", out) + + s.ServeHTTP(w, req) + + w = httptest.NewRecorder() + req = jsonRequest("POST", "/api/test", nil) s.ServeHTTP(w, req) + assert.Equal(t, w.Code, http.StatusCreated) } func jsonRequest(method string, path string, body interface{}) *http.Request { @@ -46,6 +90,6 @@ func jsonRequest(method string, path string, body interface{}) *http.Request { if err != nil { panic(err) } - req.Header.Set("Contet-Type", "application/json") + req.Header.Set("Content-Type", "application/json") return req } diff --git a/cmd/apidemic/main.go b/cmd/apidemic/main.go index d060999..bd0b3f5 100644 --- a/cmd/apidemic/main.go +++ b/cmd/apidemic/main.go @@ -34,7 +34,7 @@ func main() { Flags: []cli.Flag{ cli.IntFlag{ Name: "port", - Usage: "http port to run", + Usage: "HTTP port to run", Value: 3000, EnvVar: "PORT", }, diff --git a/fixtures/sample.json b/fixtures/sample.json index 5acbed0..769b0d2 100644 --- a/fixtures/sample.json +++ b/fixtures/sample.json @@ -23,6 +23,6 @@ "city:city": "Stockholm" }, "country": { - "name:ountry": "Sweden" + "name:country": "Sweden" } } \ No newline at end of file diff --git a/fixtures/sample_post_request.json b/fixtures/sample_post_request.json new file mode 100644 index 0000000..9bd613c --- /dev/null +++ b/fixtures/sample_post_request.json @@ -0,0 +1,32 @@ +{ + "endpoint": "test", + "http_method": "POST", + "payload": { + "name: first_name": "anton", + "age: digits_n,max=2": 29, + "nothing:": null, + "true": true, + "false": false, + "list:word,max=3": [ + "first", + "second" + ], + "list2": [ + { + "street:street": "Street 42", + "city:city": "Stockholm" + }, + { + "street": "Street 42", + "city": "Stockholm" + } + ], + "address": { + "street:street": "Street 42", + "city:city": "Stockholm" + }, + "country": { + "name:country": "Sweden" + } + } +} diff --git a/fixtures/sample_request.json b/fixtures/sample_request.json index 32e9404..04be248 100644 --- a/fixtures/sample_request.json +++ b/fixtures/sample_request.json @@ -25,8 +25,7 @@ "city:city": "Stockholm" }, "country": { - "name:ountry": "Sweden" + "name:country": "Sweden" } } - }