diff --git a/api/handlers/default_handler.go b/api/handlers/default_handler.go new file mode 100644 index 00000000..01ca35bc --- /dev/null +++ b/api/handlers/default_handler.go @@ -0,0 +1,15 @@ +package handlers + +import ( + "net/http" + + "github.com/gorilla/mux" +) + +func NotFound(w http.ResponseWriter, r *http.Request) { + writeJSONError(w, http.StatusNotFound, mux.ErrNotFound.Error()) +} + +func MethodNotAllowed(w http.ResponseWriter, r *http.Request) { + writeJSONError(w, http.StatusMethodNotAllowed, mux.ErrMethodMismatch.Error()) +} diff --git a/api/handlers/default_handler_test.go b/api/handlers/default_handler_test.go new file mode 100644 index 00000000..7b52e85a --- /dev/null +++ b/api/handlers/default_handler_test.go @@ -0,0 +1,43 @@ +package handlers_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/odpf/columbus/api/handlers" +) + +func TestNotFoundHandler(t *testing.T) { + handler := http.HandlerFunc(handlers.NotFound) + rr := httptest.NewRequest("GET", "/xxx", nil) + rw := httptest.NewRecorder() + handler.ServeHTTP(rw, rr) + if rw.Code != 404 { + t.Errorf("expected handler to respond with HTTP 404, got HTTP %d instead", rw.Code) + return + } + expectedResponse := "{\"reason\":\"no matching route was found\"}\n" + actualResponse := rw.Body.String() + if actualResponse != expectedResponse { + t.Errorf("expected handler response to be %q, was %q instead", expectedResponse, actualResponse) + return + } +} + +func TestMethodNotAllowedHandler(t *testing.T) { + handler := http.HandlerFunc(handlers.MethodNotAllowed) + rr := httptest.NewRequest("POST", "/ping", nil) + rw := httptest.NewRecorder() + handler.ServeHTTP(rw, rr) + if rw.Code != 405 { + t.Errorf("expected handler to respond with HTTP 405, got HTTP %d instead", rw.Code) + return + } + expectedResponse := "{\"reason\":\"method is not allowed\"}\n" + actualResponse := rw.Body.String() + if actualResponse != expectedResponse { + t.Errorf("expected handler response to be %q, was %q instead", expectedResponse, actualResponse) + return + } +} diff --git a/api/middlewares.go b/api/middlewares.go index bd0b98df..35d52de9 100644 --- a/api/middlewares.go +++ b/api/middlewares.go @@ -21,7 +21,6 @@ func decodeURLMiddleware(logger logrus.FieldLogger) mux.MiddlewareFunc { newVars[key] = decodedVal } - r = mux.SetURLVars(r, newVars) h.ServeHTTP(rw, r) }) diff --git a/api/routes.go b/api/routes.go index ff387cde..56b4c8f2 100644 --- a/api/routes.go +++ b/api/routes.go @@ -21,15 +21,16 @@ type Config struct { LineageProvider handlers.LineageProvider } -func RegisterRoutes(router *mux.Router, config Config) { - // By default mux will decode url and then match the decoded url against the route - // we reverse the steps by telling mux to use encoded path to match the url - // then we manually decode via custom middleware (decodeURLMiddleware). - // - // This is to allow urn that has "/" to be matched correctly to the route - router.UseEncodedPath() - router.Use(decodeURLMiddleware(config.Logger)) +type Handlers struct { + Type *handlers.TypeHandler + Record *handlers.RecordHandler + Search *handlers.SearchHandler + Lineage *handlers.LineageHandler + Tag *handlers.TagHandler + TagTemplate *handlers.TagTemplateHandler +} +func initHandlers(config Config) *Handlers { typeHandler := handlers.NewTypeHandler( config.Logger.WithField("reporter", "type-handler"), config.TypeRepository, @@ -58,68 +59,35 @@ func RegisterRoutes(router *mux.Router, config Config) { config.TagTemplateService, ) - router.PathPrefix("/ping").Handler(handlers.NewHeartbeatHandler()) - setupV1TypeRoutes(router, typeHandler, recordHandler) - setupV1TagRoutes(router, "/v1/tags", tagHandler, tagTemplateHandler) - - router.Path("/v1/search"). - Methods(http.MethodGet). - HandlerFunc(searchHandler.Search) - - router.Path("/v1/search/suggest"). - Methods(http.MethodGet). - HandlerFunc(searchHandler.Suggest) - - router.PathPrefix("/v1/lineage/{type}/{id}"). - Methods(http.MethodGet). - HandlerFunc(lineageHandler.GetLineage) - - router.PathPrefix("/v1/lineage"). - Methods(http.MethodGet). - HandlerFunc(lineageHandler.ListLineage) + return &Handlers{ + Type: typeHandler, + Record: recordHandler, + Search: searchHandler, + Lineage: lineageHandler, + Tag: tagHandler, + TagTemplate: tagTemplateHandler, + } } -func setupV1TypeRoutes(router *mux.Router, th *handlers.TypeHandler, rh *handlers.RecordHandler) { - typeURL := "/v1/types" - - router.Path(typeURL). - Methods(http.MethodGet, http.MethodHead). - HandlerFunc(th.Get) - - recordURL := "/v1/types/{name}/records" - router.Path(recordURL). - Methods(http.MethodPut, http.MethodHead). - HandlerFunc(rh.UpsertBulk) - - router.Path(recordURL). - Methods(http.MethodGet, http.MethodHead). - HandlerFunc(rh.GetByType) - - router.Path(recordURL+"/{id}"). - Methods(http.MethodGet, http.MethodHead). - HandlerFunc(rh.GetOneByType) - - router.Path(recordURL+"/{id}"). - Methods(http.MethodDelete, http.MethodHead). - HandlerFunc(rh.Delete) - -} +func RegisterRoutes(router *mux.Router, config Config) { + // By default mux will decode url and then match the decoded url against the route + // we reverse the steps by telling mux to use encoded path to match the url + // then we manually decode via custom middleware (decodeURLMiddleware). + // + // This is to allow urn that has "/" to be matched correctly to the route + router.UseEncodedPath() + router.Use(decodeURLMiddleware(config.Logger)) -func setupV1TagRoutes(router *mux.Router, baseURL string, th *handlers.TagHandler, tth *handlers.TagTemplateHandler) { - router.Methods(http.MethodPost).Path(baseURL).HandlerFunc(th.Create) + handlerCollection := initHandlers(config) - url := baseURL + "/types/{type}/records/{record_urn}/templates/{template_urn}" - router.Methods(http.MethodGet).Path(url).HandlerFunc(th.FindByRecordAndTemplate) - router.Methods(http.MethodPut).Path(url).HandlerFunc(th.Update) - router.Methods(http.MethodDelete).Path(url).HandlerFunc(th.Delete) + router.PathPrefix("/ping").Handler(handlers.NewHeartbeatHandler()) - router.Methods(http.MethodGet).Path(baseURL + "/types/{type}/records/{record_urn}").HandlerFunc(th.GetByRecord) + v1Beta1SubRouter := router.PathPrefix("/v1beta1").Subrouter() + setupV1Beta1Router(v1Beta1SubRouter, handlerCollection) - templateURL := baseURL + "/templates" - router.Methods(http.MethodGet).Path(templateURL).HandlerFunc(tth.Index) - router.Methods(http.MethodPost).Path(templateURL).HandlerFunc(tth.Create) - router.Methods(http.MethodGet).Path(templateURL + "/{template_urn}").HandlerFunc(tth.Find) - router.Methods(http.MethodPut).Path(templateURL + "/{template_urn}").HandlerFunc(tth.Update) - router.Methods(http.MethodDelete).Path(templateURL + "/{template_urn}").HandlerFunc(tth.Delete) + v1SubRouter := router.PathPrefix("/v1").Subrouter() + setupV1Beta1Router(v1SubRouter, handlerCollection) + router.NotFoundHandler = http.HandlerFunc(handlers.NotFound) + router.MethodNotAllowedHandler = http.HandlerFunc(handlers.MethodNotAllowed) } diff --git a/api/v1beta1.go b/api/v1beta1.go new file mode 100644 index 00000000..ddddb493 --- /dev/null +++ b/api/v1beta1.go @@ -0,0 +1,76 @@ +package api + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/odpf/columbus/api/handlers" +) + +func setupV1Beta1Router(router *mux.Router, handlers *Handlers) *mux.Router { + setupV1Beta1TypeRoutes(router, handlers.Type, handlers.Record) + setupV1Beta1TagRoutes(router, "/tags", handlers.Tag, handlers.TagTemplate) + + router.Path("/search"). + Methods(http.MethodGet). + HandlerFunc(handlers.Search.Search) + + router.Path("/search/suggest"). + Methods(http.MethodGet). + HandlerFunc(handlers.Search.Suggest) + + router.PathPrefix("/lineage/{type}/{id}"). + Methods(http.MethodGet). + HandlerFunc(handlers.Lineage.GetLineage) + + router.PathPrefix("/lineage"). + Methods(http.MethodGet). + HandlerFunc(handlers.Lineage.ListLineage) + + return router +} + +func setupV1Beta1TypeRoutes(router *mux.Router, th *handlers.TypeHandler, rh *handlers.RecordHandler) { + typeURL := "/types" + + router.Path(typeURL). + Methods(http.MethodGet, http.MethodHead). + HandlerFunc(th.Get) + + recordURL := "/types/{name}/records" + router.Path(recordURL). + Methods(http.MethodPut, http.MethodHead). + HandlerFunc(rh.UpsertBulk) + + router.Path(recordURL). + Methods(http.MethodGet, http.MethodHead). + HandlerFunc(rh.GetByType) + + router.Path(recordURL+"/{id}"). + Methods(http.MethodGet, http.MethodHead). + HandlerFunc(rh.GetOneByType) + + router.Path(recordURL+"/{id}"). + Methods(http.MethodDelete, http.MethodHead). + HandlerFunc(rh.Delete) + +} + +func setupV1Beta1TagRoutes(router *mux.Router, baseURL string, th *handlers.TagHandler, tth *handlers.TagTemplateHandler) { + router.Methods(http.MethodPost).Path(baseURL).HandlerFunc(th.Create) + + url := baseURL + "/types/{type}/records/{record_urn}/templates/{template_urn}" + router.Methods(http.MethodGet).Path(url).HandlerFunc(th.FindByRecordAndTemplate) + router.Methods(http.MethodPut).Path(url).HandlerFunc(th.Update) + router.Methods(http.MethodDelete).Path(url).HandlerFunc(th.Delete) + + router.Methods(http.MethodGet).Path(baseURL + "/types/{type}/records/{record_urn}").HandlerFunc(th.GetByRecord) + + templateURL := baseURL + "/templates" + router.Methods(http.MethodGet).Path(templateURL).HandlerFunc(tth.Index) + router.Methods(http.MethodPost).Path(templateURL).HandlerFunc(tth.Create) + router.Methods(http.MethodGet).Path(templateURL + "/{template_urn}").HandlerFunc(tth.Find) + router.Methods(http.MethodPut).Path(templateURL + "/{template_urn}").HandlerFunc(tth.Update) + router.Methods(http.MethodDelete).Path(templateURL + "/{template_urn}").HandlerFunc(tth.Delete) + +} diff --git a/docs/concepts/internals.md b/docs/concepts/internals.md index cdb01109..1670875c 100644 --- a/docs/concepts/internals.md +++ b/docs/concepts/internals.md @@ -40,7 +40,7 @@ The script filter is designed to match a document if: To demonstrate, the following API call: ```text -curl http://localhost:3000/v1/search?text=log&filter.landscape=id +curl http://localhost:3000/v1beta1/search?text=log&filter.landscape=id ``` is internally translated to the following elasticsearch query diff --git a/swagger.yaml b/swagger.yaml index 58177dfc..c6cab8a9 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -4,7 +4,7 @@ info: description: "Data Discovery and Lineage Service" version: 0.1.0 paths: - "/v1/lineage": + "/v1beta1/lineage": get: tags: - Lineage @@ -28,7 +28,7 @@ paths: description: record not found schema: $ref: "#/definitions/Error" - "/v1/lineage/{type}/{record}": + "/v1beta1/lineage/{type}/{record}": get: tags: - Lineage @@ -49,7 +49,7 @@ paths: description: invalid type requested schema: $ref: "#/definitions/Error" - "/v1/types": + "/v1beta1/types": get: tags: - Type @@ -70,7 +70,7 @@ paths: count: type: number example: 1800 - "/v1/types/{name}/records": + "/v1beta1/types/{name}/records": put: tags: - Record @@ -132,7 +132,7 @@ paths: description: not found schema: $ref: "#/definitions/Error" - "/v1/types/{name}/records/{id}": + "/v1beta1/types/{name}/records/{id}": delete: tags: - Record @@ -158,7 +158,7 @@ paths: description: type or record cannot be found schema: $ref: "#/definitions/Error" - "/v1/types/{name}/{id}": + "/v1beta1/types/{name}/{id}": get: tags: - Record @@ -183,7 +183,7 @@ paths: description: document or type does not exist schema: $ref: "#/definitions/Error" - "/v1/search": + "/v1beta1/search": get: tags: - Search @@ -236,7 +236,7 @@ paths: description: misconfigured request parameters schema: $ref: "#/definitions/Error" - "/v1/tags": + "/v1beta1/tags": post: tags: - Tag @@ -273,7 +273,7 @@ paths: description: internal server error schema: $ref: "#/definitions/Error" - "/v1/tags/templates": + "/v1beta1/tags/templates": get: tags: - Tag @@ -327,7 +327,7 @@ paths: description: internal server error schema: $ref: "#/definitions/Error" - "/v1/tags/templates/{template_urn}": + "/v1beta1/tags/templates/{template_urn}": get: tags: - Tag @@ -417,7 +417,7 @@ paths: description: internal server error schema: $ref: "#/definitions/Error" - "/v1/tags/types/{type}/records/{record_urn}": + "/v1beta1/tags/types/{type}/records/{record_urn}": get: tags: - Tag @@ -445,7 +445,7 @@ paths: description: internal server error schema: $ref: "#/definitions/Error" - "/v1/tags/types/{type}/records/{record_urn}/templates/{template_urn}": + "/v1beta1/tags/types/{type}/records/{record_urn}/templates/{template_urn}": get: tags: - Tag