263 lines
7.9 KiB
Nim
263 lines
7.9 KiB
Nim
#
|
|
# REST API framework implementation
|
|
# (c) Copyright 2021-Present
|
|
# Status Research & Development GmbH
|
|
#
|
|
# Licensed under either of
|
|
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
|
# MIT license (LICENSE-MIT)
|
|
import std/[uri, strutils]
|
|
import stew/bitops2
|
|
import chronos/apps
|
|
import common
|
|
export common, apps
|
|
|
|
{.push raises: [Defect].}
|
|
|
|
type
|
|
PatternCallback* = proc(pattern: string,
|
|
value: string): int {.gcsafe, raises: [Defect].}
|
|
|
|
SegmentedPath* = object
|
|
data*: seq[string]
|
|
patternCb: PatternCallback
|
|
patterns*: uint64
|
|
|
|
KeyValueTuple* = tuple
|
|
key: string
|
|
value: string
|
|
|
|
template isPattern*(spath: SegmentedPath, pos: int): bool =
|
|
(spath.patterns and (1'u64 shl pos)) != 0'u64
|
|
|
|
template hasPatterns*(spath: SegmentedPath): bool =
|
|
spath.patterns != 0'u64
|
|
|
|
proc patternsCount*(spath: SegmentedPath): int =
|
|
## Returns number of patterns inside path ``spath``.
|
|
spath.patterns.countOnes()
|
|
|
|
iterator keys*(spath: SegmentedPath): string =
|
|
## Iterate over all patterns in path ``spath``.
|
|
var pats = spath.patterns
|
|
while pats != 0'u64:
|
|
let index = firstOne(pats) - 1
|
|
doAssert(index < len(spath.data))
|
|
doAssert(len(spath.data[index]) > 2)
|
|
yield spath.data[index][1 .. ^2]
|
|
pats = pats and not(1'u64 shl index)
|
|
|
|
iterator indexes*(spath: SegmentedPath): int =
|
|
## Iterate over all patterns indexes in path ``spath``.
|
|
var pats = spath.patterns
|
|
while pats != 0'u64:
|
|
let index = firstOne(pats) - 1
|
|
doAssert(index < len(spath.data))
|
|
if index < len(spath.data):
|
|
yield index
|
|
pats = pats and not(1'u64 shl index)
|
|
|
|
proc getPatterns*(spath: SegmentedPath): seq[string] =
|
|
## Returns all the patterns in path ``spath``.
|
|
var res = newSeq[string]()
|
|
for item in spath.keys():
|
|
res.add(item)
|
|
res
|
|
|
|
proc isEqual*(spath1, spath2: SegmentedPath): bool =
|
|
## Returns ``true`` if both path has equal patterns (number of patterns are
|
|
## equal, and pattern names are equal).
|
|
let pats1 = spath1.getPatterns()
|
|
let pats2 = spath2.getPatterns()
|
|
if len(pats1) != len(pats2):
|
|
false
|
|
else:
|
|
for item in pats1:
|
|
if item notin pats2:
|
|
return false
|
|
true
|
|
|
|
iterator pairs*(spath: SegmentedPath, vpath: SegmentedPath): KeyValueTuple =
|
|
doAssert(len(spath.data) == len(vpath.data))
|
|
for index in spath.indexes():
|
|
yield (spath.data[index], vpath.data[index])
|
|
|
|
proc getPairs*(spath: SegmentedPath, vpath: SegmentedPath): seq[KeyValueTuple] =
|
|
var res: seq[tuple[key: string, value: string]]
|
|
for item in pairs(spath, vpath):
|
|
res.add(item)
|
|
res
|
|
|
|
proc getValue(data: seq[KeyValueTuple], key: string): Option[string] =
|
|
for item in data:
|
|
if item.key == key:
|
|
return some(item.value)
|
|
return none[string]()
|
|
|
|
proc rewritePath*(spath: SegmentedPath, dpath: SegmentedPath,
|
|
vpath: SegmentedPath): SegmentedPath =
|
|
doAssert(spath.patternsCount() == dpath.patternsCount())
|
|
let values = getPairs(spath, vpath)
|
|
if len(values) == 0:
|
|
SegmentedPath(data: dpath.data)
|
|
else:
|
|
var res = SegmentedPath(data: dpath.data)
|
|
var k = 0
|
|
for i in 0 ..< len(res.data):
|
|
if dpath.isPattern(i):
|
|
let vres = values.getValue(dpath.data[i])
|
|
doAssert(vres.isSome())
|
|
res.data[i] = vres.get()
|
|
inc(k)
|
|
res
|
|
|
|
proc `==`*(s1, s2: SegmentedPath): bool =
|
|
if len(s1.data) == len(s2.data):
|
|
if not(s1.hasPatterns()) and not(s2.hasPatterns()):
|
|
s1.data == s2.data
|
|
else:
|
|
for i in 0 ..< len(s1.data):
|
|
if s1.isPattern(i):
|
|
if s2.isPattern(i):
|
|
# comparison of segments with patterns
|
|
if s1.data[i] != s2.data[i]:
|
|
return false
|
|
else:
|
|
# comparison of pattern segment with value segment
|
|
if s1.patternCb(s1.data[i], s2.data[i]) != 0:
|
|
return false
|
|
else:
|
|
if s2.isPattern(i):
|
|
# comparison of pattern segment with value segment
|
|
if s2.patternCb(s2.data[i], s1.data[i]) != 0:
|
|
return false
|
|
else:
|
|
# comparison of value segments
|
|
if s1.data[i] != s2.data[i]:
|
|
return false
|
|
true
|
|
else:
|
|
false
|
|
|
|
proc `<`*(s1, s2: SegmentedPath): bool =
|
|
let ls1 = len(s1.data)
|
|
let ls2 = len(s2.data)
|
|
if ls1 < ls2:
|
|
# `s1` has less segments than `s2`
|
|
true
|
|
elif ls1 > ls2:
|
|
# `s1` has more segments than `s2`
|
|
false
|
|
else:
|
|
# `s1` and `s2` has equal number of segments.
|
|
for i in 0 ..< ls1:
|
|
if s1.isPattern(i):
|
|
if s2.isPattern(i):
|
|
# comparison of segments with patterns
|
|
let res = cmp(s1.data[i], s2.data[i])
|
|
if res != 0:
|
|
return (res < 0)
|
|
else:
|
|
# comparison of pattern segment with value segment
|
|
if s1.patternCb(s1.data[i], s2.data[i]) != 0:
|
|
return false
|
|
else:
|
|
if s2.isPattern(i):
|
|
# comparison of pattern segment with value segment
|
|
if s2.patternCb(s2.data[i], s1.data[i]) != 0:
|
|
return false
|
|
else:
|
|
# comparison of value segments
|
|
let res = cmp(s1.data[i], s2.data[i])
|
|
if res != 0:
|
|
return (res < 0)
|
|
false
|
|
|
|
proc init*(st: typedesc[SegmentedPath],
|
|
upath: string): RestResult[SegmentedPath] =
|
|
var data = upath.split("/")
|
|
if len(data) <= 64:
|
|
for i in 0 ..< len(data):
|
|
data[i] = decodeUrl(data[i], true)
|
|
ok(SegmentedPath(patternCb: nil, patterns: 0'u64, data: data))
|
|
else:
|
|
err("Path has too many segments (more then 64)")
|
|
|
|
proc init*(st: typedesc[SegmentedPath],
|
|
request: HttpMethod, upath: string): RestResult[SegmentedPath] =
|
|
let path =
|
|
if upath.startsWith('/'):
|
|
$request & upath
|
|
else:
|
|
$request & "/" & upath
|
|
init(st, path)
|
|
|
|
proc init*(st: typedesc[SegmentedPath],
|
|
upath: string, patternCb: PatternCallback): SegmentedPath =
|
|
var res = SegmentedPath(patternCb: patternCb, patterns: 0'u64)
|
|
var counter = 0
|
|
var patterns: seq[string]
|
|
for item in upath.split("/"):
|
|
doAssert(counter < 64, "Path has too many segments (more then 64)")
|
|
if len(item) >= 2:
|
|
if item[0] == '{' and item[^1] == '}':
|
|
doAssert(len(item) > 2, "Patterns with empty names are not allowed")
|
|
res.patterns = res.patterns or (1'u64 shl counter)
|
|
if item in patterns:
|
|
raiseAssert "Only unique patterns allowed in path"
|
|
else:
|
|
patterns.add(item)
|
|
res.data.add(item)
|
|
inc(counter)
|
|
continue
|
|
res.data.add(encodeUrl(item))
|
|
inc(counter)
|
|
res
|
|
|
|
proc init*(st: typedesc[SegmentedPath], request: HttpMethod,
|
|
upath: string, patternCb: PatternCallback): SegmentedPath =
|
|
let path =
|
|
if upath.startsWith('/'):
|
|
$request & upath
|
|
else:
|
|
$request & "/" & upath
|
|
SegmentedPath.init(path, patternCb)
|
|
|
|
proc createPath*(upath: string, values: openarray[KeyValueTuple]): string =
|
|
var data: seq[string]
|
|
var counter = 0
|
|
var valuesCount = 0
|
|
var patterns: seq[string]
|
|
for item in upath.split("/"):
|
|
doAssert(counter < 64, "Path has too many segments (more then 64)")
|
|
let value =
|
|
if len(item) >= 2:
|
|
if item[0] == '{' and item[^1] == '}':
|
|
doAssert(len(item) > 2, "Patterns with empty names are not allowed")
|
|
if item in patterns:
|
|
raiseAssert "Only unique patterns allowed in path"
|
|
else:
|
|
patterns.add(item)
|
|
let searchKey = item[1 .. ^2]
|
|
var res = ""
|
|
for item in values:
|
|
if item.key == searchKey:
|
|
res = item.value
|
|
if len(res) == 0:
|
|
raiseAssert "Pattern key has not been found in values array"
|
|
inc(valuesCount)
|
|
res
|
|
else:
|
|
item
|
|
else:
|
|
item
|
|
data.add(encodeUrl(value, true))
|
|
inc(counter)
|
|
if len(values) != valuesCount:
|
|
raiseAssert(
|
|
"Size of values array do not equal to number of patterns in path")
|
|
data.join("/")
|
|
|
|
proc `$`*(seg: SegmentedPath): string =
|
|
seg.data.join("/")
|