Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(handler): requested format with Accept header and suffix #134

Merged
merged 1 commit into from
Nov 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
benoitdm-oslandia marked this conversation as resolved.
Show resolved Hide resolved
}
}

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
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)
nick-rv marked this conversation as resolved.
Show resolved Hide resolved
addRoute(router, "/home.{fmt}", handleRoot)

// consistent with pg_tileserv
addRoute(router, "/index{.fmt}", handleRoot)
addRoute(router, "/index", handleRoot)
nick-rv marked this conversation as resolved.
Show resolved Hide resolved
addRoute(router, "/index.{fmt}", handleRoot)

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

Expand All @@ -67,33 +72,27 @@ 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)
addRouteWithMethod(router, "/collections/{id}/items", handleCreateCollectionItem, "POST")
addRouteWithMethod(router, "/collections/{id}/items/{fid}", handleDeleteCollectionItem, "DELETE")
addRoute(router, "/collections/{cid}/items", handleCollectionItems)
addRoute(router, "/collections/{cid}/items.{fmt}", handleCollectionItems)
addRouteWithMethod(router, "/collections/{cid}/items", handleCreateCollectionItem, "POST")
addRouteWithMethod(router, "/collections/{cid}/items/{fid}", handleDeleteCollectionItem, "DELETE")

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)

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, "/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 @@ -175,7 +174,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 @@ -268,7 +267,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 @@ -306,7 +307,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 @@ -346,7 +347,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 @@ -395,8 +396,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 @@ -430,13 +431,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 @@ -449,20 +451,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)
nick-rv marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -636,8 +640,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 @@ -694,8 +698,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 @@ -749,8 +753,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 @@ -973,7 +977,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 @@ -1014,7 +1018,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 @@ -1030,7 +1034,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