diff --git a/lua/spec/match2_spec.lua b/lua/spec/match2_spec.lua index 3dd7e0c0e14..218835f98d1 100644 --- a/lua/spec/match2_spec.lua +++ b/lua/spec/match2_spec.lua @@ -3,6 +3,7 @@ describe('match2', function() local tournamentData = require('test_assets.tournaments').dummy insulate('matchlist', function() local Json = require('Module:Json') + before_each(function () local dataSaved, dataSavedOpponent, dataSavedPlayer, dataSavedGame = {}, {}, {}, {} -- Mock the lpdb functions diff --git a/lua/spec/orm_spec.lua b/lua/spec/orm_spec.lua index a39de6abd09..298dc64128a 100644 --- a/lua/spec/orm_spec.lua +++ b/lua/spec/orm_spec.lua @@ -1,7 +1,16 @@ --- Triple Comment to Enable our LLS Plugin describe('LPDB Object-Relational Mapping', function() + local FeatureFlag = require('Module:FeatureFlag') local Lpdb = require('Module:Lpdb') + before_each(function () + FeatureFlag.set('force_type_check', true) + end) + + after_each(function () + FeatureFlag.set('force_type_check', false) + end) + describe('setting data', function() it('assign value on init', function() local match2 = Lpdb.Match2:new({bestof = 10}) @@ -33,7 +42,7 @@ describe('LPDB Object-Relational Mapping', function() describe('saving data', function() it('saving', function() local stub = stub(mw.ext.LiquipediaDB, 'lpdb_match2') - Lpdb.Match2:new({match2id = 'Foo', match2bracketid = 'Bar', bestof = 3, game = 'r6s'}):save() + Lpdb.Match2:new({match2id = 'Foo', match2bracketid = 'Bar', bestof = 3, game = 'r6s', parent = 'DummyPage'}):save() assert.stub(stub).called_with('Foo', { bestof = 3, date = 0, @@ -52,7 +61,7 @@ describe('LPDB Object-Relational Mapping', function() match2id = 'Foo', match2opponents = {}, mode = '', - parent = '', + parent = 'DummyPage', patch = '', publishertier = '', section = '', diff --git a/lua/wikis/commons/Lpdb.lua b/lua/wikis/commons/Lpdb.lua index aa4ca1e309a..75b3878ad98 100644 --- a/lua/wikis/commons/Lpdb.lua +++ b/lua/wikis/commons/Lpdb.lua @@ -9,10 +9,13 @@ local Lua = require('Module:Lua') local Array = Lua.import('Module:Array') local Class = Lua.import('Module:Class') +local FeatureFlag = Lua.import('Module:FeatureFlag') local FnUtil = Lua.import('Module:FnUtil') local Logic = Lua.import('Module:Logic') +local Opponent = Lua.import('Module:Opponent/Custom') local Table = Lua.import('Module:Table') local TextSanitizer = Lua.import('Module:TextSanitizer') +local TypeUtil = Lua.import('Module:TypeUtil') local Variables = Lua.import('Module:Variables') local Lpdb = {} @@ -103,9 +106,9 @@ end --- LPDB Object-Relational Mapping ----@alias ModelColumnData {name: string, fieldType: string|any, default: any} +---@alias ModelColumnData {name: string, fieldType: TypeUtilType, default: any} ----@class Model +---@class Model: BaseClass ---@field tableName string ---@field tableColumns ModelColumnData[] local Model = Class.new(function(self, name, columns) @@ -113,7 +116,7 @@ local Model = Class.new(function(self, name, columns) self.tableColumns = columns end) ----@class ModelRow +---@class ModelRow: BaseClass ---@field private tableName string ---@field private tableColumns ModelColumnData[] ---@field private fields table @@ -140,7 +143,14 @@ function ModelRow:_validateField(columnData) if not self.fields[columnData.name] then error(self.tableName .. ' expects ' .. columnData.name .. ' to be set') end - -- TODO: Verify types (at least when running tests) + + local typeCheckFeature = FeatureFlag.get('force_type_check') + if not typeCheckFeature then + return + end + TypeUtil.assertValue( + self.fields[columnData.name], columnData.fieldType, {name = self.tableName .. '.' .. columnData.name} + ) end ---@private @@ -230,10 +240,10 @@ Lpdb.Match2 = Model('match2', { {name = 'section', fieldType = 'string', default = ''}, {name = 'game', fieldType = 'string', default = ''}, {name = 'patch', fieldType = 'string', default = ''}, - {name = 'date', fieldType = 'string', default = 0}, + {name = 'date', fieldType = TypeUtil.union('string', 'integer'), default = 0}, {name = 'dateexact', fieldType = 'number', default = 0}, - {name = 'stream', fieldType = 'struct', default = {}}, - {name = 'links', fieldType = 'struct', default = {}}, + {name = 'stream', fieldType = 'table', default = {}}, + {name = 'links', fieldType = 'table', default = {}}, {name = 'bestof', fieldType = 'number', default = 0}, {name = 'vod', fieldType = 'string', default = ''}, {name = 'tournament', fieldType = 'string', default = ''}, @@ -243,11 +253,11 @@ Lpdb.Match2 = Model('match2', { {name = 'series', fieldType = 'string', default = ''}, {name = 'icon', fieldType = 'string', default = ''}, {name = 'icondark', fieldType = 'string', default = ''}, - {name = 'liquipediatier', fieldType = 'string|number', default = ''}, + {name = 'liquipediatier', fieldType = TypeUtil.union('string', 'number'), default = ''}, {name = 'liquipediatiertype', fieldType = 'string', default = ''}, {name = 'publishertier', fieldType = 'string', default = ''}, - {name = 'extradata', fieldType = 'struct', default = {}}, - {name = 'match2bracketdata', fieldType = 'struct', default = {}}, + {name = 'extradata', fieldType = 'table', default = {}}, + {name = 'match2bracketdata', fieldType = 'table', default = {}}, {name = 'match2opponents', fieldType = 'array', default = {}}, {name = 'match2games', fieldType = 'array', default = {}}, }) @@ -260,7 +270,7 @@ Lpdb.Placement = Model('placement', { {name = 'parent', fieldType = 'pagename', default = ''}, {name = 'shortname', fieldType = 'string', default = ''}, {name = 'startdate', fieldType = 'string', default = 0}, - {name = 'date', fieldType = 'string', default = 0}, + {name = 'date', fieldType = TypeUtil.union('string', 'integer'), default = 0}, {name = 'placement', fieldType = 'string', default = ''}, {name = 'prizemoney', fieldType = 'number', default = 0}, {name = 'individualprizemoney', fieldType = 'number', default = 0}, @@ -268,21 +278,21 @@ Lpdb.Placement = Model('placement', { {name = 'weight', fieldType = 'number', default = 0}, {name = 'mode', fieldType = 'string', default = ''}, {name = 'type', fieldType = 'string', default = ''}, - {name = 'liquipediatier', fieldType = 'string|number', default = ''}, + {name = 'liquipediatier', fieldType = TypeUtil.union('string', 'number'), default = ''}, {name = 'liquipediatiertype', fieldType = 'string', default = ''}, {name = 'publishertier', fieldType = 'string', default = ''}, {name = 'icon', fieldType = 'string', default = ''}, {name = 'icondark', fieldType = 'string', default = ''}, {name = 'game', fieldType = 'string', default = ''}, - {name = 'lastvsdata', fieldType = 'struct', default = {}}, + {name = 'lastvsdata', fieldType = 'table', default = {}}, {name = 'opponentname', fieldType = 'string', default = ''}, {name = 'opponenttemplate', fieldType = 'string', default = ''}, - {name = 'opponenttype', fieldType = 'string', default = ''}, - {name = 'opponentplayers', fieldType = 'struct', default = {}}, + {name = 'opponenttype', fieldType = Opponent.types.OpponentType, default = ''}, + {name = 'opponentplayers', fieldType = 'table', default = {}}, {name = 'qualifier', fieldType = 'string', default = ''}, - {name = 'qualifierpage', fieldType = 'string', default = ''}, + {name = 'qualifierpage', fieldType = 'pagename?', default = ''}, {name = 'qualifierurl', fieldType = 'string', default = ''}, - {name = 'extradata', fieldType = 'struct', default = {}}, + {name = 'extradata', fieldType = 'table', default = {}}, }) ---@class SquadPlayerModel:Model @@ -308,7 +318,7 @@ Lpdb.SquadPlayer = Model('squadplayer', { {name = 'joindate', fieldType = 'string', default = ''}, {name = 'leavedate', fieldType = 'string', default = ''}, {name = 'inactivedate', fieldType = 'string', default = ''}, - {name = 'extradata', fieldType = 'struct', default = {}}, + {name = 'extradata', fieldType = 'table', default = {}}, }) ---@class DataPoint:Model @@ -320,7 +330,7 @@ Lpdb.DataPoint = Model('datapoint', { {name = 'image', fieldType = 'string', default = ''}, {name = 'imagedark', fieldType = 'string', default = ''}, {name = 'date', fieldType = 'string', default = 0}, - {name = 'extradata', fieldType = 'struct', default = {}}, + {name = 'extradata', fieldType = 'table', default = {}}, }) return Lpdb diff --git a/lua/wikis/commons/Opponent.lua b/lua/wikis/commons/Opponent.lua index 82a21c2178e..9ff61e7354a 100644 --- a/lua/wikis/commons/Opponent.lua +++ b/lua/wikis/commons/Opponent.lua @@ -15,7 +15,6 @@ local Logic = Lua.import('Module:Logic') local Page = Lua.import('Module:Page') local PlayerExt = Lua.import('Module:Player/Ext/Custom') local String = Lua.import('Module:StringUtils') -local Table = Lua.import('Module:Table') local TeamTemplate = Lua.import('Module:TeamTemplate') local TypeUtil = Lua.import('Module:TypeUtil') @@ -100,6 +99,10 @@ Opponent.types.Opponent = TypeUtil.union( Opponent.types.LiteralOpponent ) +Opponent.types.OpponentType = TypeUtil.literalUnion( + Opponent.solo, Opponent.duo, Opponent.trio, Opponent.quad, Opponent.team, Opponent.literal +) + ---Checks if the provided opponent type is a party type ---@param type OpponentType? ---@return boolean @@ -206,7 +209,7 @@ end ---@param type string ---@return boolean function Opponent.isType(type) - return Table.includes(Opponent.types, type) + return #TypeUtil.checkValue(type, Opponent.types.OpponentType) == 0 end ---Reads an opponent type. @@ -214,13 +217,13 @@ end ---@param type string ---@return OpponentType? function Opponent.readType(type) - return Table.includes(Opponent.types, type) and type or nil + return Opponent.isType(type) and type or nil end ---Asserts that an arbitrary value is a valid representation of an opponent ---@param opponent any function Opponent.assertOpponent(opponent) - assert(Opponent.isOpponent(opponent), 'Invalid opponent') + TypeUtil.assertValue(opponent, Opponent.types.Opponent, {name = 'Opponent'}) end ---Validates that an arbitrary value is a valid representation of an opponent diff --git a/lua/wikis/commons/TypeUtil.lua b/lua/wikis/commons/TypeUtil.lua index d5d8290da1f..873ca70eba3 100644 --- a/lua/wikis/commons/TypeUtil.lua +++ b/lua/wikis/commons/TypeUtil.lua @@ -136,8 +136,7 @@ function TypeUtil.table (keyType, valueType) end --[[ -Type for tables that are arrays. Not strict - arrays may have additional fields -besides numeric indexes, and may have gaps in indexes. +Type for tables that are arrays. ]] ---@param elemType TypeUtilType ---@return TypeUtilArrayType @@ -172,6 +171,8 @@ function TypeUtil.valueIsTypeNoTable (value, typeSpec) elseif typeSpec == 'pagename' then -- A pagename is a string, with first letter capitalized and may not contains spaces return type(value) == 'string' and value:find('^%u') and not value:find(' ') + elseif typeSpec == 'array' then + return Array.isArray(value) elseif String.endsWith(typeSpec, '?') then return value == nil or TypeUtil.valueIsTypeNoTable(value, typeSpec:sub(1, -2)) elseif typeSpec == 'any' then @@ -192,8 +193,10 @@ function TypeUtil.valueIsTypeNoTable (value, typeSpec) typeSpec.types, function(t) return TypeUtil.valueIsTypeNoTable(value, t) end ) - elseif typeSpec.op == 'table' or typeSpec.op == 'struct' or typeSpec.op == 'array' then + elseif typeSpec.op == 'table' or typeSpec.op == 'struct' then return type(value) == 'table' + elseif typeSpec.op == 'array' then + return Array.isArray(value) end end return true