Add optimized implementation for preferredContentType() which avoid sorting and double iteration. (#299)

Fix tests to use 80cpl.
This commit is contained in:
Eugene Kabanov 2022-08-06 13:53:40 +03:00 committed by GitHub
parent 939195626f
commit 15d7e0ebb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 85 additions and 21 deletions

View File

@ -471,9 +471,10 @@ proc preferredContentMediaType*(acceptHeader: string): MediaType =
MediaType.init("*", "*")
proc preferredContentType*(acceptHeader: string,
types: varargs[MediaType]): Result[MediaType, cstring] =
## Match or obtain preferred content-type using ``Accept`` header specified by
## string ``acceptHeader``.
types: varargs[MediaType]
): Result[MediaType, cstring] =
## Match or obtain preferred content type using ``Accept`` header specified by
## string ``acceptHeader`` and server preferred content types ``types``.
##
## If ``Accept`` header is missing in client's request - ``types[0]`` or
## ``*/*`` value will be returned as result.
@ -481,10 +482,15 @@ proc preferredContentType*(acceptHeader: string,
## If ``Accept`` header has incorrect format in client's request -
## ``types[0]`` or ``*/*`` value will be returned as result.
##
## If ``Accept`` header is present and has one or more content types supported
## by client, the best value will be selected from ``types`` using
## quality value (weight) reported in ``Accept`` header. If client do not
## support any methods in ``types`` error will be returned.
## If ``Accept`` header is present in request to server and it has one or more
## content types supported by client, the best value will be selected from
## ``types`` using position and quality value (weight) reported in ``Accept``
## header. If client do not support any methods in ``types`` error
## will be returned.
##
## Note: Quality value (weight) for content type has priority over server's
## preferred content-type.
doAssert(len(types) < 99, "Maximum number of types is 99")
if len(types) == 0:
if len(acceptHeader) == 0:
# If `Accept` header is missing, return `*/*`.
@ -496,10 +502,19 @@ proc preferredContentType*(acceptHeader: string,
ok(wildCardMediaType)
else:
let mediaTypes = res.get().data
if len(mediaTypes) > 0:
ok(mediaTypes[0].mediaType)
else:
var
currentType = MediaType()
currentWeight = 0.0
# `Accept` header values array is not sorted, so we need to find value
# with the biggest ``q-value``.
for item in mediaTypes:
if currentWeight < item.qvalue:
currentType = item.mediaType
currentWeight = item.qvalue
if len(currentType.media) == 0 and len(currentType.subtype) == 0:
ok(wildCardMediaType)
else:
ok(currentType)
else:
if len(acceptHeader) == 0:
# If `Accept` header is missing, client accepts any type of content.
@ -510,7 +525,46 @@ proc preferredContentType*(acceptHeader: string,
# If `Accept` header is incorrect, client accepts any type of content.
ok(types[0])
else:
selectContentType(ares.get().data, types)
# This algorithm exploits ``q-value`` range which is [0.000, 1.000],
# by using smaller values for sorting server's preferred content types.
# So between 0.000 and 0.001 there present 99 possible values
# [0.00001, 0.00099] which can be used.
# Server's preferred types marked with `preferred-value` which is
# has highest value for first index and the lowest for the last.
# ``maxWeight`` represents maximum possible weight value which can be
# obtained.
let maxWeight = 1.0 + float(len(types)) / float(100_000)
var
currentType = MediaType()
currentWeight = 0.0
for itemType in ares.get().data:
let
preferredIndex = types.find(itemType.mediaType)
preferredWeight =
if preferredIndex == -1:
0.0
else:
# Calculate weight which depends on position in ``types``
# array. Because ``preferredIndex`` is always less than
# ``len(types)`` this weight could not be ``0.0``.
float(len(types) - preferredIndex) / float(100_000)
if preferredWeight != 0:
let weight = itemType.qvalue + preferredWeight
if currentWeight < weight:
currentType = types[preferredIndex]
currentWeight = weight
if currentWeight == maxWeight:
# There is no reason to continue search, because maximum possible
# weight is already achieved, so this is the best match.
break
if currentWeight == 0.0:
err("Preferred content type not found")
else:
ok(currentType)
proc preferredContentMediaType*(request: HttpRequestRef): MediaType =
## Returns preferred content-type using ``Accept`` header specified by
@ -518,7 +572,8 @@ proc preferredContentMediaType*(request: HttpRequestRef): MediaType =
preferredContentMediaType(request.headers.getString(AcceptHeaderName))
proc preferredContentType*(request: HttpRequestRef,
types: varargs[MediaType]): Result[MediaType, cstring] =
types: varargs[MediaType]
): Result[MediaType, cstring] =
## Match or obtain preferred content-type using ``Accept`` header specified by
## client in request ``request``.
preferredContentType(request.headers.getString(AcceptHeaderName), types)

View File

@ -866,7 +866,7 @@ suite "HTTP server testing suite":
table.add(key, value)
check toString(table) == vector[2]
test "Preferred Accept handling test":
test "preferredContentType() test":
const
jsonMediaType = MediaType.init("application/json")
sszMediaType = MediaType.init("application/octet-stream")
@ -999,7 +999,8 @@ suite "HTTP server testing suite":
]
),
(
createRequest("text/plain, application/json;q=0.8, application/octet-stream;q=0.8"),
createRequest("text/plain, application/json;q=0.8, " &
"application/octet-stream;q=0.8"),
@[
"text/plain",
"application/json",
@ -1011,7 +1012,8 @@ suite "HTTP server testing suite":
]
),
(
createRequest("text/plain, application/json;q=0.8, application/octet-stream;q=0.5"),
createRequest("text/plain, application/json;q=0.8, " &
"application/octet-stream;q=0.5"),
@[
"text/plain",
"application/json",
@ -1023,7 +1025,8 @@ suite "HTTP server testing suite":
]
),
(
createRequest("text/plain;q=0.8, application/json, application/octet-stream;q=0.8"),
createRequest("text/plain;q=0.8, application/json, " &
"application/octet-stream;q=0.8"),
@[
"application/json",
"application/json",
@ -1035,7 +1038,8 @@ suite "HTTP server testing suite":
]
),
(
createRequest("text/*, application/json;q=0.8, application/octet-stream;q=0.8"),
createRequest("text/*, application/json;q=0.8, " &
"application/octet-stream;q=0.8"),
@[
"text/*",
"application/json",
@ -1047,7 +1051,8 @@ suite "HTTP server testing suite":
]
),
(
createRequest("text/*, application/json;q=0.8, application/octet-stream;q=0.5"),
createRequest("text/*, application/json;q=0.8, " &
"application/octet-stream;q=0.5"),
@[
"text/*",
"application/json",
@ -1058,7 +1063,8 @@ suite "HTTP server testing suite":
"text/plain"
]
),
(createRequest("image/jpg, text/plain, application/octet-stream, application/json"),
(createRequest("image/jpg, text/plain, application/octet-stream, " &
"application/json"),
@[
"image/jpg",
"application/json",
@ -1069,7 +1075,9 @@ suite "HTTP server testing suite":
"image/jpg"
]
),
(createRequest("image/jpg;q=1, text/plain;q=0.2, application/octet-stream;q=0.2, application/json;q=0.2"),
(createRequest("image/jpg;q=1, text/plain;q=0.2, " &
"application/octet-stream;q=0.2, " &
"application/json;q=0.2"),
@[
"image/jpg",
"application/json",
@ -1081,7 +1089,8 @@ suite "HTTP server testing suite":
]
),
(
createRequest("*/*, application/json;q=0.8, application/octet-stream;q=0.5"),
createRequest("*/*, application/json;q=0.8, " &
"application/octet-stream;q=0.5"),
@[
"*/*",
"application/json",