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 Feb 15, 2023
1 parent dbce243 commit 68c4710
Show file tree
Hide file tree
Showing 5 changed files with 293 additions and 119 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
104 changes: 53 additions & 51 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,36 +72,28 @@ 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/{id}/items/{fid}", handleDeleteCollectionItem, "DELETE")
addRouteWithMethod(router, "/collections/{cid}/items", handleCreateCollectionItem, "POST")
addRouteWithMethod(router, "/collections/{cid}/items/{fid}", handleDeleteCollectionItem, "DELETE")
addRouteWithMethod(router, "/collections/{cid}/items/{fid}", handlePartialUpdateItem, "PATCH")
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)

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

addRouteWithMethod(router, "/collections/{id}/items/{fid}", handleReplaceItem, "PUT")
addRouteWithMethod(router, "/collections/{id}/items/{fid}.{fmt}", handleReplaceItem, "PUT")
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 @@ -178,7 +175,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 @@ -271,7 +268,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 @@ -309,7 +308,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 @@ -349,7 +348,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 @@ -398,8 +397,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 @@ -433,13 +432,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 @@ -452,20 +452,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 @@ -639,8 +641,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 @@ -697,8 +699,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 @@ -752,8 +754,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 @@ -976,7 +978,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 @@ -1017,7 +1019,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 @@ -1033,7 +1035,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 68c4710

Please sign in to comment.