Skip to content

Commit

Permalink
feat(specification-subfields-validation) implement "Missing Subfield"…
Browse files Browse the repository at this point in the history
… Rule Validation
  • Loading branch information
SvitlanaKovalova1 committed Sep 3, 2024
1 parent 15e9c86 commit 694c3b1
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 4 deletions.
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
* implement Non-Repeatable Required 1XX Fields validator ([MRSPECS-42](https://folio-org.atlassian.net/browse/MRSPECS-42))
* implement Invalid Field Tag validator ([MRSPECS-44](https://folio-org.atlassian.net/browse/MRSPECS-44))
* implement Invalid Indicator validation ([MRSPECS-45](https://folio-org.atlassian.net/browse/MRSPECS-45))
* implement Missing Subfield validation([MRSPECS-47](https://folio-org.atlassian.net/browse/MRSPECS-47))

#### General
* Implement build dependants GitHub workflow on PR creation ([MRSPECS-9](https://folio-org.atlassian.net//browse/MRSPECS-9))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.folio.rspec.domain.dto.SpecificationDto;
import org.folio.rspec.domain.dto.SpecificationFieldDto;
import org.folio.rspec.domain.dto.SpecificationRuleDto;
import org.folio.rspec.domain.dto.SubfieldDto;

@UtilityClass
public class SpecificationUtils {
Expand All @@ -35,4 +36,11 @@ public static Optional<SpecificationFieldDto> findField(SpecificationDto specifi
.filter(fieldDto -> tag.equals(fieldDto.getTag()))
.findFirst();
}

public static Map<Character, SubfieldDto> requiredSubfields(List<SubfieldDto> subfieldDto) {
return subfieldDto == null ? Map.of() : subfieldDto
.stream()
.filter(subfield -> Boolean.TRUE.equals(subfield.getRequired()))
.collect(Collectors.toMap(subfield -> subfield.getCode().charAt(0), Function.identity()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
import org.folio.rspec.validation.validator.marc.model.MarcField;
import org.folio.rspec.validation.validator.marc.model.MarcIndicator;
import org.folio.rspec.validation.validator.marc.model.MarcRecord;
import org.folio.rspec.validation.validator.marc.model.MarcSubfield;

public class MarcRecordRuleValidator implements SpecificationRuleValidator<MarcRecord, SpecificationDto> {

private final List<SpecificationRuleValidator<Map<String, List<MarcField>>, SpecificationDto>> fieldSetValidators;
private final List<SpecificationRuleValidator<MarcField, SpecificationFieldDto>> fieldValidators;
private final List<SpecificationRuleValidator<List<MarcIndicator>, SpecificationFieldDto>> indicatorValidators;
private final List<SpecificationRuleValidator<List<MarcSubfield>, SpecificationFieldDto>> subfieldValidators;

public MarcRecordRuleValidator(TranslationProvider translationProvider) {
this.fieldSetValidators = List.of(
Expand All @@ -38,6 +40,7 @@ public MarcRecordRuleValidator(TranslationProvider translationProvider) {
this.indicatorValidators = List.of(
new InvalidIndicatorRuleValidator(translationProvider)
);
this.subfieldValidators = List.of(new MissingSubfieldRuleValidator(translationProvider));
}

@Override
Expand All @@ -64,6 +67,11 @@ public List<ValidationError> validate(MarcRecord marcRecord, SpecificationDto sp
validationErrors.addAll(validator.validate(field.indicators(), fieldDefinition));
}
}
for (var validator : subfieldValidators) {
if (ruleIsEnabled(validator.ruleCode(), specification) && marcField instanceof MarcDataField field) {
validationErrors.addAll(validator.validate(field.subfields(), fieldDefinition));
}
}
});
}
return validationErrors;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.folio.rspec.validation.validator.marc.impl;

import java.util.List;
import org.folio.rspec.domain.dto.DefinitionType;
import org.folio.rspec.domain.dto.SeverityType;
import org.folio.rspec.domain.dto.SpecificationFieldDto;
import org.folio.rspec.domain.dto.SubfieldDto;
import org.folio.rspec.domain.dto.ValidationError;
import org.folio.rspec.i18n.TranslationProvider;
import org.folio.rspec.utils.SpecificationUtils;
import org.folio.rspec.validation.validator.SpecificationRuleCode;
import org.folio.rspec.validation.validator.SpecificationRuleValidator;
import org.folio.rspec.validation.validator.marc.model.MarcRuleCode;
import org.folio.rspec.validation.validator.marc.model.MarcSubfield;
import org.folio.rspec.validation.validator.marc.model.Reference;
import org.springframework.util.CollectionUtils;

public class MissingSubfieldRuleValidator
implements SpecificationRuleValidator<List<MarcSubfield>, SpecificationFieldDto> {

private static final String CODE_KEY = "code";

private final TranslationProvider translationProvider;

MissingSubfieldRuleValidator(TranslationProvider translationProvider) {
this.translationProvider = translationProvider;
}

@Override
public List<ValidationError> validate(List<MarcSubfield> subfields, SpecificationFieldDto specification) {
var requiredSubFields = SpecificationUtils.requiredSubfields(specification.getSubfields());

return requiredSubFields.keySet().stream()
.filter(subFieldCode -> isMissing(subfields, subFieldCode))
.map(subFieldCode -> buildError(specification.getTag(), requiredSubFields.get(subFieldCode)))
.toList();
}

@Override
public SpecificationRuleCode supportedRule() {
return MarcRuleCode.MISSING_SUBFIELD;
}

@Override
public DefinitionType definitionType() {
return DefinitionType.SUBFIELD;
}

@Override
public SeverityType severity() {
return SeverityType.ERROR;
}

private ValidationError buildError(String tag, SubfieldDto definition) {
var message = translationProvider.format(ruleCode(), CODE_KEY);
return ValidationError.builder()
.path(Reference.forSubfield(Reference.forTag(tag), definition.getCode().charAt(0)).toString())
.definitionType(definitionType())
.definitionId(definition.getId())
.severity(severity())
.ruleCode(ruleCode())
.message(message)
.build();
}

private boolean isMissing(List<MarcSubfield> marcSubfields, Character subFieldCode) {
return CollectionUtils.isEmpty(marcSubfields) || marcSubfields.stream()
.noneMatch(subfield -> isSubfieldEquals(subfield, subFieldCode));
}

private boolean isSubfieldEquals(MarcSubfield subfield, Character subFieldCode) {
return subfield.reference() != null
&& subfield.reference().getSubfield() != null
&& subfield.reference().getSubfield().equals(subFieldCode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ public enum MarcRuleCode implements SpecificationRuleCode {
NON_REPEATABLE_1XX_FIELD("nonRepeatable1XXField"),
NON_REPEATABLE_REQUIRED_1XX_FIELD("nonRepeatableRequired1XXField"),
NON_REPEATABLE_FIELD("nonRepeatableField"),
INVALID_INDICATOR("invalidIndicator");
INVALID_INDICATOR("invalidIndicator"),
MISSING_SUBFIELD("missingSubfield");

private final String code;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.folio.rspec.domain.dto.SpecificationDto;
import org.folio.rspec.domain.dto.SpecificationFieldDto;
import org.folio.rspec.domain.dto.SpecificationRuleDto;
import org.folio.rspec.domain.dto.SubfieldDto;
import org.folio.spring.testing.type.UnitTest;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -78,4 +79,21 @@ void findField_returnsField() {
assertTrue(result.isPresent());
assertEquals("tag1", result.get().getTag());
}

@Test
void requiredSubfields_returnsRequiredSubfields() {
Map<Character, SubfieldDto> result = SpecificationUtils.requiredSubfields(
List.of(
getSubfieldDto('a', true),
getSubfieldDto('b', false),
getSubfieldDto('c', true)));

assertEquals(2, result.size());
assertTrue(result.containsKey('a'));
assertTrue(result.containsKey('c'));
}

private SubfieldDto getSubfieldDto(Character code, boolean isRequired) {
return new SubfieldDto().code(code.toString()).required(isRequired);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static org.assertj.core.api.Assertions.tuple;
import static org.folio.support.TestDataProvider.getSpecification;
import static org.folio.support.TestDataProvider.getSpecificationWithIndicators;
import static org.folio.support.TestDataProvider.getSpecificationWithSubfields;
import static org.folio.support.TestDataProvider.getSpecificationWithTags;

import java.util.stream.Stream;
Expand Down Expand Up @@ -91,6 +92,22 @@ void testInvalidIndicatorValidation() {
);
}

@Test
void testMarcRecordSubfieldValidation() {
var marc4jRecord = TestRecordProvider.getMarc4jRecord("testdata/marc-subfield-record.json");
var validationErrors = validator.validate(marc4jRecord, getSpecificationWithSubfields());
assertThat(validationErrors)
.hasSize(5)
.extracting(ValidationError::getPath, ValidationError::getRuleCode)
.containsExactlyInAnyOrder(
tuple("650[0]$d[0]", MarcRuleCode.MISSING_SUBFIELD.getCode()),
tuple("035[0]$d[0]", MarcRuleCode.MISSING_SUBFIELD.getCode()),
tuple("047[0]$d[0]", MarcRuleCode.MISSING_SUBFIELD.getCode()),
tuple("245[0]$d[0]", MarcRuleCode.MISSING_SUBFIELD.getCode()),
tuple("010[0]$d[0]", MarcRuleCode.MISSING_SUBFIELD.getCode())
);
}

private static Stream<Arguments> provide1xxArguments() {
return Stream.of(
// Multiple 1xx fields with same undefined tag
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.folio.rspec.domain.dto.SpecificationDto;
import org.folio.rspec.domain.dto.SpecificationFieldDto;
import org.folio.rspec.domain.dto.SpecificationRuleDto;
import org.folio.rspec.domain.dto.SubfieldDto;
import org.folio.rspec.validation.validator.marc.model.MarcRuleCode;

public class TestDataProvider {
Expand Down Expand Up @@ -42,6 +43,15 @@ public static SpecificationDto getSpecificationWithIndicators() {
.fields(indicatorsFieldDefinitions());
}

public static SpecificationDto getSpecificationWithSubfields() {
return new SpecificationDto()
.id(UUID.randomUUID())
.family(Family.MARC)
.profile(FamilyProfile.BIBLIOGRAPHIC)
.rules(allEnabledRules())
.fields(subfieldDefinitions());
}

private static List<SpecificationFieldDto> commonFieldDefinitions() {
List<SpecificationFieldDto> fields = new ArrayList<>();
fields.add(requiredNonRepeatableField("000"));
Expand All @@ -66,6 +76,16 @@ private static List<SpecificationFieldDto> indicatorsFieldDefinitions() {
return fields;
}

private static List<SpecificationFieldDto> subfieldDefinitions() {
return List.of(requiredNonRepeatableField("000"),
defaultFieldWithSubfields("010"),
defaultFieldWithSubfields("035"),
defaultFieldWithSubfields("047"),
defaultFieldWithSubfields("100"),
defaultFieldWithSubfields("245"),
defaultFieldWithSubfields("650"));
}

private static List<SpecificationFieldDto> fieldDefinitions() {
List<SpecificationFieldDto> fields = commonFieldDefinitions();
fields.add(defaultField("035"));
Expand Down Expand Up @@ -102,6 +122,10 @@ private static SpecificationFieldDto defaultFieldWithIndicator(String tag) {
return defaultField(tag).indicators(List.of(getIndicator(1), getIndicator(2)));
}

private static SpecificationFieldDto defaultFieldWithSubfields(String tag) {
return defaultField(tag).subfields(List.of(getSubfield("a"), getSubfield("d")));
}

private static SpecificationFieldDto fieldDefinition(String tag, boolean required, boolean deprecated,
boolean repeatable) {
return new SpecificationFieldDto()
Expand All @@ -126,4 +150,13 @@ private static FieldIndicatorDto getIndicator(int order) {
.order(order)
.addCodesItem(new IndicatorCodeDto().code("code"));
}

private static SubfieldDto getSubfield(String code) {
return new SubfieldDto()
.id(UUID.randomUUID())
.required(true)
.deprecated(false)
.repeatable(true)
.code(code);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{
"fields": [
{
"010": {
"ind1": "#",
"ind2": " ",
"subfields": [
{
"a": "LC control number"
}
]
}
},
{
"035": {
"ind1": "#",
"ind2": " ",
"subfields": [
{
"a": "System control number"
}
]
}
},
{
"047": {
"ind1": "#",
"ind2": " ",
"subfields": [
{
"a": "Form of musical composition code"
}
]
}
},
{
"100": {
"ind1": "#",
"ind2": " ",
"subfields": [
{
"a": "Personal name"
},
{
"d": "1756-1791."
},
{
"0": "12345"
}
]
}
},
{
"245": {
"ind1": "#",
"ind2": " ",
"subfields": [
{
"a": "Title"
}
]
}
},
{
"650": {
"ind1": "#",
"ind2": " ",
"subfields": [
{
"a": "Topical term"
}
]
}
}
],
"leader": "01750ccm a2200421 4500"
}
6 changes: 3 additions & 3 deletions translations/mod-record-specifications/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@
"validation.nonRepeatableRequired1XXField": "Field 1XX is non-repeatable and required.",
"validation.nonRepeatableField": "Field is non-repeatable.",
"validation.undefinedField": "Field is undefined.",
"validation.invalidIndicator": "Indicator must contain one character and can only accept numbers 0-9, letters a-z or a '#'."

}
"validation.invalidIndicator": "Indicator must contain one character and can only accept numbers 0-9, letters a-z or a '#'.",
"validation.missingSubfield": "Subfield {code} is required."
}

0 comments on commit 694c3b1

Please sign in to comment.