Jump to content

MediaWiki:Gadget-libLua.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/*  ___________________________________________________________________________
 * |                                                                           |
 * |                    === 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.<function name>. 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 );