Add initial version of the extension API

This commit is contained in:
R.E.J. Wellner 2021-03-30 17:14:14 +02:00
parent 1d3ebe04cf
commit 1fc6c6171c
4 changed files with 108 additions and 1 deletions

29
EXTENSION_API.md Normal file
View File

@ -0,0 +1,29 @@
This document lays out the API provided for extension authors by bitser.
An extension object is a table that contains at least the following keys:
* `"bitser-type"`: a string, like `"userdata"`. The extension will only be
used to serialize a value `v` if `type(v)` equals the value of this key.
A value of this type is called a "potential match" in the rest of this
document. Note that this type does not need to be natively supported by
bitser.
* `"bitser-match"`: a function that takes a single argument and returns a
boolean. The extension will only be used if this function returns `true`.
It does not need to check `type(v)`, as it will only be called for potential
matches.
* `"bitser-dump"`: a function that takes a single potential match as argument
and returns a single value that bitser is able to serialize (either natively
or through an extension).
* `"bitser-load"`: a function that takes a single argument that is a
deserialized copy of a value previously returned by the dump function, that
returns a potential match.
All other keys will be ignored, but string keys that start with `"bitser-"`
are reserved for future versions of this API.
Extension authors SHOULD NOT call `bitser.registerExtension`. This should be
left to extension users, so they may choose an ID that does not conflict with
other extensions they may be using.
The matching function SHOULD be highly performant, as it is called for every
potential match that is to be serialized.

View File

@ -10,8 +10,10 @@
* [`bitser.includeMetatables`](#includeMetatables)
* [`bitser.register`](#register)
* [`bitser.registerClass`](#registerclass)
* [`bitser.registerExtension`](#registerextension)
* [`bitser.unregister`](#unregister)
* [`bitser.unregisterClass`](#unregisterclass)
* [`bitser.unregisterExtension`](#unregisterextension)
* [`bitser.reserveBuffer`](#reservebuffer)
* [`bitser.clearBuffer`](#clearbuffer)
@ -202,6 +204,24 @@ Returns the registered class as a convenience.
See also: [`bitser.unregisterClass`](#unregisterclass).
## registerExtension
```lua
bitser.registerExtension(extension_id, extension)
```
Registers the extension `extension` and give it the identifier `extension_id`. This is a way to allow 3rd party libraries to
extend the functionality of bitser, for example to be able to serialize exotic data, or to allow certain optimisations.
To implement your own bitser extensions, see
[EXTENSION_API.md](EXTENSION_API.md).
The name `extension_id` must not conflict with that of other extensions you're
using. Strings are recommended, but other primitive types will work.
See also: [`bitser.unregisterExtension`](#unregisterextension).
## unregister
```lua
@ -223,6 +243,16 @@ which is useful in a context where you don't have a reference to the class you w
See also: [`bitser.registerClass`](#registerclass).
## unregisterExtension
```lua
bitser.unregister(extension_id)
```
Deregisters the previously registered extension with the id `extension_id`.
See also: [`bitser.registerExtension`](#registerextension).
## reserveBuffer
```lua

View File

@ -143,6 +143,12 @@ local class_registry = {}
local class_name_registry = {}
local classkey_registry = {}
local class_deserialize_registry = {}
local extension_registry = {}
local extensions_by_type = {}
local EXTENSION_TYPE_KEY = 'bitser-type'
local EXTENSION_MATCH_KEY = 'bitser-match'
local EXTENSION_LOAD_KEY = 'bitser-load'
local EXTENSION_DUMP_KEY = 'bitser-dump'
local serialize_value
@ -276,6 +282,17 @@ serialize_value = function(value, seen)
end
return
end
if not disable_extensions and extensions_by_type[t] then
for extension_id, extension in pairs(extensions_by_type[t]) do
if extension[EXTENSION_MATCH_KEY](value) then
-- extension
Buffer_write_byte(254)
serialize_value(extension_id, seen)
serialize_value(extension[EXTENSION_DUMP_KEY](value), seen)
return
end
end
end
(types[t] or
error("cannot serialize type " .. t)
)(value, seen)
@ -388,6 +405,10 @@ local function deserialize_value(seen)
local read_into = ffi.typeof('$[1]', ctype)()
Buffer_read_raw(read_into, len)
return ctype(read_into[0])
elseif t == 254 then
--extension
local extension_id = deserialize_value(seen)
return extension_registry[extension_id][EXTENSION_LOAD_KEY](deserialize_value(seen))
else
error("unsupported serialized type " .. t)
end
@ -493,4 +514,16 @@ end, unregisterClass = function(name)
classkey_registry[name] = nil
class_deserialize_registry[name] = nil
class_registry[name] = nil
end, registerExtension = function(extension_id, extension)
assert(not extension_registry[extension_id], 'extension with id ' .. extension_id .. ' already registered')
local ty = extension[EXTENSION_TYPE_KEY]
assert(type(ty) == 'string' and type(extension[EXTENSION_MATCH_KEY]) == 'function' and type(extension[EXTENSION_LOAD_KEY]) == 'function' and type(extension[EXTENSION_DUMP_KEY]) == 'function', 'not a valid extension')
extension_registry[extension_id] = extension
if not extensions_by_type[ty] then
extensions_by_type[ty] = {}
end
extensions_by_type[ty][extension_id] = extension
end, unregisterExtension = function(extension_id)
extensions_by_type[extension_registry[extension_id][EXTENSION_TYPE_KEY]][extension_id] = nil
extension_registry[extension_id] = nil
end, reserveBuffer = Buffer_prereserve, clearBuffer = Buffer_clear, version = VERSION}

View File

@ -274,7 +274,6 @@ describe("bitser", function()
assert.has_error(function() bitser.registerClass('Horse', {mane = 'majestic'}) end, "no deserializer given for unsupported class library")
end)
it("cannot deserialize values from unassigned type bytes", function()
assert.has_error(function() bitser.loads("\254") end, "unsupported serialized type 254")
assert.has_error(function() bitser.loads("\255") end, "unsupported serialized type 255")
end)
it("can load from raw data", function()
@ -348,4 +347,20 @@ describe("bitser", function()
assert.is_nil(serdeser(t).bar)
bitser.includeMetatables(true) -- revert back to default for potential other tests
end)
it("provides a simple extension mechanism", function()
local MATCH_CALLS = 0
local LOAD_CALLS = 0
local DUMP_CALLS = 0
bitser.registerExtension('test', {
['bitser-type'] = 'number',
['bitser-match'] = function(value) MATCH_CALLS = MATCH_CALLS + 1; return value > 0 end,
['bitser-load'] = function(value) LOAD_CALLS = LOAD_CALLS + 1; return tonumber(value) end,
['bitser-dump'] = function(value) DUMP_CALLS = DUMP_CALLS + 1; return tostring(value) end })
local t = {1.0, -1.0, 0., -1/0, 'strings should not match'}
test_serdeser(t)
assert.are.same(4, MATCH_CALLS)
assert.are.same(1, LOAD_CALLS)
assert.are.same(1, DUMP_CALLS)
bitser.unregisterExtension('test')
end)
end)