diff --git a/cgi.d b/cgi.d index 5741a8b..849b5d7 100644 --- a/cgi.d +++ b/cgi.d @@ -53,6 +53,8 @@ +/ module arsd.cgi; +enum long defaultMaxContentLength = 5_000_000; + /* To do a file download offer in the browser: @@ -110,7 +112,7 @@ int locationOf(T)(T[] data, string item) { /// If you are doing a custom cgi class, mixing this in can take care of /// the required constructors for you mixin template ForwardCgiConstructors() { - this(long maxContentLength = 5_000_000, + this(long maxContentLength = defaultMaxContentLength, string[string] env = null, const(ubyte)[] delegate() readdata = null, void delegate(const(ubyte)[]) _rawDataOutput = null, @@ -208,7 +210,7 @@ class Cgi { CommandLine } /** Initializes it using a CGI or CGI-like interface */ - this(long maxContentLength = 5_000_000, + this(long maxContentLength = defaultMaxContentLength, // use this to override the environment variable listing in string[string] env = null, // and this should return a chunk of data. return empty when done @@ -1613,7 +1615,7 @@ Cgi dummyCgi(Cgi.RequestMethod method = Cgi.RequestMethod.GET, string url = null env["CONTENT_LENGTH"] = to!string(data.length); auto cgi = new Cgi( - 5_000_000, + 0, env, { return data; }, outputSink, @@ -1900,8 +1902,8 @@ string toHex(int num) { // the generic mixins /// Use this instead of writing your own main -mixin template GenericMain(alias fun, T...) { - mixin CustomCgiMain!(Cgi, fun, T); +mixin template GenericMain(alias fun, long maxContentLength = defaultMaxContentLength) { + mixin CustomCgiMain!(Cgi, fun, maxContentLength); } private string simpleHtmlEncode(string s) { @@ -1951,7 +1953,7 @@ bool handleException(Cgi cgi, Throwable t) { } /// If you want to use a subclass of Cgi with generic main, use this mixin. -mixin template CustomCgiMain(CustomCgi, alias fun, T...) if(is(CustomCgi : Cgi)) { +mixin template CustomCgiMain(CustomCgi, alias fun, long maxContentLength = defaultMaxContentLength) if(is(CustomCgi : Cgi)) { // kinda hacky - the T... is passed to Cgi's constructor in standard cgi mode, and ignored elsewhere void main(string[] args) { @@ -2092,7 +2094,7 @@ mixin template CustomCgiMain(CustomCgi, alias fun, T...) if(is(CustomCgi : Cgi)) Cgi cgi; try { - cgi = new CustomCgi(5_000_000, headers, &getScgiChunk, &writeScgi, &flushScgi); + cgi = new CustomCgi(maxContentLength, headers, &getScgiChunk, &writeScgi, &flushScgi); } catch(Throwable t) { sendAll(connection, plainHttpError(true, "400 Bad Request", t)); connection.close(); @@ -2151,7 +2153,7 @@ mixin template CustomCgiMain(CustomCgi, alias fun, T...) if(is(CustomCgi : Cgi)) Cgi cgi; try { - cgi = new CustomCgi(5_000_000, fcgienv, &getFcgiChunk, &writeFcgi, &flushFcgi); + cgi = new CustomCgi(maxContentLength, fcgienv, &getFcgiChunk, &writeFcgi, &flushFcgi); } catch(Throwable t) { FCGX_PutStr(cast(ubyte*) t.msg.ptr, t.msg.length, error); writeFcgi(cast(const(ubyte)[]) plainHttpError(true, "400 Bad Request", t)); @@ -2174,7 +2176,7 @@ mixin template CustomCgiMain(CustomCgi, alias fun, T...) if(is(CustomCgi : Cgi)) // standard CGI is the default version Cgi cgi; try { - cgi = new CustomCgi(T); + cgi = new CustomCgi(maxContentLength); } catch(Throwable t) { stderr.writeln(t.msg); // the real http server will probably handle this; @@ -2257,6 +2259,7 @@ void hackAroundLinkerError() { writeln(typeid(const(immutable(char)[][])[immutable(char)[]])); writeln(typeid(immutable(char)[][][immutable(char)[]])); writeln(typeid(Cgi.UploadedFile[immutable(char)[]])); + writeln(typeid(Cgi.UploadedFile[][immutable(char)[]])); writeln(typeid(immutable(Cgi.UploadedFile)[immutable(char)[]])); writeln(typeid(immutable(Cgi.UploadedFile[])[immutable(char)[]])); writeln(typeid(immutable(char[])[immutable(char)[]])); diff --git a/color.d b/color.d index 95ce208..d9d4e0b 100644 --- a/color.d +++ b/color.d @@ -39,7 +39,7 @@ struct Color { if(a == 255) return format("#%02x%02x%02x", r, g, b); else - return format("rgba(%d, %d, %d, %f)", r, g, b, cast(real)a / 255.0); + return format("rgba(%d, %d, %d, %s)", r, g, b, cast(real)a / 255.0); } string toString() { diff --git a/curl.d b/curl.d index a1cc96e..45ceee7 100644 --- a/curl.d +++ b/curl.d @@ -65,6 +65,7 @@ struct CurlOptions { } */ +//import std.digest.md; import std.md5; import std.file; /// this automatically caches to a local file for the given time. it ignores the expires header in favor of your time to keep. diff --git a/database.d b/database.d index e9a2911..f56a7d5 100644 --- a/database.d +++ b/database.d @@ -167,8 +167,28 @@ class SelectBuilder : SqlBuilder { int limit; int limitStart; + Variant[string] vars; + void setVariable(T)(string name, T value) { + vars[name] = Variant(value); + } + + Database db; + this(Database db = null) { + this.db = db; + } + + /* + It would be nice to put variables right here in the builder + + ?name + + will prolly be the syntax, and we'll do a Variant[string] of them. + + Anything not translated here will of course be in the ending string too + */ + SelectBuilder cloned() { - auto s = new SelectBuilder(); + auto s = new SelectBuilder(this.db); s.fields = this.fields.dup; s.table = this.table; s.joins = this.joins.dup; @@ -178,6 +198,9 @@ class SelectBuilder : SqlBuilder { s.limit = this.limit; s.limitStart = this.limitStart; + foreach(k, v; this.vars) + s.vars[k] = v; + return s; } @@ -247,31 +270,88 @@ class SelectBuilder : SqlBuilder { sql ~= to!string(limit); } - return sql; + if(db is null) + return sql; + + return escapedVariants(db, sql, vars); } } -// /////////////////////////////////////////////////////// +// /////////////////////sql////////////////////////////////// +// used in the internal placeholder thing +string toSql(Database db, Variant a) { + auto v = a.peek!(void*); + if(v && (*v is null)) + return "NULL"; + else { + string str = to!string(a); + return '\'' ~ db.escape(str) ~ '\''; + } + + assert(0); +} + +// just for convenience; "str".toSql(db); +string toSql(string s, Database db) { + if(s is null) + return "NULL"; + return '\'' ~ db.escape(s) ~ '\''; +} + +string toSql(long s, Database db) { + return to!string(s); +} + +string escapedVariants(Database db, in string sql, Variant[string] t) { + if(t.keys.length <= 0 || sql.indexOf("?") == -1) { + return sql; + } + + string fixedup; + int currentStart = 0; +// FIXME: let's make ?? render as ? so we have some escaping capability + foreach(int i, dchar c; sql) { + if(c == '?') { + fixedup ~= sql[currentStart .. i]; + + int idxStart = i + 1; + int idxLength; + + bool isFirst = true; + + while(idxStart + idxLength < sql.length) { + char C = sql[idxStart + idxLength]; + + if((C >= 'a' && C <= 'z') || (C >= 'A' && C <= 'Z') || C == '_' || (!isFirst && C >= '0' && C <= '9')) + idxLength++; + else + break; + + isFirst = false; + } + + auto idx = sql[idxStart .. idxStart + idxLength]; + + if(idx in t) { + fixedup ~= toSql(db, t[idx]); + currentStart = idxStart + idxLength; + } else { + // just leave it there, it might be done on another layer + currentStart = i; + } + } + } + + fixedup ~= sql[currentStart .. $]; + + return fixedup; +} /// Note: ?n params are zero based! string escapedVariants(Database db, in string sql, Variant[] t) { - - string toSql(Variant a) { - auto v = a.peek!(void*); - if(v && (*v is null)) - return "NULL"; - else { - string str = to!string(a); - return '\'' ~ db.escape(str) ~ '\''; - } - - assert(0); - } - - - +// FIXME: let's make ?? render as ? so we have some escaping capability // if nothing to escape or nothing to escape with, don't bother if(t.length > 0 && sql.indexOf("?") != -1) { string fixedup; @@ -298,7 +378,7 @@ string escapedVariants(Database db, in string sql, Variant[] t) { if(idx < 0 || idx >= t.length) throw new Exception("SQL Parameter index is out of bounds: " ~ to!string(idx) ~ " at `"~sql[0 .. i]~"`"); - fixedup ~= toSql(t[idx]); + fixedup ~= toSql(db, t[idx]); } } diff --git a/dom.d b/dom.d index 47db2d8..eeae346 100644 --- a/dom.d +++ b/dom.d @@ -1945,10 +1945,10 @@ class Element { return new ElementStream(this); } - // I moved these from Form because they are generally useful. // Ideally, I'd put them in arsd.html and use UFCS, but that doesn't work with the opDispatch here. /// Tags: HTML, HTML5 + // FIXME: add overloads for other label types... Element addField(string label, string name, string type = "text", FormFieldOptions fieldOptions = FormFieldOptions.none) { auto fs = this; auto i = fs.addChild("label"); @@ -1968,6 +1968,25 @@ class Element { return i; } + Element addField(Element label, string name, string type = "text", FormFieldOptions fieldOptions = FormFieldOptions.none) { + auto fs = this; + auto i = fs.addChild("label"); + i.addChild(label); + Element input; + if(type == "textarea") + input = i.addChild("textarea"). + setAttribute("name", name). + setAttribute("rows", "6"); + else + input = i.addChild("input"). + setAttribute("name", name). + setAttribute("type", type); + + // these are html 5 attributes; you'll have to implement fallbacks elsewhere. In Javascript or maybe I'll add a magic thing to html.d later. + fieldOptions.applyToElement(input); + return i; + } + Element addField(string label, string name, FormFieldOptions fieldOptions) { return addField(label, name, "text", fieldOptions); } diff --git a/email.d b/email.d index 47d52d9..dbadf73 100644 --- a/email.d +++ b/email.d @@ -60,7 +60,7 @@ class EmailMessage { } - string toString() { + override string toString() { string boundary = "0016e64be86203dd36047610926a"; // FIXME assert(!isHtml || (isHtml && isMime)); @@ -137,6 +137,8 @@ class EmailMessage { void send(RelayInfo mailServer = RelayInfo("smtp://localhost")) { auto smtp = new SMTP(mailServer.server); + if(mailServer.username.length) + smtp.setAuthentication(mailServer.username, mailServer.password); const(char)[][] allRecipients = cast(const(char)[][]) (to ~ cc ~ bcc); // WTF cast smtp.mailTo(allRecipients); smtp.mailFrom = from; @@ -145,11 +147,11 @@ class EmailMessage { } } -void email(string to, string subject, string message, string from) { +void email(string to, string subject, string message, string from, RelayInfo mailServer = RelayInfo("smtp://localhost")) { auto msg = new EmailMessage(); msg.from = from; msg.to = [to]; msg.subject = subject; msg.textBody = message; - msg.send(); + msg.send(mailServer); } diff --git a/html.d b/html.d index de7e712..f579073 100644 --- a/html.d +++ b/html.d @@ -317,6 +317,9 @@ Form makePostLink(string href, Element submitButtonContents, string[string] para return makePostLink_impl(href, params, submit); } +import arsd.cgi; +import std.range; + Form makePostLink_impl(string href, string[string] params, Element submitButton) { auto form = require!Form(Element.make("form")); form.method = "POST"; @@ -791,6 +794,8 @@ mixin template opDispatches(R) { The passed code is evaluated lazily. */ + +/+ class ClientSideScript : Element { private Stack!(string*) codes; this(Document par) { @@ -993,6 +998,7 @@ class ClientSideScript : Element { return *v; } } ++/ /* Interesting things with scripts: @@ -1088,6 +1094,7 @@ import std.stdio; import std.json; import std.traits; +/+ string toJavascript(T)(T a) { static if(is(T == ClientSideScript.Variable)) { return a.name; @@ -1197,6 +1204,7 @@ string translateJavascriptSourceWithDToStandardScript(string src)() { return result; } +/ ++/ abstract class CssPart { override string toString() const; diff --git a/mysql.d b/mysql.d index 1f53a70..9e5ee1f 100644 --- a/mysql.d +++ b/mysql.d @@ -691,7 +691,7 @@ string fromCstring(cstring c, int len = -1) { // FIXME: this should work generically with all database types and them moved to database.d -Ret queryOneRow(Ret = Row, DB, T...)(DB db, string sql, T t) if( +Ret queryOneRow(Ret = Row, DB, string file = __FILE__, size_t line = __LINE__, T...)(DB db, string sql, T t) if( (is(DB : Database)) // && (is(Ret == Row) || is(Ret : DataObject))) ) @@ -699,12 +699,12 @@ Ret queryOneRow(Ret = Row, DB, T...)(DB db, string sql, T t) if( static if(is(Ret : DataObject) && is(DB == MySql)) { auto res = db.queryDataObject!Ret(sql, t); if(res.empty) - throw new Exception("result was empty"); + throw new Exception("result was empty", file, line); return res.front; } else static if(is(Ret == Row)) { auto res = db.query(sql, t); if(res.empty) - throw new Exception("result was empty"); + throw new Exception("result was empty", file, line); return res.front; } else static assert(0, "Unsupported single row query return value, " ~ Ret.stringof); } diff --git a/oauth.d b/oauth.d index 7b1ad71..c121b07 100644 --- a/oauth.d +++ b/oauth.d @@ -30,12 +30,13 @@ class FacebookApiException : Exception { import arsd.curl; import arsd.sha; -import std.md5; +import std.digest.md; import std.file; -Variant[string] postToFacebookWall(string[] info, string id, string message, string picture = null, string link = null, long when = 0) { +// note when is a d_time, so unix_timestamp * 1000 +Variant[string] postToFacebookWall(string[] info, string id, string message, string picture = null, string link = null, long when = 0, string linkDescription = null) { string url = "https://graph.facebook.com/" ~ id ~ "/feed"; @@ -46,8 +47,12 @@ Variant[string] postToFacebookWall(string[] info, string id, string message, str data ~= "&picture=" ~ std.uri.encodeComponent(picture); if(link !is null && link.length) data ~= "&link=" ~ std.uri.encodeComponent(link); - if(when) - data ~= "&scheduled_publish_time=" ~ to!string(when); + if(when) { + data ~= "&scheduled_publish_time=" ~ to!string(when / 1000); + data ~= "&published=false"; + } + if(linkDescription.length) + data ~= "&description=" ~ std.uri.encodeComponent(linkDescription); auto response = curl(url, data); diff --git a/web.d b/web.d index 96285f4..f19c526 100644 --- a/web.d +++ b/web.d @@ -1,5 +1,69 @@ module arsd.web; +enum RequirePost; + +/// Attribute for the default formatting (html, table, json, etc) +struct DefaultFormat { + string format; +} + +/// Sets the preferred request method, used by things like other code generators. +/// While this is preferred, the function is still callable from any request method. +/// +/// By default, the preferred method is GET if the name starts with "get" and POST otherwise. +/// +/// See also: RequirePost, ensureGoodPost, and using Cgi.RequestMethod as an attribute +struct PreferredMethod { + Cgi.RequestMethod preferredMethod; +} + +/// With this attribute, the function is only called if the input data's +/// content type is what you specify here. Makes sense for POST and PUT +/// verbs. +struct IfInputContentType { + string contentType; + string dataGoesInWhichArgument; +} + +/** + URL Mapping + + By default, it is the method name OR the method name separated by dashes instead of camel case +*/ + + +/+ + Attributes + + // this is different than calling ensureGoodPost because + // it is only called on direct calls. ensureGoodPost is flow oriented + enum RequirePost; + + // path info? One could be the name of the current function, one could be the stuff past it... + + // Incomplete form handler + + // overrides the getGenericContainer + struct DocumentContainer {} + + // custom formatter for json and other user defined types + + // custom title for the page + + // do we prefill from url? something else? default? + struct Prefill {} + + // btw prefill should also take a function + // perhaps a FormFinalizer + + // for automatic form creation + struct ParameterSuggestions { + string[] suggestions; + bool showDropdown; /* otherwise it is just autocomplete on a text box */ + } + ++/ + // FIXME: if a method has a default value of a non-primitive type, // it's still liable to screw everything else. @@ -233,7 +297,7 @@ string linkCall(alias Func, Args...)(Args args) { /// This function works pretty ok. You're going to want to append a string to the return /// value to actually call .get() or whatever; it only does the name and arglist. string jsCall(alias Func, Args...)(Args args) /*if(is(__traits(parent, Func) : WebDotDBaseType))*/ { - static if(!__traits(compiles, Func(args))) { + static if(!is(typeof(Func(args)))) { //__traits(compiles, Func(args))) { static assert(0, "Your function call doesn't compile. If you need client side dynamic data, try building the call as a string."); } @@ -359,10 +423,12 @@ class ApiProvider : WebDotDBaseType { /// we have to add these things to the document... override void _postProcess(Document document) { - foreach(pp; documentPostProcessors) - pp(document); + if(document !is null) { + foreach(pp; documentPostProcessors) + pp(document); - addCsrfTokens(document); + addCsrfTokens(document); + } super._postProcess(document); } @@ -472,12 +538,13 @@ class ApiProvider : WebDotDBaseType { /// This tentatively redirects the user - depends on the envelope fomat /// You can temporarily disable this using disableRedirects() - void redirect(string location, bool important = false) { + string redirect(string location, bool important = false, string status = null) { if(redirectsSuppressed) - return; + return location; auto f = cgi.request("envelopeFormat", "document"); - if(f == "document" || f == "redirect") - cgi.setResponseLocation(location, important); + if(f == "document" || f == "redirect" || f == "json_enable_redirects") + cgi.setResponseLocation(location, important, status); + return location; } /// Returns a list of links to all functions in this class or sub-classes @@ -899,6 +966,7 @@ immutable(ReflectionInfo*) prepareReflectionImpl(alias PM, alias Parent)(Parent reflection.structs[member] = i; } else static if( is(typeof(__traits(getMember, Class, member)) == function) + && __traits(getProtection, __traits(getMember, Class, member)) == "export" && ( member.length < 5 || @@ -1100,9 +1168,10 @@ CallInfo parseUrl(in ReflectionInfo* reflection, string url, string defaultFunct void run(Provider)(Cgi cgi, Provider instantiation, size_t pathInfoStartingPoint = 0, bool handleAllExceptions = true) if(is(Provider : ApiProvider)) { assert(instantiation !is null); + instantiation.cgi = cgi; + if(instantiation.reflection is null) { instantiation.reflection = prepareReflection!(Provider)(instantiation); - instantiation.cgi = cgi; instantiation._initialize(); // FIXME: what about initializing child objects? } @@ -1354,6 +1423,7 @@ void run(Provider)(Cgi cgi, Provider instantiation, size_t pathInfoStartingPoint cgi.setResponseLocation(redirect, false); break; case "json": + case "json_enable_redirects": // this makes firefox ugly //cgi.setResponseContentType("application/json"); auto json = toJsonValue(result); @@ -1452,6 +1522,8 @@ void run(Provider)(Cgi cgi, Provider instantiation, size_t pathInfoStartingPoint if(realObject !is null) postProcessors ~= &(realObject._postProcess); postProcessors ~= &(base._postProcess); + + // FIXME: cgi is sometimes null in te post processor... wtf foreach(pp; postProcessors) { if(pp in run) continue; @@ -1975,6 +2047,17 @@ JSONValue toJsonValue(T, R = ApiProvider)(T a, string formatToStringAs = null, R val.type = JSON_TYPE.STRING; val.str = a.toString(); } + } else static if(is(T == long)) { + // FIXME: let's get a better range... I think it goes up to like 1 << 50 on positive and negative + // but this works for now + if(a < int.max && a > int.min) { + val.type = JSON_TYPE.INTEGER; + val.integer = to!long(a); + } else { + // WHY? because javascript can't actually store all 64 bit numbers correctly + val.type = JSON_TYPE.STRING; + val.str = to!string(a); + } } else static if(isIntegral!(T)) { val.type = JSON_TYPE.INTEGER; val.integer = to!long(a); @@ -2530,7 +2613,6 @@ string beautify(string name) { -import std.md5; import core.stdc.stdlib; import core.stdc.time; import std.file; @@ -2538,6 +2620,7 @@ import std.file; /// meant to give a generic useful hook for sessions. kinda sucks at this point. /// use the Session class instead. If you just construct it, the sessionId property /// works fine. Don't set any data and it won't save any file. +version(none) deprecated string getSessionId(Cgi cgi) { string token; // FIXME: should this actually be static? it seems wrong if(token is null) { @@ -2551,6 +2634,7 @@ deprecated string getSessionId(Cgi cgi) { } } + import std.md5; return getDigestString(cgi.remoteAddress ~ "\r\n" ~ cgi.userAgent ~ "\r\n" ~ token); } @@ -2591,7 +2675,7 @@ class Session { this._readOnly = readOnly; bool isNew = false; - string token; + // string token; // using a member, see the note below if(cookieParams.name in cgi.cookies && cgi.cookies[cookieParams.name].length) token = cgi.cookies[cookieParams.name]; else { @@ -2725,7 +2809,10 @@ class Session { return token; } - private void setOurCookie(string data) { + // FIXME: hack, see note on member string token + // don't use this, it is meant to be private (...probably) + /*private*/ void setOurCookie(string data) { + this.token = data; if(!_readOnly) cgi.setCookie(cookieParams.name, data, cookieParams.expiresIn, cookieParams.path, cookieParams.host, true, cookieParams.httpsOnly); @@ -2937,6 +3024,8 @@ class Session { private string _sessionId; private Cgi cgi; // used to regenerate cookies, etc. + string token; // this isn't private, but don't use it FIXME this is a hack to allow cross domain session sharing on the same server.... + //private Variant[string] data; /* Variant* opBinary(string op)(string key) if(op == "in") { @@ -2966,6 +3055,7 @@ void setLoginCookie(Cgi cgi, string name, string value) { immutable(string[]) monthNames = [ + null, "January", "February", "March", @@ -2998,11 +3088,14 @@ struct TemplateFilters { // string (string replacement, string[], in Element, string) string date(string replacement, string[], in Element, string) { - auto dateTicks = to!time_t(replacement); - auto date = SysTime( unixTimeToStdTime(dateTicks/1_000) ); + if(replacement.length == 0) + return replacement; + auto dateTicks = to!long(replacement); + auto date = SysTime( unixTimeToStdTime(cast(time_t)(dateTicks/1_000)) ); auto day = date.day; auto year = date.year; + assert(date.month < monthNames.length, to!string(date.month)); auto month = monthNames[date.month]; replacement = format("%s %d, %d", month, day, year); @@ -3079,6 +3172,21 @@ struct TemplateFilters { return s; } + string stringArray(string replacement, string[] args, in Element, string) { + if(replacement.length == 0) + return replacement; + int idx = to!int(replacement); + if(idx < 0 || idx >= args.length) + return replacement; + return args[idx]; + } + + string boolean(string replacement, string[] args, in Element, string) { + if(replacement == "1") + return "yes"; + return "no"; + } + static auto defaultThings() { string delegate(string, string[], in Element, string)[string] pipeFunctions; TemplateFilters filters; @@ -3421,6 +3529,9 @@ Table structToTable(T)(Document document, T s, string[] fieldsToSkip = null) if( /// This adds a custom attribute to links in the document called qsa which modifies the values on the query string void translateQsa(Document document, Cgi cgi, string logicalScriptName = null) { + if(document is null || cgi is null) + return; + if(logicalScriptName is null) logicalScriptName = cgi.logicalScriptName; @@ -4285,6 +4396,44 @@ enum string javascriptBaseImpl = q{ "_wantScriptExecution" : true, }; + +template hasAnnotation(alias f, Attr) { + bool helper() { + foreach(attr; __traits(getAttributes, f)) + static if(is(attr == Attr) || is(typeof(attr) == Attr)) + return true; + return false; + + } + enum bool hasAnnotation = helper; +} + +template hasValueAnnotation(alias f, Attr) { + bool helper() { + foreach(attr; __traits(getAttributes, f)) + static if(is(typeof(attr) == Attr)) + return true; + return false; + + } + enum bool hasValueAnnotation = helper; +} + + + +template getAnnotation(alias f, Attr) if(hasValueAnnotation!(f, Attr)) { + auto helper() { + foreach(attr; __traits(getAttributes, f)) + static if(is(typeof(attr) == Attr)) + return attr; + assert(0); + } + + enum getAnnotation = helper; +} + + + /* Copyright: Adam D. Ruppe, 2010 - 2012 License: Boost License 1.0.