Bruker:Jeblad/Module:JSONstat
The purpose of this module is to demonstrate handling of JSON-stat in Lua with closure-based instances modelled as lazy facades. The instances will not be created before boundraries are passed, that is a method returns a value that is wrapped in a closure. This makes a fairly efficient implementation for simple access.
The library can be used as simple calls through the invoke parser function, or as a support library. A main purpose for the lib is to provide Vega-formatted data, often constrained or adapted somehow to fit better with other available data.
Primary access point is the load-method, which will parse one of several sources and return an instance according to the provided data. In Lua-code the instance can be navigated by using provided methods, while in wikitext it is possible to use layout (format) parameters. One such predefined layout could be to create data for Vega, which is nothing more than the dataset values as an expanded table.
Usage
In wikicode the module will typically be called like one of the following
{{#invoke:JSONstat | load | updated }}
{{#invoke:JSONstat | load | Q123456 | label }}
{{#invoke:JSONstat | load | Bruker:Jeblad/SSB01222.json | ${source:%.10s} }}
{{#invoke:JSONstat | load | { ... } | expanded }}
The first call use the connected item to identify the external JSON-stat dataset (not implemented). The layout updated
reports the datestamp when the dataset was last updated.
The second call identifies an item to use for further identification of a JSON-dataset (not implemented). The layout label
reports the label for the dataset.
The third call identifies a page that contain the dataset. This use an inline format for layout, and reports a truncated source string.
The fourth call use an inline definition of the dataset. This use a layout that reports the values from the expanded form, that is a form that can be used in Vega.
Similar calls in Lua-code would be like the following
local jsonstat = require('Module:JSONstat')
jsonstat.load().updated()
jsonstat.load( 'updated' )
jsonstat.load( 'Q123456' ).label()
jsonstat.load( 'Q123456', 'label' )
jsonstat.load( 'Bruker:Jeblad/SSB01222.json' ).layout( '${source:%.10s}' )
jsonstat.load( 'Bruker:Jeblad/SSB01222.json', '${source:%.10s}' )
jsonstat.load( '{ ... }' ).expanded()
jsonstat.load( '{ ... }', 'expanded' )
-- module for processing JSON-stat
-- © John Erling Blad, Creative Commons by Attribution 3.0
-- dump provided structure
-- @param any out structure to dump
-- @return string representing the structure
function dump( out )
if type( out ) == 'table' then
local s = '{ '
for k,v in pairs( out ) do
if type( k ) ~= 'number' then k = '"'..k..'"' end
s = s .. ' '..k..' = ' .. dump( v ) .. ','
end
return s .. '} '
else
return tostring( out )
end
end
-- @var forward declaration
local JSONstat
-- Constructor for axis'
-- Note that this has no explicit structure in JSON-stat
-- @param table init structure kept for later
-- @return self
local function JsAxis( init )
local self = {}
local data = init
-- constructure signature
-- @return string
function self.signature()
return 'JsAxis'
end
-- provide a valid table structure
-- @return table
function self.toTable()
return data
end
-- this is our instance
return self
end
local function JsCollection( init )
local self = {}
local data = init
-- constructure signature
-- @return string
function self.signature()
return 'JsCollection'
end
-- provide a valid table structure
-- @return table
function self.toTable()
return data
end
-- get the class value
-- note that this may not exist
-- @return string
function self.class()
return data['class']
end
-- this is our instance
return self
end
local function JsDimension( init )
local self = {}
local data = init
-- constructure signature
-- @return string
function self.signature()
return 'JsDimension'
end
-- provide a valid table structure
-- @return table
function self.toTable()
return data
end
-- get the class value
-- note that this may not exist
-- @return string
function self.class()
return data['class']
end
-- get the time of the update
-- note that this may not exist
-- @return string formatted according to ISO8601
function self.updated()
return data['updated']
end
-- get the id of all axis'
-- should always exist
-- @return table
function self.id()
return data['id']
end
-- get the size of all axis'
-- should always exist
-- @return table
function self.size()
return data['size']
end
-- @todo
function self.role()
return data['role']
end
-- evaluate provided function for each id, supplying the axis'
-- @param function f callback to be evaluated
function self.each( f )
for k,v in data['id'] do
f( k, v, JsAxis(data[v]) )
end
end
-- this is our instance
return self
end
local function JsDataset( init )
local self = {}
local data = init
-- constructure signature
-- @return string
function self.signature()
return 'JsDataset'
end
-- provide a valid table structure
-- @return table
function self.toTable()
return data
end
-- get the class value
-- note that this may not exist
-- @return string
function self.class()
return data['class']
end
-- this has no children
-- this will change type if sparse
function self.value(...)
local indexes = arg
return data['value']
end
-- this has no children
-- this can be a table
function self.status()
return data['status']
end
-- at this level it should be a string
function self.label()
return data['label']
end
-- a language dependant string
-- note that there are no language marker
function self.source()
return data['source']
end
-- get the time of the update
-- note that this may not exist
-- @return string formatted according to ISO8601
function self.updated()
return data['updated']
end
--
-- @return JsDimension
function self.dimension()
return JsDimension( data['dimension'] )
end
-- evaluate the provided function for each table cell
-- @todo this only uses the linear index
-- @param function f the callback to be evaluated for each data value
-- @return none
function self.each( f )
local size = data['dimension']['size']
for k,v in ipairs( data['value'] ) do
local idx = {}
local remainder = k
for k2,v2 in ipair( size ) do
remainder = remainder % k2
insert(idx, remainder)
end
f( idx, v )
end
end
-- reformat a table by slicng through it
-- @param varargs
-- @return JsDataset
function self.slice(...)
end
-- this is our instance
return self
end
local function JsBundle( init )
local self = {}
local data = init
-- constructure signature
-- @return string
function self.signature()
return 'JsBundle'
end
-- provide a valid table structure
-- @return table
function self.toTable()
return data
end
function self.get(...)
local ids = {}
for _,v in ipairs( arg ) do
ids[v] = true
end
local items = {}
for k,v in ipairs( data ) do
if ids[k] then
table.insert( items, JSONstat( v ) )
end
end
return items
end
-- this is our instance
return self
end
-- helper to do all the clean typecasts
-- @param table init data structure to recast as instances
-- @return function closures representing instances
local function JSONstat( init )
local class = init['class']
if not class then
if init['dataset'] then
return JsDataset( init['dataset'] )
elseif init['dimension'] then
return JsDimension( init['dimension'] )
elseif init['collection'] then
return JsCollection( init['collection'] )
end
return JsBundle( init ) -- this seems to be correct
elseif class == 'dataset' then
return JsDataset( init )
elseif class == 'dimension' then
return JsDimension( init )
elseif class == 'collection' then
return JsCollection( init )
end
return JsBundle( init ) -- not sure if this is correct
end
-- alternate reports to be made
-- this is mostly for test cases
local report = {}
report['dump'] = function( object ) return object and dump( object ) or 'nil' end
report['signature'] = function( object ) return object and object.signature() or 'nil' end
report['label'] = function( object ) return object and object.label() or 'nil' end
report['updated'] = function( object ) return object and object.updated() or 'nil' end
report['dimension-signature'] = function( object ) return object and object.dimension().signature() or 'nil' end
-- replace named args by using the provided structure
function replaceNamedArgs( layout, struct )
local function redirect( str )
local part = struct
local pth, fmt = unpack( mw.text.split( str, ":", true ) )
for entry in mw.text.gsplit( pth, ".", true ) do
part = part[mw.text.trim( entry )]
if not part then
return '∅'
end
end
return fmt and string.format( fmt, part ) or part
end
local replaced, count = layout:gsub('${%s*([^{}]-)%s*}', redirect )
return replaced
end
-- @var exported table
local js = {}
-- helper to do all the dirty parsing
-- @todo make a better description
-- @param vararg
-- - nil is assumed to imply indirection
-- - table is assumed to be a frame
-- @return function closures representing instances
function js.load(...)
local anon = arg[1]
local layout = arg[2]
local frame
if type( anon ) == 'table' then
frame = anon
anon = frame.args[1]
layout = frame.args[2]
end
mw.log('anon: ' .. anon)
mw.log('layout: ' .. layout)
local instance
if type(anon) == 'nil' then
mw.log('at: nil-section')
-- @todo follow wikidata to stat description and load accordingly
elseif type(anon) == 'string' then
mw.log('at: string-section')
if string.match(anon, '^%s*{') then
mw.log('at: inline-json-subsection')
-- inline definition of data
local data = mw.text.jsonDecode( anon )
if data then
instance = JSONstat( data )
end
elseif string.match(anon, '^%s*[QP]%d+%s*$') then
mw.log('at: entity-subsection')
-- @todo go to defined page at wikidata to get a stat description
elseif string.match(anon, '%.json%s*$') then
mw.log('at: json-subsection')
-- go to defined page to get a stat description
local frame = mw.getCurrentFrame()
if frame then
local raw = frame:expandTemplate{ title = mw.text.trim( anon ) }
if raw then
local data = mw.text.jsonDecode( raw )
if data then
instance = JSONstat( data )
end
end
end
end
elseif type(anon) == 'function' then
mw.log('at: function-section')
instance = JSONstat( anon() )
end
mw.log('instance: ' .. (instance and 'true' or 'false'))
layout = mw.text.trim( layout )
if layout and report[layout] then
mw.log('at: prepared layout')
return report[layout]( instance )
elseif layout then
mw.log('at: free layout')
return replaceNamedArgs( layout, instance.toTable() )
end
mw.log('done')
return instance
end
-- export the accesspoint
return js