-
-
Save jdonaldson/8f35f8c977a7e1722b853e39a9b8ef71 to your computer and use it in GitHub Desktop.
class Main { | |
static function main(){ | |
var router = Paths.buildRouter(Page); | |
var o = router(["Home"]); | |
trace(o + " is the value for o"); | |
var p = router(["Foo","Baz", "1"]); | |
trace(p + " is the value for p"); | |
var q = router(["Scales","Guitar","Chromatic"]); | |
trace(q + " is the value for q"); | |
} | |
} | |
enum abstract Bar(String) from String to String { | |
var Baz ='heeey'; | |
var Bing='hii'; | |
var Boo = 'hooo'; | |
} | |
enum Page { | |
Home; | |
Foo(bar : Bar, val : Int); | |
Scales(instrument:GuitarMember, scale:Scale); | |
Intervals(instrument:GuitarMember, scale:Scale, key:Note); | |
ChordProgressionPage(instrument:GuitarMember, scale:Scale, key:Note, highlighted:Scale); | |
SuspendedChordsPage(instrument:GuitarMember, scale:Scale, key:Note, highlighted:Scale); | |
PowerChordsPage(instrument:GuitarMember, scale:Scale, key:Note, highlighted:Scale); | |
ScaleNotesPage(instrument:GuitarMember, scale:Scale, key:Note); | |
ChordNotesPage(instrument:GuitarMember, scale:Scale, key:Note); | |
NoteOverviewPage(instrument:GuitarMember, key:Note); | |
} | |
@:enum abstract GuitarMember(String) from String to String { | |
var Guitar = "guitar"; | |
var Ukulele = "ukulele"; | |
var BassGuitar = "bass-guitar"; | |
var Banjo = "banjo"; | |
var Mandolin = "mandolin"; | |
} | |
@:enum abstract Scale(String) from String to String { | |
var Chromatic = "chromatic"; | |
var NaturalMinor = "natural-minor"; | |
var NaturalMajor = "natural-major"; | |
var MinorPentatonic = "minor-pentatonic"; | |
var MajorPentatonic = "major-pentatonic"; | |
var MelodicMinor = "melodic-minor"; | |
var HarmonicMinor = "harmonic-minor"; | |
var Blues = "blues"; | |
var Ionian = "ionian"; | |
var Dorian = "dorian"; | |
var Phygian = "phygian"; | |
var Lydian = "lydian"; | |
var Mixolydian = "mixolydian"; | |
var Aeolian = "aeolian"; | |
var Locrian = "locrian"; | |
} | |
@:enum abstract Note(String) from String to String { | |
/* 1 */ var C = "e"; | |
/* 2 */ var CSharp = "c-sharp"; | |
/* 3 */ var D = "d"; | |
/* 4 */ var DSharp = "d-sharp"; | |
/* 5 */ var E = "e"; | |
/* 6 */ var F = "f"; | |
/* 7 */ var FSharp = "f-sharp"; | |
/* 8 */ var G = "g"; | |
/* 9 */ var GSharp = "g-sharp"; | |
/* 10 */ var A = "a"; | |
/* 11 */ var ASharp = "a-sharp"; | |
/* 12 */ var B = "b"; | |
} |
Main.hx|7 info| Home is the value for o | |
Main.hx|10 info| Foo(heeey,1) is the value for p | |
Main.hx|13 info| Scales(guitar,chromatic) is the value for q |
#if macro | |
import haxe.macro.Expr; | |
import haxe.macro.Type; | |
import haxe.macro.Context; | |
import haxe.macro.Context.currentPos as pos; | |
import haxe.macro.Context.followWithAbstracts as follow; | |
#end | |
class Paths { | |
macro public static function buildRouter(enm : Expr) : Expr { | |
return macro function(paths : Array<String>) { | |
return ${buildSwitchExpr(enm)} | |
}; | |
} | |
#if macro | |
public static function buildExprFromType(type : Type, idx = 0) : Expr { | |
return switch type { | |
case TEnum(_,_) : buildSwitchFromType(type); | |
case TAbstract(abs, []) if (abs.get().module == "StdTypes") : { | |
switch (abs.get().name) { | |
case "String" : macro paths.shift(); | |
case "Int" : macro Std.parseInt(paths.shift()); | |
case "Float" : macro Std.parseFloat(paths.shift()); | |
default : macro null; | |
} | |
} | |
case TAbstract(abs, []) : { | |
var impl = abs.get().impl.get(); | |
buildSwitchFromAbsImpl(impl); | |
} | |
default : macro null; | |
} | |
} | |
public static function buildCaseFromEnumField(enmf: EnumField) : Case { | |
return switch enmf.type { | |
case TEnum(enm, []) : { | |
return { | |
values : [macro $v{enmf.name}], | |
expr : macro $i{enmf.name} | |
} | |
} | |
case TFun(args, ret) : { | |
var arg_exprs = Lambda.map(args, a->{ | |
return buildExprFromType(a.t); | |
}); | |
return { | |
values : [macro $v{enmf.name}], | |
expr : macro $i{enmf.name}($a{arg_exprs}) | |
} | |
} | |
default : null; | |
}; | |
} | |
public static function buildSwitchFromAbsImpl(impl : ClassType ) : Expr { | |
var cases = Lambda.map(impl.statics.get(), s->{ | |
return { | |
values : [macro $v{s.name}], | |
expr : macro $i{s.name}, | |
guard : null | |
}; | |
}); | |
cases.push({ | |
values : [macro _], | |
expr : macro null, | |
guard : null | |
}); | |
return { | |
expr : ESwitch(macro paths.shift(), cases, null), | |
pos : Context.currentPos() | |
}; | |
} | |
public static function buildSwitchFromType(type : Type, idx = 0) : Expr { | |
return switch type { | |
case TEnum(enm, []) : { | |
var enme = enm.get(); | |
var cases : Array<Case> = Lambda.map(enme.constructs, c-> { | |
return buildCaseFromEnumField(enme.constructs.get(c.name)); | |
}); | |
cases.push({ | |
values : [macro _], | |
expr : macro null | |
}); | |
return { | |
expr : ESwitch( macro paths.shift(), cases, null), | |
pos : Context.currentPos() | |
} | |
} | |
default : macro null; | |
} | |
} | |
public static function buildSwitchExpr(expr : Expr , idx = 0) : Expr { | |
return switch expr.expr{ | |
case EConst(CIdent(val)) : buildSwitchFromType(Context.getType(val)); | |
case _: throw new Error("not an enum", Context.currentPos()); | |
} | |
} | |
#end | |
} | |
That's great, thanks for the ideas! I put my latest efforts up on a wip repo : https://github.com/jdonaldson/paths
Reversing the enum to a path is exactly what I was going to do next. I'll give a shot at merging everything soon.
FWIW, I feel pretty good about this ADT parsing approach for handling basic paths. However, I've run into some issues when trying to handle parameters. I can parse a parameter string ( e.g. ?option=1&foo=bar&baz=2
) into a given structural type ({option:Int, foo:String, baz:Int}
). That's no problem. Also, Haxe pattern matching + extractors seem like a great way of checking/ensuring different fields are present/typed:
switch params {
case {option : n} : {
trace(n);
}
}
I had in mind that you could switch across 3 different relevant components to define your controller logic (path, parameters, and headers). This could be done using Haxe's switchable array notation:
switch [path, params, headers] {
case [Home, {option : opt}, GET(cookie)] : HomeControllerLogic(opt, cookie.user_id)
// ...
}
I hit a snag with parameters though. Switching on a structural type means that you would need to define that type beforehand. The structural types needed to encode params will likely differ between paths, and so I wound up creating different parameter structural types for each path, and then wrapping it up in another enum. That winds up feeling cumbersome, I'm trying to think of a different way of doing it.
switch [path, params, headers] {
case [Home, HomeParams({option : opt}), GET(cookie)] : HomeControllerLogic(opt, cookie.user_id)
case [Foo, FooParams({bar :n}), GET(cookie)] if n > 10: FooControllerLogic(n, cookie.user_id)
// ...
}
Having a path enum, a parameter enum and object literals seems a bit much. On the other hand you might have a lot of optional parameters which will look odd when converted back to a path (e.g, Home///
or Home/null/null/
).
In my project I only put a few identifying parameters into my Page enum, I didn't go for all parameters. E.g. some pages have filter and sorting features, and they still use regular GET parameters - I might add them at some point.
I'll keep an eye on your repository!
I tried using your routing macro in a project in the last few days. It works great, though I had to make a few adjustments (see https://gist.github.com/AlexHaxe/7a2618835b3b24d10ce7bc103f2e20dd).
I also needed a way to reverse an enum back to a path, so I integrated your
toPath
macro (also with adjustments).I've added a few testcases and a
@catchrest
meta so that you can have one case that matches all unmatched paths (e.g. because they come from a database or someplace else).