commit 81ed9b652cd8bcfe0b5c6859357955c979092458 Author: Mark Spanbroek Date: Fri Mar 5 21:07:48 2021 +0100 Initial version of questionable Syntactic sugar for std/options, pkg/result and pkg/stew diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1963f8d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,5 @@ +[*] +indent_style = space +insert_final_newline = true +indent_size = 2 +trim_trailing_whitespace = true \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..690f688 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +* +!*/ +!*.* +nimbledeps diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..991c54c --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nim 1.4.4 diff --git a/questionable.nim b/questionable.nim new file mode 100644 index 0000000..e3e55e3 --- /dev/null +++ b/questionable.nim @@ -0,0 +1,3 @@ +import ./questionable/options + +export options diff --git a/questionable.nimble b/questionable.nimble new file mode 100644 index 0000000..55bf19d --- /dev/null +++ b/questionable.nimble @@ -0,0 +1,10 @@ +version = "0.1.0" +author = "Questionable Authors" +description = "Elegant optional types" +license = "MIT" + +task test, "Runs the test suite": + for module in ["options", "result", "stew"]: + withDir "testmodules/" & module: + exec "nimble install -d -y" + exec "nimble test -y" diff --git a/questionable/errorban.nim b/questionable/errorban.nim new file mode 100644 index 0000000..d2becfc --- /dev/null +++ b/questionable/errorban.nim @@ -0,0 +1,8 @@ +## Include this file to indicate that your module does not raise Errors. +## Disables compiler hints about unused declarations in Nim < 1.4.0 + +when (NimMajor, NimMinor, NimPatch) >= (1, 4, 0): + {.push raises:[].} +else: + {.push raises: [Defect].} + {.hint[XDeclaredButNotUsed]: off.} diff --git a/questionable/options.nim b/questionable/options.nim new file mode 100644 index 0000000..f33532f --- /dev/null +++ b/questionable/options.nim @@ -0,0 +1,33 @@ +import std/options + +include ./errorban + +export options + +template `?`*(T: typed): type Option[T] = + Option[T] + +template `.?`*(option: ?typed, field: untyped{nkIdent}): ?untyped = + type T = type option.get.field + if option.isSome: + option.unsafeGet().field.some + else: + T.none + +template `[]`*(option: ?typed, index: typed): ?typed = + type T = type option.get[index] + if option.isSome: + option.unsafeGet()[index].some + else: + T.none + +template `|?`*[T](option: ?T, fallback: T): T = + if option.isSome: + option.unsafeGet() + else: + fallback + +template `=?`*[T](name: untyped{nkIdent}, option: ?T): bool = + template name: T {.used.} = option.unsafeGet() + option.isSome + diff --git a/questionable/results.nim b/questionable/results.nim new file mode 100644 index 0000000..3c56c58 --- /dev/null +++ b/questionable/results.nim @@ -0,0 +1,35 @@ +import ./resultsbase + +include ./errorban + +export resultsbase + +template `?!`*(T: typed): type Result[T, ref CatchableError] = + Result[T, ref CatchableError] + +template success*[T](value: T): ?!T = + ok(?!T, value) + +template failure*(T: type, error: ref CatchableError): ?!T = + err(?!T, error) + +template `.?`*(value: ?!typed, field: untyped{nkIdent}): ?!untyped = + type T = type value.get.field + if value.isOk: + ok(?!T, value.unsafeGet().field) + else: + err(?!T, error(value)) + +template `[]`*(value: ?!typed, index: typed): ?!typed = + type T = type value.get[index] + if value.isOk: + ok(?!T, value.unsafeGet()[index]) + else: + err(?!T, error(value)) + +template `|?`*[T](value: ?!T, fallback: T): T = + value.valueOr(fallback) + +template `=?`*[T](name: untyped{nkIdent}, value: ?!T): bool = + template name: T {.used.} = value.unsafeGet() + value.isOk diff --git a/questionable/resultsbase.nim b/questionable/resultsbase.nim new file mode 100644 index 0000000..c8383df --- /dev/null +++ b/questionable/resultsbase.nim @@ -0,0 +1,8 @@ +template tryImport(module) = import module + +when compiles tryimport pkg/result: + import pkg/result/../results +else: + import pkg/stew/results + +export results diff --git a/testmodules/options/nim.cfg b/testmodules/options/nim.cfg new file mode 100644 index 0000000..1c2f0c1 --- /dev/null +++ b/testmodules/options/nim.cfg @@ -0,0 +1 @@ +--path:"../.." diff --git a/testmodules/options/nimbledeps/.keep b/testmodules/options/nimbledeps/.keep new file mode 100644 index 0000000..e69de29 diff --git a/testmodules/options/test.nim b/testmodules/options/test.nim new file mode 100644 index 0000000..980f2a2 --- /dev/null +++ b/testmodules/options/test.nim @@ -0,0 +1,58 @@ +import std/unittest +import pkg/questionable + +suite "optionals": + + test "?Type is shorthand for Option[Type]": + check (?int is Option[int]) + check (?string is Option[string]) + check (?seq[bool] is Option[seq[bool]]) + + test ".? can be used for chaining optionals": + let a: ?seq[int] = @[41, 42].some + let b: ?seq[int] = seq[int].none + check a.?len == 2.some + check b.?len == int.none + check a.?len.?uint8 == 2'u8.some + check b.?len.?uint8 == uint8.none + + test "[] can be used for indexing optionals": + let a: ?seq[int] = @[1, 2, 3].some + let b: ?seq[int] = seq[int].none + check a[1] == 2.some + check a[^1] == 3.some + check a[0..1] == @[1, 2].some + check b[1] == int.none + + test "|? can be used to specify a fallback value": + check 42.some |? 40 == 42 + check int.none |? 42 == 42 + + test "=? can be used for optional binding": + if a =? int.none: + check false + + if b =? 42.some: + check b == 42 + else: + check false + + while a =? 42.some: + check a == 42 + break + + while a =? int.none: + check false + break + + test "=? can appear multiple times in conditional expression": + if a =? 42.some and b =? "foo".some: + check a == 42 + check b == "foo" + else: + check false + + test "=? works with variable hiding": + let a = 42.some + if a =? a: + check a == 42 diff --git a/testmodules/options/test.nimble b/testmodules/options/test.nimble new file mode 100644 index 0000000..d389ae1 --- /dev/null +++ b/testmodules/options/test.nimble @@ -0,0 +1,7 @@ +version = "0.1.0" +author = "Questionable Authors" +description = "Questionable tests for std/option" +license = "MIT" + +task test, "Runs the test suite": + exec "nim c -f -r test.nim" diff --git a/testmodules/result/nim.cfg b/testmodules/result/nim.cfg new file mode 100644 index 0000000..1c2f0c1 --- /dev/null +++ b/testmodules/result/nim.cfg @@ -0,0 +1 @@ +--path:"../.." diff --git a/testmodules/result/nimbledeps/.keep b/testmodules/result/nimbledeps/.keep new file mode 100644 index 0000000..e69de29 diff --git a/testmodules/result/test.nim b/testmodules/result/test.nim new file mode 100644 index 0000000..04b5bd7 --- /dev/null +++ b/testmodules/result/test.nim @@ -0,0 +1,65 @@ +import std/unittest +import std/strutils +import pkg/questionable/results + +suite "result": + + let error = newException(CatchableError, "error") + + test "?!Type is shorthand for Result[Type, ref CatchableError]": + check (?!int is Result[int, ref CatchableError]) + check (?!string is Result[string, ref CatchableError]) + check (?!seq[bool] is Result[seq[bool], ref CatchableError]) + + test ".? can be used for chaining results": + let a: ?!seq[int] = @[41, 42].success + let b: ?!seq[int] = seq[int].failure error + check a.?len == 2.success + check b.?len == int.failure error + check a.?len.?uint8 == 2'u8.success + check b.?len.?uint8 == uint8.failure error + + test "[] can be used for indexing optionals": + let a: ?!seq[int] = @[1, 2, 3].success + let b: ?!seq[int] = seq[int].failure error + check a[1] == 2.success + check a[^1] == 3.success + check a[0..1] == @[1, 2].success + check b[1] == int.failure error + + test "|? can be used to specify a fallback value": + check 42.success |? 40 == 42 + check int.failure(error) |? 42 == 42 + + test "=? can be used for optional binding": + if a =? int.failure(error): + check false + + if b =? 42.success: + check b == 42 + else: + check false + + while a =? 42.success: + check a == 42 + break + + while a =? int.failure(error): + check false + break + + test "=? can appear multiple times in conditional expression": + if a =? 42.success and b =? "foo".success: + check a == 42 + check b == "foo" + else: + check false + + test "=? works with variable hiding": + let a = 42.success + if a =? a: + check a == 42 + + test "catch can be used to convert exceptions to results": + check parseInt("42").catch == 42.success + check parseInt("foo").catch.error of ValueError diff --git a/testmodules/result/test.nimble b/testmodules/result/test.nimble new file mode 100644 index 0000000..df14d96 --- /dev/null +++ b/testmodules/result/test.nimble @@ -0,0 +1,9 @@ +version = "0.1.0" +author = "Questionable Authors" +description = "Questionable tests for pkg/result" +license = "MIT" + +requires "result" + +task test, "Runs the test suite": + exec "nim c -f -r test.nim" diff --git a/testmodules/stew/nim.cfg b/testmodules/stew/nim.cfg new file mode 100644 index 0000000..1c2f0c1 --- /dev/null +++ b/testmodules/stew/nim.cfg @@ -0,0 +1 @@ +--path:"../.." diff --git a/testmodules/stew/nimbledeps/.keep b/testmodules/stew/nimbledeps/.keep new file mode 100644 index 0000000..e69de29 diff --git a/testmodules/stew/test.nim b/testmodules/stew/test.nim new file mode 100644 index 0000000..ec17617 --- /dev/null +++ b/testmodules/stew/test.nim @@ -0,0 +1 @@ +include ../result/test diff --git a/testmodules/stew/test.nimble b/testmodules/stew/test.nimble new file mode 100644 index 0000000..0f0cf1d --- /dev/null +++ b/testmodules/stew/test.nimble @@ -0,0 +1,9 @@ +version = "0.1.0" +author = "Questionable Authors" +description = "Questionable tests for pkg/stew" +license = "MIT" + +requires "stew" + +task test, "Runs the test suite": + exec "nim c -f -r test.nim"