diff --git a/questionable/binding.nim b/questionable/binding.nim index 488caa2..a23684d 100644 --- a/questionable/binding.nim +++ b/questionable/binding.nim @@ -1,5 +1,6 @@ import std/options import std/macros +import std/sequtils import ./private/binderror proc option[T](option: Option[T]): Option[T] = @@ -30,14 +31,45 @@ template bindVar(name, expression): bool = placeholder(T) option.isSome +macro bindTuple(name, expression): bool = + let stmtList = newStmtList() + let opt = genSym(nskLet, "option") + let T = genSym(nskType, "T") + + stmtList.add quote do: + let evaluated = `expression` + let `opt` = evaluated.option + type `T` = typeof(`opt`.unsafeGet()) + + let valueNode = quote do: + if `opt`.isSome: + `opt`.unsafeGet() + else: + bindFailed(evaluated) + placeholder(`T`) + + stmtList.add nnkStmtList.newTree( + nnkLetSection.newTree( + nnkVarTuple.newTree( + name.children.toSeq.concat( + @[newEmptyNode(), valueNode] + ) + ) + ) + ) + stmtList.add quote do: `opt`.isSome + return stmtList + 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}) + name.expectKind({nnkIdent, nnkVarTy, nnkTupleConstr}) if name.kind == nnkIdent: quote do: bindLet(`name`, `expression`) + elif name.kind == nnkTupleConstr: + quote do: bindTuple(`name`, `expression`) else: let name = name[0] quote do: bindVar(`name`, `expression`) diff --git a/testmodules/options/test.nim b/testmodules/options/test.nim index f0699bb..558e758 100644 --- a/testmodules/options/test.nim +++ b/testmodules/options/test.nim @@ -165,6 +165,77 @@ suite "optionals": else: fail() + test "=? binds and unpacks tuples": + if (a, b) =? (some ("test", 1)): + check a == "test" + 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 and unpacks tuples returned from proc": + proc returnsTuple(): Option[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(): Option[(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) =? ("test", 1): + check b == 1 + else: + fail() + + test "=? binds and unpacks tuples with named fields": + if (a, b) =? (desc: "test", id: 1): + check a == "test" + check b == 1 + else: + fail() + + test "=? binds variable to tuples with named fields": + if t =? (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 "without statement can be used for early returns": proc test1 = without a =? 42.some: