Skip to content

Commit

Permalink
More cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinresol committed May 11, 2019
1 parent c978cf2 commit 1e12bb8
Show file tree
Hide file tree
Showing 10 changed files with 331 additions and 524 deletions.
111 changes: 111 additions & 0 deletions src/tink/web/macros/Arguments.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package tink.web.macros;

import haxe.macro.Expr;
import haxe.macro.Type;
import haxe.macro.Context;
import tink.web.macros.Parameters;

using tink.CoreApi;
using tink.MacroApi;
using Lambda;

// hold information extracted from the function argument list
class Arguments {
var list:Array<RouteArg> = [];
static var CONTEXT:Lazy<Type> = Context.getType.bind('tink.web.routing.Context');

public function new(args:Array<{t:Type, opt:Bool, name:String}>, paths:Paths, params:Parameters, pos:Position) {
for(a in args) list.push({
name: a.name,
type: a.t,
optional: a.opt,
kind: switch [a.name, a.t.reduce()] {
case [_, _] if(a.t.unifiesWith(CONTEXT)):
AKSingle(ATContext);
case ['user', _] if(a.name == 'user'):
AKSingle(ATUser(a.t));
case ['body', _.getID() => 'haxe.io.Bytes' | 'String']:
AKSingle(ATParam(PKBody(None)));
case ['query' | 'header' | 'body', t = TAnonymous(_)]:
anon(t, function(name) return ATParam(Parameters.LOCATION_FACTORY[a.name](name)));
case [name, TAnonymous(_.get() => {fields: fields})]:
AKObject([for(field in fields) {
name: field.name,
type: field.type,
target: getArgTarget(paths, params, Drill(name, field.name), a.opt, pos),
}]);
case [name, _]:
AKSingle(getArgTarget(paths, params, Plain(name), a.opt, pos));
}
});
}

public inline function iterator() return list.iterator();

static function getArgTarget(paths:Paths, params:Parameters, access:ArgAccess, optional:Bool, pos:Position) {
return switch [paths.hasCapture(access), params.get(access)] {
case [true, Some(param)]:
param.source.pos.error('`${stringifyArgAccess(access)}` is both captured in path and specified as parameter with @:params(${param.source.toString()})');
case [false, Some(param)]:
ATParam(param.kind);
case [true, None]:
ATCapture;
case [false, None]:
if(!optional) {
// trace(access);
// for(p in params) trace(p.source.toString(), p.access, p.kind);
// for(p in paths) trace(p.parts);
pos.error('`${stringifyArgAccess(access)}` is not used. Please specify its use with the @:params metadata or capture it in the route paths');
} else {
ATCapture;
}
}
}

static function stringifyArgAccess(access:ArgAccess) {
return switch access {
case Plain(name): name;
case Drill(name, field): '$name.$field';
}
}


static function anon(type:Type, factory:String->ArgTarget):ArgKind {
return switch type {
case TAnonymous(_.get() => {fields: fields}):
AKObject([for(field in fields) {
name: field.name,
type: field.type,
target: factory(field.name), // TODO: support meta to alter the native name
}]);
case _:
throw 'unreachable';
}
}
}


typedef RouteArg = {
var name(default, null):String;
var type(default, null):Type;
var optional(default, null):Bool;
var kind(default, null):ArgKind;
}

enum ArgAccess {
Plain(name:String);
Drill(name:String, field:String);
}

enum ArgKind {
AKSingle(target:ArgTarget);
AKObject(fields:Array<{name:String, type:Type, target:ArgTarget}>);
}

enum ArgTarget {
ATContext;
ATUser(type:Type);
ATSession(type:Type);
ATCapture;
ATParam(kind:ParamKind);
}
154 changes: 154 additions & 0 deletions src/tink/web/macros/Parameters.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package tink.web.macros;

import haxe.macro.Expr;
import haxe.macro.Type;
import haxe.macro.Context;
import tink.web.macros.Arguments;

using tink.CoreApi;
using tink.MacroApi;
using Lambda;

// hold information extracted from the @:params metadata
class Parameters {
var params:Array<ParamMapping> = [];

public static var LOCATION_FACTORY = [
'query' => PKQuery,
'body' => function(name) return PKBody(Some(name)),
'header' => PKHeader,
];

public function new(meta:MetaAccess, types:Map<String, Type>) {
for (entry in meta.extract(':params'))
for (p in entry.params) {

function validate(name:String) {
if (reserved(name)) p.reject('`$name` is reserved');
if (!types.exists(name)) p.reject('`$name` does not appear in the function argument list');
}

function hasField(type:Type, name:String) {
return switch type {
case TAnonymous(_.get() => {fields: fields}): fields.exists(function(field) return field.name == name);
case _: false;
}
}

function add(access, kind) params.push({source: p, access: access, kind: kind});

switch p {
case macro $i{name} in $i{pos = 'query' | 'header' | 'body'}:
validate(name);
add(Plain(name), LOCATION_FACTORY[pos](name));

case macro $i{name} = $i{pos = 'query' | 'header' | 'body'}:
validate(name);
switch types[name].reduce() {
case TAnonymous(_.get() => {fields: fields}):
for(field in fields) add(Drill(name, field.name), LOCATION_FACTORY[pos](field.name));
case _:
p.reject('`$name` should be anonymous structure');
}

case macro $i{name} = $i{pos = 'query' | 'header' | 'body'}[$v{(native:String)}]:
validate(name);
add(Plain(name), LOCATION_FACTORY[pos](native));

case macro $i{name}.$field in $i{pos = 'query' | 'header' | 'body'}:
validate(name);
if(!hasField(types[name], field)) p.reject('`$name` does not has field "$field"');
add(Drill(name, field), LOCATION_FACTORY[pos](field));

case macro $i{name}.$field = $i{pos = 'query' | 'header' | 'body'}[$v{(native:String)}]:
validate(name);
if(!hasField(types[name], field)) p.reject('`$name` does not has field "$field"');
add(Drill(name, field), LOCATION_FACTORY[pos](native));

default:
p.reject('Invalid syntax for @:params, only the following are supported:
@:params(<ident> in <query|header|body>)
@:params(<ident> = <query|header|body>)
@:params(<ident> = <query|header|body>["native"])
@:params(<ident.field> in <query|header|body>)
@:params(<ident.field> = <query|header|body>["native"])');

}
}

checkForConflict();
}

public inline function iterator() return params.iterator();

function checkForConflict() {
var checked:Array<ParamMapping> = [];
for(current in params) {
for(prev in checked) {
if(conflictAccess(prev.access, current.access))
current.source.reject('Conflicting argument access with "${prev.source.toString()}"'); // TODO: print the actual enum in a human-friendly way
if(conflictKind(prev.kind, current.kind))
current.source.reject('Conflicting param kind with "${prev.source.toString()}"'); // TODO: print the actual enum in a human-friendly way
checked.push(current);
}
}
}

static function conflictAccess(a1:ArgAccess, a2:ArgAccess) {
return switch [a1, a2] {
case [Plain(n1), Plain(n2)] | [Drill(n1, _), Plain(n2)] | [Plain(n1), Drill(n2, _)]: n1 == n2;
case [Drill(n1, f1), Drill(n2, f2)]: n1 == n2 && f1 == f2;
}
}

static function conflictKind(k1:ParamKind, k2:ParamKind) {
return switch [k1, k2] {
case [PKBody(None), PKBody(None)]: true;
case [PKQuery(n1), PKQuery(n2)]
| [PKHeader(n1), PKHeader(n2)]
| [PKBody(Some(n1)), PKBody(Some(n2))]: n1 == n2;
case _: false;
}
}

public function byName(name:String):Array<ParamMapping> {
return params.filter(function(p) return switch p.access {
case Plain(n) | Drill(n, _): n == name;
});
}

public function get(access:ArgAccess):Option<ParamMapping> {
for(p in params)
switch [access, p.access] {
case [Plain(n1), Plain(n2)] if(n1 == n2): return Some(p);
case [Drill(n1, f1), Drill(n2, f2)] if(n1 == n2 && f1 == f2): return Some(p);
case _:
}
return None;
}

static function reserved(name:String) {
return switch name {
case 'user' | 'query' | 'header' | 'body': true;
case _: false;
}
}
}

typedef ParamMapping = {
source:Expr, // original expr specified in `@:params`
access:ArgAccess,
kind:ParamKind,
}

enum ParamTarget {
PTQuery(name:String);
PTHeader(name:String);
PTBody(name:Option<String>); // None means the entire body
}

enum ParamKind {
PKQuery(name:String);
PKHeader(name:String);
PKBody(name:Option<String>); // None denotes the entire body
}
2 changes: 1 addition & 1 deletion src/tink/web/macros/Paths.hx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import haxe.macro.Expr;
import haxe.macro.Type;
import tink.http.Method;
import tink.url.Portion;
import tink.web.macros.RouteSignature;
import tink.web.macros.Arguments;

using tink.CoreApi;
using tink.MacroApi;
Expand Down
4 changes: 2 additions & 2 deletions src/tink/web/macros/Proxify.hx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import tink.web.macros.Route;
import tink.web.macros.Paths;
import tink.web.macros.Variant;
import tink.web.macros.MimeType;
import tink.web.macros.RouteSignature;
import tink.web.macros.Signature;

using tink.CoreApi;
using tink.MacroApi;
Expand Down Expand Up @@ -97,7 +97,7 @@ class Proxify {
pos: f.field.pos,
name: f.field.name,
kind: FFun({
args: [for (arg in f.signature.args2) switch arg.kind {
args: [for (arg in f.signature.args) switch arg.kind {
case AKSingle(ATUser(_) | ATContext): continue;
case _: { name: arg.name, type: arg.type.toComplex(), opt: arg.optional };
}],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import haxe.macro.Context;
using tink.CoreApi;
using tink.MacroApi;

class RouteResult {
class Result {

var call:Lazy<CallResponse>;
var type:Type;
Expand Down
14 changes: 8 additions & 6 deletions src/tink/web/macros/Route.hx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package tink.web.macros;

import tink.http.Method;
import tink.web.macros.Variant;
import tink.web.macros.RouteSignature;
import tink.web.macros.RouteResult;
import tink.web.macros.Signature;
import tink.web.macros.Arguments;
import tink.web.macros.Parameters;
import tink.web.macros.Result;
import haxe.ds.Option;
import haxe.macro.Type;
import haxe.macro.Expr;
Expand All @@ -21,14 +23,14 @@ class Route {

public var field(default, null):ClassField;
public var kind(default, null):RouteKind;
public var signature(default, null):RouteSignature;
public var signature(default, null):Signature;
public var consumes(default, null):Array<MimeType>;
public var produces(default, null):Array<MimeType>;
public var restricts(default, null):Array<Expr>;

public function new(f, consumes, produces) {
field = f;
signature = new RouteSignature(f);
signature = new Signature(f);
switch [hasCall(f), hasSub(f)] {
case [false, false]:
f.pos.error('No routes on this field'); // should not happen actually
Expand Down Expand Up @@ -81,7 +83,7 @@ class Route {
public function getPayload():Payload {
var payload = [];
var i = 0;
for(arg in signature.args2) {
for(arg in signature.args) {
switch arg.kind {
case AKSingle(ATParam(kind)):
payload.push({id: i++, access: Plain(arg.name), type: arg.type, kind: kind});
Expand Down Expand Up @@ -195,7 +197,7 @@ enum RoutePayload {
}


abstract Payload(Pair<Position, Array<{id:Int, access:ArgAccess, type:Type, kind:ParamKind2}>>) {
abstract Payload(Pair<Position, Array<{id:Int, access:ArgAccess, type:Type, kind:ParamKind}>>) {
public inline function new(pos, arr) this = new Pair(pos, arr);

public function toTypes() {
Expand Down
Loading

0 comments on commit 1e12bb8

Please sign in to comment.