MediaWiki:Gadget-libLua.js

/* ___________________________________________________________________________ * |                                                                           | * |                    === WARNING: GLOBAL GADGET FILE ===                    | * |                 Changes to this page affect many users. | * | Please discuss changes on the talk page or on Wikipedia_talk:Gadget  | * | before editing. | * |___________________________________________________________________________| * * * libLua provides functions for interacting with Lua modules from JavaScript. * *  *                              === USAGE === * * The library should be loaded as a MediaWiki gadget, using mw.loader.load, * mw.loader.using, or similar. The name of the gadget is * "ext.gadget.libLua". Once the gadget is loaded, you can access its * functions from mw.libs.lua. . Documentation for the * functions can be found in the JSDoc comment blocks in the library code. For * example: * * // Call p.main("foo", "bar") in Module:Example * mw.loader.using( [ 'ext.gadget.libLua' ], function { *     mw.libs.lua.call( { *        module: 'Example', *        func: 'main', *        args: [ 'foo', 'bar' ] *    } ).then( function ( result ) { *        // Do something with the result *    } ); * } ); *  *  *                             === LICENCE === * * Author: Mr. Stradivarius * Licence: MIT * * The MIT License (MIT) * * Copyright (c) 2016 Mr. Stradivarius * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */

( function ( $, mw, undefined ) {	'use strict';

/**	* Encode a string for including in a Lua question. * At the moment this is just a wrapper for JSON.stringify, as that does * what we need. However, encoding a Lua string is conceptually different * from encoding JSON, so we use different function names for the two tasks. * This will also make it easier to update the code in the future, if * necessary. * @private */	function makeLuaString( s ) { return JSON.stringify( s ); }

/**	* Make a Lua question string from a module name, a function name and an * optional args array. * @private */	function makeQuestion( module, func, args ) { var escapedModule = makeLuaString( 'Module:' + module ), escapedFunc = makeLuaString( func ), json, escapedJson, argString; if ( args ) { json = JSON.stringify( args ); escapedJson = makeLuaString( json ); argString = 'unpack(mw.text.jsonDecode(' + escapedJson + '))'; } else { argString = ''; }		return '=require(' + escapedModule + ')[' + escapedFunc + '](' + argString + ')'; }

/**	* Reject a deferred object with the specified error code and error message. * If no deferred object is supplied with the third parameter, a new one is * created. We use this particular format for the error objects as it is the * same one used by the MediaWiki API, and so clients will only have to * worry about errors being formatted in one way. * @private */	function rejectDeferred( code, msg, deferred ) { if ( !deferred ) { deferred = $.Deferred; }		return deferred.reject(			code,			{ error: {				code: code,				info: msg			} }		); }

mw.libs.lua = {

/**		* Call a function in a Lua module. The function call is made * asynchronously through the MediaWiki Action API, and its result is * wrapped in a jQuery promise. *		* @param {Object} options *		* @param {string} options.module - The name of the module to load. * (Don't use a "Module:" prefix.) *		* @param {string} options.func - The name of the function to call. * Only strings are accepted as function names. *		* @param {*[]} [options.args] - An array of arguments to pass to the * function. These must be serializable as JSON. The arguments will be		* unpacked when passed to the function; when calling a function "func", * an args array of ["foo", "bar", "baz"] will be called as * func("foo", "bar", "baz"). There are limitations in what can be		* decoded from JSON in Lua: for example, keys may be dropped from * arrays containing null values. See * https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#mw.text.jsonDecode * for more details. For this reason, calls like func('foo', nil, 'bar') * cannot be made directly. To work around this you can define an		* intermediary function in a Lua module that calls the desired function * indirectly, and then call that function from this library instead. *		* @param {('string'|'json')} [options.format=string] - The expected * return format. If this is "string" or undefined, then the return value * will be a string. (If the Lua function call returns a non-string value		* it will be converted to a string, and if the function call returns		* multiple values then they will be converted to strings and		* concatenated with tabs as separators.) If this is "json", then the * return string from the function call is assumed to be JSON, and is * converted to a JavaScript object using JSON.parse. If the return * string is not valid JSON, the promise returned from the function is * rejected, but no error is thrown. *		* @param {mw.Api} [options.api] - An mw.Api object to use for API * calls. If this is not specified, a new mw.Api object using default * values is created. *		* @return {$.Promise} * A jQuery Promise that is resolved with the result of the function * call. *		@example // Load the gadget mw.loader.using( 'ext.gadget.libLua', function {			// Call p.main( "foo", "bar", "baz" ) in Module:Example.			mw.libs.lua.call( { "module": "Example", "func": "main", "args": [ "foo", "bar", "baz" ] } ).done( function( resultString ) { doSomething( resultString ); } );		} );		*		@example // Load the gadget mw.loader.using( 'ext.gadget.libLua', function {			// Call p.getJson( "foo" ) in Module:Example.			mw.libs.lua.call( { "format": "json", "module": "Example", "func": "getJson", "args": [ "foo" ] } ).done( function( data ) { doSomething( data.bar.baz ); } );		} );		*		*/		call: function ( options ) { // Deal with bad arguments if ( !( options instanceof Object ) ) { return rejectDeferred(					'liblua-call-options-type-error',					"type error in arg #1 to 'call' (object expected)"				); } else if ( typeof options.module !== 'string' ) { return rejectDeferred(					'liblua-call-module-type-error',					'type error in options.module (string expected)'				); } else if ( typeof options.func !== 'string' ) { return rejectDeferred(					'liblua-call-func-type-error',					'type error in options.func (string expected)'				); } else if ( options.args !== undefined && !$.isArray( options.args ) ) { return rejectDeferred(					'liblua-call-invalid-args',					'options.args was defined but was not an array'				); } else if ( options.format !== undefined					&& options.format !== 'json'					&& options.format !== 'string' ) { return rejectDeferred(					'liblua-call-format-type-error',					"invalid format specified (must be 'json', 'string' or undefined)"				); } else if ( options.api !== undefined && !( options.api instanceof mw.Api ) ) { return rejectDeferred(					'liblua-call-invalid-api-object',					'options.api is not a valid mw.Api object.'				); }

// Generate a new API object if we weren't passed one. var api = options.api || new mw.Api;

// Make the API call. // The title field in scribunto-console doesn't seem to allow us to			// use the p variable to load the module content, so set it to a			// dummy value with blank content and load the module in the // question instead. return api.postWithToken( 'csrf', {				action: 'scribunto-console',				format: 'json',				title: 'Example',				content: '',				question: makeQuestion( options.module, options.func, options.args ),				clear: true			} ).then( function ( obj ) {				// Wrap the API query in a new jQuery Deferred object so that				// we can reject API results that are invalid Lua but not				// treated as errors by the API.				return $.Deferred( function ( deferred ) { // Deal with any errors from the API or from Lua. if ( obj.type === 'error' ) { // Lua command failed but API call succeeded return rejectDeferred(							obj.messagename,							obj.message,							deferred						); } else if ( obj.error ) { // API call failed return deferred.reject( obj.error.code, obj ); } else if ( obj.type !== 'normal' ) { // Unknown API response return rejectDeferred(							'liblua-call-unknown-api-response',							'Unknown API response',							deferred						); }

var result = obj['return'];

// Try to parse JSON if options.format equals 'json' if ( options.format == 'json' ) { try { result = JSON.parse( result ); } catch ( e ) { if ( e instanceof SyntaxError ) { return rejectDeferred(									'liblua-call-json-syntax-error',									'The Lua function call returned invalid JSON: ' + e.message,									deferred								); } else { return rejectDeferred(									'liblua-call-json-unexpected-error',									'An unexpected error occurred while trying to ' +										'parse the JSON returned from the Lua function call',									deferred								); }						}					}

return deferred.resolve( result ); } ).promise;			} ); }	}; } )( jQuery, mediaWiki );