diff --git a/src/preserves.nim b/src/preserves.nim index bf67d53..3062c75 100644 --- a/src/preserves.nim +++ b/src/preserves.nim @@ -6,6 +6,14 @@ import std/[base64, endians, hashes, options, sets, sequtils, streams, tables, t from std/json import escapeJson, escapeJsonUnquoted from std/macros import hasCustomPragma, getCustomPragmaVal +import ./preserves/private/dot +from std/strutils import parseEnum + +when defined(tracePreserves): + template trace(args: varargs[untyped]) = + echo args +else: + template trace(args: varargs[untyped]) = discard type PreserveKind* = enum @@ -52,7 +60,6 @@ type DictEntry[E] = tuple[key: Preserve[E], val: Preserve[E]] - proc `==`*[A, B](x: Preserve[A]; y: Preserve[B]): bool = ## Check `x` and `y` for equivalence. if x.kind == y.kind and x.embedded == y.embedded: @@ -207,6 +214,15 @@ proc `[]`*(pr, key: Preserve): Preserve = else: raise newException(ValueError, "`[]` is not valid for " & $pr.kind) +proc getOrDefault(pr: Preserve; key: Preserve): Preserve = + ## Retrieves the value of `pr[key]` if `pr` is a dictionary containing `key` + ## or returns the `#f` Preserves value. + if pr.kind == pkDictionary: + for (k, v) in pr.dict: + if key == k: + result = v + break + proc incl*(pr: var Preserve; key: Preserve) = ## Include `key` in the Preserves set `pr`. for i in 0..pr.set.high: @@ -239,7 +255,7 @@ proc `[]=`*(pr: var Preserve; key, val: Preserve) = return pr.dict.add((key, val, )) -proc symbol*[E](s: string): Preserve[E] {.inline.} = +proc toSymbol*(s: sink string; E = void): Preserve[E] {.inline.} = ## Create a Preserves symbol value. Preserve[E](kind: pkSymbol, symbol: s) @@ -270,6 +286,30 @@ proc embed*[E](e: E): Preserve[E] = ## Create a Preserves value that embeds ``e``. Preserve[E](kind: pkEmbedded, embed: e) +proc mapEmbeds*[A, B](pr: Preserve[A]; op: proc (v: A): B): Preserve[B] = + ## Convert `Preserve[A]` to `Preserve[B]` using an `A -> B` procedure. + case pr.kind + of pkBoolean, pkFloat, pkDouble, pkSignedInteger, pkBigInteger, pkString, pkByteString, pkSymbol: + result = cast[Preserve[B]](pr) + of pkRecord: + result = Preserve[B](kind: pr.kind) + result.record = map(pr.record) do (x: Preserve[A]) -> Preserve[B]: + mapEmbeds(x, op) + of pkSequence: + result = Preserve[B](kind: pr.kind) + result.sequence = map(pr.sequence) do (x: Preserve[A]) -> Preserve[B]: + mapEmbeds(x, op) + of pkSet: + result = Preserve[B](kind: pr.kind) + result.set = map(pr.set) do (x: Preserve[A]) -> Preserve[B]: + mapEmbeds(x, op) + of pkDictionary: + result = Preserve[B](kind: pr.kind) + result.dict = map(pr.dict) do (e: DictEntry[A]) -> DictEntry[B]: + (mapEmbeds(e.key, op), mapEmbeds(e.val, op)) + of pkEmbedded: + result = embed op(pr.embed) + proc len*(pr: Preserve): int = ## Return the shallow count of values in ``pr``, that is the number of ## fields in a record, items in a sequence, items in a set, or key-value pairs @@ -582,11 +622,37 @@ proc decodePreserves*(s: seq[byte]; E = void): Preserve[E] = ## Decode a byte-string of binary-encoded Preserves. decodePreserves(cast[string](s), E) -template record*(label: string) {.pragma.} - ## Serialize this object or tuple as a record. See ``toPreserve``. +template preservesRecord*(label: string) {.pragma.} + ## Serialize this object or tuple as a record. + ## See ``toPreserve``. + +template preservesTuple*() {.pragma.} + ## Serialize this object or tuple as a tuple. + ## See ``toPreserve``. + +template preservesTupleTail*() {.pragma.} + ## Serialize this object field to the end of its containing tuple. + ## See ``toPreserve``. + +template preservesDictionary*() {.pragma.} + ## Serialize this object or tuple as a dictionary. + ## See ``toPreserve``. + +template preservesSymbol*() {.pragma.} + ## Serialize this string as a symbol. + ## See ``toPreserve``. + +template preservesOr*() {.pragma.} + ## Serialize this object as an ``or`` alternative. + ## See ``toPreserve``. + +template preservesLiteral*(value: typed) {.pragma.} + ## Serialize a Preserves literal within this object. + ## See ``toPreserve``. template unpreservable*() {.pragma.} ## Pragma to forbid a type from being converted by ``toPreserve``. + ## See ``toPreserve``. proc toPreserve*[T](x: T; E = void): Preserve[E] = ## Serializes ``x`` to Preserves. Can be customized by defining @@ -594,8 +660,13 @@ proc toPreserve*[T](x: T; E = void): Preserve[E] = ## Any ``toPreserveHook`` that does not compile will be discarded; ## *Write tests for your hooks!* when (T is Preserve[E]): result = x + elif T is E: result = embed(x) elif compiles(toPreserveHook(x, E)): result = toPreserveHook(x, E) + elif T is distinct: + result = toPreserve(x.distinctBase, E) + elif T is enum: + result = toSymbol($x, E) elif T is Bigint: result = Preserve[E](kind: pkBigInteger, bigint: x) elif T is seq[byte]: @@ -605,33 +676,71 @@ proc toPreserve*[T](x: T; E = void): Preserve[E] = for v in x.items: result.sequence.add(toPreserve(v, E)) elif T is bool: result = Preserve[E](kind: pkBoolean, bool: x) - elif T is distinct: - result = toPreserve(x.distinctBase, E) elif T is float: result = Preserve[E](kind: pkFloat, float: x) elif T is float64: result = Preserve[E](kind: pkDouble, double: x) - elif T is object | tuple: - when T.hasCustomPragma(unpreservable): {.fatal: "unpreservable type".} - elif T.hasCustomPragma(record): - result = Preserve[E](kind: pkRecord) - for _, f in x.fieldPairs: result.record.add(toPreserve(f, E)) - result.record.add(symbol[E](T.getCustomPragmaVal(record))) - else: - result = Preserve[E](kind: pkDictionary) - for k, v in x.fieldPairs: - result[symbol[E](k)] = toPreserve(v, E) + elif T is tuple: + result = Preserve[E](kind: pkSequence, + sequence: newSeqOfCap[Preserve[E]](tupleLen(T))) + for xf in fields(x): + result.sequence.add(toPreserve(xf, E)) elif T is Ordinal: result = Preserve[E](kind: pkSignedInteger, int: x.ord.BiggestInt) elif T is ptr | ref: - if system.`==`(x, nil): result = symbol[E]("null") + if system.`==`(x, nil): result = toSymbol("null", E) else: result = toPreserve(x[], E) elif T is string: result = Preserve[E](kind: pkString, string: x) elif T is SomeInteger: result = Preserve[E](kind: pkSignedInteger, int: x.BiggestInt) - else: - raiseAssert("unpreservable type" & $T) + elif T is object: + trace T, " is object" + template fieldToPreserve(key: string; val: typed): Preserve = + when x.dot(key).hasCustomPragma(preservesSymbol): + toSymbol(val, E) + elif x.dot(key).hasCustomPragma(preservesLiteral): + const lit = parsePreserves x.dot(key).getCustomPragmaVal(preservesLiteral) + lit + else: + toPreserve(val, E) + when T.hasCustomPragma(unpreservable): {.fatal: "unpreservable type".} + elif T.hasCustomPragma(preservesOr): + var hasKind, hasVariant: bool + for k, v in x.fieldPairs: + trace T, ": iterate to ", k + if k == "orKind": + assert(not hasKind) + hasKind = true + else: + assert(hasKind and not hasVariant) + result = fieldToPreserve(k, v) + hasVariant = true + if not hasVariant: + trace T, ": no value found" + elif T.hasCustomPragma(preservesRecord): + result = Preserve[E](kind: pkRecord) + for k, v in x.fieldPairs: + result.record.add(fieldToPreserve(k, v)) + result.record.add(tosymbol(T.getCustomPragmaVal(preservesRecord), E)) + elif T.hasCustomPragma(preservesTuple): + result = initSequence[E]() + for label, field in x.fieldPairs: + when x.dot(label).hasCustomPragma(preservesTupleTail): + for y in field.items: + result.sequence.add(toPreserve(y, E)) + # TODO: what if there are fields after the tail? + else: + result.sequence.add(fieldToPreserve(label, field)) + elif T.hasCustomPragma(preservesDictionary): + trace T, ": convert to a dictionary" + result = initDictionary[E]() + for key, val in x.fieldPairs: + result[toSymbol(key, E)] = fieldToPreserve(key, val) + else: result = toPreserveHook(x, E) + else: result = toPreserveHook(x, E) + # the hook doesn't compile but produces a useful error + trace T, " -> ", result proc toPreserveHook*[A](pr: Preserve[A]; E: typedesc): Preserve[E] = ## Hook for converting ``Preserve`` values with different embedded types. @@ -648,10 +757,21 @@ proc toPreserveHook*[T](set: HashSet[T]; E: typedesc): Preserve[E] = result = Preserve[E](kind: pkSet, set: newSeqOfCap[Preserve[E]](set.len)) for e in set: result.incl toPreserve(e, E) +#[ +when isMainModule: + var h: HashSet[string] + var pr = h.toPreserveHook(void) + assert fromPreserveHook(h, pr) +]# + proc toPreserveHook*[A, B](table: Table[A, B]|TableRef[A, B], E: typedesc): Preserve[E] = ## Hook for preserving ``Table``. result = initDictionary[E]() - for k, v in table.pairs: result[toPreserve(k, E)] = toPreserve(v, E) + for k, v in table.pairs: + when A is string: + result[toSymbol(k, E)] = toPreserve(v, E) + else: + result[toPreserve(k, E)] = toPreserve(v, E) proc fromPreserve*[T, E](v: var T; pr: Preserve[E]): bool = ## Inplace version of `preserveTo`. Returns ``true`` on @@ -668,14 +788,23 @@ proc fromPreserve*[T, E](v: var T; pr: Preserve[E]): bool = assert(fromPreserve(foo, parsePreserves(""""""))) assert(foo.x == 1) assert(foo.y == 2) - type Value = Preserve - when T is Value: + when T is Preserve[E]: v = pr result = true elif T is E: - result = pr.embed + if pr.kind == pkEmbedded: + v = pr.embed + result = true elif compiles(fromPreserveHook(v, pr)): result = fromPreserveHook(v, pr) + elif T is distinct: + result = fromPreserve(result.distinctBase, pr) + elif T is enum: + if pr.isSymbol: + try: + v = parseEnum[T](pr.symbol) + result = true + except ValueError: discard elif T is Bigint: case pr.kind of pkSignedInteger: @@ -697,15 +826,19 @@ proc fromPreserve*[T, E](v: var T; pr: Preserve[E]): bool = if pr.kind == pkFloat: v = pr.float result = true - elif T is seq: - if T is seq[byte] and pr.kind == pkByteString: + elif T is seq[byte]: + if pr.kind == pkByteString: v = pr.bytes result = true - elif pr.kind == pkSequence: + elif T is seq: + if pr.kind == pkSequence: v.setLen(pr.len) result = true for i, e in pr.sequence: result = result and fromPreserve(v[i], e) + if not result: + v.setLen 0 + break elif T is float64: case pr.kind of pkFloat: @@ -714,51 +847,112 @@ proc fromPreserve*[T, E](v: var T; pr: Preserve[E]): bool = of pkDouble: v = pr.double result = true - elif T is object | tuple: - case pr.kind - of pkRecord: - when T.hasCustomPragma(record): - if pr.record[pr.record.high].isSymbol T.getCustomPragmaVal(record): - result = true - var i = 0 - for fname, field in v.fieldPairs: - if not result or (i == pr.record.high): break - result = result and fromPreserve(field, pr.record[i]) - inc(i) - result = result and (i == pr.record.high) # arity equivalence check= - of pkDictionary: - result = true - var fieldCount = 0 - for key, val in v.fieldPairs: - inc fieldCount - for (pk, pv) in pr.dict.items: - var sym = symbol[E](key) - if sym == pk: - result = result and fromPreserve(val, pv) - break - result = result and pr.dict.len == fieldCount - else: discard elif T is Ordinal | SomeInteger: if pr.kind == pkSignedInteger: v = (T)pr.int result = true - elif T is ref: - if pr != symbol[E]("null"): - new v - result = fromPreserve(v[], pr) elif T is string: - if pr.kind == pkString: + case pr.kind + of pkString: v = pr.string result = true - elif T is distinct: - result = fromPreserve(result.distinctBase, pr) + of pkSymbol: + v = pr.symbol + result = true + else: discard + elif T is tuple: + case pr.kind + of pkRecord, pkSequence: + if pr.len <= tupleLen(T): + result = true + var i: int + for f in fields(v): + if result and i < pr.len: + result = result and fromPreserve(f, pr[i]) + inc i + of pkDictionary: + if tupleLen(T) == pr.len: + result = true + for key, val in fieldPairs(v): + if result: + result = result and fromPreserve(val, pr[toSymbol(key, E)]) + else: discard + elif T is ref: + if isNil(v): new(v) + result = fromPreserve(v[], pr) + elif T is object: + template fieldFromPreserve(key: string; val: typed; pr: Preserve[E]): bool {.used.} = + when v.dot(key).hasCustomPragma(preservesSymbol): + if pr.isSymbol: + fromPreserve(val, pr) + else: + false + elif v.dot(key).hasCustomPragma(preservesLiteral): + const lit = parsePreserves v.dot(key).getCustomPragmaVal(preservesLiteral) + pr == lit + else: + fromPreserve(val, pr) + when T.hasCustomPragma(preservesRecord): + if pr.isRecord and pr.label.isSymbol(T.getCustomPragmaVal(preservesRecord)): + result = true + var i: int + for key, val in fieldPairs(v): + if result and i <= pr.len: + result = result and fieldFromPreserve(key, val, pr.record[i]) + inc i + result = result and (i == pr.len) + elif T.hasCustomPragma(preservesTuple): + if pr.isSequence: + result = true + var i: int + for name, field in fieldPairs(v): + when v.dot(name).hasCustomPragma(preservesTupleTail): + setLen(v.dot(name), pr.len - i) + var j: int + while result and i < pr.len: + result = result and fieldFromPreserve(name, v.dot(name)[j], pr.sequence[i]) + inc i + inc j + else: + if result and i < pr.len: + result = result and fromPreserve(field, pr.sequence[i]) + inc i + result = result and (i == pr.len) + elif T.hasCustomPragma(preservesDictionary): + trace T, " is a preservesDictionary" + if pr.isDictionary: + result = true + var i: int + for key, _ in fieldPairs(v): + let val = pr.getOrDefault(toSymbol(key, E)) + result = result and fieldFromPreserve( + key, v.dot(key), val) + if not result: break + inc i + result = result and (i == pr.len) + elif T.hasCustomPragma(preservesOr): + for kind in typeof(T.orKind): + v = T(orKind: kind) + var fieldWasFound = false + for key, val in fieldPairs(v): + when key != "orKind": # fieldPairs unwraps early + result = fieldFromPreserve(key, v.dot(key), pr) + fieldWasFound = true + break + if not fieldWasFound: + # hopefully a `discard` of-branch, so discard `pr` + result = true + if result: break + else: result = fromPreserveHook(v, pr) + else: result = fromPreserveHook(v, pr) + # the hook doesn't compile but produces a useful error + if not result: + trace T, " !- ", pr else: - raiseAssert("no conversion of type Preserve to " & $T) - if not result: reset v + trace T, " <- ", pr proc preserveTo*(pr: Preserve; T: typedesc): Option[T] = ## Reverse of `toPreserve`. - ## # TODO: {.raises: [].} runnableExamples: import std/options, preserves, preserves/parse @@ -788,11 +982,18 @@ proc fromPreserveHook*[A,B,E](t: var (Table[A,B]|TableRef[A,B]); pr: Preserve[E] result = true var a: A var b: B - for (k, v) in pr.dict: + for (k, v) in pr.dict.items: result = fromPreserve(a, k) and fromPreserve(b, v) - if not result: break + if not result: + clear t + break t[move a] = move b +when isMainModule: + var t: Table[int, string] + var pr = t.toPreserveHook(void) + assert fromPreserveHook(t, pr) + proc concat[E](result: var string; pr: Preserve[E]) = if pr.embedded: result.add("#!") case pr.kind: @@ -854,6 +1055,8 @@ proc concat[E](result: var string; pr: Preserve[E]) = result.concat(key) result.add(": ") result.concat(value) + if i < pr.dict.high: + result.add(',') inc i result.add('}') of pkEmbedded: @@ -863,13 +1066,3 @@ proc concat[E](result: var string; pr: Preserve[E]) = proc `$`*(pr: Preserve): string = concat(result, pr) ## Generate the textual representation of ``pr``. - -when isMainModule: - block: - var t: Table[int, string] - var pr = t.toPreserveHook(void) - assert fromPreserveHook(t, pr) - block: - var h: HashSet[string] - var pr = h.toPreserveHook(void) - assert fromPreserveHook(h, pr) diff --git a/src/preserves/jsonhooks.nim b/src/preserves/jsonhooks.nim index 9e5bf57..9abb70e 100644 --- a/src/preserves/jsonhooks.nim +++ b/src/preserves/jsonhooks.nim @@ -14,10 +14,10 @@ proc toPreserveHook*(js: JsonNode; E: typedesc): Preserve[E] = result = Preserve[E](kind: pkDouble, double: js.fnum) of JBool: result = case js.bval - of false: symbol[E]"false" - of true: symbol[E]"true" + of false: toSymbol("false", E) + of true: toSymbol("true", E) of JNull: - result = symbol[E]"null" + result = toSymbol("null", E) of JObject: result = Preserve[E](kind: pkDictionary) for key, val in js.fields.pairs: diff --git a/src/preserves/private/dot.nim b/src/preserves/private/dot.nim new file mode 100644 index 0000000..255f588 --- /dev/null +++ b/src/preserves/private/dot.nim @@ -0,0 +1,7 @@ +import std/macros + + +macro dot*(obj: object, fld: string): untyped = + ## Turn ``obj.dot("fld")`` into ``obj.fld``. + + newDotExpr(obj, newIdentNode(fld.strVal)) diff --git a/tests/test_conversions.nim b/tests/test_conversions.nim index 4839bca..dcc1903 100644 --- a/tests/test_conversions.nim +++ b/tests/test_conversions.nim @@ -6,26 +6,26 @@ import bigints, preserves suite "conversions": test "dictionary": - type Bar = object + type Bar = tuple s: string - type Foobar = tuple + type Foobar {.preservesDictionary.} = object a, b: int c: Bar let - c: Foobar = (a: 1, b: 2, c: Bar(s: "ku", )) + c = Foobar(a: 1, b: 2, c: ("ku", )) b = toPreserve(c) a = preserveTo(b, Foobar) check(a.isSome and (get(a) == c)) check(b.kind == pkDictionary) test "records": - type Bar {.record: "bar".} = object + type Bar {.preservesRecord: "bar".} = object s: string - type Foobar {.record: "foo".} = tuple + type Foobar {.preservesRecord: "foo".} = object a, b: int c: Bar let - tup: Foobar = (a: 1, b: 2, c: Bar(s: "ku", )) + tup = Foobar(a: 1, b: 2, c: Bar(s: "ku", )) prs = toPreserve(tup) check(prs.kind == pkRecord) check($prs == """>""") @@ -35,7 +35,7 @@ suite "conversions": var a: Table[int, string] for i, s in ["a", "b", "c"]: a[i] = s let b = toPreserve(a) - check($b == """{0: "a" 1: "b" 2: "c"}""") + check($b == """{0: "a", 1: "b", 2: "c"}""") var c: Table[int, string] check(fromPreserve(c, b)) check(a == c)