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