diff --git a/src/preserves.nim b/src/preserves.nim index 58e0f6b..70f0e77 100644 --- a/src/preserves.nim +++ b/src/preserves.nim @@ -2,7 +2,7 @@ # SPDX-License-Identifier: ISC import bigints -import std/[base64, endians, hashes, macros, sets, streams, strutils, tables, typetraits] +import std/[base64, endians, hashes, macros, options, sets, streams, strutils, tables, typetraits] import json except `%`, `%*` @@ -223,13 +223,14 @@ proc concat(result: var string; prs: Preserve) = result.add('}') of pkDictionary: result.add('{') + var i = 0 for (key, value) in prs.dict.pairs: + if i > 0: + result.add(' ') result.concat(key) result.add(": ") result.concat(value) - result.add(' ') - if prs.dict.len > 1: - result.setLen(result.high) + inc i result.add('}') of pkEmbedded: result.add(prs.embedded.repr) @@ -256,6 +257,9 @@ func isRecord*(prs: Preserve): bool = result = true assert(prs.record.len > 0) +func isDictionary*(prs: Preserve): bool = + prs.kind == pkDictionary + proc label*(prs: Preserve): Preserve {.inline.} = ## Return the label of a record value. prs.record[prs.record.high] @@ -572,25 +576,27 @@ macro `%*`*(x: untyped): untyped = template record*(label: string) {.pragma.} ## Serialize this object or tuple as a record. + ## ## ``` ## type Foo {.record: "foobar".} = tuple ## a, b: int ## let r: Foo = (1, 2) - ## echo $(toPreserve(r)) - ## # + ## assert($r.toPreserve == "") ## ``` template unpreservable*() {.pragma.} ## Pragma to forbid a type from being converted by `toPreserve`. proc toPreserve*[T](x: T): Preserve = - ## Serializes `x` to Preserves; uses `toPreserveHook(x: A)` if it's in scope to + ## Serializes `x` to Preserves; uses `toPreserveHook(x: T)` if it's in scope to ## customize serialization. when T is Preserve: result = x - elif T is Bigint: - result = Preserve(kind: pkBigInteger, bigint: x) elif compiles(toPreserveHook(x)): result = toPreserveHook(x) + elif T is Bigint: + result = Preserve(kind: pkBigInteger, bigint: x) + elif T is seq[byte]: + result = Preserve(kind: pkByteString, bytes: x) elif T is array | seq: result = Preserve(kind: pkSequence) for v in x.items: result.sequence.add(toPreserve(v)) @@ -620,8 +626,10 @@ proc toPreserve*[T](x: T): Preserve = result = Preserve(kind: pkString, string: x) elif T is SomeInteger: result = Preserve(kind: pkSignedInteger, int: x.BiggestInt) - else: + elif compiles(%x): result = %x + else: + raiseAssert("unpreservable type" & $T) proc toPreserveHook*[T](set: HashSet[T]): Preserve = Preserve(kind: pkSet, set: set.map(toPreserve)) @@ -697,66 +705,119 @@ proc toJsonHook*(prs: Preserve): JsonNode = of pkEmbedded: raise newException(ValueError, "cannot convert embedded value to JSON") -proc checkRecordLabel(T: typedesc; prs: Preserve) = - when T.hasCustomPragma(record): - const label = symbol(T.getCustomPragmaVal(record)) - if prs.label != label: - raise newException(ValueError, $prs & " is not a record of class " & label.string) - else: - raise newException(Defect, $T & " lacks a {.record: \"…\".} annotation") +proc fromPreserve*[T](v: var T; prs: Preserve): bool = + ## Inplace version of `preserveTo`. + ## Partial matches on compond values may leave artifacts in ``v``. + # TODO: {.raises: [].} + runnableExamples: + import std/options, preserves, preserves/parse + type Foo {.record: "foo".} = object + x, y, z: int -proc fromPreserve*[T](result: var T; prs: Preserve) = - # Inplace version of `preserveTo`. - when compiles(fromPreserveHook(result, prs)): - fromPreserveHook(result, prs) + var foo: Foo + assert(fromPreserve(foo, parsePreserves(""""""))) + assert(foo.x == 1) + assert(foo.y == 2) + assert(foo.z == 3) + + when compiles(fromPreserveHook(v, prs)): + result = fromPreserveHook(v, prs) elif T is Preserve: - result = prs + v = prs + result = true elif T is Bigint: - result = prs.bigint + case prs.kind + of pkSignedInteger: + v = initBigint(prs.int) + result = true + of pkBigInteger: + v = prs.bigint + result = true + else: disard elif T is bool: - result = prs.bool + if prs.kind == pkBoolean: + v = prs.bool + result = true elif T is SomeInteger: - result = T(prs.int) + if prs.kind == pkSignedInteger: + v = T(prs.int) + result = true elif T is float: - result = prs.float + if prs.kind == pkFloat: + v = prs.float + result = true elif T is seq: - result.setLen(prs.sequence.len) - for i, val in prs.sequence: - fromPreserve(result[i], val) + if T is seq[byte] and prs.kind == pkByteString: + v = prs.bytes + result = true + elif prs.kind == pkSequence: + v.setLen(prs.len) + result = true + for i, e in prs.sequence: + result = result and fromPreserve(v[i], e) elif T is float64: - result = prs.double + case prs.kind + of pkFloat: + v = prs.float + result = true + of pkDouble: + v = prs.double + result = true elif T is object | tuple: case prs.kind of pkRecord: - # checkRecordLabel(T, prs) - var i: int - for k, v in result.fieldPairs: - fromPreserve(v, prs.record[i]) - inc(i) + when T.hasCustomPragma(record): + const label = symbol(T.getCustomPragmaVal(record)) + if prs.record[prs.record.high] == label: + result = true + var i = 0 + for fname, field in v.fieldPairs: + if not result or (i == prs.record.high): break + result = result and fromPreserve(field, prs.record[i]) + inc(i) + result = result and (i == prs.record.high) # arity equivalence check= of pkDictionary: - for k, v in result.fieldPairs: - fromPreserve(v, prs.dict[symbol(k)]) - else: - raise newException(ValueError, "cannot convert to Preserves value to type " & $T) + result = true + for key, val in v.fieldPairs: + result = result and fromPreserve(val, prs.dict.getOrDefault(symbol(key))) + else: discard elif T is Ordinal | SomeInteger: - result = (T)prs.int + if prs.kind == pkSignedInteger: + v = (T)prs.int + result = true elif T is ref: if prs != symbol"null": - new result - fromPreserve(result[], prs) + new v + result = fromPreserve(v[], prs) elif T is string: - result = prs.string + if prs.kind == pkString: + v = prs.string + result = true elif T is distinct: - fromPreserve(result.distinctBase, prs) + result = fromPreserve(result.distinctBase, prs) else: - static: raiseAssert("cannot convert from Preserves: " & $T) + raiseAssert("no conversion of type Preserve to " & $T) -proc preserveTo*(prs: Preserve; T: typedesc): T = +proc preserveTo*(prs: Preserve; T: typedesc): Option[T] = ## Reverse of `toPreserve`. - fromPreserve(result, prs) + # TODO: {.raises: [].} + runnableExamples: + import std/options, preserves, preserves/parse + type Foo {.record: "foo".} = object + x, y, z: int -proc fromPreserveHook*[A,B](result: var Table[A,B]|TableRef[A,B]; prs: Preserve) = - for k, v in prs.pairs: result[preserveTo(k,A)] = preserveTo(k,B) + assert(parsePreserves("""""").preserveTo(Foo).isNone) + assert(parsePreserves("""""").preserveTo(Foo).isNone) + assert(parsePreserves("""""").preserveTo(Foo).isSome) + var v: T + if fromPreserve(v, prs): + result = some(move v) + +proc fromPreserveHook*[A,B](t: var Table[A,B]|TableRef[A,B]; prs: Preserve): bool = + if prs.isDictionary: + for k, v in prs.pairs: + t[preserveTo(k,A)] = preserveTo(k,B) + result = true proc len*(prs: Preserve): int = ## Return the number of values one level below ``prs``. diff --git a/tests/test_conversions.nim b/tests/test_conversions.nim index 3bbfda2..a6f7937 100644 --- a/tests/test_conversions.nim +++ b/tests/test_conversions.nim @@ -1,6 +1,6 @@ # SPDX-License-Identifier: ISC -import streams, strutils, unittest +import std/[options, streams, strutils, unittest] import bigints, preserves, preserves/records suite "conversions": @@ -14,7 +14,7 @@ suite "conversions": c: Foobar = (a: 1, b: 2, c: Bar(s: "ku",)) b = toPreserve(c) a = preserveTo(b, Foobar) - check(a == c) + check(a.isSome and (get(a) == c)) check(b.kind == pkDictionary) test "records": @@ -27,7 +27,8 @@ suite "conversions": tup: Foobar = (a: 1, b: 2, c: Bar(s: "ku",)) prs = toPreserve(tup) check(prs.kind == pkRecord) - check(preserveTo(prs, Foobar) == tup) + check($prs == """>""") + check(preserveTo(prs, Foobar) == some(tup)) check(classOf(tup) == classOf(prs)) suite "%":