Skip to content

Commit

Permalink
fix(handler): requested format with Accept header and suffix (#134)
Browse files Browse the repository at this point in the history
  • Loading branch information
nick-rv authored and benoitdm-oslandia committed Sep 6, 2024
1 parent 6bfd121 commit 2dbb5dd
Show file tree
Hide file tree
Showing 6 changed files with 296 additions and 120 deletions.
83 changes: 49 additions & 34 deletions internal/api/net.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,23 +75,10 @@ func RequestedFormat(r *http.Request) string {
// first check explicit path
path := r.URL.EscapedPath()

// Accept header value
hdrAcceptValue := r.Header.Get("Accept")

// Extension value
splittedPath := strings.Split(path, "/")
pathEnd := splittedPath[len(splittedPath)-1]
extension := ""
pos := strings.LastIndex(pathEnd, ".")
if pos != -1 {
extension = pathEnd[pos+1:]
}

// TODO: case when extension and header Accept are provided at the same time
// -> Bad Request ?

if extension != "" && hdrAcceptValue == "" {
switch extension {
suffix := PathSuffix(path)
if suffix != "" {
switch suffix {
case "json":
return FormatJSON
case "html":
Expand All @@ -101,23 +88,38 @@ func RequestedFormat(r *http.Request) string {
case "svg":
return FormatSVG
default:
return extension
return suffix
}
}

if hdrAcceptValue != "" {
switch hdrAcceptValue {
case ContentTypeJSON:
return FormatJSON
case ContentTypeSchemaJSON, ContentTypeSchemaPatchJSON:
return FormatSchemaJSON
case ContentTypeHTML:
return FormatHTML
case ContentTypeText:
return FormatText
case ContentTypeSVG:
return FormatSVG
default:
} else {
// Accept header value
hdrAcceptValue := r.Header.Get("Accept")
if hdrAcceptValue != "" {
// Accept header fields preferences:
// -> https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.1

// Examples:
// "Accept: application/json"
// "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
preferredFormats := strings.Split(hdrAcceptValue, ",")
for _, value := range preferredFormats {
mediaTypeValue := value
lastSemicolon := strings.LastIndex(value, ";")
if lastSemicolon > 0 {
mediaTypeValue = value[:lastSemicolon] // 'q' quality parameter not used
}
switch mediaTypeValue {
case ContentTypeJSON:
return FormatJSON
case ContentTypeSchemaJSON, ContentTypeSchemaPatchJSON:
return FormatSchemaJSON
case ContentTypeHTML:
return FormatHTML
case ContentTypeText:
return FormatText
case ContentTypeSVG:
return FormatSVG
}
}
return hdrAcceptValue
}
}
Expand All @@ -139,12 +141,25 @@ func SentDataFormat(r *http.Request) string {

// PathStripFormat removes a format extension from a path
func PathStripFormat(path string) string {
if strings.HasSuffix(path, ".html") || strings.HasSuffix(path, ".json") {
return path[0 : len(path)-5]
pos := strings.LastIndex(path, ".")
if pos != -1 {
path = path[:pos]
}
return path
}

// PathSuffix returns the format extension from a path following a dot character
func PathSuffix(path string) string {
splittedPath := strings.Split(path, "/")
pathEnd := splittedPath[len(splittedPath)-1]
pos := strings.LastIndex(pathEnd, ".")
if pos != -1 {
return pathEnd[pos+1:]
} else {
return ""
}
}

// URLQuery gets the query part of a URL
func URLQuery(url *url.URL) string {
uri := url.RequestURI()
Expand Down
8 changes: 4 additions & 4 deletions internal/data/cache_test/cache_redis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func (t *CacheTests) TestRedisValidAddress() {
t.Test.Run("TestRedisValidAddress", func(t *testing.T) {

cache := data.CacheRedis{}
err := cache.Init(url)
err := cache.Init(url, "")
util.Assert(t, err == nil, "No error in CacheRedis initialization expected")

util.Assert(t, cache.String() == "Redis Cache running on "+url, "Invalid CacheRedis string")
Expand All @@ -50,7 +50,7 @@ func (t *CacheTests) TestRedisContainsWeakEtag() {
t.Test.Run("TestRedisContainsWeakEtag", func(t *testing.T) {

cache := data.CacheRedis{}
err := cache.Init(url)
err := cache.Init(url, "")
util.Assert(t, err == nil, "No error in CacheRedis initialization expected")

// Test invalid etag use
Expand Down Expand Up @@ -90,7 +90,7 @@ func (t *CacheTests) TestRedisAddWeakEtag() {
t.Test.Run("TestRedisAddWeakEtag", func(t *testing.T) {

cache := data.CacheRedis{}
err := cache.Init(url)
err := cache.Init(url, "")
util.Assert(t, err == nil, "No error in CacheRedis initialization expected")

validWeakEtag := "collection"
Expand Down Expand Up @@ -120,7 +120,7 @@ func (t *CacheTests) TestRedisRemoveWeakEtag() {
t.Test.Run("TestRedisRemoveWeakEtag", func(t *testing.T) {

cache := data.CacheRedis{}
err := cache.Init(url)
err := cache.Init(url, "")
util.Assert(t, err == nil, "No error in CacheRedis initialization expected")

validWeakEtag := "collection"
Expand Down
102 changes: 53 additions & 49 deletions internal/service/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ import (
)

const (
routeVarID = "id"
routeVarFeatureID = "fid"
routeVarStrongEtag = "etag"
routeVarCollectionID = "cid"
routeVarFeatureID = "fid"
routeVarFunctionID = "funid"
routeVarStrongEtag = "etag"
)

func InitRouter(basePath string) *mux.Router {
Expand All @@ -52,9 +53,13 @@ func InitRouter(basePath string) *mux.Router {
Subrouter()

addRoute(router, "/", handleRoot)
addRoute(router, "/home{.fmt}", handleRoot)

addRoute(router, "/home", handleRoot)
addRoute(router, "/home.{fmt}", handleRoot)

// consistent with pg_tileserv
addRoute(router, "/index{.fmt}", handleRoot)
addRoute(router, "/index", handleRoot)
addRoute(router, "/index.{fmt}", handleRoot)

addRoute(router, "/etags/decodestrong/{etag}", handleDecodeStrongEtag)

Expand All @@ -67,37 +72,31 @@ func InitRouter(basePath string) *mux.Router {
addRoute(router, "/collections", handleCollections)
addRoute(router, "/collections.{fmt}", handleCollections)

addRoute(router, "/collections/{id}", handleCollection)
addRoute(router, "/collections/{id}.{fmt}", handleCollection)
addRoute(router, "/collections/{cid}", handleCollection)

addRoute(router, "/collections/{id}/items", handleCollectionItems)
addRoute(router, "/collections/{id}/items.{fmt}", handleCollectionItems)
addRoute(router, "/collections/{cid}/items", handleCollectionItems)
addRoute(router, "/collections/{cid}/items.{fmt}", handleCollectionItems)

if conf.Configuration.Database.AllowWrite {
addRouteWithMethod(router, "/collections/{id}/items", handleCreateCollectionItem, "POST")
addRouteWithMethod(router, "/collections/{cid}/items", handleCreateCollectionItem, "POST")

addRouteWithMethod(router, "/collections/{id}/items/{fid}", handleDeleteCollectionItem, "DELETE")
addRouteWithMethod(router, "/collections/{cid}/items/{fid}", handleDeleteCollectionItem, "DELETE")

addRouteWithMethod(router, "/collections/{id}/items/{fid}", handlePartialUpdateItem, "PATCH")
addRouteWithMethod(router, "/collections/{id}/items/{fid}.{fmt}", handlePartialUpdateItem, "PATCH")
addRouteWithMethod(router, "/collections/{cid}/items/{fid}", handlePartialUpdateItem, "PATCH")

addRouteWithMethod(router, "/collections/{id}/items/{fid}", handleReplaceItem, "PUT")
addRouteWithMethod(router, "/collections/{id}/items/{fid}.{fmt}", handleReplaceItem, "PUT")
addRouteWithMethod(router, "/collections/{cid}/items/{fid}", handleReplaceItem, "PUT")

addRoute(router, "/collections/{id}/schema", handleCollectionSchemas)
addRoute(router, "/collections/{cid}/schema", handleCollectionSchemas)
}

addRoute(router, "/collections/{id}/items/{fid}", handleItem)
addRoute(router, "/collections/{id}/items/{fid}.{fmt}", handleItem)
addRoute(router, "/collections/{cid}/items/{fid}", handleItem)

addRoute(router, "/functions", handleFunctions)
addRoute(router, "/functions.{fmt}", handleFunctions)

addRoute(router, "/functions/{id}", handleFunction)
addRoute(router, "/functions/{id}.{fmt}", handleFunction)
addRoute(router, "/functions/{funid}", handleFunction)

addRoute(router, "/functions/{id}/items", handleFunctionItems)
addRoute(router, "/functions/{id}/items.{fmt}", handleFunctionItems)
addRoute(router, "/functions/{funid}/items", handleFunctionItems)

return router
}
Expand Down Expand Up @@ -179,7 +178,7 @@ func linkAlt(urlBase string, path string, desc string) *api.Link {

func handleDecodeStrongEtag(w http.ResponseWriter, r *http.Request) *appError {
//--- extract request parameters
etag := getRequestVar(routeVarStrongEtag, r)
etag := getRequestVarStrip(routeVarStrongEtag, r)
decodedEtag, err := api.DecodeStrongEtag(etag)
if err != nil {
return appErrorBadRequest(err, "Malformed etag")
Expand Down Expand Up @@ -272,7 +271,9 @@ func handleCollection(w http.ResponseWriter, r *http.Request) *appError {
format := api.RequestedFormat(r)
urlBase := serveURLBase(r)

name := getRequestVar(routeVarID, r)
// the collection is at the end of the URL, this is why we strip the extension
// it may be an issue if the schema name is provided here
name := getRequestVarStrip(routeVarCollectionID, r)

tbl, err := catalogInstance.TableByName(name)
if tbl == nil && err == nil {
Expand Down Expand Up @@ -310,7 +311,7 @@ func handleCollectionSchemas(w http.ResponseWriter, r *http.Request) *appError {
format := api.RequestedFormat(r)

//--- extract request parameters
name := getRequestVar(routeVarID, r)
name := getRequestVar(routeVarCollectionID, r)
tbl, err1 := catalogInstance.TableByName(name)
if err1 != nil {
return appErrorInternal(err1, api.ErrMsgCollectionAccess, name)
Expand Down Expand Up @@ -350,7 +351,7 @@ func handleCreateCollectionItem(w http.ResponseWriter, r *http.Request) *appErro
urlBase := serveURLBase(r)

//--- extract request parameters
name := getRequestVar(routeVarID, r)
name := getRequestVar(routeVarCollectionID, r)

//--- check query parameters
queryValues := r.URL.Query()
Expand Down Expand Up @@ -399,8 +400,8 @@ func handleCreateCollectionItem(w http.ResponseWriter, r *http.Request) *appErro
func handleDeleteCollectionItem(w http.ResponseWriter, r *http.Request) *appError {

//--- extract request parameters
name := getRequestVar(routeVarID, r)
fid := getRequestVar(routeVarFeatureID, r)
name := getRequestVar(routeVarCollectionID, r)
fid := getRequestVarStrip(routeVarFeatureID, r)

//--- check request parameters
index, err := strconv.Atoi(fid)
Expand Down Expand Up @@ -434,13 +435,14 @@ func handleDeleteCollectionItem(w http.ResponseWriter, r *http.Request) *appErro
}

func handleCollectionItems(w http.ResponseWriter, r *http.Request) *appError {
// "/collections/{id}/items"
// TODO: determine content from request header?
format := api.RequestedFormat(r)
urlBase := serveURLBase(r)
query := api.URLQuery(r.URL)

//--- extract request parameters
name := getRequestVar(routeVarID, r)
name := getRequestVar(routeVarCollectionID, r)
reqParam, err := parseRequestParams(r)
if err != nil {
return appErrorBadRequest(err, err.Error())
Expand All @@ -453,20 +455,22 @@ func handleCollectionItems(w http.ResponseWriter, r *http.Request) *appError {
if tbl == nil {
return appErrorNotFound(err1, api.ErrMsgCollectionNotFound, name)
}
param, err := createQueryParams(&reqParam, tbl.Columns, tbl.Srid)
if err != nil {
return appErrorBadRequest(err, err.Error())
}
param, errQuery := createQueryParams(&reqParam, tbl.Columns, tbl.Srid)
param.Filter = parseFilter(reqParam.Values, tbl.DbTypes)

ctx := r.Context()
switch format {
case api.FormatJSON:
return writeItemsJSON(ctx, w, name, param, urlBase)
case api.FormatHTML:
return writeItemsHTML(w, tbl, name, query, urlBase)
if errQuery == nil {
ctx := r.Context()
switch format {
case api.FormatJSON:
return writeItemsJSON(ctx, w, name, param, urlBase)
case api.FormatHTML:
return writeItemsHTML(w, tbl, name, query, urlBase)
default:
return appErrorNotAcceptable(nil, api.ErrMsgNotSupportedFormat, format)
}
} else {
return appErrorBadRequest(errQuery, api.ErrMsgInvalidQuery)
}
return nil
}

func writeCreateItemSchemaJSON(ctx context.Context, w http.ResponseWriter, table *api.Table) *appError {
Expand Down Expand Up @@ -640,8 +644,8 @@ func handleItem(w http.ResponseWriter, r *http.Request) *appError {

query := api.URLQuery(r.URL)
//--- extract request parameters
name := getRequestVar(routeVarID, r)
fid := getRequestVar(routeVarFeatureID, r)
name := getRequestVar(routeVarCollectionID, r)
fid := getRequestVarStrip(routeVarFeatureID, r)
reqParam, err := parseRequestParams(r)
if err != nil {
return appErrorBadRequest(err, err.Error())
Expand Down Expand Up @@ -698,8 +702,8 @@ func handleItem(w http.ResponseWriter, r *http.Request) *appError {

func handlePartialUpdateItem(w http.ResponseWriter, r *http.Request) *appError {
// extract request parameters
name := getRequestVar(routeVarID, r)
fid := getRequestVar(routeVarFeatureID, r)
name := getRequestVar(routeVarCollectionID, r)
fid := getRequestVarStrip(routeVarFeatureID, r)

// check query parameters
queryValues := r.URL.Query()
Expand Down Expand Up @@ -753,8 +757,8 @@ func handlePartialUpdateItem(w http.ResponseWriter, r *http.Request) *appError {
func handleReplaceItem(w http.ResponseWriter, r *http.Request) *appError {

// extract request parameters
name := getRequestVar(routeVarID, r)
fid := getRequestVar(routeVarFeatureID, r)
name := getRequestVar(routeVarCollectionID, r)
fid := getRequestVarStrip(routeVarFeatureID, r)

// check query parameters
queryValues := r.URL.Query()
Expand Down Expand Up @@ -977,7 +981,7 @@ func handleFunction(w http.ResponseWriter, r *http.Request) *appError {
format := api.RequestedFormat(r)
urlBase := serveURLBase(r)

shortName := getRequestVar(routeVarID, r)
shortName := getRequestVarStrip(routeVarFunctionID, r)
name := data.FunctionQualifiedId(shortName)

fn, err := catalogInstance.FunctionByName(name)
Expand Down Expand Up @@ -1018,7 +1022,7 @@ func handleFunctionItems(w http.ResponseWriter, r *http.Request) *appError {
urlBase := serveURLBase(r)

//--- extract request parameters
name := data.FunctionQualifiedId(getRequestVar(routeVarID, r))
name := data.FunctionQualifiedId(getRequestVarStrip(routeVarFunctionID, r))
reqParam, err := parseRequestParams(r)
if err != nil {
return appErrorBadRequest(err, err.Error())
Expand All @@ -1034,7 +1038,7 @@ func handleFunctionItems(w http.ResponseWriter, r *http.Request) *appError {
return appErrorBadRequest(err, err.Error())
}
fnArgs := restrict(reqParam.Values, fn.InNames)
//log.Debugf("Function request args: %v ", fnArgs)
// log.Debugf("Function request args: %v ", fnArgs)

ctx := r.Context()
switch format {
Expand Down
Loading

0 comments on commit 2dbb5dd

Please sign in to comment.