Skip to content

Commit

Permalink
Make the flavors support optional when defining formats
Browse files Browse the repository at this point in the history
Other changes:

* The format declarations have been broken down in multiple
  parts. This makes it possible to structure the implementation
  libraries in a way such that the format name is accessible even
  when importing only reader or writer modules.

* Flavors lead to more complicated Reader and Writer types.
  Instead of requiring the user to write down long type names
  such as `JsonReader[DefaultFlavor]`, it's now possible to refer
  to the reader and writer type of each format using expressions
  such as `Json.Reader` and `Json.Writer`.

* Fixed a typo (PreferredOutput)
  • Loading branch information
zah committed Mar 19, 2021
1 parent 84ffa54 commit f9a1121
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 73 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ serializationFormat Json, # This is the name of the for
Reader = JsonReader, # The associated Reader type.
Writer = JsonWriter, # The associated Writer type.
PreferedOutput = string, # APIs such as `Json.encode` will return this type.
PreferredOutput = string, # APIs such as `Json.encode` will return this type.
# The type must support the following operations:
# proc initWithCapacity(_: type T, n: int)
# proc add(v: var T, bytes: openarray[byte])
# By default, the PreferedOutput is `seq[byte]`.
# By default, the PreferredOutput is `seq[byte]`.
mimeType = "application/json", # Mime type associated with the format (Optional).
fileExt = "json" # File extension associated with the format (Optional).
Expand All @@ -48,7 +48,7 @@ serializationFormat Json, # This is the name of the for
Most of the time, you'll be using the following high-level APIs when encoding
and decoding values:

#### `Format.encode(value: auto, params: varargs): Format.PreferedOutput`
#### `Format.encode(value: auto, params: varargs): Format.PreferredOutput`

Encodes a value in the specified format returning the preferred output type
for the format (usually `string` or `seq[byte]`). All extra params will be
Expand Down
79 changes: 29 additions & 50 deletions serialization.nim
Original file line number Diff line number Diff line change
@@ -1,49 +1,24 @@
import
typetraits,
stew/shims/macros, faststreams,
serialization/[object_serialization, errors]
serialization/[object_serialization, errors, formats]

export
faststreams, object_serialization, errors

template serializationFormatImpl(Name: untyped,
Reader, Writer, PreferedOutput: distinct type,
mimeTypeName: static string = "") {.dirty.} =
# This indirection is required in order to be able to generate the
# `mimeType` accessor template. Without the indirection, the template
# mechanism of Nim will try to expand the `mimeType` param in the position
# of the `mimeType` template name which will result in error.
type Name* = object
template ReaderType*(T: type Name, F: distinct type = DefaultFlavor): type = Reader[F]
template WriterType*(T: type Name, F: distinct type = DefaultFlavor): type = Writer[F]
template PreferedOutputType*(T: type Name): type = PreferedOutput
template mimeType*(T: type Name): string = mimeTypeName

template serializationFormat*(Name: untyped,
Reader, Writer, PreferedOutput: distinct type,
mimeType: static string = "") =
serializationFormatImpl(Name, Reader, Writer, PreferedOutput, mimeType)

template createFlavor*(ModifiedFormat, FlavorName: untyped) =
type FlavorName* = object
template ReaderType*(T: type FlavorName): type = ReaderType(ModifiedFormat, FlavorName)
template WriterType*(T: type FlavorName): type = WriterType(ModifiedFormat, FlavorName)
template PreferedOutputType*(T: type FlavorName): type = PreferedOutputType(ModifiedFormat)
template mimeType*(T: type FlavorName): string = mimeType(ModifiedFormat)
faststreams, object_serialization, errors, formats

template encode*(Format: type, value: auto, params: varargs[untyped]): auto =
mixin init, WriterType, writeValue, PreferedOutputType
mixin init, Writer, writeValue, PreferredOutputType
{.noSideEffect.}:
# We assume that there is no side-effects here, because we are
# using a `memoryOutput`. The computed side-effects are coming
# from the fact that the dynamic dispatch mechanisms used in
# faststreams may be writing to a file or a network device.
try:
var s = memoryOutput()
type Writer = WriterType(Format)
var writer = unpackArgs(init, [Writer, s, params])
type WriterType = Writer(Format)
var writer = unpackArgs(init, [WriterType, s, params])
writeValue writer, value
s.getOutput PreferedOutputType(Format)
s.getOutput PreferredOutputType(Format)
except IOError:
raise (ref Defect)() # a memoryOutput cannot have an IOError

Expand All @@ -60,15 +35,16 @@ template decode*(Format: distinct type,
params: varargs[untyped]): auto =
# TODO, this is dusplicated only due to a Nim bug:
# If `input` was `string|openarray[byte]`, it won't match `seq[byte]`
mixin init, ReaderType
mixin init, Reader
{.noSideEffect.}:
# We assume that there are no side-effects here, because we are
# using a `memoryInput`. The computed side-effects are coming
# from the fact that the dynamic dispatch mechanisms used in
# faststreams may be reading from a file or a network device.
try:
var stream = unsafeMemoryInput(input)
var reader = unpackArgs(init, [ReaderType(Format), stream, params])
type ReaderType = Reader(Format)
var reader = unpackArgs(init, [ReaderType, stream, params])
reader.readValue(RecordType)
except IOError:
raise (ref Defect)() # memory inputs cannot raise an IOError
Expand All @@ -79,15 +55,16 @@ template decode*(Format: distinct type,
params: varargs[untyped]): auto =
# TODO, this is dusplicated only due to a Nim bug:
# If `input` was `string|openarray[byte]`, it won't match `seq[byte]`
mixin init, ReaderType
mixin init, Reader
{.noSideEffect.}:
# We assume that there are no side-effects here, because we are
# using a `memoryInput`. The computed side-effects are coming
# from the fact that the dynamic dispatch mechanisms used in
# faststreams may be reading from a file or a network device.
try:
var stream = unsafeMemoryInput(input)
var reader = unpackArgs(init, [ReaderType(Format), stream, params])
type ReaderType = Reader(Format)
var reader = unpackArgs(init, [ReaderType, stream, params])
reader.readValue(RecordType)
except IOError:
raise (ref Defect)() # memory inputs cannot raise an IOError
Expand All @@ -96,11 +73,12 @@ template loadFile*(Format: distinct type,
filename: string,
RecordType: distinct type,
params: varargs[untyped]): auto =
mixin init, ReaderType, readValue
mixin init, Reader, readValue

var stream = memFileInput(filename)
try:
var reader = unpackArgs(init, [ReaderType(Format), stream, params])
type ReaderType = Reader(Format)
var reader = unpackArgs(init, [ReaderType, stream, params])
reader.readValue(RecordType)
finally:
close stream
Expand All @@ -112,12 +90,12 @@ template loadFile*[RecordType](Format: type,
record = loadFile(Format, filename, RecordType, params)

template saveFile*(Format: type, filename: string, value: auto, params: varargs[untyped]) =
mixin init, WriterType, writeValue
mixin init, Writer, writeValue

var stream = fileOutput(filename)
try:
type Writer = WriterType(Format)
var writer = unpackArgs(init, [Writer, stream, params])
type WriterType = Writer(Format)
var writer = unpackArgs(init, [WriterType, stream, params])
writer.writeValue(value)
finally:
close stream
Expand Down Expand Up @@ -146,16 +124,16 @@ template borrowSerialization*(Alias: distinct type,

template serializesAsBase*(SerializedType: distinct type,
Format: distinct type) =
mixin ReaderType, WriterType
mixin Reader, Writer

type Reader = ReaderType(Format)
type Writer = WriterType(Format)
type ReaderType = Reader(Format)
type WriterType = Writer(Format)

template writeValue*(writer: var Writer, value: SerializedType) =
template writeValue*(writer: var WriterType, value: SerializedType) =
mixin writeValue
writeValue(writer, distinctBase value)

template readValue*(reader: var Reader, value: var SerializedType) =
template readValue*(reader: var ReaderType, value: var SerializedType) =
mixin readValue
value = SerializedType reader.readValue(distinctBase SerializedType)

Expand All @@ -169,16 +147,17 @@ template readValue*(stream: InputStream,
Format: type,
ValueType: type,
params: varargs[untyped]): untyped =
mixin ReaderType, init, readValue
var reader = unpackArgs(init, [ReaderType(Format), stream, params])
mixin Reader, init, readValue
type ReaderType = Reader(Format)
var reader = unpackArgs(init, [ReaderType, stream, params])
readValue reader, ValueType

template writeValue*(stream: OutputStream,
Format: type,
value: auto,
params: varargs[untyped]) =
mixin WriterType, init, writeValue
type Writer = WriterType(Format)
var writer = unpackArgs(init, [Writer, stream])
mixin Writer, init, writeValue
type WriterType = Writer(Format)
var writer = unpackArgs(init, [WriterType, stream, params])
writeValue writer, value

40 changes: 40 additions & 0 deletions serialization/formats.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import
std/typetraits

template serializationFormatImpl(Name: untyped,
mimeTypeName: static string = "") {.dirty.} =
# This indirection is required in order to be able to generate the
# `mimeType` accessor template. Without the indirection, the template
# mechanism of Nim will try to expand the `mimeType` param in the position
# of the `mimeType` template name which will result in error.
type Name* = object
template mimeType*(T: type Name): string = mimeTypeName

template serializationFormat*(Name: untyped, mimeType: static string = "") =
serializationFormatImpl(Name, mimeType)

template setReader*(Format, FormatReader: distinct type) =
when arity(FormatReader) > 1:
template ReaderType*(T: type Format, F: distinct type = DefaultFlavor): type = FormatReader[F]
template Reader*(T: type Format, F: distinct type = DefaultFlavor): type = FormatReader[F]
else:
template ReaderType*(T: type Format): type = FormatReader
template Reader*(T: type Format): type = FormatReader

template setWriter*(Format, FormatWriter, PreferredOutput: distinct type) =
when arity(FormatWriter) > 1:
template WriterType*(T: type Format, F: distinct type = DefaultFlavor): type = FormatWriter[F]
template Writer*(T: type Format, F: distinct type = DefaultFlavor): type = FormatWriter[F]
else:
template WriterType*(T: type Format): type = FormatWriter
template Writer*(T: type Format): type = FormatWriter

template PreferredOutputType*(T: type Format): type = PreferredOutput

template createFlavor*(ModifiedFormat, FlavorName: untyped) =
type FlavorName* = object
template Reader*(T: type FlavorName): type = Reader(ModifiedFormat, FlavorName)
template Writer*(T: type FlavorName): type = Writer(ModifiedFormat, FlavorName)
template PreferredOutputType*(T: type FlavorName): type = PreferredOutputType(ModifiedFormat)
template mimeType*(T: type FlavorName): string = mimeType(ModifiedFormat)

30 changes: 15 additions & 15 deletions serialization/object_serialization.nim
Original file line number Diff line number Diff line change
Expand Up @@ -191,12 +191,12 @@ template writeFieldIMPL*[Writer](writer: var Writer,
mixin writeValue
writer.writeValue(fieldVal)

proc makeFieldReadersTable(RecordType, Reader: distinct type):
seq[FieldReader[RecordType, Reader]] =
proc makeFieldReadersTable(RecordType, ReaderType: distinct type):
seq[FieldReader[RecordType, ReaderType]] =
mixin enumAllSerializedFields, readFieldIMPL, handleReadException

enumAllSerializedFields(RecordType):
proc readField(obj: var RecordType, reader: var Reader)
proc readField(obj: var RecordType, reader: var ReaderType)
{.gcsafe, nimcall, raises: [SerializationError, Defect].} =
when RecordType is tuple:
const i = fieldName.parseInt
Expand All @@ -222,17 +222,17 @@ proc makeFieldReadersTable(RecordType, Reader: distinct type):

result.add((fieldName, readField))

proc fieldReadersTable*(RecordType, Reader: distinct type):
ptr seq[FieldReader[RecordType, Reader]] =
proc fieldReadersTable*(RecordType, ReaderType: distinct type):
ptr seq[FieldReader[RecordType, ReaderType]] =
mixin readValue

# careful: https://github.com/nim-lang/Nim/issues/17085
# TODO why is this even here? one could just return the function pointer
# to the field reader directly instead of going through this seq etc
var tbl {.threadvar.}: ref seq[FieldReader[RecordType, Reader]]
var tbl {.threadvar.}: ref seq[FieldReader[RecordType, ReaderType]]
if tbl == nil:
tbl = new typeof(tbl)
tbl[] = makeFieldReadersTable(RecordType, Reader)
tbl[] = makeFieldReadersTable(RecordType, ReaderType)
return addr(tbl[])

proc findFieldReader*(fieldsTable: FieldReadersTable,
Expand Down Expand Up @@ -342,16 +342,16 @@ proc genCustomSerializationForField(Format, field,

if readBody != nil:
result.add quote do:
type Reader = ReaderType(`Format`)
type ReaderType = Reader(`Format`)
proc readFieldIMPL*(F: type FieldTag[`RecordType`, `fieldName`, auto],
`readerSym`: var Reader): `FieldType`
`readerSym`: var ReaderType): `FieldType`
{.raises: [IOError, SerializationError, Defect].} =
`readBody`

if writeBody != nil:
result.add quote do:
type Writer = WriterType(`Format`)
proc writeFieldIMPL*(`writerSym`: var Writer,
type WriterType = Writer(`Format`)
proc writeFieldIMPL*(`writerSym`: var WriterType,
F: type FieldTag[`RecordType`, `fieldName`, auto],
`valueSym`: auto,
`holderSym`: `RecordType`)
Expand All @@ -364,15 +364,15 @@ proc genCustomSerializationForType(Format, typ: NimNode,

if readBody != nil:
result.add quote do:
type Reader = ReaderType(`Format`)
proc readValue*(`readerSym`: var Reader, T: type `typ`): `typ`
type ReaderType = Reader(`Format`)
proc readValue*(`readerSym`: var ReaderType, T: type `typ`): `typ`
{.raises: [IOError, SerializationError, Defect].} =
`readBody`

if writeBody != nil:
result.add quote do:
type Writer = WriterType(`Format`)
proc writeValue*(`writerSym`: var Writer, `valueSym`: `typ`)
type WriterType = Writer(`Format`)
proc writeValue*(`writerSym`: var WriterType, `valueSym`: `typ`)
{.raises: [IOError, SerializationError, Defect].} =
`writeBody`

Expand Down
10 changes: 5 additions & 5 deletions serialization/testing/generic_suite.nim
Original file line number Diff line number Diff line change
Expand Up @@ -317,14 +317,14 @@ proc executeRoundtripTests*(Format: type) =
roundtrip namedT

proc executeReaderWriterTests*(Format: type) =
mixin init, ReaderType, WriterType
mixin init, Reader, Writer

type
Reader = ReaderType Format
ReaderType = Reader Format

suite(typetraits.name(Format) & " read/write tests"):
test "Low-level field reader test":
let barFields = fieldReadersTable(Bar, Reader)
let barFields = fieldReadersTable(Bar, ReaderType)
var idx = 0

var fieldReader = findFieldReader(barFields[], "b", idx)
Expand All @@ -336,7 +336,7 @@ proc executeReaderWriterTests*(Format: type) =

var bytes = Format.encode("test")
var stream = unsafeMemoryInput(bytes)
var reader = Reader.init(stream)
var reader = ReaderType.init(stream)

var bar: Bar
fieldReader(bar, reader)
Expand All @@ -345,7 +345,7 @@ proc executeReaderWriterTests*(Format: type) =

test "Ignored fields should not be included in the field readers table":
var pos = 0
let bazFields = fieldReadersTable(Baz, Reader)
let bazFields = fieldReadersTable(Baz, ReaderType)
check:
len(bazFields[]) == 2
findFieldReader(bazFields[], "f", pos) != nil
Expand Down

0 comments on commit f9a1121

Please sign in to comment.