Модуль:WDBackend (Bk;rl,&WDBackend)
Модуль предназначен для получения информации из Викиданных по задаваемой схеме. Схема описывается в формате таблицы Lua.
Модуль используется в модуле Модуль:CiteGost/WDSource для получения информации об источниках информации. Модуль WDSource, в свою очередь, используется в модуле Модуль:CiteGost для оформления библиографических записей.
Использование
[править код]Создать экземпляр объекта форматирования можно функцией модуля new() с указанием языка получения данных. Для получения данных по схеме можно использовать методы полученного объекта:
- fetchEntity(таблица, элемент, схема) — получить данных по схеме из указанного элемента Викиданных.
- fetch(таблица, схема) — получить данных по схеме (элементы Викиданных уже указаны в схеме).
- ensureLang() — ассерт на то, что язык точно выбран.
Формат схемы
[править код]Формат схемы в общем виде:
{
-- Поле с указаным элементом Викиданных:
{
name = 'Имя поля с QID',
-- Получить из элемента данные:
get = {
-- 1-е поле
{
name = 'Имя получаемого поля',
property = 'P-идентификатор свойства',
match = Значение, по которому будут обрабатываться квалификаторы (работает совместно с qualifiers),
getValue = Опциональная функция получения значения (должна возвращать два аргумента: текст и язык текста),
getData = Опциональная функция получения данных о значении (помимо значения, например, может возвращать его язык и флаг fromLabel получения данных из метки элемента),
getLabel = Опциональный флаг получения метки элемента Викиданных по его идентификатору, заданному через параметр entity или уже присутствующему в таблице.
max = Опциональное максимальное количество обрабатываемых значений,
-- Подмена элементов Викиданных согласно отображению (ключ заменяется на значение):
mapEntities = { QID 1 = 'QID 2', ... },
-- Фильтрация по разрешённых элементам Викиданных:
allowedEntities = { QID 1, QID 2, ... },
defaultUnit = QID единицы измерения по умолчанию,
-- Фильтрация по разрешённым единицам измерения:
allowedUnits = { QID единицы измерения 1, QID единицы измерения 2, ... },
-- Перезаписать поле, если у него уже задано значение:
overwrite = true,
-- Перезаписать идентификатор элемента Викиданных родительского поля (для безымянных полей):
overwriteEntity = true,
-- Перезаписать значение родительского поля (для безымянных полей):
overwriteValue = true,
-- Сделать поле вложенным полем для родительского (поместить в components):
isLocal = true,
-- Пометить значение как точное (например, как указано в источнике):
exact = true,
-- Принудительно делать из поля массив:
isArray = true,
-- Подставить значение текущего поля в другое по шаблону, заданному ещё одним полем:
substInto = {
name = 'Целевое поле (куда записываем)',
template = {
name = 'Имя поля, в значении которого записан шаблон',
},
}
-- Получить другие поля из элемента Викиданных данного поля:
get = {
-- ...
},
-- Если текущее поле не удалось получить, то получить другие поля:
elseGet = {
-- ...
},
},
-- ...
},
},
-- ...
}
Общий формат отдельного поля:
{
value = Значение (в случае даты — таблица)
entity = Идентификатор элемента Викиданных
unitEntity = Идентификатор элемента Викиданных единицы измерения, соответствующей значению value
retrieved = Флаг, обозначающий, что поле было получено из Викиданных (а не заполнено вручную)
exact = Флаг, обозначающий, что получено уточнённое значение поля
lang = Язык, соответствующий значению
components = {
-- Вложенные поля
-- ...
},
}
Принцип работы
[править код]При обходе полей схемы, если в поле уже есть значение, то второй раз оно уже не будет получено, если только не указано overwrite=true
, что подразумевает перезапись ранее заданного или полученного значения. Таким образом, можно в одно и то же поле пытаться получать значение по очереди из разных свойств. Получено будет первое попавшееся значение. По этому принципу (порядок получения) можно выстраивать приоритет получения данных из разных ствойств.
Если при обходе элементов значение очередного элемента было получено (ранее не было задано), то обходятся квалификаторы соответствующего элемента. В противном случае квалификаторы не обходятся. В случае неименованных аргументов квалификаторы обходятся всегда.
Если в поле не задано имя, то полученное значение никуда не записывается. Если же задан параметр overwriteEntity
, перезаписывается идентификатор родительского элемента. Если задан параметр overwriteValue
, — значение родительского элемента. Безымянные поля удобны для перезаписи значений родительских полей из квалификаторов, либо же для получения свойств из полученного в безымянном поле элемента через get
.
Внесение изменений
[править код]При исправлении ошибки, пожалуйста, сначала добавьте тест, который будет проваливаться из-за обнаруженной ошибки, и только затем вносите исправление. При внесении исправления проверьте, чтобы все тесты проходили. Вносить исправление можно только, если оно не ломает другие тесты.
Добавление нового функционала рекомендуется делать у себя в песочнице, скопировав в неё модуль. В правке копирования необходимо указать тот факт, что делается копирование, и сделать ссылку на оригинальный модуль в виде викитекста. При добавлении нового функционала сначала желательно добавить тест на этот функционал, затем добавить сам функционал, убедившись, что все тесты при этом проходят.
Тесты
[править код]Все тесты пройдены.
Название | Ожидается | Фактически | |
---|---|---|---|
test_fetchEntity_array | |||
test_fetchEntity_baseTypes | |||
test_fetchEntity_components | |||
test_fetchEntity_defaultUnit | |||
test_fetchEntity_elseGet_exists | |||
test_fetchEntity_elseGet_in_inLocal_with_overwriteEntity | |||
test_fetchEntity_elseGet_notExists | |||
test_fetchEntity_forceGet | |||
test_fetchEntity_forceGet_predefined_value | |||
test_fetchEntity_get | |||
test_fetchEntity_getValue | |||
test_fetchEntity_has | |||
test_fetchEntity_isArray | |||
test_fetchEntity_isLocal_in_array | |||
test_fetchEntity_isLocal_in_unnamed | |||
test_fetchEntity_isLocal_with_qualifiers_and_get_by_entity_with_isLocal | |||
test_fetchEntity_map | |||
test_fetchEntity_max | |||
test_fetchEntity_noOverwrite | |||
test_fetchEntity_overwrite | |||
test_fetchEntity_overwriteByQualifier | |||
test_fetchEntity_overwriteEntity | |||
test_fetchEntity_overwriteValue | |||
test_fetchEntity_overwriteValueByQualifier | |||
test_fetchEntity_qualifiers | |||
test_fetchEntity_substInto |
Разработка
[править код]План работ:
|
require('strict')
local p = {}
local NS_MODULE = 828 --: https://www.mediawiki.org/wiki/Extension_default_namespaces
local moduleNamespace = mw.site.namespaces[NS_MODULE].name
local base = require(moduleNamespace .. ':WDBase')
local Backend = {}
function Backend:new(lang)
local defaultLangObj = mw.getContentLanguage()
local defaultLang = defaultLangObj:getCode()
local obj = {
lang = lang,
defaultLang = defaultLang
}
setmetatable(obj, self)
self.__index = self
return obj
end
function Backend:safeField(source, fieldName, parentField)
if not parentField then
parentField = source
end
local info = parentField[fieldName]
if not info then
info = {}
end
return info
end
function Backend:parseFieldPath(source, map, parentField)
local fieldName = map.name
local currParentComponents = source
local currParentField = nil
if map.isLocal or not fieldName then
currParentField = parentField
if parentField then
currParentComponents = currParentField.components or {}
end
end
if type(fieldName) == 'table' then
local lastParentField = currParentField
local lastParentComponents = currParentComponents
local lastNotFound = false
for i, name in ipairs(fieldName) do
if i ~= 1 then
-- It's much easier to break at last iteration, but it's a problem
-- to get the count of parts if the map will be loaded by mw.loadData()
if lastNotFound then
return nil
end
currParentField = lastParentField
currParentComponents = lastParentComponents
end
if lastParentComponents then
lastParentField = lastParentComponents[name]
lastParentComponents = lastParentField and lastParentField.components
else
lastNotFound = true
end
fieldName = name
end
currParentComponents = currParentComponents or {}
end
return currParentField, currParentComponents, fieldName
end
function Backend:trySetField(source, fieldName, info, parentField)
if info.value or info.entity then
if not parentField then
parentField = source
end
parentField[fieldName] = info
end
end
local function inArray(value, array)
for _, currValue in ipairs(array) do
if currValue == value then
return true
end
end
return false
end
function Backend:getLang(map)
return (map.useDefaultLang and self.defaultLang) or self.lang
end
function Backend:fetchFieldsByQualifiers(source, fieldMap, qualifiers, parentField)
for _, map in ipairs(fieldMap) do
local qualifier = qualifiers[map.property]
if qualifier then
if map.filter then
qualifier = map.filter(qualifier, self:getLang(map))
end
self:fetchFieldByMap(source, map, qualifier, base.dataBySnak, parentField)
end
end
end
function Backend:fetchFieldItem(source, map, statementOrSnak, getData)
local lang = self:getLang(map)
local item = getData(statementOrSnak, lang, map.cache)
if item then
item.retrieved = true
else
item = {}
end
if map.mapEntity then
local entity = map.mapEntity[item.entity]
if not entity then
return nil
end
item = base.dataByEntity(entity, lang, map.cache)
if item then
item.retrieved = true
end
end
if item.entity then
if map.getData then
item = map.getData(item.entity, lang)
item.retrieved = true
elseif map.getValue then
item.value, item.lang = map.getValue(item.entity, lang)
end
end
if map.allowedEntities and item.entity and not inArray(item.entity, map.allowedEntities) then
return nil
end
if map.defaultUnit and not item.unitEntity then
item.unitEntity = map.defaultUnit
end
if map.allowedUnits then
if not item.unitEntity or not inArray(item.unitEntity, map.allowedUnits) then
return nil
end
end
if not item.value and not item.entity then
return nil
end
if map.exact then
item.exact = true
end
return item
end
local function skipGetIf(item, cond)
if not cond then
return false
end
for key, value in pairs(cond) do
if item[key] == value then
return true
end
end
return false
end
function Backend:tryGet(source, map, getTable, items, currParentField)
if not getTable then
return
end
local fieldName = map.name
if fieldName then
for j, item in ipairs(items) do
if not skipGetIf(item, map.skipGetIf) then
self:fetchFieldsByMap(source, item.entity, getTable, item)
end
end
else
for j, item in ipairs(items) do
if not skipGetIf(item, map.skipGetIf) then
self:fetchFieldsByMap(source, item.entity, getTable, currParentField)
end
end
end
end
function Backend:fetchFieldByMap(source, map, statementsOrSnaks, getData, parentField)
local currParentField, currParentComponents, fieldName
= self:parseFieldPath(source, map, parentField)
if not currParentComponents then
return
end
if fieldName then
local fieldTable = self:safeField(source, fieldName, currParentField)
if map.match and getData == base.dataByStatement and fieldTable.value then
local statement = base.searchStatementByValue(statementsOrSnaks, fieldTable.value)
if not statement then
return
end
local item = self:fetchFieldItem(source, map, statement, getData)
if item then
currParentComponents[fieldName] = item
if map.qualifiers and statement.qualifiers then
self:fetchFieldsByQualifiers(source, map.qualifiers, statement.qualifiers, item)
end
local getTable = map.forceGet or map.get
if getTable and not skipGetIf(item, map.skipGetIf) then
self:fetchFieldsByMap(source, item.entity, getTable, currParentField)
end
else
if map.elseGet then
self:fetchFieldsByMap(source, parentField.entity, map.elseGet, currParentField)
end
end
return
end
if fieldTable.value and not map.overwrite then
if map.substInto and map.substInto.force then
self:substFieldInto(source, map, parentField)
end
local items = fieldTable
if table.getn(items) == 0 then
items = { items }
end
self:tryGet(source, map, map.forceGet, items, currParentField)
return
end
end
local maxCount = map.max
if not maxCount then
maxCount = math.huge
end
local items = {}
local indices = {}
for i, statementOrSnak in ipairs(statementsOrSnaks) do
if i > maxCount then
break
end
if statementOrSnak ~= nil then
local item = self:fetchFieldItem(source, map, statementOrSnak, getData)
if item then
table.insert(items, item)
indices[i] = table.getn(items)
else
indices[i] = 0
end
end
end
local triggerElseGet = false
if fieldName then
if table.getn(items) == 1 and not map.isArray then
currParentComponents[fieldName] = items[1]
elseif next(items) ~= nil then
currParentComponents[fieldName] = items
else
triggerElseGet = true
end
elseif next(items) ~= nil then
if map.overwriteValue then
parentField.value = items[1].value
parentField.exact = map.exact
end
if map.overwriteEntity then
parentField.entity = items[1].entity
end
else
triggerElseGet = true
end
if currParentField and next(currParentComponents) ~= nil then
currParentField.components = currParentComponents
end
if map.substInto then
self:substFieldInto(source, map, parentField)
end
for i, statementOrSnak in ipairs(statementsOrSnaks) do
if i > maxCount then
break
end
local item = items[indices[i]]
if map.qualifiers and statementOrSnak.qualifiers then
self:fetchFieldsByQualifiers(source, map.qualifiers, statementOrSnak.qualifiers, item)
end
end
self:tryGet(source, map, map.forceGet or map.get, items, currParentField)
if triggerElseGet and map.elseGet then
self:fetchFieldsByMap(source, parentField.entity, map.elseGet, parentField)
end
end
function Backend:fetchFieldByCustomFunc(source, entity, map, parentField)
local currParentField, currParentComponents, fieldName
= self:parseFieldPath(source, map, parentField)
if not currParentComponents then
return
end
local fieldTable = currParentComponents[fieldName]
if fieldTable and fieldTable.value and not map.overwrite then
return
end
local lang = self:getLang(map)
if map.getData then
fieldTable = map.getData(entity, lang)
else
fieldTable = fieldTable or {}
fieldTable.value = map.getValue(entity, lang)
end
fieldTable.retrieved = true
self:trySetField(source, fieldName, fieldTable, currParentComponents)
if currParentField and next(currParentComponents) ~= nil then
currParentField.components = currParentComponents
end
end
function Backend:fetchFieldLabel(source, map, parentField)
local currParentField, currParentComponents, fieldName
= self:parseFieldPath(source, map, parentField)
if not currParentComponents then
return
end
local fieldTable = currParentComponents[fieldName]
local entity
if fieldTable then
if fieldTable.value then
return
end
entity = fieldTable.entity
end
entity = map.entity or fieldTable.entity
if not entity then
return
end
local components = fieldTable and fieldTable.components
fieldTable = base.dataByEntity(entity, self:getLang(map), map.cache)
fieldTable.components = components
currParentComponents[fieldName] = fieldTable
if currParentField and next(currParentComponents) ~= nil then
currParentField.components = currParentComponents
end
end
local function getPropertyByPath(source, propertyPath)
local currField = source
for _, pathEntry in ipairs(propertyPath) do
currField = currField[pathEntry]
if not currField then
return nil
end
end
return currField
end
local function filterStatementsByPropertiesAndValues(statements, properties)
local filtered = {}
local propsCount = table.getn(properties)
for _, statement in ipairs(statements) do
local matched = 0
local qualifiers = statement.qualifiers
if qualifiers then
for _, propInfo in ipairs(properties) do
local propQualifiers = qualifiers[propInfo.property]
if propQualifiers then
for _, propQualifier in ipairs(propQualifiers) do
if propInfo.value then
local value = base.valueBySnak(propQualifier)
if value == propInfo.value then
matched = matched + 1
else
break
end
else
matched = matched + 1
end
end
end
end
end
if matched == propsCount then
table.insert(filtered, statement)
end
end
if not next(filtered) then
return nil
end
return filtered
end
function Backend:tryForceGetByMap(source, map, parentField)
local currParentField, currParentComponents, fieldName
= self:parseFieldPath(source, map, parentField)
if not currParentComponents then
return
end
local fieldTable = self:safeField(source, fieldName, currParentComponents)
local items = fieldTable
if table.getn(items) == 0 then
items = { fieldTable }
end
self:tryGet(source, map, map.forceGet, items, currParentField)
end
function Backend:substFieldInto(source, map, parentField)
local currParentField, currParentComponents, fieldName
= self:parseFieldPath(source, map, parentField)
if not currParentComponents then
return
end
local fieldTable = currParentComponents[fieldName]
if not fieldTable then
return
end
local targetCurrParentField, targetCurrParentComponents, targetFieldName
= self:parseFieldPath(source, map.substInto, parentField)
if not targetCurrParentComponents then
return
end
local targetFieldTable = self:safeField(source, targetFieldName, targetCurrParentComponents)
local templateCurrParentField, templateCurrParentComponents, templateFieldName
= self:parseFieldPath(source, map.substInto.template, parentField)
if not templateCurrParentComponents then
return
end
local templateFieldTable = templateCurrParentComponents[templateFieldName]
if not templateFieldTable then
return
end
targetFieldTable.value = templateFieldTable.value:gsub('%$1', fieldTable.value)
self:trySetField(source, targetFieldName, targetFieldTable, targetCurrParentComponents)
if targetCurrParentField and next(targetCurrParentComponents) ~= nil then
targetCurrParentField.components = targetCurrParentComponents
end
end
function Backend:fetchFieldsByMap(source, entity, fieldMap, parentField)
for _, map in ipairs(fieldMap) do
local currEntity = entity
if map.entity then
currEntity = map.entity
end
if currEntity then
local propertySpecified = false
local statements
if map.property then
statements = base.statements(currEntity, map.property, map.cache)
propertySpecified = true
elseif map.properties then
statements = base.statementsByProperties(currEntity, map.properties)
propertySpecified = true
elseif map.propertyPath then
local property = getPropertyByPath(source, map.propertyPath)
if property then
statements = base.statements(currEntity, property, map.cache)
end
propertySpecified = true
end
if propertySpecified then
if statements then
if map.filter then
statements = map.filter(statements, self:getLang(map))
end
if map.has then
statements = filterStatementsByPropertiesAndValues(statements, map.has)
end
self:fetchFieldByMap(source, map, statements, base.dataByStatement, parentField)
else
if map.elseGet then
self:fetchFieldsByMap(source, currEntity, map.elseGet, parentField)
end
end
else
if map.getData or map.getValue then
self:fetchFieldByCustomFunc(source, currEntity, map, parentField)
end
if map.getLabel then
self:fetchFieldLabel(source, map, parentField)
end
end
if not propertySpecified or not statements then
self:tryForceGetByMap(source, map, parentField)
end
else
self:tryForceGetByMap(source, map, parentField)
end
end
end
function Backend:fetch(source, fieldsMap)
for _, map in ipairs(fieldsMap) do
local fieldTable = self:safeField(source, map.name)
if next(fieldTable) ~= nil then
if map.get then
self:fetchFieldsByMap(source, fieldTable.entity, map.get, fieldTable)
end
if map.substInto then
self:substFieldInto(source, map)
end
end
end
end
function Backend:fetchEntity(source, entity, fieldMap)
self:fetchFieldsByMap(source, entity, fieldMap)
end
function Backend:ensureLang()
if self.lang then
return
end
self.lang = self.defaultLang
end
function Backend:assertLang()
assert(self.lang, 'No language selected.')
end
function p.new(lang)
return Backend:new(lang)
end
return p