mirror of
https://github.com/logos-messaging/logos-messaging-nim.git
synced 2026-01-02 14:03:06 +00:00
chore: extend RequestBroker with supporting native and external types and added possibility to define non-async (aka sync) requests for simplicity and performance (#3665)
* chore: extend RequestBroker with supporting native and external types and added possibility to define non-async (aka sync) requests for simplicity and performance * Adapt gcsafe pragma for RequestBroker sync requests and provider signatures as requirement --------- Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com>
This commit is contained in:
parent
2477c4980f
commit
3323325526
@ -6,6 +6,10 @@ import std/strutils
|
||||
|
||||
import waku/common/broker/request_broker
|
||||
|
||||
## ---------------------------------------------------------------------------
|
||||
## Async-mode brokers + tests
|
||||
## ---------------------------------------------------------------------------
|
||||
|
||||
RequestBroker:
|
||||
type SimpleResponse = object
|
||||
value*: string
|
||||
@ -31,11 +35,14 @@ RequestBroker:
|
||||
suffix: string
|
||||
): Future[Result[DualResponse, string]] {.async.}
|
||||
|
||||
RequestBroker:
|
||||
RequestBroker(async):
|
||||
type ImplicitResponse = ref object
|
||||
note*: string
|
||||
|
||||
suite "RequestBroker macro":
|
||||
static:
|
||||
doAssert typeof(SimpleResponse.request()) is Future[Result[SimpleResponse, string]]
|
||||
|
||||
suite "RequestBroker macro (async mode)":
|
||||
test "serves zero-argument providers":
|
||||
check SimpleResponse
|
||||
.setProvider(
|
||||
@ -52,7 +59,7 @@ suite "RequestBroker macro":
|
||||
|
||||
test "zero-argument request errors when unset":
|
||||
let res = waitFor SimpleResponse.request()
|
||||
check res.isErr
|
||||
check res.isErr()
|
||||
check res.error.contains("no zero-arg provider")
|
||||
|
||||
test "serves input-based providers":
|
||||
@ -78,7 +85,6 @@ suite "RequestBroker macro":
|
||||
.setProvider(
|
||||
proc(key: string, subKey: int): Future[Result[KeyedResponse, string]] {.async.} =
|
||||
raise newException(ValueError, "simulated failure")
|
||||
ok(KeyedResponse(key: key, payload: ""))
|
||||
)
|
||||
.isOk()
|
||||
|
||||
@ -90,7 +96,7 @@ suite "RequestBroker macro":
|
||||
|
||||
test "input request errors when unset":
|
||||
let res = waitFor KeyedResponse.request("foo", 2)
|
||||
check res.isErr
|
||||
check res.isErr()
|
||||
check res.error.contains("input signature")
|
||||
|
||||
test "supports both provider types simultaneously":
|
||||
@ -109,11 +115,11 @@ suite "RequestBroker macro":
|
||||
.isOk()
|
||||
|
||||
let noInput = waitFor DualResponse.request()
|
||||
check noInput.isOk
|
||||
check noInput.isOk()
|
||||
check noInput.value.note == "base"
|
||||
|
||||
let withInput = waitFor DualResponse.request("-extra")
|
||||
check withInput.isOk
|
||||
check withInput.isOk()
|
||||
check withInput.value.note == "base-extra"
|
||||
check withInput.value.count == 6
|
||||
|
||||
@ -129,7 +135,7 @@ suite "RequestBroker macro":
|
||||
DualResponse.clearProvider()
|
||||
|
||||
let res = waitFor DualResponse.request()
|
||||
check res.isErr
|
||||
check res.isErr()
|
||||
|
||||
test "implicit zero-argument provider works by default":
|
||||
check ImplicitResponse
|
||||
@ -140,14 +146,14 @@ suite "RequestBroker macro":
|
||||
.isOk()
|
||||
|
||||
let res = waitFor ImplicitResponse.request()
|
||||
check res.isOk
|
||||
check res.isOk()
|
||||
|
||||
ImplicitResponse.clearProvider()
|
||||
check res.value.note == "auto"
|
||||
|
||||
test "implicit zero-argument request errors when unset":
|
||||
let res = waitFor ImplicitResponse.request()
|
||||
check res.isErr
|
||||
check res.isErr()
|
||||
check res.error.contains("no zero-arg provider")
|
||||
|
||||
test "no provider override":
|
||||
@ -171,7 +177,7 @@ suite "RequestBroker macro":
|
||||
check DualResponse.setProvider(overrideProc).isErr()
|
||||
|
||||
let noInput = waitFor DualResponse.request()
|
||||
check noInput.isOk
|
||||
check noInput.isOk()
|
||||
check noInput.value.note == "base"
|
||||
|
||||
let stillResponse = waitFor DualResponse.request(" still works")
|
||||
@ -191,8 +197,306 @@ suite "RequestBroker macro":
|
||||
check DualResponse.setProvider(overrideProc).isOk()
|
||||
|
||||
let nowSuccWithOverride = waitFor DualResponse.request()
|
||||
check nowSuccWithOverride.isOk
|
||||
check nowSuccWithOverride.isOk()
|
||||
check nowSuccWithOverride.value.note == "something else"
|
||||
check nowSuccWithOverride.value.count == 1
|
||||
|
||||
DualResponse.clearProvider()
|
||||
|
||||
## ---------------------------------------------------------------------------
|
||||
## Sync-mode brokers + tests
|
||||
## ---------------------------------------------------------------------------
|
||||
|
||||
RequestBroker(sync):
|
||||
type SimpleResponseSync = object
|
||||
value*: string
|
||||
|
||||
proc signatureFetch*(): Result[SimpleResponseSync, string]
|
||||
|
||||
RequestBroker(sync):
|
||||
type KeyedResponseSync = object
|
||||
key*: string
|
||||
payload*: string
|
||||
|
||||
proc signatureFetchWithKey*(
|
||||
key: string, subKey: int
|
||||
): Result[KeyedResponseSync, string]
|
||||
|
||||
RequestBroker(sync):
|
||||
type DualResponseSync = object
|
||||
note*: string
|
||||
count*: int
|
||||
|
||||
proc signatureNoInput*(): Result[DualResponseSync, string]
|
||||
proc signatureWithInput*(suffix: string): Result[DualResponseSync, string]
|
||||
|
||||
RequestBroker(sync):
|
||||
type ImplicitResponseSync = ref object
|
||||
note*: string
|
||||
|
||||
static:
|
||||
doAssert typeof(SimpleResponseSync.request()) is Result[SimpleResponseSync, string]
|
||||
doAssert not (
|
||||
typeof(SimpleResponseSync.request()) is Future[Result[SimpleResponseSync, string]]
|
||||
)
|
||||
doAssert typeof(KeyedResponseSync.request("topic", 1)) is
|
||||
Result[KeyedResponseSync, string]
|
||||
|
||||
suite "RequestBroker macro (sync mode)":
|
||||
test "serves zero-argument providers (sync)":
|
||||
check SimpleResponseSync
|
||||
.setProvider(
|
||||
proc(): Result[SimpleResponseSync, string] =
|
||||
ok(SimpleResponseSync(value: "hi"))
|
||||
)
|
||||
.isOk()
|
||||
|
||||
let res = SimpleResponseSync.request()
|
||||
check res.isOk()
|
||||
check res.value.value == "hi"
|
||||
|
||||
SimpleResponseSync.clearProvider()
|
||||
|
||||
test "zero-argument request errors when unset (sync)":
|
||||
let res = SimpleResponseSync.request()
|
||||
check res.isErr()
|
||||
check res.error.contains("no zero-arg provider")
|
||||
|
||||
test "serves input-based providers (sync)":
|
||||
var seen: seq[string] = @[]
|
||||
check KeyedResponseSync
|
||||
.setProvider(
|
||||
proc(key: string, subKey: int): Result[KeyedResponseSync, string] =
|
||||
seen.add(key)
|
||||
ok(KeyedResponseSync(key: key, payload: key & "-payload+" & $subKey))
|
||||
)
|
||||
.isOk()
|
||||
|
||||
let res = KeyedResponseSync.request("topic", 1)
|
||||
check res.isOk()
|
||||
check res.value.key == "topic"
|
||||
check res.value.payload == "topic-payload+1"
|
||||
check seen == @["topic"]
|
||||
|
||||
KeyedResponseSync.clearProvider()
|
||||
|
||||
test "catches provider exception (sync)":
|
||||
check KeyedResponseSync
|
||||
.setProvider(
|
||||
proc(key: string, subKey: int): Result[KeyedResponseSync, string] =
|
||||
raise newException(ValueError, "simulated failure")
|
||||
)
|
||||
.isOk()
|
||||
|
||||
let res = KeyedResponseSync.request("neglected", 11)
|
||||
check res.isErr()
|
||||
check res.error.contains("simulated failure")
|
||||
|
||||
KeyedResponseSync.clearProvider()
|
||||
|
||||
test "input request errors when unset (sync)":
|
||||
let res = KeyedResponseSync.request("foo", 2)
|
||||
check res.isErr()
|
||||
check res.error.contains("input signature")
|
||||
|
||||
test "supports both provider types simultaneously (sync)":
|
||||
check DualResponseSync
|
||||
.setProvider(
|
||||
proc(): Result[DualResponseSync, string] =
|
||||
ok(DualResponseSync(note: "base", count: 1))
|
||||
)
|
||||
.isOk()
|
||||
|
||||
check DualResponseSync
|
||||
.setProvider(
|
||||
proc(suffix: string): Result[DualResponseSync, string] =
|
||||
ok(DualResponseSync(note: "base" & suffix, count: suffix.len))
|
||||
)
|
||||
.isOk()
|
||||
|
||||
let noInput = DualResponseSync.request()
|
||||
check noInput.isOk()
|
||||
check noInput.value.note == "base"
|
||||
|
||||
let withInput = DualResponseSync.request("-extra")
|
||||
check withInput.isOk()
|
||||
check withInput.value.note == "base-extra"
|
||||
check withInput.value.count == 6
|
||||
|
||||
DualResponseSync.clearProvider()
|
||||
|
||||
test "clearProvider resets both entries (sync)":
|
||||
check DualResponseSync
|
||||
.setProvider(
|
||||
proc(): Result[DualResponseSync, string] =
|
||||
ok(DualResponseSync(note: "temp", count: 0))
|
||||
)
|
||||
.isOk()
|
||||
DualResponseSync.clearProvider()
|
||||
|
||||
let res = DualResponseSync.request()
|
||||
check res.isErr()
|
||||
|
||||
test "implicit zero-argument provider works by default (sync)":
|
||||
check ImplicitResponseSync
|
||||
.setProvider(
|
||||
proc(): Result[ImplicitResponseSync, string] =
|
||||
ok(ImplicitResponseSync(note: "auto"))
|
||||
)
|
||||
.isOk()
|
||||
|
||||
let res = ImplicitResponseSync.request()
|
||||
check res.isOk()
|
||||
|
||||
ImplicitResponseSync.clearProvider()
|
||||
check res.value.note == "auto"
|
||||
|
||||
test "implicit zero-argument request errors when unset (sync)":
|
||||
let res = ImplicitResponseSync.request()
|
||||
check res.isErr()
|
||||
check res.error.contains("no zero-arg provider")
|
||||
|
||||
test "implicit zero-argument provider raises error (sync)":
|
||||
check ImplicitResponseSync
|
||||
.setProvider(
|
||||
proc(): Result[ImplicitResponseSync, string] =
|
||||
raise newException(ValueError, "simulated failure")
|
||||
)
|
||||
.isOk()
|
||||
|
||||
let res = ImplicitResponseSync.request()
|
||||
check res.isErr()
|
||||
check res.error.contains("simulated failure")
|
||||
|
||||
ImplicitResponseSync.clearProvider()
|
||||
|
||||
## ---------------------------------------------------------------------------
|
||||
## POD / external type brokers + tests (distinct/alias behavior)
|
||||
## ---------------------------------------------------------------------------
|
||||
|
||||
type ExternalDefinedTypeAsync = object
|
||||
label*: string
|
||||
|
||||
type ExternalDefinedTypeSync = object
|
||||
label*: string
|
||||
|
||||
type ExternalDefinedTypeShared = object
|
||||
label*: string
|
||||
|
||||
RequestBroker:
|
||||
type PodResponse = int
|
||||
|
||||
proc signatureFetch*(): Future[Result[PodResponse, string]] {.async.}
|
||||
|
||||
RequestBroker:
|
||||
type ExternalAliasedResponse = ExternalDefinedTypeAsync
|
||||
|
||||
proc signatureFetch*(): Future[Result[ExternalAliasedResponse, string]] {.async.}
|
||||
|
||||
RequestBroker(sync):
|
||||
type ExternalAliasedResponseSync = ExternalDefinedTypeSync
|
||||
|
||||
proc signatureFetch*(): Result[ExternalAliasedResponseSync, string]
|
||||
|
||||
RequestBroker(sync):
|
||||
type DistinctStringResponseA = distinct string
|
||||
|
||||
RequestBroker(sync):
|
||||
type DistinctStringResponseB = distinct string
|
||||
|
||||
RequestBroker(sync):
|
||||
type ExternalDistinctResponseA = distinct ExternalDefinedTypeShared
|
||||
|
||||
RequestBroker(sync):
|
||||
type ExternalDistinctResponseB = distinct ExternalDefinedTypeShared
|
||||
|
||||
suite "RequestBroker macro (POD/external types)":
|
||||
test "supports non-object response types (async)":
|
||||
check PodResponse
|
||||
.setProvider(
|
||||
proc(): Future[Result[PodResponse, string]] {.async.} =
|
||||
ok(PodResponse(123))
|
||||
)
|
||||
.isOk()
|
||||
|
||||
let res = waitFor PodResponse.request()
|
||||
check res.isOk()
|
||||
check int(res.value) == 123
|
||||
|
||||
PodResponse.clearProvider()
|
||||
|
||||
test "supports aliased external types (async)":
|
||||
check ExternalAliasedResponse
|
||||
.setProvider(
|
||||
proc(): Future[Result[ExternalAliasedResponse, string]] {.async.} =
|
||||
ok(ExternalAliasedResponse(ExternalDefinedTypeAsync(label: "ext")))
|
||||
)
|
||||
.isOk()
|
||||
|
||||
let res = waitFor ExternalAliasedResponse.request()
|
||||
check res.isOk()
|
||||
check ExternalDefinedTypeAsync(res.value).label == "ext"
|
||||
|
||||
ExternalAliasedResponse.clearProvider()
|
||||
|
||||
test "supports aliased external types (sync)":
|
||||
check ExternalAliasedResponseSync
|
||||
.setProvider(
|
||||
proc(): Result[ExternalAliasedResponseSync, string] =
|
||||
ok(ExternalAliasedResponseSync(ExternalDefinedTypeSync(label: "ext")))
|
||||
)
|
||||
.isOk()
|
||||
|
||||
let res = ExternalAliasedResponseSync.request()
|
||||
check res.isOk()
|
||||
check ExternalDefinedTypeSync(res.value).label == "ext"
|
||||
|
||||
ExternalAliasedResponseSync.clearProvider()
|
||||
|
||||
test "distinct response types avoid overload ambiguity (sync)":
|
||||
check DistinctStringResponseA
|
||||
.setProvider(
|
||||
proc(): Result[DistinctStringResponseA, string] =
|
||||
ok(DistinctStringResponseA("a"))
|
||||
)
|
||||
.isOk()
|
||||
|
||||
check DistinctStringResponseB
|
||||
.setProvider(
|
||||
proc(): Result[DistinctStringResponseB, string] =
|
||||
ok(DistinctStringResponseB("b"))
|
||||
)
|
||||
.isOk()
|
||||
|
||||
check ExternalDistinctResponseA
|
||||
.setProvider(
|
||||
proc(): Result[ExternalDistinctResponseA, string] =
|
||||
ok(ExternalDistinctResponseA(ExternalDefinedTypeShared(label: "ea")))
|
||||
)
|
||||
.isOk()
|
||||
|
||||
check ExternalDistinctResponseB
|
||||
.setProvider(
|
||||
proc(): Result[ExternalDistinctResponseB, string] =
|
||||
ok(ExternalDistinctResponseB(ExternalDefinedTypeShared(label: "eb")))
|
||||
)
|
||||
.isOk()
|
||||
|
||||
let resA = DistinctStringResponseA.request()
|
||||
let resB = DistinctStringResponseB.request()
|
||||
check resA.isOk()
|
||||
check resB.isOk()
|
||||
check string(resA.value) == "a"
|
||||
check string(resB.value) == "b"
|
||||
|
||||
let resEA = ExternalDistinctResponseA.request()
|
||||
let resEB = ExternalDistinctResponseB.request()
|
||||
check resEA.isOk()
|
||||
check resEB.isOk()
|
||||
check ExternalDefinedTypeShared(resEA.value).label == "ea"
|
||||
check ExternalDefinedTypeShared(resEB.value).label == "eb"
|
||||
|
||||
DistinctStringResponseA.clearProvider()
|
||||
DistinctStringResponseB.clearProvider()
|
||||
ExternalDistinctResponseA.clearProvider()
|
||||
ExternalDistinctResponseB.clearProvider()
|
||||
|
||||
@ -6,8 +6,15 @@
|
||||
## Worth considering using it in a single provider, many requester scenario.
|
||||
##
|
||||
## Provides a declarative way to define an immutable value type together with a
|
||||
## thread-local broker that can register an asynchronous provider, dispatch typed
|
||||
## requests and clear provider.
|
||||
## thread-local broker that can register an asynchronous or synchronous provider,
|
||||
## dispatch typed requests and clear provider.
|
||||
##
|
||||
## For consideration use `sync` mode RequestBroker when you need to provide simple value(s)
|
||||
## where there is no long-running async operation involved.
|
||||
## Typically it act as a accessor for the local state of generic setting.
|
||||
##
|
||||
## `async` mode is better to be used when you request date that may involve some long IO operation
|
||||
## or action.
|
||||
##
|
||||
## Usage:
|
||||
## Declare your desired request type inside a `RequestBroker` macro, add any number of fields.
|
||||
@ -24,6 +31,56 @@
|
||||
## proc signature*(arg1: ArgType, arg2: AnotherArgType): Future[Result[TypeName, string]]
|
||||
##
|
||||
## ```
|
||||
##
|
||||
## Sync mode (no `async` / `Future`) can be generated with:
|
||||
##
|
||||
## ```nim
|
||||
## RequestBroker(sync):
|
||||
## type TypeName = object
|
||||
## field1*: FieldType
|
||||
##
|
||||
## proc signature*(): Result[TypeName, string]
|
||||
## proc signature*(arg1: ArgType): Result[TypeName, string]
|
||||
## ```
|
||||
##
|
||||
## Note: When the request type is declared as a native type / alias / externally-defined
|
||||
## type (i.e. not an inline `object` / `ref object` definition), RequestBroker
|
||||
## will wrap it in `distinct` automatically unless you already used `distinct`.
|
||||
## This avoids overload ambiguity when multiple brokers share the same
|
||||
## underlying base type (Nim overload resolution does not consider return type).
|
||||
##
|
||||
## This means that for non-object request types you typically:
|
||||
## - construct values with an explicit cast/constructor, e.g. `MyType("x")`
|
||||
## - unwrap with a cast when needed, e.g. `string(myVal)` or `BaseType(myVal)`
|
||||
##
|
||||
## Example (native response type):
|
||||
## ```nim
|
||||
## RequestBroker(sync):
|
||||
## type MyCount = int # exported as: `distinct int`
|
||||
##
|
||||
## MyCount.setProvider(proc(): Result[MyCount, string] = ok(MyCount(42)))
|
||||
## let res = MyCount.request()
|
||||
## if res.isOk():
|
||||
## let raw = int(res.get())
|
||||
## ```
|
||||
##
|
||||
## Example (externally-defined type):
|
||||
## ```nim
|
||||
## type External = object
|
||||
## label*: string
|
||||
##
|
||||
## RequestBroker:
|
||||
## type MyExternal = External # exported as: `distinct External`
|
||||
##
|
||||
## MyExternal.setProvider(
|
||||
## proc(): Future[Result[MyExternal, string]] {.async.} =
|
||||
## ok(MyExternal(External(label: "hi")))
|
||||
## )
|
||||
## let res = await MyExternal.request()
|
||||
## if res.isOk():
|
||||
## let base = External(res.get())
|
||||
## echo base.label
|
||||
## ```
|
||||
## The 'TypeName' object defines the requestable data (but also can be seen as request for action with return value).
|
||||
## The 'signature' proc defines the provider(s) signature, that is enforced at compile time.
|
||||
## One signature can be with no arguments, another with any number of arguments - where the input arguments are
|
||||
@ -31,12 +88,12 @@
|
||||
##
|
||||
## After this, you can register a provider anywhere in your code with
|
||||
## `TypeName.setProvider(...)`, which returns error if already having a provider.
|
||||
## Providers are async procs or lambdas that take no arguments and return a Future[Result[TypeName, string]].
|
||||
## Providers are async procs/lambdas in default mode and sync procs in sync mode.
|
||||
## Only one provider can be registered at a time per signature type (zero arg and/or multi arg).
|
||||
##
|
||||
## Requests can be made from anywhere with no direct dependency on the provider by
|
||||
## calling `TypeName.request()` - with arguments respecting the signature(s).
|
||||
## This will asynchronously call the registered provider and return a Future[Result[TypeName, string]].
|
||||
## In async mode, this returns a Future[Result[TypeName, string]]. In sync mode, it returns Result[TypeName, string].
|
||||
##
|
||||
## Whenever you no want to process requests (or your object instance that provides the request goes out of scope),
|
||||
## you can remove it from the broker with `TypeName.clearProvider()`.
|
||||
@ -49,10 +106,10 @@
|
||||
## text*: string
|
||||
##
|
||||
## ## Define the request and provider signature, that is enforced at compile time.
|
||||
## proc signature*(): Future[Result[Greeting, string]]
|
||||
## proc signature*(): Future[Result[Greeting, string]] {.async.}
|
||||
##
|
||||
## ## Also possible to define signature with arbitrary input arguments.
|
||||
## proc signature*(lang: string): Future[Result[Greeting, string]]
|
||||
## proc signature*(lang: string): Future[Result[Greeting, string]] {.async.}
|
||||
##
|
||||
## ...
|
||||
## Greeting.setProvider(
|
||||
@ -60,6 +117,23 @@
|
||||
## ok(Greeting(text: "hello"))
|
||||
## )
|
||||
## let res = await Greeting.request()
|
||||
##
|
||||
##
|
||||
## ...
|
||||
## # using native type as response for a synchronous request.
|
||||
## RequestBroker(sync):
|
||||
## type NeedThatInfo = string
|
||||
##
|
||||
##...
|
||||
## NeedThatInfo.setProvider(
|
||||
## proc(): Result[NeedThatInfo, string] =
|
||||
## ok("this is the info you wanted")
|
||||
## )
|
||||
## let res = NeedThatInfo.request().valueOr:
|
||||
## echo "not ok due to: " & error
|
||||
## NeedThatInfo(":-(")
|
||||
##
|
||||
## echo string(res)
|
||||
## ```
|
||||
## If no `signature` proc is declared, a zero-argument form is generated
|
||||
## automatically, so the caller only needs to provide the type definition.
|
||||
@ -77,7 +151,11 @@ proc errorFuture[T](message: string): Future[Result[T, string]] {.inline.} =
|
||||
fut.complete(err(Result[T, string], message))
|
||||
fut
|
||||
|
||||
proc isReturnTypeValid(returnType, typeIdent: NimNode): bool =
|
||||
type RequestBrokerMode = enum
|
||||
rbAsync
|
||||
rbSync
|
||||
|
||||
proc isAsyncReturnTypeValid(returnType, typeIdent: NimNode): bool =
|
||||
## Accept Future[Result[TypeIdent, string]] as the contract.
|
||||
if returnType.kind != nnkBracketExpr or returnType.len != 2:
|
||||
return false
|
||||
@ -92,6 +170,23 @@ proc isReturnTypeValid(returnType, typeIdent: NimNode): bool =
|
||||
return false
|
||||
inner[2].kind == nnkIdent and inner[2].eqIdent("string")
|
||||
|
||||
proc isSyncReturnTypeValid(returnType, typeIdent: NimNode): bool =
|
||||
## Accept Result[TypeIdent, string] as the contract.
|
||||
if returnType.kind != nnkBracketExpr or returnType.len != 3:
|
||||
return false
|
||||
if returnType[0].kind != nnkIdent or not returnType[0].eqIdent("Result"):
|
||||
return false
|
||||
if returnType[1].kind != nnkIdent or not returnType[1].eqIdent($typeIdent):
|
||||
return false
|
||||
returnType[2].kind == nnkIdent and returnType[2].eqIdent("string")
|
||||
|
||||
proc isReturnTypeValid(returnType, typeIdent: NimNode, mode: RequestBrokerMode): bool =
|
||||
case mode
|
||||
of rbAsync:
|
||||
isAsyncReturnTypeValid(returnType, typeIdent)
|
||||
of rbSync:
|
||||
isSyncReturnTypeValid(returnType, typeIdent)
|
||||
|
||||
proc cloneParams(params: seq[NimNode]): seq[NimNode] =
|
||||
## Deep copy parameter definitions so they can be inserted in multiple places.
|
||||
result = @[]
|
||||
@ -109,73 +204,122 @@ proc collectParamNames(params: seq[NimNode]): seq[NimNode] =
|
||||
continue
|
||||
result.add(ident($nameNode))
|
||||
|
||||
proc makeProcType(returnType: NimNode, params: seq[NimNode]): NimNode =
|
||||
proc makeProcType(
|
||||
returnType: NimNode, params: seq[NimNode], mode: RequestBrokerMode
|
||||
): NimNode =
|
||||
var formal = newTree(nnkFormalParams)
|
||||
formal.add(returnType)
|
||||
for param in params:
|
||||
formal.add(param)
|
||||
let pragmas = newTree(nnkPragma, ident("async"))
|
||||
newTree(nnkProcTy, formal, pragmas)
|
||||
case mode
|
||||
of rbAsync:
|
||||
let pragmas = newTree(nnkPragma, ident("async"))
|
||||
newTree(nnkProcTy, formal, pragmas)
|
||||
of rbSync:
|
||||
let raisesPragma = newTree(
|
||||
nnkExprColonExpr, ident("raises"), newTree(nnkBracket, ident("CatchableError"))
|
||||
)
|
||||
let pragmas = newTree(nnkPragma, raisesPragma, ident("gcsafe"))
|
||||
newTree(nnkProcTy, formal, pragmas)
|
||||
|
||||
macro RequestBroker*(body: untyped): untyped =
|
||||
proc parseMode(modeNode: NimNode): RequestBrokerMode =
|
||||
## Parses the mode selector for the 2-argument macro overload.
|
||||
## Supported spellings: `sync` / `async` (case-insensitive).
|
||||
let raw = ($modeNode).strip().toLowerAscii()
|
||||
case raw
|
||||
of "sync":
|
||||
rbSync
|
||||
of "async":
|
||||
rbAsync
|
||||
else:
|
||||
error("RequestBroker mode must be `sync` or `async` (default is async)", modeNode)
|
||||
|
||||
proc ensureDistinctType(rhs: NimNode): NimNode =
|
||||
## For PODs / aliases / externally-defined types, wrap in `distinct` unless
|
||||
## it's already distinct.
|
||||
if rhs.kind == nnkDistinctTy:
|
||||
return copyNimTree(rhs)
|
||||
newTree(nnkDistinctTy, copyNimTree(rhs))
|
||||
|
||||
proc generateRequestBroker(body: NimNode, mode: RequestBrokerMode): NimNode =
|
||||
when defined(requestBrokerDebug):
|
||||
echo body.treeRepr
|
||||
echo "RequestBroker mode: ", $mode
|
||||
var typeIdent: NimNode = nil
|
||||
var objectDef: NimNode = nil
|
||||
var isRefObject = false
|
||||
for stmt in body:
|
||||
if stmt.kind == nnkTypeSection:
|
||||
for def in stmt:
|
||||
if def.kind != nnkTypeDef:
|
||||
continue
|
||||
if not typeIdent.isNil():
|
||||
error("Only one type may be declared inside RequestBroker", def)
|
||||
|
||||
typeIdent = baseTypeIdent(def[0])
|
||||
let rhs = def[2]
|
||||
var objectType: NimNode
|
||||
|
||||
## Support inline object types (fields are auto-exported)
|
||||
## AND non-object types / aliases (e.g. `string`, `int`, `OtherType`).
|
||||
case rhs.kind
|
||||
of nnkObjectTy:
|
||||
objectType = rhs
|
||||
let recList = rhs[2]
|
||||
if recList.kind != nnkRecList:
|
||||
error("RequestBroker object must declare a standard field list", rhs)
|
||||
var exportedRecList = newTree(nnkRecList)
|
||||
for field in recList:
|
||||
case field.kind
|
||||
of nnkIdentDefs:
|
||||
ensureFieldDef(field)
|
||||
var cloned = copyNimTree(field)
|
||||
for i in 0 ..< cloned.len - 2:
|
||||
cloned[i] = exportIdentNode(cloned[i])
|
||||
exportedRecList.add(cloned)
|
||||
of nnkEmpty:
|
||||
discard
|
||||
else:
|
||||
error(
|
||||
"RequestBroker object definition only supports simple field declarations",
|
||||
field,
|
||||
)
|
||||
objectDef = newTree(
|
||||
nnkObjectTy, copyNimTree(rhs[0]), copyNimTree(rhs[1]), exportedRecList
|
||||
)
|
||||
of nnkRefTy:
|
||||
isRefObject = true
|
||||
if rhs.len != 1 or rhs[0].kind != nnkObjectTy:
|
||||
error(
|
||||
"RequestBroker ref object must wrap a concrete object definition", rhs
|
||||
if rhs.len != 1:
|
||||
error("RequestBroker ref type must have a single base", rhs)
|
||||
if rhs[0].kind == nnkObjectTy:
|
||||
let obj = rhs[0]
|
||||
let recList = obj[2]
|
||||
if recList.kind != nnkRecList:
|
||||
error("RequestBroker object must declare a standard field list", obj)
|
||||
var exportedRecList = newTree(nnkRecList)
|
||||
for field in recList:
|
||||
case field.kind
|
||||
of nnkIdentDefs:
|
||||
ensureFieldDef(field)
|
||||
var cloned = copyNimTree(field)
|
||||
for i in 0 ..< cloned.len - 2:
|
||||
cloned[i] = exportIdentNode(cloned[i])
|
||||
exportedRecList.add(cloned)
|
||||
of nnkEmpty:
|
||||
discard
|
||||
else:
|
||||
error(
|
||||
"RequestBroker object definition only supports simple field declarations",
|
||||
field,
|
||||
)
|
||||
let exportedObjectType = newTree(
|
||||
nnkObjectTy, copyNimTree(obj[0]), copyNimTree(obj[1]), exportedRecList
|
||||
)
|
||||
objectType = rhs[0]
|
||||
else:
|
||||
continue
|
||||
if not typeIdent.isNil():
|
||||
error("Only one object type may be declared inside RequestBroker", def)
|
||||
typeIdent = baseTypeIdent(def[0])
|
||||
let recList = objectType[2]
|
||||
if recList.kind != nnkRecList:
|
||||
error("RequestBroker object must declare a standard field list", objectType)
|
||||
var exportedRecList = newTree(nnkRecList)
|
||||
for field in recList:
|
||||
case field.kind
|
||||
of nnkIdentDefs:
|
||||
ensureFieldDef(field)
|
||||
var cloned = copyNimTree(field)
|
||||
for i in 0 ..< cloned.len - 2:
|
||||
cloned[i] = exportIdentNode(cloned[i])
|
||||
exportedRecList.add(cloned)
|
||||
of nnkEmpty:
|
||||
discard
|
||||
objectDef = newTree(nnkRefTy, exportedObjectType)
|
||||
else:
|
||||
error(
|
||||
"RequestBroker object definition only supports simple field declarations",
|
||||
field,
|
||||
)
|
||||
let exportedObjectType = newTree(
|
||||
nnkObjectTy,
|
||||
copyNimTree(objectType[0]),
|
||||
copyNimTree(objectType[1]),
|
||||
exportedRecList,
|
||||
)
|
||||
if isRefObject:
|
||||
objectDef = newTree(nnkRefTy, exportedObjectType)
|
||||
## `ref SomeType` (SomeType can be defined elsewhere)
|
||||
objectDef = ensureDistinctType(rhs)
|
||||
else:
|
||||
objectDef = exportedObjectType
|
||||
## Non-object type / alias (e.g. `string`, `int`, `SomeExternalType`).
|
||||
objectDef = ensureDistinctType(rhs)
|
||||
if typeIdent.isNil():
|
||||
error("RequestBroker body must declare exactly one object type", body)
|
||||
error("RequestBroker body must declare exactly one type", body)
|
||||
|
||||
when defined(requestBrokerDebug):
|
||||
echo "RequestBroker generating type: ", $typeIdent
|
||||
@ -183,7 +327,6 @@ macro RequestBroker*(body: untyped): untyped =
|
||||
let exportedTypeIdent = postfix(copyNimTree(typeIdent), "*")
|
||||
let typeDisplayName = sanitizeIdentName(typeIdent)
|
||||
let typeNameLit = newLit(typeDisplayName)
|
||||
let isRefObjectLit = newLit(isRefObject)
|
||||
var zeroArgSig: NimNode = nil
|
||||
var zeroArgProviderName: NimNode = nil
|
||||
var zeroArgFieldName: NimNode = nil
|
||||
@ -211,10 +354,14 @@ macro RequestBroker*(body: untyped): untyped =
|
||||
if params.len == 0:
|
||||
error("Signature must declare a return type", stmt)
|
||||
let returnType = params[0]
|
||||
if not isReturnTypeValid(returnType, typeIdent):
|
||||
error(
|
||||
"Signature must return Future[Result[`" & $typeIdent & "`, string]]", stmt
|
||||
)
|
||||
if not isReturnTypeValid(returnType, typeIdent, mode):
|
||||
case mode
|
||||
of rbAsync:
|
||||
error(
|
||||
"Signature must return Future[Result[`" & $typeIdent & "`, string]]", stmt
|
||||
)
|
||||
of rbSync:
|
||||
error("Signature must return Result[`" & $typeIdent & "`, string]", stmt)
|
||||
let paramCount = params.len - 1
|
||||
if paramCount == 0:
|
||||
if zeroArgSig != nil:
|
||||
@ -258,14 +405,20 @@ macro RequestBroker*(body: untyped): untyped =
|
||||
var typeSection = newTree(nnkTypeSection)
|
||||
typeSection.add(newTree(nnkTypeDef, exportedTypeIdent, newEmptyNode(), objectDef))
|
||||
|
||||
let returnType = quote:
|
||||
Future[Result[`typeIdent`, string]]
|
||||
let returnType =
|
||||
case mode
|
||||
of rbAsync:
|
||||
quote:
|
||||
Future[Result[`typeIdent`, string]]
|
||||
of rbSync:
|
||||
quote:
|
||||
Result[`typeIdent`, string]
|
||||
|
||||
if not zeroArgSig.isNil():
|
||||
let procType = makeProcType(returnType, @[])
|
||||
let procType = makeProcType(returnType, @[], mode)
|
||||
typeSection.add(newTree(nnkTypeDef, zeroArgProviderName, newEmptyNode(), procType))
|
||||
if not argSig.isNil():
|
||||
let procType = makeProcType(returnType, cloneParams(argParams))
|
||||
let procType = makeProcType(returnType, cloneParams(argParams), mode)
|
||||
typeSection.add(newTree(nnkTypeDef, argProviderName, newEmptyNode(), procType))
|
||||
|
||||
var brokerRecList = newTree(nnkRecList)
|
||||
@ -316,33 +469,69 @@ macro RequestBroker*(body: untyped): untyped =
|
||||
quote do:
|
||||
`accessProcIdent`().`zeroArgFieldName` = nil
|
||||
)
|
||||
result.add(
|
||||
quote do:
|
||||
proc request*(
|
||||
_: typedesc[`typeIdent`]
|
||||
): Future[Result[`typeIdent`, string]] {.async: (raises: []).} =
|
||||
let provider = `accessProcIdent`().`zeroArgFieldName`
|
||||
if provider.isNil():
|
||||
return err(
|
||||
"RequestBroker(" & `typeNameLit` & "): no zero-arg provider registered"
|
||||
)
|
||||
let catchedRes = catch:
|
||||
await provider()
|
||||
case mode
|
||||
of rbAsync:
|
||||
result.add(
|
||||
quote do:
|
||||
proc request*(
|
||||
_: typedesc[`typeIdent`]
|
||||
): Future[Result[`typeIdent`, string]] {.async: (raises: []).} =
|
||||
let provider = `accessProcIdent`().`zeroArgFieldName`
|
||||
if provider.isNil():
|
||||
return err(
|
||||
"RequestBroker(" & `typeNameLit` & "): no zero-arg provider registered"
|
||||
)
|
||||
let catchedRes = catch:
|
||||
await provider()
|
||||
|
||||
if catchedRes.isErr():
|
||||
return err("Request failed:" & catchedRes.error.msg)
|
||||
if catchedRes.isErr():
|
||||
return err(
|
||||
"RequestBroker(" & `typeNameLit` & "): provider threw exception: " &
|
||||
catchedRes.error.msg
|
||||
)
|
||||
|
||||
let providerRes = catchedRes.get()
|
||||
when `isRefObjectLit`:
|
||||
let providerRes = catchedRes.get()
|
||||
if providerRes.isOk():
|
||||
let resultValue = providerRes.get()
|
||||
if resultValue.isNil():
|
||||
return err(
|
||||
"RequestBroker(" & `typeNameLit` & "): provider returned nil result"
|
||||
)
|
||||
return providerRes
|
||||
when compiles(resultValue.isNil()):
|
||||
if resultValue.isNil():
|
||||
return err(
|
||||
"RequestBroker(" & `typeNameLit` & "): provider returned nil result"
|
||||
)
|
||||
return providerRes
|
||||
|
||||
)
|
||||
)
|
||||
of rbSync:
|
||||
result.add(
|
||||
quote do:
|
||||
proc request*(
|
||||
_: typedesc[`typeIdent`]
|
||||
): Result[`typeIdent`, string] {.gcsafe, raises: [].} =
|
||||
let provider = `accessProcIdent`().`zeroArgFieldName`
|
||||
if provider.isNil():
|
||||
return err(
|
||||
"RequestBroker(" & `typeNameLit` & "): no zero-arg provider registered"
|
||||
)
|
||||
|
||||
var providerRes: Result[`typeIdent`, string]
|
||||
try:
|
||||
providerRes = provider()
|
||||
except CatchableError as e:
|
||||
return err(
|
||||
"RequestBroker(" & `typeNameLit` & "): provider threw exception: " &
|
||||
e.msg
|
||||
)
|
||||
|
||||
if providerRes.isOk():
|
||||
let resultValue = providerRes.get()
|
||||
when compiles(resultValue.isNil()):
|
||||
if resultValue.isNil():
|
||||
return err(
|
||||
"RequestBroker(" & `typeNameLit` & "): provider returned nil result"
|
||||
)
|
||||
return providerRes
|
||||
|
||||
)
|
||||
if not argSig.isNil():
|
||||
result.add(
|
||||
quote do:
|
||||
@ -363,10 +552,7 @@ macro RequestBroker*(body: untyped): untyped =
|
||||
let argNameIdents = collectParamNames(requestParamDefs)
|
||||
let providerSym = genSym(nskLet, "provider")
|
||||
var formalParams = newTree(nnkFormalParams)
|
||||
formalParams.add(
|
||||
quote do:
|
||||
Future[Result[`typeIdent`, string]]
|
||||
)
|
||||
formalParams.add(copyNimTree(returnType))
|
||||
formalParams.add(
|
||||
newTree(
|
||||
nnkIdentDefs,
|
||||
@ -378,8 +564,14 @@ macro RequestBroker*(body: untyped): untyped =
|
||||
for paramDef in requestParamDefs:
|
||||
formalParams.add(paramDef)
|
||||
|
||||
let requestPragmas = quote:
|
||||
{.async: (raises: []), gcsafe.}
|
||||
let requestPragmas =
|
||||
case mode
|
||||
of rbAsync:
|
||||
quote:
|
||||
{.async: (raises: []).}
|
||||
of rbSync:
|
||||
quote:
|
||||
{.gcsafe, raises: [].}
|
||||
var providerCall = newCall(providerSym)
|
||||
for argName in argNameIdents:
|
||||
providerCall.add(argName)
|
||||
@ -396,23 +588,49 @@ macro RequestBroker*(body: untyped): untyped =
|
||||
"): no provider registered for input signature"
|
||||
)
|
||||
)
|
||||
requestBody.add(
|
||||
quote do:
|
||||
let catchedRes = catch:
|
||||
await `providerCall`
|
||||
if catchedRes.isErr():
|
||||
return err("Request failed:" & catchedRes.error.msg)
|
||||
|
||||
let providerRes = catchedRes.get()
|
||||
when `isRefObjectLit`:
|
||||
case mode
|
||||
of rbAsync:
|
||||
requestBody.add(
|
||||
quote do:
|
||||
let catchedRes = catch:
|
||||
await `providerCall`
|
||||
if catchedRes.isErr():
|
||||
return err(
|
||||
"RequestBroker(" & `typeNameLit` & "): provider threw exception: " &
|
||||
catchedRes.error.msg
|
||||
)
|
||||
|
||||
let providerRes = catchedRes.get()
|
||||
if providerRes.isOk():
|
||||
let resultValue = providerRes.get()
|
||||
if resultValue.isNil():
|
||||
return err(
|
||||
"RequestBroker(" & `typeNameLit` & "): provider returned nil result"
|
||||
)
|
||||
return providerRes
|
||||
)
|
||||
when compiles(resultValue.isNil()):
|
||||
if resultValue.isNil():
|
||||
return err(
|
||||
"RequestBroker(" & `typeNameLit` & "): provider returned nil result"
|
||||
)
|
||||
return providerRes
|
||||
)
|
||||
of rbSync:
|
||||
requestBody.add(
|
||||
quote do:
|
||||
var providerRes: Result[`typeIdent`, string]
|
||||
try:
|
||||
providerRes = `providerCall`
|
||||
except CatchableError as e:
|
||||
return err(
|
||||
"RequestBroker(" & `typeNameLit` & "): provider threw exception: " & e.msg
|
||||
)
|
||||
|
||||
if providerRes.isOk():
|
||||
let resultValue = providerRes.get()
|
||||
when compiles(resultValue.isNil()):
|
||||
if resultValue.isNil():
|
||||
return err(
|
||||
"RequestBroker(" & `typeNameLit` & "): provider returned nil result"
|
||||
)
|
||||
return providerRes
|
||||
)
|
||||
# requestBody.add(providerCall)
|
||||
result.add(
|
||||
newTree(
|
||||
@ -436,3 +654,17 @@ macro RequestBroker*(body: untyped): untyped =
|
||||
|
||||
when defined(requestBrokerDebug):
|
||||
echo result.repr
|
||||
|
||||
return result
|
||||
|
||||
macro RequestBroker*(body: untyped): untyped =
|
||||
## Default (async) mode.
|
||||
generateRequestBroker(body, rbAsync)
|
||||
|
||||
macro RequestBroker*(mode: untyped, body: untyped): untyped =
|
||||
## Explicit mode selector.
|
||||
## Example:
|
||||
## RequestBroker(sync):
|
||||
## type Foo = object
|
||||
## proc signature*(): Result[Foo, string]
|
||||
generateRequestBroker(body, parseMode(mode))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user