Skip to content

Commit

Permalink
Simplify custom serialization and parsing.
Browse files Browse the repository at this point in the history
  • Loading branch information
back2dos committed May 2, 2018
1 parent 58bf91d commit 7e7ccbe
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 19 deletions.
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,44 @@ var o:{a:Option<Null<Int>>} = {a: Some(1)}
tink.Json.stringify(o) == '{"a":1}'; // true
```

### Custom parsers

Using `@:jsonParse` on a type, you can specify how it should be parsed. The metadata must have exactly on argument:

- a function that consumes the data as it is expected to be found in the JSON document and must produce the type to be parsed.
- a class that must provide an instance method called `parse` with the same signature and also have a constructor that accepts a `tink.json.Parser.BasicParser` - which will reference the parser from which your custom parser is invoked. You can use it's `plugins` field to share state between custom parsers. See the [tink_core documentation](https://haxetink.github.io/#/tink_core) for more details on that.

Example:

```haxe
@:jsonParse(function (json) return new Car(json.speed, json.make));
class Car {
public var speed(default, null):Int;
public var make(default, null):String;
public function new(speed:Int, make:String) {
this.speed = speed;
this.make = make;
}
}
```

**Note**: Imports and usings have no effect on the code in `@:jsonParse`, so you must use fully qualified paths at all times.

### Custom serializers

Similarly to `@:jsonParse`, you can use `@:jsonStringify` on a type to control how it should be parsed. The metadata must have exactly on argument:

- a function that consumes the data to be serialized and produces the data that should be serialized into the final JSON document.
- a class that must provide an instance method called `prepared` with the same signature and also have a constructor that accepts a `tink.json.Writer.BasicWriter`, that again allows you to share state between custom parsers through its `plugins`.

Example:

```haxe
@:jsonStringify(function (car:Car) return { speed: car.speed, make: car.make })
class Car {
// implementation the same as above
}
```

## Performance

Expand Down
8 changes: 8 additions & 0 deletions src/tink/json/macros/CustomRule.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package tink.json.macros;

import haxe.macro.Expr;

enum CustomRule {
WithClass(cls:Expr);
WithFunction(expr:Expr);
}
11 changes: 9 additions & 2 deletions src/tink/json/macros/GenBase.hx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class GenBase {
function processSerialized(pos:Position):Expr
return throw 'abstract';

function processCustom(custom:Expr, original:Type, gen:Type->Expr):Expr
function processCustom(custom:CustomRule, original:Type, gen:Type->Expr):Expr
return throw 'abstract';

public function drive(type:Type, pos:Position, gen:Type->Position->Expr):Expr
Expand All @@ -54,7 +54,14 @@ class GenBase {
}
case v:
switch v[0].extract(customMeta)[0] {
case { params: [custom] }: processCustom(custom, type, drive.bind(_, pos, gen));
case { params: [custom] }:
var rule:CustomRule =
switch custom {
case { expr: EFunction(_, _) }: WithFunction(custom);
case _.typeof().sure().reduce() => TFun(_, _): WithFunction(custom);
default: WithClass(custom);
}
processCustom(rule, type, drive.bind(_, pos, gen));
case v: v.pos.error('@$customMeta must have exactly one parameter');
}
}
Expand Down
20 changes: 14 additions & 6 deletions src/tink/json/macros/GenReader.hx
Original file line number Diff line number Diff line change
Expand Up @@ -355,14 +355,22 @@ class GenReader extends GenBase {
override function processSerialized(pos)
return macro @:pos(pos) this.parseSerialized();

override function processCustom(parser:Expr, original:Type, gen:Type->Expr) {
var path = parser.toString().asTypePath();

override function processCustom(c:CustomRule, original:Type, gen:Type->Expr) {
var original = original.toComplex();

var rep = (macro @:pos(parser.pos) { var f = null; (new $path(null).parse(f()) : $original); f(); }).typeof().sure();

return macro @:pos(parser.pos) this.plugins.get($parser).parse(${gen(rep)});
return switch c {
case WithClass(parser):
var path = parser.toString().asTypePath();

var rep = (macro @:pos(parser.pos) { var f = null; (new $path(null).parse(f()) : $original); f(); }).typeof().sure();

macro @:pos(parser.pos) this.plugins.get($parser).parse(${gen(rep)});
case WithFunction(e):

var rep = (macro @:pos(e.pos) { var f = null; ($e(f()) : $original); f(); }).typeof().sure();

macro @:pos(e.pos) $e(${gen(rep)});
}
}

override function processRepresentation(pos:Position, actual:Type, representation:Type, value:Expr):Expr {
Expand Down
26 changes: 18 additions & 8 deletions src/tink/json/macros/GenWriter.hx
Original file line number Diff line number Diff line change
Expand Up @@ -316,15 +316,25 @@ class GenWriter extends GenBase {
override function processSerialized(pos:Position):Expr
return macro @:pos(pos) this.output(value);

override function processCustom(writer:Expr, original:Type, gen:Type->Expr):Expr {
var path = writer.toString().asTypePath();
override function processCustom(c:CustomRule, original:Type, gen:Type->Expr):Expr {
var original = original.toComplex();
var rep = (macro @:pos(writer.pos) { var f = null; new $path(null).prepare((f():$original)); }).typeof().sure();

return macro @:pos(writer.pos) {
var value = this.plugins.get($writer).prepare(value);
${gen(rep)};
}
return switch c {
case WithClass(writer):
var path = writer.toString().asTypePath();
var rep = (macro @:pos(writer.pos) { var f = null; new $path(null).prepare((f():$original)); }).typeof().sure();

return macro @:pos(writer.pos) {
var value = this.plugins.get($writer).prepare(value);
${gen(rep)};
}
case WithFunction(e):
//TODO: the two cases look suspiciously similar
var rep = (macro @:pos(e.pos) { var f = null; $e((f():$original)); }).typeof().sure();
return macro @:pos(e.pos) {
var value = $e(value);
${gen(rep)};
}
}
}

override public function drive(type:Type, pos:Position, gen:Type->Position->Expr):Expr
Expand Down
2 changes: 1 addition & 1 deletion src/tink/json/macros/Macro.hx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ class Macro {
static function writer(ctx:BuildContext):TypeDefinition {
var name = ctx.name,
ct = ctx.type.toComplex();

var cl = macro class $name extends tink.json.Writer.BasicWriter {
public function new() super();
}
Expand Down
9 changes: 8 additions & 1 deletion tests/ParserTest.hx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,14 @@ class ParserTest {

public function custom() {
var f:Fruit = parse('{"name":"apple","weight":0.2}');
return assert(Std.is(f, Fruit) && f.name == 'apple' && f.weight == .2);
asserts.assert(Std.is(f, Fruit) && f.name == 'apple' && f.weight == .2);
// var r:Rocket = parse('{"alt":100}');
// asserts.assert(r.altitude == 100);
// var r:Rocket2 = parse('[100]');
// asserts.assert(r.altitude == 100);
var r:Rocket3 = parse('{"alt":100}');
asserts.assert(r.altitude == 100);
return asserts.done();
}

public function date() {
Expand Down
11 changes: 10 additions & 1 deletion tests/Types.hx
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,22 @@ class RocketWriter {
}

class RocketWriter2 {
public function new(v:Dynamic) {}
public function new(v:tink.json.Writer.BasicWriter) {}
public function prepare(r:Rocket) {
return VArray([VNumber(r.altitude)]);
}
}

@:jsonParse(function (o) return new Types.Rocket(o.alt))
@:jsonStringify(function (r) return { alt: r.altitude })
@:forward
abstract Rocket3(Rocket) from Rocket to Rocket {
public inline function new(alt) this = new Rocket(alt);
}


@:jsonStringify(Types.RocketWriter2)
@:forward
abstract Rocket2(Rocket) from Rocket to Rocket {
public inline function new(alt) this = new Rocket(alt);
}
Expand Down
1 change: 1 addition & 0 deletions tests/WriterTest.hx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ class WriterTest {

public function custom() {
asserts.assert(stringify(new Rocket(100)) == '{"alt":100}');
asserts.assert(stringify(new Rocket3(100)) == '{"alt":100}');
asserts.assert(stringify(new Rocket2(100)) == '[100]');
return asserts.done();
}
Expand Down

0 comments on commit 7e7ccbe

Please sign in to comment.