Custom macro haxe.xml.Proxy completion

03.04.2014 2565 0

I use haxe.xml.Proxy for a long time now in order to manage my texts when I write a website.
You can see a good post about it here.
It says if you have an xml file in your project which looks like that for example :

<?xml version="1.0" encoding="utf-8" ?>
	<t id="wrongLogin"><![CDATA[Wrong login or password]]></t>
	<t id="invalidData"><![CDATA[Invalid data]]></t>

And this magic class in your project :

import haxe.ds.StringMap;

class MyTexts extends haxe.xml.Proxy<"bin/texts.xml", String> {
	public static var all	= new StringMap<String>();
	public static var list	= new MyTexts( all.get );

Then you get compiler auto-completion for your strings stored in the xml file like that :

MyTexts.list.	// Here you get propositions for : "wrongLogin" or "invalidData"


That's the compile-time trick, but to make it works at run-time, you have to "fill" the "all" StringMap with the words contained in the xml file, it can be done with a function like that :

static function fill( xml : Xml ) : Void{
	for ( tNode in xml.firstElement().elementsNamed( "t" ) ) {
		all.set( tNode.get( "id" ), tNode.firstChild().nodeValue );

It's nothing hard, but as you can see, here, you have to write this function in your "MyTexts", sotred in your project folder as in all projects' the same. Or you can write a kind of "generic" function that fills your custom class "MyTexts" with the right xml content.
So, 2 separated classes : one common for all projects that hold all my common "Text's" methods and one custom magic class in my project that only holds the path to the compile-time completion from the xml.
Assuming that I have always the same scheme of "localization" files in all my projects, so all my projects' strings are managed the same way with multilingual support, I don't see any reason to have always the same class in all my projects.
I can set in all my projects build's script the path to my xml for compile-time completion like that :

-D texts_path=bin/texts.xml

And using macros, fill for example my "generic" class' instance (this one that holds my common static methods like "fill", "parseBBCode",...) with the id's contained in the xml as class instance's fields.
You can find a good post about using macros to get "completion of everythig" here.
I also want it only on compile-time so I don't want it to be generated, so I'll use the "extern" keyword here : it will only exist at compile-time like that.
So my common "Texts" class can look like that :

#if !macro @:build( ) #end
class Texts{
	public static var list	: Texts;
    public static function init( path : String ) : Void;
    public static function fill( xml : Xml ) : Void;
	public function new() : Void;
	public function resolve( s : String ) : String;

#if !macro
class TextsBuilder {
	macro public static function run() : Array<Field> {
		var path	= Compiler.getDefine( "texts_path" );
		if ( path == null ) {
			Context.error( "Texts path not set. Use -D texts_path=my/path.xml ie.", Context.currentPos() );
		var fields	= Context.getBuildFields();
		var s	= null;
		try {
			s	= File.getContent( path );
		}catch ( e : Dynamic ) {
			Context.error( "Texts path not found : " + path, Context.currentPos() );
		var xml	= Parser.parse( s );
		for ( elt in xml.firstElement().elementsNamed( "t" ) ) {
			var field	= { 
				name	: elt.get( "id" ),
				access	: [ APublic ],
				pos		: Context.currentPos(),
				kind	: FieldType.FVar( ComplexType.TPath( { pack : [],	name : "String", params : [] } ) ),
				meta	: [],
				doc		: haxe.Utf8.decode( elt.firstChild().nodeValue )
			fields.push( field );
		return fields;

The 2 classes can be in the same haxe *.hx file. Thanks to the "extern" keyword too in the !macro context, the TextBuilder class won't exist at run-time.
As we can see, the "run" method only adds xml's id's as fields of the class' instance. I also added here some errors displayed if the project's xml path is wrong or not set, and I added an extra on the "doc" field that is the content, so the real word or sentence appears in the completion .
It's done for the compile-time.


Another good post expains here how have another implementation of the same class at compile-time and run-time.
Here it can look like that :

@:native( "Texts" )
class _Texts{
	public static var list	= new _Texts();
	public static function init( path : String ) {
		#if sys
		var content	= File.getContent( path );
		var content	= Http.requestUrl( path );
		var xml		= Parser.parse( content );
		fill( xml );
	public static function fill( xml : Xml ) {
		for ( tNode in xml.firstElement().elementsNamed( "t" ) ) {
			Reflect.setField( list, tNode.get( "id" ), tNode.firstChild().nodeValue );
	public function new() {}
	public function resolve( s : String ) {
		return Reflect.field( this,s );

All is done with the "native" keyword : At compile-time, this class doesn't interest me, and at run-time, it's called "Texts", so it plays at the place of the upper one.
I also added the ":keep" metadata that force the compiler to "keep" this class in the generated output instead of DCE, since this class is never used as it in our project so never referenced...

All together :

All the compile-time macro and the run-time class can be written in the same haxe *.hx file. One class to manage all your localisation in all your projects .
Here is how use that :

Texts.init( 'texts.xml' );
Texts.list.invalidData;		// <-- You get completion here
Texts.list.resolve( "invalidData" );	// for a "dynamic" access

I've done some benchmark and even if it's not really important, this class is faster than the original haxe.xmlProxy class. I can also note that it takes less memory than the original one.
The reason is mainly that haxe.xml.proxy uses the "resolve" method and is a kind of implementing Dynamic, so it uses the "resolve" method if a field is called onto it without being excplicitly declared...
I tried to make it working using the implement of Dynamic, but if I implement Dynamic, I won't get the xml ids completion anymore, and since _Texts class isn't referenced directly, the compiler doesn't know where it must "generate" a dynamic call to the "resolve" method call.

I'm sure you can reproduce exactly the same behaviour with haxe macros that haxe.xml.Proxy class.
Anyway, this is a simple post that mix macro with special haxe keywords ("keep", "extern" and "native") in order to easily "emulate" haxe.xml.Proxy class and the results are good for me that wrote some monthes ago all the bad I think about "extern" keyword and haxe macros !

-George Washington


Write a comment