Skip to content

Instantly share code, notes, and snippets.

@jdonaldson
Created December 27, 2019 20:02
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jdonaldson/8f35f8c977a7e1722b853e39a9b8ef71 to your computer and use it in GitHub Desktop.
Save jdonaldson/8f35f8c977a7e1722b853e39a9b8ef71 to your computer and use it in GitHub Desktop.
Paths example
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
}
@AlexHaxe
Copy link

AlexHaxe commented Jan 4, 2020

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).

@jdonaldson
Copy link
Author

jdonaldson commented Jan 4, 2020

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)
  // ...
}

@AlexHaxe
Copy link

AlexHaxe commented Jan 4, 2020

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment