Skip to content

Commit 1dc2f4e

Browse files
committed
fixes #530
better handling during import of manifests if language maps have non-`none` objects.
1 parent 948910a commit 1dc2f4e

2 files changed

Lines changed: 129 additions & 8 deletions

File tree

test/local/utility.validation.test.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,68 @@ describe('Validation utilities', () => {
6060
const filteredObject = scrubDefaultRoles({ OWNER: ['*'], CUSTOM_ROLE: ['READ_*_*'] })
6161
assert.deepEqual(filteredObject, { CUSTOM_ROLE: ['READ_*_*'] }, 'OWNER key must be stripped from a roles map')
6262
})
63+
64+
it('accepts metadata with non-"none" language tags', () => {
65+
const payload = buildValidProjectPayload()
66+
payload.metadata = [
67+
{ label: { en: ['Author'] }, value: { en: ['John Doe'] } },
68+
{ label: { fr: ['Auteur'] }, value: { fr: ['Jean Dupont'] } },
69+
{ label: { la: ['Auctor'] }, value: { la: ['Iohannes'] } }
70+
]
71+
72+
const result = validateProjectPayload(payload)
73+
74+
assert.equal(result.isValid, true)
75+
assert.equal(result.errors, null)
76+
})
77+
78+
it('accepts metadata with mixed language tags and plain strings', () => {
79+
const payload = buildValidProjectPayload()
80+
payload.metadata = [
81+
{ label: 'Source', value: 'Test Source' },
82+
{ label: { en: ['Title'] }, value: 'Plain String Value' },
83+
{ label: 'Date', value: { en: ['2024-01-01'] } }
84+
]
85+
86+
const result = validateProjectPayload(payload)
87+
88+
assert.equal(result.isValid, true)
89+
assert.equal(result.errors, null)
90+
})
91+
92+
it('rejects metadata with empty language map', () => {
93+
const payload = buildValidProjectPayload()
94+
payload.metadata = [
95+
{ label: {}, value: 'test value' }
96+
]
97+
98+
const result = validateProjectPayload(payload)
99+
100+
assert.equal(result.isValid, false)
101+
assert.match(result.errors ?? '', /language map/)
102+
})
103+
104+
it('rejects metadata with language map containing non-string values', () => {
105+
const payload = buildValidProjectPayload()
106+
payload.metadata = [
107+
{ label: { en: ['Author'] }, value: { en: [123] } }
108+
]
109+
110+
const result = validateProjectPayload(payload)
111+
112+
assert.equal(result.isValid, false)
113+
assert.match(result.errors ?? '', /language map/)
114+
})
115+
116+
it('rejects metadata with language map containing empty array', () => {
117+
const payload = buildValidProjectPayload()
118+
payload.metadata = [
119+
{ label: { en: ['Author'] }, value: { en: [] } }
120+
]
121+
122+
const result = validateProjectPayload(payload)
123+
124+
assert.equal(result.isValid, false)
125+
assert.match(result.errors ?? '', /language map/)
126+
})
63127
})

utilities/validatePayload.js

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,63 @@
1+
/**
2+
* Validates that a language map object (per IIIF spec) has at least one language key
3+
* with an array of string values.
4+
*
5+
* @param {Object} langMap - Object keyed by IETF language tag (e.g., { "en": ["value"], "fr": ["valeur"] }).
6+
* @returns {boolean} - True if the language map is valid.
7+
*/
8+
function validateLanguageMap(langMap) {
9+
if (typeof langMap !== 'object' || langMap === null)
10+
return false
11+
12+
const keys = Object.keys(langMap)
13+
if (keys.length === 0)
14+
return false
15+
16+
return keys.some(key => {
17+
const values = langMap[key]
18+
return Array.isArray(values) && values.length > 0 && values.every(v => typeof v === 'string')
19+
})
20+
}
21+
22+
/**
23+
* Validates a metadata label or value field that can be a plain string or a language map.
24+
*
25+
* @param {string} fieldName - The field name for error messages ("label" or "value").
26+
* @param {*} fieldValue - The value to validate.
27+
* @param {boolean} allowEmpty - Whether empty strings are permitted (true for values, false for labels).
28+
* @returns {Object|null} - Returns { isValid: false, errors: string } on failure, or null on success.
29+
*/
30+
function validateMetadataField(fieldName, fieldValue, allowEmpty) {
31+
const isString = typeof fieldValue === 'string'
32+
const isLangMap = typeof fieldValue === 'object' && fieldValue !== null
33+
34+
// Invalid type entirely
35+
if (!isString && !isLangMap) {
36+
const message = fieldName === 'label'
37+
? `metadata item label must be a string or language map object`
38+
: 'metadata item value cannot be processed'
39+
return { isValid: false, errors: message }
40+
}
41+
42+
// Plain string
43+
if (isString) {
44+
if (!allowEmpty && fieldValue.trim() === '') {
45+
return { isValid: false, errors: `metadata item ${fieldName} must be a non-empty string` }
46+
}
47+
return null
48+
}
49+
50+
// Language map
51+
if (!validateLanguageMap(fieldValue)) {
52+
const message = allowEmpty
53+
? `metadata item ${fieldName} must be a valid language map with string values`
54+
: `metadata item ${fieldName} must be a valid language map with non-empty string values`
55+
return { isValid: false, errors: message }
56+
}
57+
58+
return null
59+
}
60+
161
/**
262
* Validate that the provided data payload is a valid Project object.
363
* This function validates both the presence and the data types of required project properties.
@@ -49,15 +109,12 @@ export function validateProjectPayload(payload) {
49109
return { isValid: false, errors: 'metadata item must have label and value properties' }
50110
}
51111

52-
// Validate label and value are non-empty strings
53-
if (typeof metadataItem.label?.none?.[0] === 'string' && metadataItem.label?.none?.[0].trim() === '')
54-
return { isValid: false, errors: 'metadata item label must be a non-empty language map' }
55-
56-
if (typeof metadataItem.label === 'string' && metadataItem.label.trim() === '')
57-
return { isValid: false, errors: 'metadata item label must be a non-empty string' }
112+
// Validate label and value - accept plain string or language-mapped object (any IETF language tag)
113+
const labelError = validateMetadataField('label', metadataItem.label, false)
114+
if (labelError) return labelError
58115

59-
if (typeof metadataItem.value?.none?.[0] !== 'string' && typeof metadataItem.value !== 'string')
60-
return { isValid: false, errors: 'metadata item value cannot be processed' }
116+
const valueError = validateMetadataField('value', metadataItem.value, true)
117+
if (valueError) return valueError
61118

62119
// Ensure no extra properties beyond label and value
63120
const allowedProps = ['label', 'value']

0 commit comments

Comments
 (0)