module arsd.web; /* FIXME: in params on the wrapped functions generally don't work Running from the command line: ./myapp function positional args.... ./myapp --format=json function _GET _POST _PUT _DELETE ./myapp --make-nested-call Procedural vs Object Oriented right now it is procedural: root/function root/module/function what about an object approach: root/object root/class/object static ApiProvider.getObject Formatting data: CoolApi.myFunc().getFormat('Element', [...same as get...]); You should also be able to ask for json, but with a particular format available as toString format("json", "html") -- gets json, but each object has it's own toString. Actually, the object adds a member called formattedSecondarily that is the other thing. Note: the array itself cannot be changed in format, only it's members. Note: the literal string of the formatted object is often returned. This may more than double the bandwidth of the call Note: BUG: it only works with built in formats right now when doing secondary // formats are: text, html, json, table, and xml // except json, they are all represented as strings in json values string toString -> formatting as text Element makeHtmlElement -> making it html (same as fragment) JSONValue makeJsonValue -> formatting to json Table makeHtmlTable -> making a table (not implemented) toXml -> making it into an xml document Arrays can be handled too: static (converts to) string makeHtmlArray(typeof(this)[] arr); Envelope format: document (default), json, none */ public import arsd.dom; public import arsd.cgi; // you have to import this in the actual usage file or else it won't link; surely a compiler bug import arsd.sha; public import std.string; public import std.array; public import std.stdio : writefln; public import std.conv; import std.random; public import std.range; public import std.traits; import std.json; struct Envelope { bool success; string type; string errorMessage; string userData; JSONValue result; // use result.str if the format was anything other than json debug string dFullString; } string linkTo(alias func, T...)(T args) { auto reflection = __traits(parent, func).reflection; assert(reflection !is null); auto name = func.stringof; int idx = name.indexOf("("); if(idx != -1) name = name[0 .. idx]; auto funinfo = reflection.functions[name]; return funinfo.originalName; } /// Everything should derive from this instead of the old struct namespace used before /// Your class must provide a default constructor. class ApiProvider { Cgi cgi; static immutable(ReflectionInfo)* reflection; string _baseUrl; // filled based on where this is called from on this request /// Override this if you have initialization work that must be done *after* cgi and reflection is ready. /// It should be used instead of the constructor for most work. void _initialize() {} /// This one is called at least once per call. (_initialize is only called once per process) void _initializePerCall() {} /// Override this if you want to do something special to the document void _postProcess(Document document) {} /// This tentatively redirects the user - depends on the envelope fomat void redirect(string location) { if(cgi.request("envelopeFormat", "document") == "document") cgi.setResponseLocation(location, false); } Element _sitemap() { auto container = _getGenericContainer(); auto list = container.addChild("ul"); string[string] handled; foreach(func; reflection.functions) { if(func.originalName in handled) continue; handled[func.originalName] = func.originalName; list.addChild("li", new Link(_baseUrl ~ "/" ~ func.name, beautify(func.originalName))); } return list.parentNode.removeChild(list); } Document _defaultPage() { throw new Exception("no default"); return null; } Element _getGenericContainer() out(ret) { assert(ret !is null); } body { auto document = new Document(""); auto container = document.getElementById("body"); return container; } void _catchAll(string path) { throw new NoSuchPageException(_errorMessageForCatchAll); } private string _errorMessageForCatchAll; private void _catchallEntry(string path, string funName, string errorMessage) { if(!errorMessage.length) { string allFuncs, allObjs; foreach(n, f; reflection.functions) allFuncs ~= n ~ "\n"; foreach(n, f; reflection.objects) allObjs ~= n ~ "\n"; errorMessage = "no such function " ~ funName ~ "\n functions are:\n" ~ allFuncs ~ "\n\nObjects are:\n" ~ allObjs; } _errorMessageForCatchAll = errorMessage; } /// When in website mode, you can use this to beautify the error message Document delegate(Throwable) _errorFunction; } class ApiObject { /* abstract this(ApiProvider parent, string identifier) */ } struct ReflectionInfo { FunctionInfo[string] functions; EnumInfo[string] enums; StructInfo[string] structs; const(ReflectionInfo)*[string] objects; bool needsInstantiation; // the overall namespace string name; // this is also used as the object name in the JS api string defaultOutputFormat = "html"; int versionOfOutputFormat = 2; // change this in your constructor if you still need the (deprecated) old behavior // bool apiMode = false; // no longer used - if format is json, apiMode behavior is assumed. if format is html, it is not. // FIXME: what if you want the data formatted server side, but still in a json envelope? // should add format-payload: } struct EnumInfo { string name; int[] values; string[] names; } struct StructInfo { string name; // a struct is sort of like a function constructor... StructMemberInfo[] members; } struct StructMemberInfo { string name; string staticType; string defaultValue; } struct FunctionInfo { WrapperFunction dispatcher; JSONValue delegate(Cgi cgi, in string[][string] sargs) documentDispatcher; // should I also offer dispatchers for other formats like Variant[]? string name; string originalName; //string uriPath; Parameter[] parameters; string returnType; bool returnTypeIsDocument; Document function(in string[string] args) createForm; } struct Parameter { string name; string value; string type; string staticType; string validator; // for radio and select boxes string[] options; string[] optionValues; } string makeJavascriptApi(const ReflectionInfo* mod, string base) { assert(mod !is null); string script = `var `~mod.name~` = { "_apiBase":'`~base~`',`; script ~= javascriptBase; script ~= "\n\t"; bool[string] alreadyDone; bool outp = false; foreach(s; mod.enums) { if(outp) script ~= ",\n\t"; else outp = true; script ~= "'"~s.name~"': {\n"; bool outp2 = false; foreach(i, n; s.names) { if(outp2) script ~= ",\n"; else outp2 = true; // auto v = s.values[i]; auto v = "'" ~ n ~ "'"; // we actually want to use the name here because to!enum() uses member name. script ~= "\t\t'"~n~"':" ~ to!string(v); } script ~= "\n\t}"; } foreach(s; mod.structs) { if(outp) script ~= ",\n\t"; else outp = true; script ~= "'"~s.name~"': function("; bool outp2 = false; foreach(n; s.members) { if(outp2) script ~= ", "; else outp2 = true; script ~= n.name; } script ~= ") { return {\n"; outp2 = false; script ~= "\t\t'_arsdTypeOf':'"~s.name~"'"; if(s.members.length) script ~= ","; script ~= " // metadata, ought to be read only\n"; // outp2 is still false because I put the comma above foreach(n; s.members) { if(outp2) script ~= ",\n"; else outp2 = true; auto v = n.defaultValue; script ~= "\t\t'"~n.name~"': (typeof "~n.name~" == 'undefined') ? "~n.name~" : '" ~ to!string(v) ~ "'"; } script ~= "\n\t}; }"; } // FIXME: it should output the classes too /* foreach(obj; mod.objects) { if(outp) script ~= ",\n\t"; else outp = true; script ~= makeJavascriptApi(obj, base); } */ foreach(func; mod.functions) { if(func.originalName in alreadyDone) continue; // there's url friendly and code friendly, only need one alreadyDone[func.originalName] = true; if(outp) script ~= ",\n\t"; else outp = true; string args; string obj; bool outputted = false; foreach(i, arg; func.parameters) { if(outputted) { args ~= ","; obj ~= ","; } else outputted = true; args ~= arg.name; // FIXME: we could probably do better checks here too like on type obj ~= `'`~arg.name~`':(typeof `~arg.name ~ ` == "undefined" ? this._raiseError('InsufficientParametersException', '`~func.originalName~`: argument `~to!string(i) ~ " (" ~ arg.staticType~` `~arg.name~`) is not present') : `~arg.name~`)`; } /* if(outputted) args ~= ","; args ~= "callback"; */ script ~= `'` ~ func.originalName ~ `'`; script ~= ":"; script ~= `function(`~args~`) {`; if(obj.length) script ~= ` var argumentsObject = { `~obj~` }; return this._serverCall('`~func.name~`', argumentsObject, '`~func.returnType~`');`; else script ~= ` return this._serverCall('`~func.name~`', null, '`~func.returnType~`');`; script ~= ` }`; } script ~= "\n}"; // some global stuff to put in script ~= ` if(typeof arsdGlobalStuffLoadedForWebDotD == "undefined") { arsdGlobalStuffLoadedForWebDotD = true; var oldObjectDotPrototypeDotToString = Object.prototype.toString; Object.prototype.toString = function() { if(this.formattedSecondarily) return this.formattedSecondarily; return oldObjectDotPrototypeDotToString.call(this); } } `; return script; } template isEnum(alias T) if(is(T)) { static if (is(T == enum)) enum bool isEnum = true; else enum bool isEnum = false; } // WTF, shouldn't is(T == xxx) already do this? template isEnum(T) if(!is(T)) { enum bool isEnum = false; } template isStruct(alias T) if(is(T)) { static if (is(T == struct)) enum bool isStruct = true; else enum bool isStruct = false; } // WTF template isStruct(T) if(!is(T)) { enum bool isStruct = false; } template isApiObject(alias T) if(is(T)) { static if (is(T : ApiObject)) enum bool isApiObject = true; else enum bool isApiObject = false; } // WTF template isApiObject(T) if(!is(T)) { enum bool isApiObject = false; } template isApiProvider(alias T) if(is(T)) { static if (is(T : ApiProvider)) enum bool isApiProvider = true; else enum bool isApiProvider = false; } // WTF template isApiProvider(T) if(!is(T)) { enum bool isApiProvider = false; } template Passthrough(T) { T Passthrough; } template PassthroughType(T) { alias T PassthroughType; } auto generateGetter(PM, Parent, string member, alias hackToEnsureMultipleFunctionsWithTheSameSignatureGetTheirOwnInstantiations)(string io, Parent instantiation) { static if(is(PM : ApiObject)) { auto i = new PM(instantiation, io); return &__traits(getMember, i, member); } else { return &__traits(getMember, instantiation, member); } } immutable(ReflectionInfo*) prepareReflection(alias PM)(Cgi cgi, PM instantiation, ApiObject delegate(string) instantiateObject = null) if(is(PM : ApiProvider) || is(PM: ApiObject) ) { return prepareReflectionImpl!(PM, PM)(cgi, instantiation, instantiateObject); } immutable(ReflectionInfo*) prepareReflectionImpl(alias PM, alias Parent)(Cgi cgi, Parent instantiation, ApiObject delegate(string) instantiateObject = null) if((is(PM : ApiProvider) || is(PM: ApiObject)) && is(Parent : ApiProvider) ) { assert(instantiation !is null); ReflectionInfo* reflection = new ReflectionInfo; reflection.name = PM.stringof; static if(is(PM: ApiObject)) reflection.needsInstantiation = true; // derivedMembers is changed from allMembers foreach(member; __traits(derivedMembers, PM)) { // FIXME: the filthiest of all hacks... static if(!__traits(compiles, !is(typeof(__traits(getMember, PM, member)) == function) && isEnum!(__traits(getMember, PM, member)))) continue; // must be a data member or something... else // DONE WITH FILTHIEST OF ALL HACKS //if(member.length == 0) // continue; static if( !is(typeof(__traits(getMember, PM, member)) == function) && isEnum!(__traits(getMember, PM, member)) && member[0] != '_' ) { EnumInfo i; i.name = member; foreach(m; __traits(allMembers, __traits(getMember, PM, member))) { i.names ~= m; i.values ~= cast(int) __traits(getMember, __traits(getMember, PM, member), m); } reflection.enums[member] = i; } else static if( !is(typeof(__traits(getMember, PM, member)) == function) && isStruct!(__traits(getMember, PM, member)) && member[0] != '_' ) { StructInfo i; i.name = member; typeof(Passthrough!(__traits(getMember, PM, member))) s; foreach(idx, m; s.tupleof) { StructMemberInfo mem; mem.name = s.tupleof[idx].stringof[2..$]; mem.staticType = typeof(m).stringof; mem.defaultValue = null; // FIXME i.members ~= mem; } reflection.structs[member] = i; } else static if( is(typeof(__traits(getMember, PM, member)) == function) && ( member[0] != '_' && ( member.length < 5 || ( member[$ - 5 .. $] != "_Page" && member[$ - 5 .. $] != "_Form") && !(member.length > 16 && member[$ - 16 .. $] == "_PermissionCheck") ))) { FunctionInfo f; ParameterTypeTuple!(__traits(getMember, PM, member)) fargs; f.returnType = ReturnType!(__traits(getMember, PM, member)).stringof; f.returnTypeIsDocument = is(ReturnType!(__traits(getMember, PM, member)) : Document); f.name = toUrlName(member); f.originalName = member; assert(instantiation !is null); f.dispatcher = generateWrapper!( generateGetter!(PM, Parent, member, __traits(getMember, PM, member)), __traits(getMember, PM, member), Parent, member )(reflection, instantiation); //f.uriPath = f.originalName; auto names = parameterNamesOf!(__traits(getMember, PM, member)); foreach(idx, param; fargs) { Parameter p; p.name = names[idx]; p.staticType = typeof(fargs[idx]).stringof; static if( is( typeof(param) == enum )) { p.type = "select"; foreach(opt; __traits(allMembers, typeof(param))) { p.options ~= opt; p.optionValues ~= to!string(__traits(getMember, param, opt)); } } else static if (is(typeof(param) == bool)) { p.type = "checkbox"; } else static if (is(Unqual!(typeof(param)) == Cgi.UploadedFile)) { p.type = "file"; } else { if(p.name.toLower.indexOf("password") != -1) // hack to support common naming convention p.type = "password"; else p.type = "text"; } f.parameters ~= p; } static if(__traits(hasMember, PM, member ~ "_Form")) { f.createForm = &__traits(getMember, PM, member ~ "_Form"); } reflection.functions[f.name] = f; // also offer the original name if it doesn't // conflict //if(f.originalName !in reflection.functions) reflection.functions[f.originalName] = f; } else static if( !is(typeof(__traits(getMember, PM, member)) == function) && isApiObject!(__traits(getMember, PM, member)) && member[0] != '_' ) { reflection.objects[member] = prepareReflectionImpl!( __traits(getMember, PM, member), Parent) (cgi, instantiation); } else static if( // child ApiProviders are like child modules !is(typeof(__traits(getMember, PM, member)) == function) && isApiProvider!(__traits(getMember, PM, member)) && member[0] != '_' ) { PassthroughType!(__traits(getMember, PM, member)) i; i = new typeof(i)(); auto r = prepareReflection!(__traits(getMember, PM, member))(cgi, i); reflection.objects[member] = r; if(toLower(member) !in reflection.objects) // web filenames are often lowercase too reflection.objects[member.toLower] = r; } } static if(is(PM: ApiProvider)) { instantiation.cgi = cgi; instantiation.reflection = cast(immutable) reflection; instantiation._initialize(); } return cast(immutable) reflection; } void run(Provider)(Cgi cgi, Provider instantiation, int pathInfoStartingPoint = 0) if(is(Provider : ApiProvider)) { assert(instantiation !is null); immutable(ReflectionInfo)* reflection; if(instantiation.reflection is null) prepareReflection!(Provider)(cgi, instantiation); reflection = instantiation.reflection; instantiation._baseUrl = cgi.scriptName ~ cgi.pathInfo[0 .. pathInfoStartingPoint]; if(cgi.pathInfo[pathInfoStartingPoint .. $].length <= 1) { auto document = instantiation._defaultPage(); if(document !is null) { instantiation._postProcess(document); cgi.write(document.toString()); } cgi.close(); return; } string funName = cgi.pathInfo[pathInfoStartingPoint + 1..$]; // kinda a hack, but this kind of thing should be available anyway if(funName == "functions.js") { cgi.setResponseContentType("text/javascript"); cgi.write(makeJavascriptApi(reflection, replace(cast(string) cgi.requestUri, "functions.js", ""))); cgi.close(); return; } instantiation._initializePerCall(); // what about some built in functions? /* // Basic integer operations builtin.opAdd builtin.opSub builtin.opMul builtin.opDiv // Basic array operations builtin.opConcat // use to combine calls easily builtin.opIndex builtin.opSlice builtin.length // Basic floating point operations builtin.round builtin.floor builtin.ceil // Basic object operations builtin.getMember // Basic functional operations builtin.filter // use to slice down on stuff to transfer builtin.map // call a server function on a whole array builtin.reduce // Access to the html items builtin.getAutomaticForm(method) */ const(FunctionInfo)* fun; auto envelopeFormat = cgi.request("envelopeFormat", "document"); Envelope result; result.userData = cgi.request("passedThroughUserData"); string instantiator; string objectName; try { // Built-ins string errorMessage; if(funName.length > 8 && funName[0..8] == "builtin.") { funName = funName[8..$]; switch(funName) { default: assert(0); case "getAutomaticForm": auto mfun = new FunctionInfo; mfun.returnType = "Form"; mfun.dispatcher = delegate JSONValue (Cgi cgi, string, in string[][string] sargs, in string format, in string secondaryFormat = null) { auto rfun = cgi.request("method") in reflection.functions; if(rfun is null) throw new NoSuchPageException("no such function " ~ cgi.request("method")); auto form = createAutomaticForm(new Document, *rfun); auto idx = cgi.requestUri.indexOf("builtin.getAutomaticForm"); form.action = cgi.requestUri[0 .. idx] ~ form.action; // make sure it works across the site JSONValue v; v.type = JSON_TYPE.STRING; v.str = form.toString(); return v; }; fun = cast(immutable) mfun; break; } } else { // User-defined // FIXME: modules? should be done with dots since slashes is used for api objects fun = funName in reflection.functions; if(fun is null) { auto parts = funName.split("/"); const(ReflectionInfo)* currentReflection = reflection; if(parts.length > 1) while(parts.length) { if(parts.length > 1) { objectName = parts[0]; auto object = objectName in reflection.objects; if(object is null) { // || object.instantiate is null) errorMessage = "no such object: " ~ objectName; goto noSuchFunction; } currentReflection = *object; if(!currentReflection.needsInstantiation) { parts = parts[1 .. $]; continue; } auto objectIdentifier = parts[1]; instantiator = objectIdentifier; //obj = object.instantiate(objectIdentifier); parts = parts[2 .. $]; if(parts.length == 0) { // gotta run the default function fun = (to!string(cgi.requestMethod)) in currentReflection.functions; } } else { fun = parts[0] in currentReflection.functions; if(fun is null) errorMessage = "no such method in class "~objectName~": " ~ parts[0]; parts = parts[1 .. $]; } } } } if(fun is null) { noSuchFunction: instantiation._catchallEntry( cgi.pathInfo[pathInfoStartingPoint + 1..$], funName, errorMessage); } assert(fun !is null); assert(fun.dispatcher !is null); assert(cgi !is null); result.type = fun.returnType; string format = cgi.request("format", reflection.defaultOutputFormat); string secondaryFormat = cgi.request("secondaryFormat", ""); if(secondaryFormat.length == 0) secondaryFormat = null; JSONValue res; if(envelopeFormat == "document" && fun.documentDispatcher !is null) { res = fun.documentDispatcher(cgi, cgi.requestMethod == Cgi.RequestMethod.POST ? cgi.postArray : cgi.getArray); envelopeFormat = "html"; } else res = fun.dispatcher(cgi, instantiator, cgi.requestMethod == Cgi.RequestMethod.POST ? cgi.postArray : cgi.getArray, format, secondaryFormat); //if(cgi) // cgi.setResponseContentType("application/json"); result.success = true; result.result = res; } catch (Throwable e) { result.success = false; result.errorMessage = e.msg; result.type = e.classinfo.name; debug result.dFullString = e.toString(); if(envelopeFormat == "document" || envelopeFormat == "html") { auto ipe = cast(InsufficientParametersException) e; if(ipe !is null) { assert(fun !is null); Form form; if(0 || fun.createForm !is null) { // FIXME: if 0 // go ahead and use it to make the form page auto doc = fun.createForm(cgi.requestMethod == Cgi.RequestMethod.POST ? cgi.post : cgi.get); } else { Parameter[] params = fun.parameters.dup; foreach(i, p; fun.parameters) { string value = ""; if(p.name in cgi.get) value = cgi.get[p.name]; if(p.name in cgi.post) value = cgi.post[p.name]; params[i].value = value; } form = createAutomaticForm(new Document, *fun);// params, beautify(fun.originalName)); foreach(k, v; cgi.get) form.setValue(k, v); form.setValue("envelopeFormat", envelopeFormat); auto n = form.getElementById("function-name"); if(n) n.innerText = beautify(fun.originalName); } assert(form !is null); result.result.str = form.toString(); } else { if(instantiation._errorFunction !is null) { auto document = instantiation._errorFunction(e); if(document is null) goto gotnull; result.result.str = (document.toString()); } else { gotnull: auto document = new Document; auto code = document.createElement("pre"); code.innerText = e.toString(); result.result.str = (code.toString()); } } } } finally { switch(envelopeFormat) { case "redirect": auto redirect = cgi.request("_arsd_redirect_location", cgi.referrer); // FIXME: is this safe? it'd make XSS super easy // add result to url if(!result.success) goto case "none"; cgi.setResponseLocation(redirect, false); break; case "json": // this makes firefox ugly //cgi.setResponseContentType("application/json"); auto json = toJsonValue(result); cgi.write(toJSON(&json)); break; case "none": cgi.setResponseContentType("text/plain"); if(result.success) { if(result.result.type == JSON_TYPE.STRING) { cgi.write(result.result.str); } else { cgi.write(toJSON(&result.result)); } } else { cgi.write(result.errorMessage); } break; case "document": case "html": default: cgi.setResponseContentType("text/html"); if(result.result.type == JSON_TYPE.STRING) { auto returned = result.result.str; if((fun !is null) && envelopeFormat != "html") { Document document; if(fun.returnTypeIsDocument) { // probably not super efficient... document = new TemplatedDocument(returned); } else { auto e = instantiation._getGenericContainer(); document = e.parentDocument; // FIXME: slow, esp if func return element e.innerHTML = returned; } if(envelopeFormat == "document") instantiation._postProcess(document); returned = document.toString; } cgi.write(returned); } else cgi.write(htmlEntitiesEncode(toJSON(&result.result))); break; } cgi.close(); } } mixin template FancyMain(T, Args...) { void fancyMainFunction(Cgi cgi) { //string[] args) { // auto cgi = new Cgi; // there must be a trailing slash for relative links.. if(cgi.pathInfo.length == 0) { cgi.setResponseLocation(cgi.requestUri ~ "/"); cgi.close(); return; } // FIXME: won't work for multiple objects T instantiation = new T(); auto reflection = prepareReflection!(T)(cgi, instantiation); run(cgi, instantiation); /+ if(args.length > 1) { string[string][] namedArgs; foreach(arg; args[2..$]) { auto lol = arg.indexOf("="); if(lol == -1) throw new Exception("use named args for all params"); //namedArgs[arg[0..lol]] = arg[lol+1..$]; // FIXME } if(!(args[1] in reflection.functions)) { throw new Exception("No such function"); } //writefln("%s", reflection.functions[args[1]].dispatcher(null, namedArgs, "string")); } else { +/ // } } mixin GenericMain!(fancyMainFunction, Args); } Form createAutomaticForm(Document document, in FunctionInfo func, string[string] fieldTypes = null) { return createAutomaticForm(document, func.name, func.parameters, beautify(func.originalName), "POST", fieldTypes); } Form createAutomaticForm(Document document, string action, in Parameter[] parameters, string submitText = "Submit", string method = "POST", string[string] fieldTypes = null) { assert(document !is null); auto form = cast(Form) document.createElement("form"); form.action = action; assert(form !is null); form.method = method; auto fieldset = document.createElement("fieldset"); auto legend = document.createElement("legend"); legend.innerText = submitText; fieldset.appendChild(legend); auto table = cast(Table) document.createElement("table"); assert(table !is null); form.appendChild(fieldset); fieldset.appendChild(table); table.appendChild(document.createElement("tbody")); static int count = 0; foreach(param; parameters) { Element input; string type = param.type; if(param.name in fieldTypes) type = fieldTypes[param.name]; if(type == "select") { input = document.createElement("select"); foreach(idx, opt; param.options) { auto option = document.createElement("option"); option.name = opt; option.value = param.optionValues[idx]; option.innerText = beautify(opt); if(option.value == param.value) option.selected = "selected"; input.appendChild(option); } input.name = param.name; } else if (type == "radio") { assert(0, "FIXME"); } else { if(type.startsWith("textarea")) { input = document.createElement("textarea"); input.name = param.name; input.innerText = param.value; auto idx = type.indexOf("-"); if(idx != -1) { idx++; input.rows = type[idx .. $]; } } else { input = document.createElement("input"); input.type = type; input.name = param.name; input.value = param.value; if(type == "file") { form.method = "POST"; form.enctype = "multipart/form-data"; } } } string n = param.name ~ "_auto-form-" ~ to!string(count); input.id = n; if(type == "hidden") { form.appendChild(input); } else { auto th = document.createElement("th"); auto label = document.createElement("label"); label.setAttribute("for", n); label.innerText = beautify(param.name) ~ ": "; th.appendChild(label); table.appendRow(th, input); } count++; }; auto fmt = document.createElement("select"); fmt.name = "format"; fmt.addChild("option", "html").setAttribute("value", "html"); fmt.addChild("option", "table").setAttribute("value", "table"); fmt.addChild("option", "json").setAttribute("value", "json"); fmt.addChild("option", "string").setAttribute("value", "string"); auto th = table.th(""); th.addChild("label", "Format:"); table.appendRow(th, fmt).className = "format-row"; auto submit = document.createElement("input"); submit.value = submitText; submit.type = "submit"; table.appendRow(Html(" "), submit); // form.setValue("format", reflection.defaultOutputFormat); return form; } /** * Returns the parameter names of the given function * * Params: * func = the function alias to get the parameter names of * * Returns: an array of strings containing the parameter names */ /+ string parameterNamesOf( alias fn )( ) { string fullName = typeof(&fn).stringof; int pos = fullName.lastIndexOf( ')' ); int end = pos; int count = 0; do { if ( fullName[pos] == ')' ) { count++; } else if ( fullName[pos] == '(' ) { count--; } pos--; } while ( count > 0 ); return fullName[pos+2..end]; } +/ template parameterNamesOf (alias func) { const parameterNamesOf = parameterNamesOfImpl!(func); } int indexOfNew(string s, char a) { foreach(i, c; s) if(c == a) return i; return -1; } /** * Returns the parameter names of the given function * * Params: * func = the function alias to get the parameter names of * * Returns: an array of strings containing the parameter names */ private string[] parameterNamesOfImpl (alias func) () { string funcStr = typeof(&func).stringof; auto start = funcStr.indexOfNew('('); auto end = funcStr.indexOfNew(')'); const firstPattern = ' '; const secondPattern = ','; funcStr = funcStr[start + 1 .. end]; if (funcStr == "") return null; funcStr ~= secondPattern; string token; string[] arr; foreach (c ; funcStr) { if (c != firstPattern && c != secondPattern) token ~= c; else { if (token) arr ~= token; token = null; } } if (arr.length == 1) return arr; string[] result; bool skip = false; foreach (str ; arr) { skip = !skip; if (skip) continue; result ~= str; } return result; } ///////////////////////////////// string toHtml(T)(T a) { string ret; static if(is(T : Document)) ret = a.toString(); else static if(isArray!(T)) { static if(__traits(compiles, typeof(T[0]).makeHtmlArray(a))) ret = to!string(typeof(T[0]).makeHtmlArray(a)); else foreach(v; a) ret ~= toHtml(v); } else static if(is(T : Element)) ret = a.toString(); else static if(__traits(compiles, a.makeHtmlElement().toString())) ret = a.makeHtmlElement().toString(); else static if(is(T == Html)) ret = a.source; else ret = htmlEntitiesEncode(std.array.replace(to!string(a), "\n", "
\n")); return ret; } string toJson(T)(T a) { auto v = toJsonValue(a); return toJSON(&v); } // FIXME: are the explicit instantiations of this necessary? JSONValue toJsonValue(T, R = ApiProvider)(T a, string formatToStringAs = null, R api = null) if(is(R : ApiProvider)) { JSONValue val; static if(is(T == JSONValue)) { val = a; } else static if(__traits(compiles, val = a.makeJsonValue())) { val = a.makeJsonValue(); // FIXME: free function to emulate UFCS? // FIXME: should we special case something like struct Html? } else static if(is(T : Element)) { if(a is null) { val.type = JSON_TYPE.NULL; } else { val.type = JSON_TYPE.STRING; val.str = a.toString(); } } else static if(isIntegral!(T)) { val.type = JSON_TYPE.INTEGER; val.integer = to!long(a); } else static if(isFloatingPoint!(T)) { val.type = JSON_TYPE.FLOAT; val.floating = to!real(a); static assert(0); } else static if(is(T == void*)) { val.type = JSON_TYPE.NULL; } else static if(isPointer!(T)) { if(a is null) { val.type = JSON_TYPE.NULL; } else { val = toJsonValue!(typeof(*a), R)(*a, formatToStringAs, api); } } else static if(is(T == bool)) { if(a == true) val.type = JSON_TYPE.TRUE; if(a == false) val.type = JSON_TYPE.FALSE; } else static if(isSomeString!(T)) { val.type = JSON_TYPE.STRING; val.str = to!string(a); } else static if(isAssociativeArray!(T)) { val.type = JSON_TYPE.OBJECT; foreach(k, v; a) { val.object[to!string(k)] = toJsonValue!(typeof(v), R)(v, formatToStringAs, api); } } else static if(isArray!(T)) { val.type = JSON_TYPE.ARRAY; val.array.length = a.length; foreach(i, v; a) { val.array[i] = toJsonValue!(typeof(v), R)(v, formatToStringAs, api); } } else static if(is(T == struct)) { // also can do all members of a struct... val.type = JSON_TYPE.OBJECT; foreach(i, member; a.tupleof) { string name = a.tupleof[i].stringof[2..$]; static if(a.tupleof[i].stringof[2] != '_') val.object[name] = toJsonValue!(typeof(member), R)(member, formatToStringAs, api); } // HACK: bug in dmd can give debug members in a non-debug build //static if(__traits(compiles, __traits(getMember, a, member))) } else { /* our catch all is to just do strings */ val.type = JSON_TYPE.STRING; val.str = to!string(a); // FIXME: handle enums } // don't want json because it could recurse if(val.type == JSON_TYPE.OBJECT && formatToStringAs !is null && formatToStringAs != "json") { JSONValue formatted; formatted.type = JSON_TYPE.STRING; formatAs!(T, R)(a, api, formatted, formatToStringAs, null /* only doing one level of special formatting */); assert(formatted.type == JSON_TYPE.STRING); val.object["formattedSecondarily"] = formatted; } return val; } /+ Document toXml(T)(T t) { auto xml = new Document; xml.parse(emptyTag(T.stringof), true, true); xml.prolog = `` ~ "\n"; xml.root = toXmlElement(xml, t); return xml; } Element toXmlElement(T)(Document document, T t) { Element val; static if(is(T == Document)) { val = t.root; //} else static if(__traits(compiles, a.makeJsonValue())) { // val = a.makeJsonValue(); } else static if(is(T : Element)) { if(t is null) { val = document.createElement("value"); val.innerText = "null"; val.setAttribute("isNull", "true"); } else val = t; } else static if(is(T == void*)) { val = document.createElement("value"); val.innerText = "null"; val.setAttribute("isNull", "true"); } else static if(isPointer!(T)) { if(t is null) { val = document.createElement("value"); val.innerText = "null"; val.setAttribute("isNull", "true"); } else { val = toXmlElement(document, *t); } } else static if(isAssociativeArray!(T)) { val = document.createElement("value"); foreach(k, v; t) { auto e = document.createElement(to!string(k)); e.appendChild(toXmlElement(document, v)); val.appendChild(e); } } else static if(isSomeString!(T)) { val = document.createTextNode(to!string(t)); } else static if(isArray!(T)) { val = document.createElement("array"); foreach(i, v; t) { auto e = document.createElement("item"); e.appendChild(toXmlElement(document, v)); val.appendChild(e); } } else static if(is(T == struct)) { // also can do all members of a struct... val = document.createElement(T.stringof); foreach(member; __traits(allMembers, T)) { if(member[0] == '_') continue; // FIXME: skip member functions auto e = document.createElement(member); e.appendChild(toXmlElement(document, __traits(getMember, t, member))); val.appendChild(e); } } else { /* our catch all is to just do strings */ val = document.createTextNode(to!string(t)); // FIXME: handle enums } return val; } +/ class InsufficientParametersException : Exception { this(string functionName, string msg) { super(functionName ~ ": " ~ msg); } } class InvalidParameterException : Exception { this(string param, string value, string expected) { super("bad param: " ~ param ~ ". got: " ~ value ~ ". Expected: " ~expected); } } void badParameter(alias T)(string expected = "") { throw new InvalidParameterException(T.stringof, T, expected); } class PermissionDeniedException : Exception { this(string msg) { super(msg); } } class NoSuchPageException : Exception { this(string msg) { super(msg); } } type fromUrlParam(type)(string[] ofInterest) { type ret; // Arrays in a query string are sent as the name repeating... static if(isArray!(type) && !isSomeString!(type)) { foreach(a; ofInterest) { ret ~= to!(ElementType!(type))(a); } } else static if(is(type : Element)) { auto doc = new Document(ofInterest[$-1], true, true); ret = doc.root; } /* else static if(is(type : struct)) { static assert(0, "struct not supported yet"); } */ else { // enum should be handled by this too ret = to!type(ofInterest[$-1]); } // FIXME: can we support classes? return ret; } WrapperFunction generateWrapper(alias getInstantiation, alias f, alias group, string funName, R)(ReflectionInfo* reflection, R api) if(is(R: ApiProvider)) { JSONValue wrapper(Cgi cgi, string instantiationIdentifier, in string[][string] sargs, in string format, in string secondaryFormat = null) { JSONValue returnValue; returnValue.type = JSON_TYPE.STRING; auto instantiation = getInstantiation(instantiationIdentifier, api); ParameterTypeTuple!(f) args; Throwable t; // the error we see // this permission check thing might be removed. It's just there so you can check before // doing the automatic form... but I think that would be better done some other way. static if(__traits(hasMember, group, funName ~ "_PermissionCheck")) { ParameterTypeTuple!(__traits(getMember, group, funName ~ "_PermissionCheck")) argsperm; foreach(i, type; ParameterTypeTuple!(__traits(getMember, group, funName ~ "_PermissionCheck"))) { string name = parameterNamesOf!(__traits(getMember, group, funName ~ "_PermissionCheck"))[i]; static if(is(type == bool)) { if(name in sargs) args[i] = true; else args[i] = false; } else { if(!(name in sargs)) { t = new InsufficientParametersException(funName, "arg " ~ name ~ " is not present for permission check"); goto maybeThrow; } argsperm[i] = to!type(sargs[name][$-1]); } } __traits(getMember, group, funName ~ "_PermissionCheck")(argsperm); } // done with arguably useless permission check // Actually calling the function foreach(i, type; ParameterTypeTuple!(f)) { string name = parameterNamesOf!(f)[i]; // We want to check the named argument first. If it's not there, // try the positional arguments string using = name; if(name !in sargs) using = "positional-arg-" ~ to!string(i); // FIXME: if it's a struct, we should do it's pieces independently here static if(is(type == bool)) { // bool is special cased because HTML checkboxes don't send anything if it isn't checked if(using in sargs) args[i] = true; // FIXME: should try looking at the value else args[i] = false; } else static if(is(Unqual!(type) == Cgi.UploadedFile)) { if(name !in cgi.files) throw new InsufficientParametersException(funName, "file " ~ name ~ " is not present"); args[i] = cast() cgi.files[name]; // casting away const for the assignment to compile FIXME: shouldn't be needed } else { if(using !in sargs) { throw new InsufficientParametersException(funName, "arg " ~ name ~ " is not present"); } // We now check the type reported by the client, if there is one // Right now, only one type is supported: ServerResult, which means // it's actually a nested function call string[] ofInterest = cast(string[]) sargs[using]; // I'm changing the reference, but not the underlying stuff, so this cast is ok if(using ~ "-type" in sargs) { string reportedType = sargs[using ~ "-type"][$-1]; if(reportedType == "ServerResult") { // FIXME: doesn't handle functions that return // compound types (structs, arrays, etc) ofInterest = null; string str = sargs[using][$-1]; int idx = str.indexOf("?"); string callingName, callingArguments; if(idx == -1) { callingName = str; } else { callingName = str[0..idx]; callingArguments = str[idx + 1 .. $]; } // find it in reflection ofInterest ~= reflection.functions[callingName]. dispatcher(cgi, null, decodeVariables(callingArguments), "string").str; } } args[i] = fromUrlParam!type(ofInterest); } } static if(!is(ReturnType!f == void)) ReturnType!(f) ret; else void* ret; static if(!is(ReturnType!f == void)) ret = instantiation(args); // version 1 didn't handle exceptions else instantiation(args); formatAs(ret, api, returnValue, format, secondaryFormat); done: return returnValue; } return &wrapper; } void formatAs(T, R)(T ret, R api, ref JSONValue returnValue, string format, string formatJsonToStringAs = null) if(is(R : ApiProvider)) { if(api !is null) { static if(__traits(compiles, api.customFormat(ret, format))) { auto customFormatted = api.customFormat(ret, format); if(customFormatted !is null) { returnValue.str = customFormatted; return; } } } switch(format) { case "html": // FIXME: should we actually post process here? /+ static if(is(typeof(ret) : Document)) { instantiation._postProcess(ret); return ret.toString(); break; } static if(__traits(hasMember, group, funName ~ "_Page")) { auto doc = __traits(getMember, group, funName ~ "_Page")(ret); instantiation._postProcess(doc); return doc.toString(); break; } +/ returnValue.str = toHtml(ret); break; case "string": static if(__traits(compiles, to!string(ret))) returnValue.str = to!string(ret); else goto badType; break; case "json": returnValue = toJsonValue!(typeof(ret), R)(ret, formatJsonToStringAs, api); break; case "table": auto document = new Document(""); static if(__traits(compiles, structToTable(document, ret))) returnValue.str = structToTable(document, ret).toString(); else goto badType; break; default: badType: throw new Exception("Couldn't get result as " ~ format); } } private string emptyTag(string rootName) { return ("<" ~ rootName ~ ">"); } alias JSONValue delegate(Cgi cgi, string, in string[][string] args, in string format, in string secondaryFormat = null) WrapperFunction; string urlToBeauty(string url) { string u = url.replace("/", ""); string ret; bool capitalize = true; foreach(c; u) { if(capitalize) { ret ~= ("" ~ c).toUpper; capitalize = false; } else { if(c == '-') { ret ~= " "; capitalize = true; } else ret ~= c; } } return ret; } string toUrlName(string name) { string res; foreach(c; name) { if(c >= 'a' && c <= 'z') res ~= c; else { res ~= '-'; if(c >= 'A' && c <= 'Z') res ~= c + 0x20; else res ~= c; } } return res; } string beautify(string name) { string n; n ~= toUpper(name[0..1]); dchar last; foreach(dchar c; name[1..$]) { if((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) { if(last != ' ') n ~= " "; } if(c == '_') n ~= " "; else n ~= c; last = c; } return n; } import std.md5; import core.stdc.stdlib; import core.stdc.time; import std.file; string getSessionId(Cgi cgi) { static string token; // FIXME: should this actually be static? it seems wrong if(token is null) { if("_sess_id" in cgi.cookies) token = cgi.cookies["_sess_id"]; else { auto tmp = uniform(0, int.max); token = to!string(tmp); cgi.setCookie("_sess_id", token, /*60 * 8 * 1000*/ 0, "/", null, true); } } return getDigestString(cgi.remoteAddress ~ "\r\n" ~ cgi.userAgent ~ "\r\n" ~ token); } void setLoginCookie(Cgi cgi, string name, string value) { cgi.setCookie(name, value, 0, "/", null, true); } string htmlTemplateWithData(in string text, in string[string] vars) { assert(text !is null); string newText = text; if(vars !is null) foreach(k, v; vars) { //assert(k !is null); //assert(v !is null); newText = newText.replace("{$" ~ k ~ "}", htmlEntitiesEncode(v).replace("\n", "
")); } return newText; } string htmlTemplate(string filename, string[string] vars) { return htmlTemplateWithData(readText(filename), vars); } class TemplatedDocument : Document { const override string toString() { string s; if(vars !is null) s = htmlTemplateWithData(super.toString(), vars); else s = super.toString(); return s; } public: string[string] vars; this(string src) { super(); parse(src, true, true); } this() { } void delegate(TemplatedDocument)[] preToStringFilters; void delegate(ref string)[] postToStringFilters; } void writeDocument(Cgi cgi, TemplatedDocument document) { foreach(f; document.preToStringFilters) f(document); auto s = document.toString(); foreach(f; document.postToStringFilters) f(s); cgi.write(s); } /* Password helpers */ string makeSaltedPasswordHash(string userSuppliedPassword, string salt = null) { if(salt is null) salt = to!string(uniform(0, int.max)); return hashToString(SHA256(salt ~ userSuppliedPassword)) ~ ":" ~ salt; } bool checkPassword(string saltedPasswordHash, string userSuppliedPassword) { auto parts = saltedPasswordHash.split(":"); return makeSaltedPasswordHash(userSuppliedPassword, parts[1]) == saltedPasswordHash; } Table structToTable(T)(Document document, T arr, string[] fieldsToSkip = null) if(isArray!(T) && !isAssociativeArray!(T)) { auto t = cast(Table) document.createElement("table"); t.border = "1"; static if(is(T == string[string][])) { string[string] allKeys; foreach(row; arr) { foreach(k; row.keys) allKeys[k] = k; } auto sortedKeys = allKeys.keys.sort; Element tr; auto thead = t.addChild("thead"); auto tbody = t.addChild("tbody"); tr = thead.addChild("tr"); foreach(key; sortedKeys) tr.addChild("th", key); bool odd = true; foreach(row; arr) { tr = tbody.addChild("tr"); foreach(k; sortedKeys) { tr.addChild("td", k in row ? row[k] : ""); } if(odd) tr.addClass("odd"); odd = !odd; } } else static if(is(typeof(T[0]) == struct)) { { auto thead = t.addChild("thead"); auto tr = thead.addChild("tr"); auto s = arr[0]; foreach(idx, member; s.tupleof) tr.addChild("th", s.tupleof[idx].stringof[2..$]); } bool odd = true; auto tbody = t.addChild("tbody"); foreach(s; arr) { auto tr = tbody.addChild("tr"); foreach(member; s.tupleof) { tr.addChild("td", to!string(member)); } if(odd) tr.addClass("odd"); odd = !odd; } } else static assert(0); return t; } // this one handles horizontal tables showing just one item Table structToTable(T)(Document document, T s, string[] fieldsToSkip = null) if(!isArray!(T) || isAssociativeArray!(T)) { static if(__traits(compiles, s.makeHtmlTable(document))) return s.makeHtmlTable(document); else { auto t = cast(Table) document.createElement("table"); static if(is(T == struct)) { main: foreach(i, member; s.tupleof) { string name = s.tupleof[i].stringof[2..$]; foreach(f; fieldsToSkip) if(name == f) continue main; string nameS = name.idup; name = ""; foreach(idx, c; nameS) { if(c >= 'A' && c <= 'Z') name ~= " " ~ c; else if(c == '_') name ~= " "; else name ~= c; } t.appendRow(t.th(name.capitalize), to!string(member)); } } else static if(is(T == string[string])) { foreach(k, v; s){ t.appendRow(t.th(k), v); } } else static assert(0); return t; } } debug string javascriptBase = ` // change this in your script to get fewer error popups "_debugMode":true,` ~ javascriptBaseImpl; else string javascriptBase = ` // change this in your script to get more details in errors "_debugMode":false,` ~ javascriptBaseImpl; enum string javascriptBaseImpl = q{ "_doRequest": function(url, args, callback, method, async) { var xmlHttp; try { xmlHttp=new XMLHttpRequest(); } catch (e) { try { xmlHttp=new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) { xmlHttp=new ActiveXObject("Microsoft.XMLHTTP"); } } if(async) xmlHttp.onreadystatechange=function() { if(xmlHttp.readyState==4) { if(callback) { callback(xmlHttp.responseText, xmlHttp.responseXML); } } } var argString = this._getArgString(args); if(method == "GET" && url.indexOf("?") == -1) url = url + "?" + argString; xmlHttp.open(method, url, async); var a = ""; if(method == "POST") { xmlHttp.setRequestHeader("Content-type","application/x-www-form-urlencoded"); a = argString; } else { xmlHttp.setRequestHeader("Content-type", "text/plain"); } xmlHttp.send(a); if(!async && callback) { return callback(xmlHttp.responseText, xmlHttp.responseXML); } return xmlHttp; }, "_raiseError":function(type, message) { var error = new Error(message); error.name = type; throw error; }, "_getUriRelativeToBase":function(name, args) { var str = name; var argsStr = this._getArgString(args); if(argsStr.length) str += "?" + argsStr; return str; }, "_getArgString":function(args) { var a = ""; var outputted = false; var i; // wow Javascript sucks! god damned global loop variables for(i in args) { if(outputted) { a += "&"; } else outputted = true; var arg = args[i]; var argType = ""; // Make sure the types are all sane if(arg && arg._arsdTypeOf && arg._arsdTypeOf == "ServerResult") { argType = arg._arsdTypeOf; arg = this._getUriRelativeToBase(arg._serverFunction, arg._serverArguments); // this arg is a nested server call a += encodeURIComponent(i) + "="; a += encodeURIComponent(arg); } else if(arg && arg.length && typeof arg != "string") { // FIXME: are we sure this is actually an array? var outputtedHere = false; for(var idx = 0; idx < arg.length; idx++) { if(outputtedHere) { a += "&"; } else outputtedHere = true; // FIXME: ought to be recursive a += encodeURIComponent(i) + "="; a += encodeURIComponent(arg[idx]); } } else { // a regular argument a += encodeURIComponent(i) + "="; a += encodeURIComponent(arg); } // else if: handle arrays and objects too if(argType.length > 0) { a += "&"; a += encodeURIComponent(i + "-type") + "="; a += encodeURIComponent(argType); } } return a; }, "_onError":function(error) { throw error; }, /// returns an object that can be used to get the actual response from the server "_serverCall": function (name, passedArgs, returnType) { var me = this; // this is the Api object var args = passedArgs; return { // type info metadata "_arsdTypeOf":"ServerResult", "_staticType":(typeof returnType == "undefined" ? null : returnType), // Info about the thing "_serverFunction":name, "_serverArguments":args, // lower level implementation "_get":function(callback, onError, async) { var resObj = this; if(args == null) args = {}; if(!args.format) args.format = "json"; args.envelopeFormat = "json"; return me._doRequest(me._apiBase + name, args, function(t, xml) { if(me._debugMode) { try { var obj = eval("(" + t + ")"); } catch(e) { alert("Bad server json: " + e + "\nOn page: " + (me._apiBase + name) + "\nGot:\n" + t); } } else { var obj = eval("(" + t + ")"); } if(obj.success) { if(typeof callback == "function") callback(obj.result); else if(typeof resObj.onSuccess == "function") { resObj.onSuccess(obj.result); } else if(typeof me.onSuccess == "function") { // do we really want this? me.onSuccess(obj.result); } else { // can we automatically handle it? // If it's an element, we should replace innerHTML by ID if possible // if a callback is given and it's a string, that's an id. Return type of element // should replace that id. return type of string should be appended // FIXME: meh just do something here. } return obj.result; } else { // how should we handle the error? I guess throwing is better than nothing // but should there be an error callback too? var error = new Error(obj.errorMessage); error.name = obj.type; error.functionUrl = me._apiBase + name; error.functionArgs = args; error.errorMessage = obj.errorMessage; // myFunction.caller should be available and checked too // btw arguments.callee is like this for functions if(me._debugMode) { var ourMessage = obj.type + ": " + obj.errorMessage + "\nOn: " + me._apiBase + name; if(args.toSource) ourMessage += args.toSource(); if(args.stack) ourMessage += "\n" + args.stack; error.message = ourMessage; // alert(ourMessage); } if(onError) // local override first... return onError(error); else if(resObj.onError) // then this object return resObj.onError(error); else if(me._onError) // then the global object return me._onError(error); throw error; // if all else fails... } // assert(0); // not reached }, (name.indexOf("get") == 0) ? "GET" : "POST", async); // FIXME: hack: naming convention used to figure out method to use }, // should pop open the thing in HTML format // "popup":null, // FIXME not implemented "onError":null, // null means call the global one "onSuccess":null, // a generic callback. generally pass something to get instead. "formatSet":false, // is the format overridden? // gets the result. Works automatically if you don't pass a callback. // You can also curry arguments to your callback by listing them here. The // result is put on the end of the arg list to the callback "get":function(callbackObj) { var callback = null; var errorCb = null; var callbackThis = null; if(callbackObj) { if(typeof callbackObj == "function") callback = callbackObj; else { if(callbackObj.length) { // array callback = callbackObj[0]; if(callbackObj.length >= 2) errorCb = callbackObj[1]; } else { if(callbackObj.onSuccess) callback = callbackObj.onSuccess; if(callbackObj.onError) errorCb = callbackObj.onError; if(callbackObj.self) callbackThis = callbackObj.self; else callbackThis = callbackObj; } } } if(arguments.length > 1) { var ourArguments = []; for(var a = 1; a < arguments.length; a++) ourArguments.push(arguments[a]); function cb(obj, xml) { ourArguments.push(obj); ourArguments.push(xml); // that null is the this object inside the function... can // we make that work? return callback.apply(callbackThis, ourArguments); } function cberr(err) { ourArguments.unshift(err); // that null is the this object inside the function... can // we make that work? return errorCb.apply(callbackThis, ourArguments); } this._get(cb, errorCb ? cberr : null, true); } else { this._get(callback, errorCb, true); } }, // If you need a particular format, use this. "getFormat":function(format /* , same args as get... */) { this.format(format); var forwardedArgs = []; for(var a = 1; a < arguments.length; a++) forwardedArgs.push(arguments[a]); this.get.apply(this, forwardedArgs); }, // sets the format of the request so normal get uses it // myapi.someFunction().format('table').get(...); // see also: getFormat and getHtml // the secondaryFormat only makes sense if format is json. It // sets the format returned by object.toString() in the returned objects. "format":function(format, secondaryFormat) { if(args == null) args = {}; args.format = format; if(typeof secondaryFormat == "string" && secondaryFormat) { if(format != "json") me._raiseError("AssertError", "secondaryFormat only works if format == json"); args.secondaryFormat = secondaryFormat; } this.formatSet = true; return this; }, "getHtml":function(/* args to get... */) { this.format("html"); this.get.apply(this, arguments); }, // FIXME: add post aliases // don't use unless you're deploying to localhost or something "getSync":function() { function cb(obj) { // no nothing, we're returning the value below } return this._get(cb, null, false); }, // takes the result and appends it as html to the given element // FIXME: have a type override "appendTo":function(what) { if(!this.formatSet) this.format("html"); this.get(me._appendContent(what)); }, // use it to replace the content of the given element "useToReplace":function(what) { if(!this.formatSet) this.format("html"); this.get(me._replaceContent(what)); }, // use to replace the given element altogether "useToReplaceElement":function(what) { if(!this.formatSet) this.format("html"); this.get(me._replaceElement(what)); }, "useToFillForm":function(what) { this.get(me._fillForm(what)); } // runAsScript has been removed, use get(eval) instead // FIXME: might be nice to have an automatic popin function too }; }, "getAutomaticForm":function(method) { return this._serverCall("builtin.getAutomaticForm", {"method":method}, "Form"); }, "_fillForm": function(what) { var e = this._getElement(what); if(this._isListOfNodes(e)) alert("FIXME: list of forms not implemented"); else return function(obj) { if(e.elements && typeof obj == "object") { for(i in obj) if(e.elements[i]) e.elements[i].value = obj[i]; // FIXME: what about checkboxes, selects, etc? } else throw new Error("unsupported response"); }; }, "_getElement": function(what) { var e; if(typeof what == "string") e = document.getElementById(what); else e = what; return e; }, "_isListOfNodes": function(what) { // length is on both arrays and lists, but some elements // have it too. We disambiguate with getAttribute return (what && (what.length && !what.getAttribute)) }, // These are some convenience functions to use as callbacks "_replaceContent": function(what) { var e = this._getElement(what); if(this._isListOfNodes(e)) return function(obj) { for(var a = 0; a < obj.length; a++) { if( (e[a].tagName.toLowerCase() == "input" && e[a].getAttribute("type") == "text") || e[a].tagName.toLowerCase() == "textarea") { e[a].value = obj; } else e[a].innerHTML = obj; } } else return function(obj) { if( (e.tagName.toLowerCase() == "input" && e.getAttribute("type") == "text") || e.tagName.toLowerCase() == "textarea") { e.value = obj; } else e.innerHTML = obj; } }, // note: what must be only a single element, FIXME: could check the static type "_replaceElement": function(what) { var e = this._getElement(what); if(this._isListOfNodes(e)) throw new Error("Can only replace individual elements since removal from a list may be unstable."); return function(obj) { var n = document.createElement("div"); n.innerHTML = obj; if(n.firstChild) { e.parentNode.replaceChild(n.firstChild, e); } else { e.parentNode.removeChild(e); } } }, "_appendContent": function(what) { var e = this._getElement(what); if(this._isListOfNodes(e)) // FIXME: repeating myself... return function(obj) { for(var a = 0; a < e.length; a++) e[a].innerHTML += obj; } else return function(obj) { e.innerHTML += obj; } }, }; /* Note for future: dom.d makes working with html easy, since you can do various forms of post processing on it to make custom formats among other things. I'm considering adding similar stuff for CSS and Javascript. dom.d now has some more css support - you can apply a stylesheet to a document and get the computed style and do some minor changes programmically. StyleSheet : css file :: Document : html file. My css lexer/parser is still pretty crappy though. Also, I'm not sure it's worth going all the way here. I'm doing some of it to support my little browser, but for server side programs, I'm not sure how useful it is to do this kind of thing. A simple textual macro would be more useful for css than a struct for it.... I kinda want nested declarations and some functions (the sass thing from ruby is kinda nice in some ways). But I'm fairly meh on it anyway. For javascript, I wouldn't mind having a D style foreach in it. But is it worth it writing a fancy javascript AST thingy just for that? Aside from that, I don't mind the language with how sparingly I use it though. Besides, writing: CoolApi.doSomething("asds").appendTo('element'); really isn't bad anyway. The benefit for html was very easy and big. I'm not so sure about css and js. */