mirror of
https://github.com/logos-storage/questionable.git
synced 2026-01-02 13:53:11 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2906828765 | ||
|
|
2e7f20392b | ||
|
|
3dcf21491d | ||
|
|
82d90b67bc | ||
|
|
b098ae696a | ||
|
|
83ae4a6409 | ||
|
|
57e467b8b0 | ||
|
|
47692e0d92 | ||
|
|
6ef525cfe2 | ||
|
|
43e7deb827 | ||
|
|
4a74d65e17 | ||
|
|
672248f431 | ||
|
|
d463d491cc | ||
|
|
1f0afff48b | ||
|
|
2dd6b6b220 | ||
|
|
0f095d6b7c | ||
|
|
cdf639c4ea | ||
|
|
fe47a19825 | ||
|
|
440debc7c3 | ||
|
|
c2a08bd703 | ||
|
|
52e11f2011 | ||
|
|
af4f194597 | ||
|
|
5c8d422ac8 | ||
|
|
e56cf86c4a | ||
|
|
416b6dd566 | ||
|
|
08581f5efd | ||
|
|
9af4ce1ca1 | ||
|
|
1569ef4526 | ||
|
|
b3cf35ac45 | ||
|
|
3bce3088a7 | ||
|
|
ffe0faa3bb | ||
|
|
0d7ce8efde | ||
|
|
58c10fb333 | ||
|
|
8daae27089 | ||
|
|
b18444a6d0 | ||
|
|
f957dd59d6 | ||
|
|
6cbbda7e4d | ||
|
|
096ca864b0 | ||
|
|
1dcef4b302 | ||
|
|
f78bdd9d58 | ||
|
|
30e4184a99 | ||
|
|
4d631b1ba9 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
nim: [stable, 1.4.8, 1.2.18]
|
||||
nim: [stable, 1.6.16, 1.4.8, 1.2.18]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: iffy/install-nim@v3
|
||||
|
||||
@ -12,14 +12,14 @@ Use the [Nimble][3] package manager to add `questionable` to an existing
|
||||
project. Add the following to its .nimble file:
|
||||
|
||||
```nim
|
||||
requires "questionable >= 0.10.5 & < 0.11.0"
|
||||
requires "questionable >= 0.10.15 & < 0.11.0"
|
||||
```
|
||||
|
||||
If you want to make use of Result types, then you also have to add either the
|
||||
[result][2] package, or the [stew][4] package:
|
||||
|
||||
```nim
|
||||
requires "result" # either this
|
||||
requires "results" # either this
|
||||
requires "stew" # or this
|
||||
```
|
||||
|
||||
@ -151,7 +151,7 @@ have to explicitly import the `questionable/results` module:
|
||||
import questionable/results
|
||||
```
|
||||
|
||||
You can use `?!` make a Result type. These Result types either hold a value or
|
||||
You can use `?!` to make a Result type. These Result types either hold a value or
|
||||
an error. For example the type `?!int` is short for `Result[int, ref
|
||||
CatchableError]`.
|
||||
|
||||
@ -226,6 +226,7 @@ Any Result can be converted to an Option:
|
||||
|
||||
```nim
|
||||
let converted = works().option # equals @[1, 1, 2, 2, 2].some
|
||||
let errOption = fails().errorOption # option that is set when the Result holds an error
|
||||
```
|
||||
|
||||
[1]: https://nim-lang.org/docs/options.html
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
# Style Check
|
||||
--styleCheck:usages
|
||||
if (NimMajor, NimMinor, NimPatch) >= (1, 6, 6):
|
||||
--styleCheck:usages
|
||||
if (NimMajor, NimMinor) < (1, 6):
|
||||
--styleCheck:hint
|
||||
else:
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
version = "0.10.5"
|
||||
version = "0.10.15"
|
||||
author = "Questionable Authors"
|
||||
description = "Elegant optional types"
|
||||
license = "MIT"
|
||||
|
||||
task test, "Runs the test suite":
|
||||
for module in ["options", "result", "stew"]:
|
||||
for module in ["options", "results", "stew"]:
|
||||
withDir "testmodules/" & module:
|
||||
delEnv "NIMBLE_DIR" # use nimbledeps dir
|
||||
exec "nimble install -d -y"
|
||||
|
||||
@ -2,15 +2,25 @@ import std/options
|
||||
import std/macros
|
||||
import ./private/binderror
|
||||
|
||||
proc option[T](option: Option[T]): Option[T] =
|
||||
when (NimMajor, NimMinor) < (1, 1):
|
||||
type SomePointer = ref | ptr | pointer
|
||||
elif (NimMajor, NimMinor) == (2, 0): # Broken in 2.0.0, fixed in 2.1.1.
|
||||
type SomePointer = ref | ptr | pointer | proc
|
||||
else:
|
||||
type SomePointer = ref | ptr | pointer | proc | iterator {.closure.}
|
||||
|
||||
template toOption[T](option: Option[T]): Option[T] =
|
||||
option
|
||||
|
||||
template toOption[T: SomePointer](value: T): Option[T] =
|
||||
value.option
|
||||
|
||||
proc placeholder(T: type): T =
|
||||
discard
|
||||
|
||||
template bindLet(name, expression): bool =
|
||||
template bindLet(name, expression): untyped =
|
||||
let evaluated = expression
|
||||
let option = evaluated.option
|
||||
let option = evaluated.toOption
|
||||
type T = typeof(option.unsafeGet())
|
||||
let name {.used.} = if option.isSome:
|
||||
option.unsafeGet()
|
||||
@ -19,9 +29,9 @@ template bindLet(name, expression): bool =
|
||||
placeholder(T)
|
||||
option.isSome
|
||||
|
||||
template bindVar(name, expression): bool =
|
||||
template bindVar(name, expression): untyped =
|
||||
let evaluated = expression
|
||||
let option = evaluated.option
|
||||
let option = evaluated.toOption
|
||||
type T = typeof(option.unsafeGet())
|
||||
var name {.used.} = if option.isSome:
|
||||
option.unsafeGet()
|
||||
@ -30,14 +40,50 @@ template bindVar(name, expression): bool =
|
||||
placeholder(T)
|
||||
option.isSome
|
||||
|
||||
proc newUnpackTupleNode(names: NimNode, value: NimNode): NimNode =
|
||||
# builds tuple unpacking statement, eg: let (a, b) = value
|
||||
let vartuple = nnkVarTuple.newTree()
|
||||
for i in 0..<names.len:
|
||||
vartuple.add names[i]
|
||||
vartuple.add newEmptyNode()
|
||||
vartuple.add value
|
||||
nnkLetSection.newTree(vartuple)
|
||||
|
||||
macro bindTuple(names, expression): bool =
|
||||
let opt = genSym(nskLet, "option")
|
||||
let evaluated = genSym(nskLet, "evaluated")
|
||||
let T = genSym(nskType, "T")
|
||||
|
||||
let value = quote do:
|
||||
if `opt`.isSome:
|
||||
`opt`.unsafeGet()
|
||||
else:
|
||||
bindFailed(`evaluated`)
|
||||
placeholder(`T`)
|
||||
|
||||
let letsection = newUnpackTupleNode(names, value)
|
||||
|
||||
quote do:
|
||||
let `evaluated` = `expression`
|
||||
let `opt` = `evaluated`.toOption
|
||||
type `T` = typeof(`opt`.unsafeGet())
|
||||
`letsection`
|
||||
`opt`.isSome
|
||||
|
||||
macro `=?`*(name, expression): bool =
|
||||
## The `=?` operator lets you bind the value inside an Option or Result to a
|
||||
## new variable. It can be used inside of a conditional expression, for
|
||||
## instance in an `if` statement.
|
||||
|
||||
name.expectKind({nnkIdent, nnkVarTy})
|
||||
when (NimMajor, NimMinor) < (1, 6):
|
||||
name.expectKind({nnkIdent, nnkVarTy, nnkTupleConstr, nnkPar})
|
||||
else:
|
||||
name.expectKind({nnkIdent, nnkVarTy, nnkTupleConstr})
|
||||
|
||||
if name.kind == nnkIdent:
|
||||
quote do: bindLet(`name`, `expression`)
|
||||
elif name.kind == nnkTupleConstr or name.kind == nnkPar:
|
||||
quote do: bindTuple(`name`, `expression`)
|
||||
else:
|
||||
let name = name[0]
|
||||
quote do: bindVar(`name`, `expression`)
|
||||
|
||||
@ -15,50 +15,34 @@ macro expectReturnType(identifier: untyped, expression: untyped): untyped =
|
||||
when compiles(`expression`) and not compiles(typeof `expression`):
|
||||
{.error: `message`.}
|
||||
|
||||
template `.?`*(option: typed, identifier: untyped{nkIdent}): untyped =
|
||||
## The `.?` chaining operator is used to safely access fields and call procs
|
||||
## on Options or Results. The expression is only evaluated when the preceding
|
||||
## Option or Result has a value.
|
||||
|
||||
template chain(option: typed, identifier: untyped{nkIdent}): untyped =
|
||||
# chain is of shape: option.?identifier
|
||||
expectReturnType(identifier, option.unsafeGet.identifier)
|
||||
option ->? option.unsafeGet.identifier
|
||||
|
||||
macro `.?`*(option: typed, infix: untyped{nkInfix}): untyped =
|
||||
## The `.?` chaining operator is used to safely access fields and call procs
|
||||
## on Options or Results. The expression is only evaluated when the preceding
|
||||
## Option or Result has a value.
|
||||
|
||||
macro chain(option: typed, infix: untyped{nkInfix}): untyped =
|
||||
# chain is of shape: option.?left `operator` right
|
||||
let infix = infix.copyNimTree()
|
||||
let left = infix[1]
|
||||
infix[1] = quote do: `option`.?`left`
|
||||
infix
|
||||
|
||||
macro `.?`*(option: typed, bracket: untyped{nkBracketExpr}): untyped =
|
||||
## The `.?` chaining operator is used to safely access fields and call procs
|
||||
## on Options or Results. The expression is only evaluated when the preceding
|
||||
## Option or Result has a value.
|
||||
|
||||
macro chain(option: typed, bracket: untyped{nkBracketExpr}): untyped =
|
||||
# chain is of shape: option.?left[right]
|
||||
let bracket = bracket.copyNimTree()
|
||||
let left = bracket[0]
|
||||
bracket[0] = quote do: `option`.?`left`
|
||||
bracket
|
||||
|
||||
macro `.?`*(option: typed, dot: untyped{nkDotExpr}): untyped =
|
||||
## The `.?` chaining operator is used to safely access fields and call procs
|
||||
## on Options or Results. The expression is only evaluated when the preceding
|
||||
## Option or Result has a value.
|
||||
|
||||
macro chain(option: typed, dot: untyped{nkDotExpr}): untyped =
|
||||
# chain is of shape: option.?left.right
|
||||
let dot = dot.copyNimTree()
|
||||
let left = dot[0]
|
||||
dot[0] = quote do: `option`.?`left`
|
||||
dot
|
||||
|
||||
macro `.?`*(option: typed, call: untyped{nkCall}): untyped =
|
||||
## The `.?` chaining operator is used to safely access fields and call procs
|
||||
## on Options or Results. The expression is only evaluated when the preceding
|
||||
## Option or Result has a value.
|
||||
|
||||
macro chain(option: typed, call: untyped{nkCall}): untyped =
|
||||
let call = call.copyNimTree()
|
||||
let procedure = call[0]
|
||||
if call.len == 1:
|
||||
# chain is of shape: option.?procedure()
|
||||
@ -81,11 +65,15 @@ macro `.?`*(option: typed, call: untyped{nkCall}): untyped =
|
||||
expectReturnType(`procedure`, `call`)
|
||||
`option` ->? `call`
|
||||
|
||||
macro `.?`*(option: typed, symbol: untyped): untyped =
|
||||
## The `.?` chaining operator is used to safely access fields and call procs
|
||||
## on Options or Results. The expression is only evaluated when the preceding
|
||||
## Option or Result has a value.
|
||||
|
||||
macro chain(option: typed, symbol: untyped): untyped =
|
||||
symbol.expectSym()
|
||||
let expression = ident($symbol)
|
||||
quote do: `option`.?`expression`
|
||||
|
||||
template `.?`*(left: typed, right: untyped): untyped =
|
||||
## The `.?` chaining operator is used to safely access fields and call procs
|
||||
## on Options or Results. The expression is only evaluated when the preceding
|
||||
## Option or Result has a value.
|
||||
block:
|
||||
let evaluated = left
|
||||
chain(evaluated, right)
|
||||
|
||||
@ -1,11 +1,32 @@
|
||||
import std/macros
|
||||
import std/options
|
||||
|
||||
proc safeGet[T](expression: seq[T] | openArray[T], index: int): Option[T] =
|
||||
if index >= expression.low and index <= expression.high:
|
||||
expression[index].some
|
||||
else:
|
||||
T.none
|
||||
|
||||
proc safeGet(expression: string, index: int): Option[char] =
|
||||
if index >= expression.low and index <= expression.high:
|
||||
expression[index].some
|
||||
else:
|
||||
char.none
|
||||
|
||||
macro `.?`*(expression: seq | string | openArray, brackets: untyped{nkBracket}): untyped =
|
||||
# chain is of shape: (seq or string or openArray).?[index]
|
||||
let index = brackets[0]
|
||||
quote do:
|
||||
block:
|
||||
safeGet(`expression`, `index`)
|
||||
|
||||
macro `.?`*(expression: typed, brackets: untyped{nkBracket}): untyped =
|
||||
# chain is of shape: expression.?[index]
|
||||
let index = brackets[0]
|
||||
quote do:
|
||||
type T = typeof(`expression`[`index`])
|
||||
try:
|
||||
`expression`[`index`].some
|
||||
except KeyError:
|
||||
T.none
|
||||
block:
|
||||
type T = typeof(`expression`[`index`])
|
||||
try:
|
||||
`expression`[`index`].some
|
||||
except KeyError:
|
||||
T.none
|
||||
|
||||
@ -1,12 +1,19 @@
|
||||
template liftUnary*(T: type, operator: untyped) =
|
||||
|
||||
template `operator`*(a: T): untyped =
|
||||
a ->? `operator`(a.unsafeGet())
|
||||
block:
|
||||
let evaluated = a
|
||||
evaluated ->? `operator`(evaluated.unsafeGet())
|
||||
|
||||
template liftBinary*(T: type, operator: untyped) =
|
||||
|
||||
template `operator`*(a: T, b: T): untyped =
|
||||
(a, b) ->? `operator`(a.unsafeGet, b.unsafeGet)
|
||||
block:
|
||||
let evalA = a
|
||||
let evalB = b
|
||||
(evalA, evalB) ->? `operator`(evalA.unsafeGet, evalB.unsafeGet)
|
||||
|
||||
template `operator`*(a: T, b: typed): untyped =
|
||||
a ->? `operator`(a.unsafeGet(), b)
|
||||
block:
|
||||
let evalA = a
|
||||
evalA ->? `operator`(evalA.unsafeGet(), b)
|
||||
|
||||
@ -44,7 +44,7 @@ template `->?`*[T,U,V](options: (?T, ?U), expression: ?V): ?V =
|
||||
template `->?`*[T,U,V](options: (?T, ?U), expression: V): ?V =
|
||||
options ->? expression.some
|
||||
|
||||
template `|?`*[T](option: ?T, fallback: T): T =
|
||||
proc `|?`*[T](option: ?T, fallback: T): T =
|
||||
## Use the `|?` operator to supply a fallback value when an Option does not
|
||||
## hold a value.
|
||||
|
||||
@ -56,11 +56,13 @@ template `|?`*[T](option: ?T, fallback: T): T =
|
||||
macro `.?`*[T](option: ?T, brackets: untyped{nkBracket}): untyped =
|
||||
let index = brackets[0]
|
||||
quote do:
|
||||
type U = typeof(`option`.unsafeGet().?[`index`].unsafeGet())
|
||||
if `option`.isSome:
|
||||
`option`.unsafeGet().?[`index`]
|
||||
else:
|
||||
U.none
|
||||
block:
|
||||
let evaluated = `option`
|
||||
type U = typeof(evaluated.unsafeGet().?[`index`].unsafeGet())
|
||||
if evaluated.isSome:
|
||||
evaluated.unsafeGet().?[`index`]
|
||||
else:
|
||||
U.none
|
||||
|
||||
Option.liftUnary(`-`)
|
||||
Option.liftUnary(`+`)
|
||||
|
||||
6
questionable/private/bareexcept.nim
Normal file
6
questionable/private/bareexcept.nim
Normal file
@ -0,0 +1,6 @@
|
||||
template ignoreBareExceptWarning*(body) =
|
||||
when defined(nimHasWarnBareExcept):
|
||||
{.push warning[BareExcept]:off warning[UnreachableCode]:off.}
|
||||
body
|
||||
when defined(nimHasWarnBareExcept):
|
||||
{.pop.}
|
||||
@ -1,24 +1,54 @@
|
||||
import std/options
|
||||
import std/macros
|
||||
|
||||
var captures {.global, compileTime.}: int
|
||||
var errorVariable: ptr ref CatchableError
|
||||
# A stack of names of error variables. Keeps track of the error variables that
|
||||
# are given to captureBindError().
|
||||
var errorVariableNames {.global, compileTime.}: seq[string]
|
||||
|
||||
template captureBindError*(error: var ref CatchableError, expression): auto =
|
||||
let previousErrorVariable = errorVariable
|
||||
errorVariable = addr error
|
||||
macro captureBindError*(error: var ref CatchableError, expression): auto =
|
||||
## Ensures that an error is assigned to the error variable when a binding (=?)
|
||||
## fails inside the expression.
|
||||
|
||||
static: inc captures
|
||||
let evaluated = expression
|
||||
static: dec captures
|
||||
# name of the error variable as a string literal
|
||||
let errorVariableName = newLit($error)
|
||||
|
||||
errorVariable = previousErrorVariable
|
||||
let evaluated = genSym(nskLet, "evaluated")
|
||||
quote do:
|
||||
# add error variable to the top of the stack
|
||||
static: errorVariableNames.add(`errorVariableName`)
|
||||
# evaluate the expression
|
||||
let `evaluated` = `expression`
|
||||
# pop error variable from the stack
|
||||
static: discard errorVariableNames.pop()
|
||||
# return the evaluated result
|
||||
`evaluated`
|
||||
|
||||
evaluated
|
||||
|
||||
func error[T](option: Option[T]): ref CatchableError =
|
||||
func unsafeCatchableError[T](_: Option[T]): ref CatchableError =
|
||||
newException(ValueError, "Option is set to `none`")
|
||||
|
||||
template bindFailed*(expression) =
|
||||
when captures > 0:
|
||||
mixin error
|
||||
errorVariable[] = expression.error
|
||||
func unsafeCatchableError[T](_: ref T): ref CatchableError =
|
||||
newException(ValueError, "ref is nil")
|
||||
|
||||
func unsafeCatchableError[T](_: ptr T): ref CatchableError =
|
||||
newException(ValueError, "ptr is nil")
|
||||
|
||||
func unsafeCatchableError[Proc: proc | iterator](_: Proc): ref CatchableError =
|
||||
newException(ValueError, "proc or iterator is nil")
|
||||
|
||||
macro bindFailed*(expression: typed) =
|
||||
## Called when a binding (=?) fails.
|
||||
## Assigns an error to the error variable (specified in captureBindError())
|
||||
## when appropriate.
|
||||
|
||||
# The `expression` parameter is typed to ensure that the compiler does not
|
||||
# expand bindFailed() before it expands invocations of captureBindError().
|
||||
|
||||
# check that we have an error variable on the stack
|
||||
if errorVariableNames.len > 0:
|
||||
# create an identifier that references the current error variable
|
||||
let errorVariable = ident errorVariableNames[^1]
|
||||
return quote do:
|
||||
# check that the error variable is in scope
|
||||
when compiles(`errorVariable`):
|
||||
# assign bind error to error variable
|
||||
`errorVariable` = `expression`.unsafeCatchableError
|
||||
|
||||
@ -7,6 +7,7 @@ import ./indexing
|
||||
import ./operators
|
||||
import ./without
|
||||
import ./withoutresult
|
||||
import ./private/bareexcept
|
||||
|
||||
include ./private/errorban
|
||||
|
||||
@ -73,6 +74,12 @@ proc isFailure*[T](value: ?!T): bool =
|
||||
|
||||
value.isErr
|
||||
|
||||
proc `$`*[T](value: ?!T): string =
|
||||
if value.isSuccess:
|
||||
"success(" & $(!value) & ")"
|
||||
else:
|
||||
"failure(\"" & $(value.error.msg) & "\")"
|
||||
|
||||
template `->?`*[T,U](value: ?!T, expression: ?!U): ?!U =
|
||||
if value.isFailure:
|
||||
U.failure(value.error)
|
||||
@ -93,7 +100,7 @@ template `->?`*[T,U,V](values: (?!T, ?!U), expression: ?!V): ?!V =
|
||||
template `->?`*[T,U,V](values: (?!T, ?!U), expression: V): ?!V =
|
||||
values ->? expression.success
|
||||
|
||||
template `|?`*[T,E](value: Result[T,E], fallback: T): T =
|
||||
proc `|?`*[T,E](value: Result[T,E], fallback: T): T =
|
||||
## Use the `|?` operator to supply a fallback value when a Result does not
|
||||
## hold a value.
|
||||
|
||||
@ -103,12 +110,30 @@ proc option*[T,E](value: Result[T,E]): ?T =
|
||||
## Converts a Result into an Option.
|
||||
|
||||
if value.isOk:
|
||||
try: # workaround for erroneous exception tracking when T is a closure
|
||||
value.unsafeGet.some
|
||||
except Exception as exception:
|
||||
raise newException(Defect, exception.msg, exception)
|
||||
ignoreBareExceptWarning:
|
||||
try: # workaround for erroneous exception tracking when T is a closure
|
||||
return value.unsafeGet.some
|
||||
except Exception as exception:
|
||||
raise newException(Defect, exception.msg, exception)
|
||||
else:
|
||||
T.none
|
||||
return T.none
|
||||
|
||||
template toOption*[T, E](value: Result[T, E]): ?T =
|
||||
## Converts a Result into an Option.
|
||||
|
||||
value.option
|
||||
|
||||
proc unsafeCatchableError*[T, E](value: Result[T, E]): ref CatchableError =
|
||||
## Returns the error from the Result, converted to `ref CatchableError` if
|
||||
## necessary. Behaviour is undefined when the result holds a value instead of
|
||||
## an error.
|
||||
when E is ref CatchableError:
|
||||
value.unsafeError
|
||||
else:
|
||||
when compiles($value.unsafeError):
|
||||
newException(ResultFailure, $value.unsafeError)
|
||||
else:
|
||||
newException(ResultFailure, "Result is an error")
|
||||
|
||||
proc errorOption*[T, E](value: Result[T, E]): ?E =
|
||||
## Returns an Option that contains the error from the Result, if it has one.
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
template tryImport(module) = import module
|
||||
|
||||
when compiles tryImport pkg/result:
|
||||
import pkg/result/../results
|
||||
when compiles tryImport pkg/results:
|
||||
import pkg/results
|
||||
else:
|
||||
import pkg/stew/results
|
||||
|
||||
|
||||
@ -2,16 +2,18 @@ import std/macros
|
||||
import ./without
|
||||
import ./private/binderror
|
||||
|
||||
const symbolKinds = {nnkSym, nnkOpenSymChoice, nnkClosedSymChoice}
|
||||
const identKinds = {nnkIdent} + symbolKinds
|
||||
|
||||
proc undoSymbolResolution(expression, ident: NimNode): NimNode =
|
||||
## Finds symbols in the expression that match the `ident` and replaces them
|
||||
## with `ident`, effectively undoing any symbol resolution that happened
|
||||
## before.
|
||||
|
||||
const symbolKinds = {nnkSym, nnkOpenSymChoice, nnkClosedSymChoice}
|
||||
|
||||
if expression.kind in symbolKinds and eqIdent($expression, $ident):
|
||||
return ident
|
||||
|
||||
let expression = expression.copyNimTree()
|
||||
for i in 0..<expression.len:
|
||||
expression[i] = undoSymbolResolution(expression[i], ident)
|
||||
|
||||
@ -21,6 +23,9 @@ macro without*(condition, errorname, body: untyped): untyped =
|
||||
## Used to place guards that ensure that a Result contains a value.
|
||||
## Exposes error when Result does not contain a value.
|
||||
|
||||
if errorname.kind notin identKinds:
|
||||
error("expected an identifier, got " & errorname.repr, errorname)
|
||||
|
||||
let errorIdent = ident $errorname
|
||||
|
||||
# Nim's early symbol resolution might have picked up a symbol with the
|
||||
@ -29,7 +34,7 @@ macro without*(condition, errorname, body: untyped): untyped =
|
||||
let body = body.undoSymbolResolution(errorIdent)
|
||||
|
||||
quote do:
|
||||
var error: ref CatchableError
|
||||
var error {.gensym.}: ref CatchableError
|
||||
|
||||
without captureBindError(error, `condition`):
|
||||
template `errorIdent`: ref CatchableError = error
|
||||
|
||||
2
testmodules/options/config.nims
Normal file
2
testmodules/options/config.nims
Normal file
@ -0,0 +1,2 @@
|
||||
--path:"../.."
|
||||
import "../../config.nims"
|
||||
@ -1 +0,0 @@
|
||||
--path:"../.."
|
||||
@ -115,17 +115,6 @@ suite "optionals":
|
||||
else:
|
||||
fail
|
||||
|
||||
test "=? evaluates optional expression only once":
|
||||
var count = 0
|
||||
if a =? (inc count; 42.some):
|
||||
let b {.used.} = a
|
||||
check count == 1
|
||||
|
||||
count = 0
|
||||
if var a =? (inc count; 42.some):
|
||||
let b {.used.} = a
|
||||
check count == 1
|
||||
|
||||
test "=? works in generic code":
|
||||
proc toString[T](option: ?T): string =
|
||||
if value =? option:
|
||||
@ -176,6 +165,150 @@ suite "optionals":
|
||||
else:
|
||||
fail()
|
||||
|
||||
test "=? works with reference types":
|
||||
var x = new int
|
||||
x[] = 42
|
||||
if a =? x:
|
||||
check a[] == 42
|
||||
else:
|
||||
fail
|
||||
|
||||
x = nil
|
||||
if a =? x:
|
||||
fail
|
||||
|
||||
var p = proc = discard
|
||||
if a =? p:
|
||||
a()
|
||||
else:
|
||||
fail
|
||||
|
||||
p = nil
|
||||
if a =? p:
|
||||
fail
|
||||
|
||||
when (NimMajor, NimMinor) >= (1, 1) and (NimMajor, NimMinor) != (2, 0):
|
||||
var it = iterator: int = yield 2
|
||||
if a =? it:
|
||||
for x in a:
|
||||
check x == 2
|
||||
else:
|
||||
fail
|
||||
|
||||
it = nil
|
||||
if a =? it:
|
||||
fail
|
||||
|
||||
test "=? rejects non-reference types":
|
||||
check `not` compiles do:
|
||||
if a =? 0:
|
||||
discard
|
||||
check `not` compiles do:
|
||||
if var a =? 0:
|
||||
discard
|
||||
check `not` compiles do:
|
||||
if (a,) =? (0,):
|
||||
discard
|
||||
|
||||
test "=? works with custom optional types":
|
||||
type MyOption = distinct int
|
||||
proc isSome(x: MyOption): bool = x.int >= 0
|
||||
proc unsafeGet(x: MyOption): int = x.int
|
||||
template toOption(x: MyOption): MyOption = x
|
||||
|
||||
if a =? MyOption 42:
|
||||
check a == 42
|
||||
else:
|
||||
fail
|
||||
|
||||
if a =? MyOption -1:
|
||||
fail
|
||||
|
||||
test "=? binds and unpacks tuples":
|
||||
if (a, b) =? (some ("test", 1)):
|
||||
check a == "test"
|
||||
check b == 1
|
||||
else:
|
||||
fail()
|
||||
|
||||
if (a, b) =? none (string, int):
|
||||
discard a
|
||||
discard b
|
||||
fail()
|
||||
|
||||
test "=? binds and unpacks tuples with named fields":
|
||||
if (a, b) =? (some (desc: "test", id: 1)):
|
||||
check a == "test"
|
||||
check b == 1
|
||||
else:
|
||||
fail()
|
||||
|
||||
test "=? binds and unpacks tuples returned from proc":
|
||||
proc returnsTuple(): ?tuple[name: string, id: int] = some ("test", 1)
|
||||
|
||||
if (a, b) =? returnsTuple():
|
||||
check a == "test"
|
||||
check b == 1
|
||||
else:
|
||||
fail()
|
||||
|
||||
test "=? binds and unpacks tuples returned from proc with unnamed fields":
|
||||
proc returnsTuple(): ?(string, int,) = some ("test", 1,)
|
||||
|
||||
if (a, b,) =? returnsTuple():
|
||||
check a == "test"
|
||||
check b == 1
|
||||
else:
|
||||
fail()
|
||||
|
||||
test "=? binds and unpacks tuples with _":
|
||||
if (_, b) =? some ("test", 1):
|
||||
check b == 1
|
||||
else:
|
||||
fail()
|
||||
|
||||
test "=? binds and unpacks tuples with named fields":
|
||||
if (a, b) =? some (desc: "test", id: 1):
|
||||
check a == "test"
|
||||
check b == 1
|
||||
else:
|
||||
fail()
|
||||
|
||||
test "=? binds variable to tuples with named fields":
|
||||
if t =? some (desc: "test", id: 1):
|
||||
check t.desc == "test"
|
||||
check t.id == 1
|
||||
else:
|
||||
fail()
|
||||
|
||||
test "=? binds to tuple types":
|
||||
type MyTuple = tuple
|
||||
desc: string
|
||||
id: int
|
||||
|
||||
let mt: MyTuple = ("test", 1)
|
||||
|
||||
if t =? (some mt):
|
||||
check t.desc == "test"
|
||||
check t.id == 1
|
||||
else:
|
||||
fail()
|
||||
|
||||
if (a, b) =? (some mt):
|
||||
check a == "test"
|
||||
check b == 1
|
||||
else:
|
||||
fail()
|
||||
|
||||
test "=? for tuples does not leak symbols into caller's scope":
|
||||
const evaluated = ""
|
||||
type T = string
|
||||
if (a,) =? some (0,):
|
||||
check a == 0
|
||||
check option is proc
|
||||
check evaluated is string
|
||||
check T is string
|
||||
|
||||
test "without statement can be used for early returns":
|
||||
proc test1 =
|
||||
without a =? 42.some:
|
||||
@ -202,6 +335,31 @@ suite "optionals":
|
||||
check table.?["a"] == 1.some
|
||||
check table.?["c"] == int.none
|
||||
|
||||
test ".?[] can be used for indexing strings without raising IndexDefect":
|
||||
let str = "a"
|
||||
check str.?[0] == 'a'.some
|
||||
check str.?[1] == char.none
|
||||
check str.?[-1] == char.none
|
||||
|
||||
test ".?[] can be used for indexing sequences without raising IndexDefect":
|
||||
let sequence = @[1]
|
||||
check sequence.?[0] == 1.some
|
||||
check sequence.?[1] == int.none
|
||||
check sequence.?[-1] == int.none
|
||||
|
||||
test ".?[] can be used for indexing openArrays without raising IndexDefect":
|
||||
proc checkOpenArray(oa: openArray[int]): void =
|
||||
check oa.?[0] == 1.some
|
||||
check oa.?[1] == int.none
|
||||
check oa.?[-1] == int.none
|
||||
|
||||
checkOpenArray(@[1])
|
||||
|
||||
test ".?[] evaluates openArray expression only once":
|
||||
var count = 0
|
||||
discard (inc count; @[1].toOpenArray(0, 0)).?[0]
|
||||
check count == 1
|
||||
|
||||
test ".?[] can be followed by calls, operators and indexing":
|
||||
let table = @{"a": @[41, 42]}.toTable
|
||||
check table.?["a"].isSome
|
||||
@ -274,6 +432,58 @@ suite "optionals":
|
||||
|
||||
check a.?[1] == 42.some
|
||||
|
||||
test ".? chain evaluates optional expression only once":
|
||||
var count = 0
|
||||
discard (inc count; @[41, 42].some).?len
|
||||
check count == 1
|
||||
|
||||
test "=? evaluates optional expression only once":
|
||||
var count = 0
|
||||
if a =? (inc count; 42.some):
|
||||
let b {.used.} = a
|
||||
check count == 1
|
||||
|
||||
count = 0
|
||||
if var a =? (inc count; 42.some):
|
||||
let b {.used.} = a
|
||||
check count == 1
|
||||
|
||||
test "|? evaluates optional expression only once":
|
||||
var count = 0
|
||||
discard (inc count; 42.some) |? 43
|
||||
check count == 1
|
||||
|
||||
test ".?[] evaluates optional expression only once":
|
||||
# indexing on optional sequence:
|
||||
block:
|
||||
var count = 0
|
||||
discard (inc count; @[41, 42].some).?[0]
|
||||
check count == 1
|
||||
# indexing on normal sequence:
|
||||
block:
|
||||
var count = 0
|
||||
discard (inc count; @[41, 42]).?[0]
|
||||
check count == 1
|
||||
|
||||
test "lifted unary operators evaluate optional expression only once":
|
||||
var count = 0
|
||||
discard -(inc count; 42.some)
|
||||
check count == 1
|
||||
|
||||
test "lifted binary operators evaluate optional expressions only once":
|
||||
# lifted operator on two options:
|
||||
block:
|
||||
var count1, count2 = 0
|
||||
discard (inc count1; 40.some) + (inc count2; 2.some)
|
||||
check count1 == 1
|
||||
check count2 == 1
|
||||
# lifted operator on option and value:
|
||||
block:
|
||||
var count1, count2 = 0
|
||||
discard (inc count1; 40.some) + (inc count2; 2)
|
||||
check count1 == 1
|
||||
check count2 == 1
|
||||
|
||||
test "examples from readme work":
|
||||
|
||||
var x: ?int
|
||||
|
||||
@ -4,7 +4,7 @@ description = "Questionable tests for std/option"
|
||||
license = "MIT"
|
||||
|
||||
task test, "Runs the test suite":
|
||||
var options = "-f -r"
|
||||
var options = "-f -r --skipParentCfg"
|
||||
when (NimMajor, NimMinor) >= (1, 4):
|
||||
options &= " --warningAsError[UnsafeDefault]:on"
|
||||
options &= " --warningAsError[ProveInit]:on"
|
||||
|
||||
@ -1 +0,0 @@
|
||||
--path:"../.."
|
||||
3
testmodules/results/config.nims
Normal file
3
testmodules/results/config.nims
Normal file
@ -0,0 +1,3 @@
|
||||
--path:"../.."
|
||||
--threads:on
|
||||
import "../../config.nims"
|
||||
@ -3,8 +3,11 @@ import std/options
|
||||
import std/sequtils
|
||||
import std/strutils
|
||||
import std/sugar
|
||||
import std/threadpool
|
||||
import pkg/questionable/results
|
||||
|
||||
{.experimental: "parallel".}
|
||||
|
||||
suite "result":
|
||||
|
||||
let error = newException(CatchableError, "error")
|
||||
@ -14,6 +17,10 @@ suite "result":
|
||||
check (?!string is Result[string, ref CatchableError])
|
||||
check (?!seq[bool] is Result[seq[bool], ref CatchableError])
|
||||
|
||||
test "conversion to string $ works for ?!Types":
|
||||
check $42.success == "success(42)"
|
||||
check $(int.failure "some error") == "failure(\"some error\")"
|
||||
|
||||
test "! gets value or raises Defect":
|
||||
check !42.success == 42
|
||||
expect Defect: discard !int.failure error
|
||||
@ -165,6 +172,82 @@ suite "result":
|
||||
|
||||
check called
|
||||
|
||||
test "=? binds and unpacks tuples":
|
||||
if (a, b) =? (success ("test", 1)):
|
||||
check a == "test"
|
||||
check b == 1
|
||||
else:
|
||||
fail()
|
||||
|
||||
if (a, b) =? (string, int).failure(error):
|
||||
discard a
|
||||
discard b
|
||||
fail()
|
||||
|
||||
test "=? binds and unpacks tuples with named fields":
|
||||
if (a, b) =? (success (desc: "test", id: 1)):
|
||||
check a == "test"
|
||||
check b == 1
|
||||
else:
|
||||
fail()
|
||||
|
||||
test "=? binds and unpacks tuples returned from proc":
|
||||
proc returnsTuple(): ?!tuple[name: string, id: int] = success ("test", 1)
|
||||
|
||||
if (a, b) =? returnsTuple():
|
||||
check a == "test"
|
||||
check b == 1
|
||||
else:
|
||||
fail()
|
||||
|
||||
test "=? binds and unpacks tuples returned from proc with unnamed fields":
|
||||
proc returnsTuple(): ?!(string, int,) = success ("test", 1,)
|
||||
|
||||
if (a, b,) =? returnsTuple():
|
||||
check a == "test"
|
||||
check b == 1
|
||||
else:
|
||||
fail()
|
||||
|
||||
test "=? binds and unpacks tuples with _":
|
||||
if (_, b) =? success ("test", 1):
|
||||
check b == 1
|
||||
else:
|
||||
fail()
|
||||
|
||||
test "=? binds and unpacks tuples with named fields":
|
||||
if (a, b) =? success (desc: "test", id: 1):
|
||||
check a == "test"
|
||||
check b == 1
|
||||
else:
|
||||
fail()
|
||||
|
||||
test "=? binds variable to tuples with named fields":
|
||||
if t =? success (desc: "test", id: 1):
|
||||
check t.desc == "test"
|
||||
check t.id == 1
|
||||
else:
|
||||
fail()
|
||||
|
||||
test "=? binds to tuple types":
|
||||
type MyTuple = tuple
|
||||
desc: string
|
||||
id: int
|
||||
|
||||
let mt: MyTuple = ("test", 1)
|
||||
|
||||
if t =? (success mt):
|
||||
check t.desc == "test"
|
||||
check t.id == 1
|
||||
else:
|
||||
fail()
|
||||
|
||||
if (a, b) =? (success mt):
|
||||
check a == "test"
|
||||
check b == 1
|
||||
else:
|
||||
fail()
|
||||
|
||||
test "without statement works for results":
|
||||
proc test1 =
|
||||
without a =? 42.success:
|
||||
@ -243,6 +326,48 @@ suite "result":
|
||||
test1()
|
||||
test2()
|
||||
|
||||
test "without statement with error handles references as well":
|
||||
proc test =
|
||||
var x: ref int = nil
|
||||
without a =? x, error:
|
||||
check error.msg == "ref is nil"
|
||||
return
|
||||
fail
|
||||
|
||||
test()
|
||||
|
||||
test "without statement with error handles pointers as well":
|
||||
proc test =
|
||||
var x: ptr int = nil
|
||||
without a =? x, error:
|
||||
check error.msg == "ptr is nil"
|
||||
return
|
||||
fail
|
||||
|
||||
test()
|
||||
|
||||
test "without statement with error handles closures as well":
|
||||
proc test =
|
||||
var x = proc = discard
|
||||
x = nil
|
||||
without a =? x, error:
|
||||
check error.msg == "proc or iterator is nil"
|
||||
return
|
||||
fail
|
||||
|
||||
test()
|
||||
|
||||
test "without statement with error handles iterators as well":
|
||||
when (NimMajor, NimMinor) != (2, 0):
|
||||
proc test =
|
||||
var x: iterator: int = nil
|
||||
without a =? x, error:
|
||||
check error.msg == "proc or iterator is nil"
|
||||
return
|
||||
fail
|
||||
|
||||
test()
|
||||
|
||||
test "without statement with error can be used more than once":
|
||||
proc test =
|
||||
without a =? 42.success, error:
|
||||
@ -321,6 +446,25 @@ suite "result":
|
||||
|
||||
foo()
|
||||
|
||||
test "without statement with error works with multiple threads":
|
||||
proc fail(number: int) =
|
||||
without _ =? int.failure "error" & $number, error:
|
||||
check error.msg == "error" & $number
|
||||
parallel:
|
||||
for i in 0..<1000:
|
||||
spawn fail(i)
|
||||
|
||||
test "without statement doesn't interfere with generic code called elsewhere":
|
||||
proc foo(_: type): ?!int =
|
||||
if error =? success(1).errorOption:
|
||||
discard
|
||||
|
||||
proc bar {.used.} = # defined, but not used
|
||||
without x =? bool.foo(), error:
|
||||
discard error
|
||||
|
||||
discard bool.foo() # same type parameter 'bool' as used in bar()
|
||||
|
||||
test "catch can be used to convert exceptions to results":
|
||||
check parseInt("42").catch == 42.success
|
||||
check parseInt("foo").catch.error of ValueError
|
||||
@ -396,6 +540,61 @@ suite "result":
|
||||
|
||||
check (a & b) == 42.success
|
||||
|
||||
test ".? chain evaluates result only once":
|
||||
var count = 0
|
||||
discard (inc count; @[41, 42].success).?len
|
||||
check count == 1
|
||||
|
||||
test "=? evaluates result only once":
|
||||
var count = 0
|
||||
if a =? (inc count; 42.success):
|
||||
let b {.used.} = a
|
||||
check count == 1
|
||||
|
||||
count = 0
|
||||
if var a =? (inc count; 42.success):
|
||||
let b {.used.} = a
|
||||
check count == 1
|
||||
|
||||
test "|? evaluates result only once":
|
||||
var count = 0
|
||||
discard (inc count; 42.success) |? 43
|
||||
check count == 1
|
||||
|
||||
test ".?[] evaluates result only once":
|
||||
var count = 0
|
||||
discard (inc count; @[41, 42].success).?[0]
|
||||
check count == 1
|
||||
|
||||
test "lifted unary operators evaluate result only once":
|
||||
var count = 0
|
||||
discard -(inc count; 42.success)
|
||||
check count == 1
|
||||
|
||||
test "lifted binary operators evaluate results only once":
|
||||
# lifted operator on two options:
|
||||
block:
|
||||
var count1, count2 = 0
|
||||
discard (inc count1; 40.success) + (inc count2; 2.success)
|
||||
check count1 == 1
|
||||
check count2 == 1
|
||||
# lifted operator on option and value:
|
||||
block:
|
||||
var count1, count2 = 0
|
||||
discard (inc count1; 40.success) + (inc count2; 2)
|
||||
check count1 == 1
|
||||
check count2 == 1
|
||||
|
||||
test "conversion to option evaluates result only once":
|
||||
var count = 0
|
||||
discard (inc count; 42.success).option
|
||||
check count == 1
|
||||
|
||||
test "conversion to error evaluates result only once":
|
||||
var count = 0
|
||||
discard (inc count; int.failure(error)).errorOption
|
||||
check count == 1
|
||||
|
||||
test "examples from readme work":
|
||||
|
||||
proc works: ?!seq[int] =
|
||||
@ -444,6 +643,19 @@ suite "result":
|
||||
someProc(42.success)
|
||||
someProc(int.failure "some error")
|
||||
|
||||
type TypeWithSideEffect = object
|
||||
proc `$`*(value: TypeWithSideEffect): string {.sideEffect.} =
|
||||
discard
|
||||
|
||||
suite "result side effects":
|
||||
|
||||
test "without statement with error works when `$` has side effects":
|
||||
proc foo =
|
||||
without x =? TypeWithSideEffect.failure("error"), error:
|
||||
discard error
|
||||
return
|
||||
fail()
|
||||
foo()
|
||||
|
||||
import pkg/questionable/resultsbase
|
||||
|
||||
@ -451,7 +663,7 @@ suite "result compatibility":
|
||||
|
||||
type R = Result[int, string]
|
||||
let good = R.ok 42
|
||||
let bad = R.err "error"
|
||||
let bad = R.err "some error"
|
||||
|
||||
test "|?, =? and .option work on other types of Result":
|
||||
check bad |? 43 == 43
|
||||
@ -469,3 +681,13 @@ suite "result compatibility":
|
||||
fail
|
||||
without b =? good:
|
||||
fail
|
||||
|
||||
test "without statement with error works on other type of Result":
|
||||
without value =? bad, error:
|
||||
check error of ResultFailure
|
||||
check error.msg == "some error"
|
||||
|
||||
test "without statement with error works on Result[T, void]":
|
||||
without value =? Result[int, void].err, error:
|
||||
check error of ResultFailure
|
||||
check error.msg == "Result is an error"
|
||||
@ -3,7 +3,7 @@ author = "Questionable Authors"
|
||||
description = "Questionable tests for pkg/result"
|
||||
license = "MIT"
|
||||
|
||||
requires "result"
|
||||
requires "results"
|
||||
|
||||
task test, "Runs the test suite":
|
||||
exec "nim c -f -r test.nim"
|
||||
exec "nim c -f -r --skipParentCfg test.nim"
|
||||
3
testmodules/stew/config.nims
Normal file
3
testmodules/stew/config.nims
Normal file
@ -0,0 +1,3 @@
|
||||
--path:"../.."
|
||||
--threads:on
|
||||
import "../../config.nims"
|
||||
@ -1 +0,0 @@
|
||||
--path:"../.."
|
||||
@ -1 +1 @@
|
||||
include ../result/test
|
||||
include ../results/test
|
||||
|
||||
@ -3,7 +3,11 @@ author = "Questionable Authors"
|
||||
description = "Questionable tests for pkg/stew"
|
||||
license = "MIT"
|
||||
|
||||
requires "stew"
|
||||
when (NimMajor, NimMinor) >= (1, 6):
|
||||
requires "stew"
|
||||
|
||||
task test, "Runs the test suite":
|
||||
exec "nim c -f -r test.nim"
|
||||
task test, "Runs the test suite":
|
||||
exec "nim c -f -r --skipParentCfg test.nim"
|
||||
else:
|
||||
task test, "Runs the test suite":
|
||||
echo "Warning: Skipping test with stew on Nim < 1.6"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user