diff --git a/api/messages.go b/api/messages.go new file mode 100644 index 0000000..8ae4ac1 --- /dev/null +++ b/api/messages.go @@ -0,0 +1,28 @@ +package api + +import ( + "net/http" + "github.com/gorilla/context" +) + +func AddMessage(r *http.Request, message string) { + rawMessages, ok := context.GetOk(r, "messages") + + if ok { + messages := rawMessages.([]string) + messages = append(messages, message) + context.Set(r, "messages", messages) + } else { + context.Set(r, "messages", []string{ message }) + } +} + +func GetMessages(r *http.Request) []string { + rawMessages, ok := context.GetOk(r, "messages") + + if ok { + return rawMessages.([]string) + } else { + return []string{} + } +} \ No newline at end of file diff --git a/api/render.go b/api/render.go index f7863e1..69c78ce 100644 --- a/api/render.go +++ b/api/render.go @@ -9,6 +9,19 @@ var defaultRenderer = render.New(render.Options{}) func Render(w http.ResponseWriter, req *http.Request, status int, v interface{}) { vars := req.URL.Query() + + messages := GetMessages(req) + if body, ok := v.(map[string]interface{}); ok && len(messages) > 0 { + if m, ok := body["meta"]; ok { + if meta, ok := m.(map[string]interface{}); ok { + meta["messages"] = messages + } + } else { + body["meta"] = map[string]interface{} { + "messages": messages, + } + } + } if callback, ok := vars["callback"]; ok { defaultRenderer.JSONP(w, status, callback[0], v) diff --git a/cmd/server.go b/cmd/server.go index 9f2a2df..aaf6a70 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -15,25 +15,28 @@ func Server() { port := viper.GetString("bloomapiPort") n := negroni.Classic() - // Middleware setup + // Pre-Router Middleware setup n.Use(middleware.NewAuthentication()) // Router setup router := mux.NewRouter() - // Current API - router.HandleFunc("/api/sources", handler.SourcesHandler).Methods("GET") - router.HandleFunc("/api/search/{source}", handler.SearchSourceHandler).Methods("GET") - router.HandleFunc("/api/sources/{source}/{id}", handler.ItemHandler).Methods("GET") - // For Backwards Compatibility Feb 13, 2015 router.HandleFunc("/api/search", handler_compat.NpiSearchHandler).Methods("GET") router.HandleFunc("/api/search/npi", handler_compat.NpiSearchHandler).Methods("GET") router.HandleFunc("/api/npis/{npi:[0-9]+}", handler_compat.NpiItemHandler).Methods("GET") router.HandleFunc("/api/sources/npi/{npi:[0-9]+}", handler_compat.NpiItemHandler).Methods("GET") + // Current API + router.HandleFunc("/api/sources", handler.SourcesHandler).Methods("GET") + router.HandleFunc("/api/search/{source}", handler.SearchSourceHandler).Methods("GET") + router.HandleFunc("/api/sources/{source}/{id}", handler.ItemHandler).Methods("GET") + n.UseHandler(router) + // Post-Router Middleware setup + n.Use(middleware.NewClearContext()) + log.Println("Running Server") n.Run(":" + port) } \ No newline at end of file diff --git a/handler/item_handler.go b/handler/item_handler.go index 7a61d2c..d6e4193 100644 --- a/handler/item_handler.go +++ b/handler/item_handler.go @@ -1,6 +1,7 @@ package handler import ( + "regexp" "net/http" "encoding/json" "strings" @@ -11,6 +12,8 @@ import ( "github.com/untoldone/bloomapi/api" ) +var validElasticSearchRegexp = regexp.MustCompile(`\A[a-zA-Z0-9\-\_\:\.]+\z`) + func ItemHandler (w http.ResponseWriter, req *http.Request) { vars := mux.Vars(req) source := strings.ToLower(vars["source"]) @@ -18,6 +21,21 @@ func ItemHandler (w http.ResponseWriter, req *http.Request) { conn := api.Conn().SearchConnection() + if !validElasticSearchRegexp.MatchString(source) { + api.Render(w, req, http.StatusNotFound, "item not found") + return + } + + if !validElasticSearchRegexp.MatchString(id) { + api.Render(w, req, http.StatusNotFound, "item not found") + return + } + + if source != "usgov.hhs.npi" { + api.AddMessage(req, "Warning: Use the dataset, '" + source + "', without an API key is for development-use only. Use of this API without a key may be rate-limited in the future. For hosted, production access, please email 'support@bloomapi.com' for an API key.") + api.AddMessage(req, "Warning: The dataset, '" + source + "', is currently in beta and is subject to change. If you'd like to be notified before changes to this dataset, email 'support@bloomapi.com' and ask for an API key referencing this message.") + } + result, err := conn.Get("source", source, id, nil) if err != nil && err.Error() == elastigo.RecordNotFound.Error() { api.Render(w, req, http.StatusNotFound, "item not found") diff --git a/handler/search_helper.go b/handler/search_helper.go index 3dd6f8b..41278f7 100644 --- a/handler/search_helper.go +++ b/handler/search_helper.go @@ -1,8 +1,10 @@ package handler import ( + "fmt" "encoding/json" "regexp" + "net/http" "github.com/mattbaird/elastigo/lib" "github.com/untoldone/bloomapi/api" @@ -10,7 +12,9 @@ import ( var esTypeExceptionRegex = regexp.MustCompile(`FormatException`) -func phraseMatches (paramSets []*SearchParamSet) []interface{} { +var experimentalOperationMessage = "Warning: The operation, '%s', is currently in beta and is subject to change. If you'd like to be notified before changes to this functionality, email 'support@bloomapi.com' and ask for an API key referencing this message." + +func phraseMatches (paramSets []*SearchParamSet, r *http.Request) []interface{} { elasticPhrases := make([]interface{}, len(paramSets)) for index, set := range paramSets { shouldValues := make([]map[string]interface{}, len(set.Values)) @@ -23,12 +27,14 @@ func phraseMatches (paramSets []*SearchParamSet) []interface{} { }, } case "fuzzy": + api.AddMessage(r, fmt.Sprintf(experimentalOperationMessage, "fuzzy")) shouldValues[vIndex] = map[string]interface{} { "fuzzy": map[string]interface{} { set.Key: value, }, } case "prefix": + api.AddMessage(r, fmt.Sprintf(experimentalOperationMessage, "prefix")) shouldValues[vIndex] = map[string]interface{} { "prefix": map[string]interface{} { set.Key: value, @@ -79,15 +85,22 @@ func phraseMatches (paramSets []*SearchParamSet) []interface{} { return elasticPhrases } -func Search(sourceType string, params *SearchParams) (map[string]interface{}, error) { +func Search(sourceType string, params *SearchParams, r *http.Request) (map[string]interface{}, error) { conn := api.Conn().SearchConnection() + if sourceType != "usgov.hhs.npi" { + api.AddMessage(r, "Warning: Use the dataset, '" + sourceType + "', without an API key is for development-use only. Use of this API without a key may be rate-limited in the future. For hosted, production access, please email 'support@bloomapi.com' for an API key.") + api.AddMessage(r, "Warning: The dataset, '" + sourceType + "', is currently in beta and is subject to change. If you'd like to be notified before changes to this dataset, email 'support@bloomapi.com' and ask for an API key referencing this message.") + } + + matches := phraseMatches(params.paramSets, r) + query := map[string]interface{} { "from": params.Offset, "size": params.Limit, "query": map[string]interface{} { "bool": map[string]interface{} { - "must": phraseMatches(params.paramSets), + "must": matches, }, }, } diff --git a/handler/search_source_handler.go b/handler/search_source_handler.go index 1063295..bb7474b 100644 --- a/handler/search_source_handler.go +++ b/handler/search_source_handler.go @@ -13,13 +13,23 @@ func SearchSourceHandler (w http.ResponseWriter, req *http.Request) { vars := mux.Vars(req) source := strings.ToLower(vars["source"]) + if !validElasticSearchRegexp.MatchString(source) { + api.Render(w, req, http.StatusOK, map[string]interface{}{ + "meta": map[string]interface{}{ + "rowCount": 0, + }, + "result": []string{}, + }) + return + } + params, err := ParseSearchParams(req.URL.Query()) if err != nil { api.Render(w, req, http.StatusBadRequest, err) return } - results, err := Search(source, params) + results, err := Search(source, params, req) if err != nil { log.Println(err) api.Render(w, req, http.StatusInternalServerError, "Internal Server Error") diff --git a/handler_compat/npi_search_handler.go b/handler_compat/npi_search_handler.go index 164f6a8..8478686 100644 --- a/handler_compat/npi_search_handler.go +++ b/handler_compat/npi_search_handler.go @@ -15,7 +15,7 @@ func NpiSearchHandler (w http.ResponseWriter, req *http.Request) { return } - results, err := handler.Search("usgov.hhs.npi", params) + results, err := handler.Search("usgov.hhs.npi", params, req) if err != nil { switch err.(type) { case api.ParamsError: diff --git a/middleware/clear_context.go b/middleware/clear_context.go new file mode 100644 index 0000000..351077d --- /dev/null +++ b/middleware/clear_context.go @@ -0,0 +1,17 @@ +package middleware + +import ( + "net/http" + "github.com/gorilla/context" +) + +type ClearContext struct {} + +func NewClearContext() *ClearContext { + return &ClearContext{} +} + +func (s *ClearContext) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + context.Clear(r) + next(rw, r) +} \ No newline at end of file