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 );