64 lines
1.8 KiB
TypeScript
64 lines
1.8 KiB
TypeScript
import { z } from 'zod'
|
|
|
|
const reservedOptionValues = new Set([
|
|
'answer',
|
|
'answers',
|
|
'label',
|
|
'option',
|
|
'options',
|
|
'question',
|
|
])
|
|
|
|
function normalized(value: string) {
|
|
return value.trim().toLocaleLowerCase().replace(/\s+/g, ' ')
|
|
}
|
|
|
|
export const CoauthorQuestionSchema = z.object({
|
|
question: z.string().trim().min(5).max(300),
|
|
options: z.array(z.string().trim().min(2).max(100)).length(3),
|
|
}).superRefine((value, context) => {
|
|
const question = normalized(value.question)
|
|
const seen = new Set<string>()
|
|
|
|
value.options.forEach((option, index) => {
|
|
const candidate = normalized(option)
|
|
if (!/[\p{L}\p{N}]/u.test(candidate) || reservedOptionValues.has(candidate)) {
|
|
context.addIssue({
|
|
code: 'custom',
|
|
message: 'Each option must be a meaningful answer.',
|
|
path: ['options', index],
|
|
})
|
|
}
|
|
if (seen.has(candidate)) {
|
|
context.addIssue({
|
|
code: 'custom',
|
|
message: 'Answer options must be distinct.',
|
|
path: ['options', index],
|
|
})
|
|
}
|
|
if (candidate === question || (candidate.length >= 20 && question.includes(candidate))) {
|
|
context.addIssue({
|
|
code: 'custom',
|
|
message: 'An answer option must not repeat the question.',
|
|
path: ['options', index],
|
|
})
|
|
}
|
|
seen.add(candidate)
|
|
})
|
|
})
|
|
|
|
export const StoredCoauthorQuestionSchema = z.object({
|
|
id: z.string().trim().min(1).max(80),
|
|
label: z.string().trim().min(5).max(300),
|
|
options: CoauthorQuestionSchema.shape.options,
|
|
}).superRefine((value, context) => {
|
|
const result = CoauthorQuestionSchema.safeParse({ question: value.label, options: value.options })
|
|
for (const issue of result.error?.issues ?? []) {
|
|
context.addIssue({
|
|
code: 'custom',
|
|
message: issue.message,
|
|
path: issue.path[0] === 'question' ? ['label', ...issue.path.slice(1)] : issue.path,
|
|
})
|
|
}
|
|
})
|