Модуль: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