Matins completed
This commit is contained in:
1 parent
79ac617aa8
commit
366aea76bb
27 files changed
+3200
No files matched your search
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* Validate data files against JSON schemas.
|
||||
* Run with: npx tsx scripts/validate-data.ts [path]
|
||||
* If no path given, validates all data in static/data/
|
||||
*/
|
||||
|
||||
import { readFileSync, readdirSync, existsSync, statSync } from 'fs';
|
||||
import { join, relative } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = new URL('.', import.meta.url).pathname;
|
||||
const DATA_DIR = join(__dirname, '..', 'static', 'data');
|
||||
|
||||
interface ValidationError {
|
||||
file: string;
|
||||
path: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
class Validator {
|
||||
private errors: ValidationError[] = [];
|
||||
private warnings: ValidationError[] = [];
|
||||
private filesChecked = 0;
|
||||
|
||||
constructor(private basePath: string) {}
|
||||
|
||||
validateFile(filePath: string, type: string): void {
|
||||
const relPath = relative(this.basePath, filePath);
|
||||
try {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
const data = JSON.parse(content);
|
||||
|
||||
switch (type) {
|
||||
case 'life':
|
||||
this.validateLife(data, relPath);
|
||||
break;
|
||||
case 'lives-bundle':
|
||||
this.validateLivesBundle(data, relPath);
|
||||
break;
|
||||
case 'calendar':
|
||||
this.validateCalendar(data, relPath);
|
||||
break;
|
||||
case 'service-template':
|
||||
this.validateServiceTemplate(data, relPath);
|
||||
break;
|
||||
case 'prayer':
|
||||
this.validatePrayer(data, relPath);
|
||||
break;
|
||||
case 'command':
|
||||
this.validateCommand(data, relPath);
|
||||
break;
|
||||
case 'commemoration':
|
||||
this.validateCommemoration(data, relPath);
|
||||
break;
|
||||
case 'fasting':
|
||||
this.validateFasting(data, relPath);
|
||||
break;
|
||||
}
|
||||
this.filesChecked++;
|
||||
} catch (e) {
|
||||
this.addError(relPath, '', `Failed to parse: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
validateDir(dirPath: string, type: string): void {
|
||||
if (!existsSync(dirPath)) return;
|
||||
for (const entry of readdirSync(dirPath)) {
|
||||
const fullPath = join(dirPath, entry);
|
||||
if (entry.endsWith('.json')) {
|
||||
this.validateFile(fullPath, type);
|
||||
} else if (statSync(fullPath).isDirectory()) {
|
||||
this.validateDir(fullPath, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateLife(data: unknown, path: string): void {
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
this.addError(path, '', 'Life entry must be an object');
|
||||
return;
|
||||
}
|
||||
const obj = data as Record<string, unknown>;
|
||||
|
||||
if (!obj.name || typeof obj.name !== 'object') {
|
||||
this.addError(path, 'name', 'Missing or invalid name');
|
||||
} else {
|
||||
const name = obj.name as Record<string, unknown>;
|
||||
if (!name.nominative || typeof name.nominative !== 'string') {
|
||||
this.addError(path, 'name.nominative', 'Missing nominative');
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.rank !== undefined && (typeof obj.rank !== 'number' || obj.rank < 0 || obj.rank > 8)) {
|
||||
this.addWarning(path, 'rank', 'Rank should be 0-8');
|
||||
}
|
||||
}
|
||||
|
||||
validateLivesBundle(data: unknown, path: string): void {
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
this.addError(path, '', 'Lives bundle must be an object');
|
||||
return;
|
||||
}
|
||||
for (const [cid, entry] of Object.entries(data as Record<string, unknown>)) {
|
||||
this.validateLife(entry, `${path}[${cid}]`);
|
||||
}
|
||||
}
|
||||
|
||||
validateCalendar(data: unknown, path: string): void {
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
this.addError(path, '', 'Calendar data must be an object');
|
||||
return;
|
||||
}
|
||||
for (const [key, entry] of Object.entries(data as Record<string, unknown>)) {
|
||||
if (typeof entry !== 'object' || entry === null) {
|
||||
this.addError(path, `[${key}]`, 'Entry must be an object');
|
||||
continue;
|
||||
}
|
||||
const obj = entry as Record<string, unknown>;
|
||||
if (!obj.id || typeof obj.id !== 'string') {
|
||||
this.addError(path, `[${key}].id`, 'Missing or invalid id');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateServiceTemplate(data: unknown, path: string): void {
|
||||
if (!Array.isArray(data)) {
|
||||
this.addError(path, '', 'Service template must be an array');
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const node = data[i];
|
||||
if (typeof node !== 'object' || node === null) {
|
||||
this.addError(path, `[${i}]`, 'Node must be an object');
|
||||
continue;
|
||||
}
|
||||
const obj = node as Record<string, unknown>;
|
||||
if (!obj.type || typeof obj.type !== 'string') {
|
||||
this.addError(path, `[${i}].type`, 'Missing or invalid type');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validatePrayer(data: unknown, path: string): void {
|
||||
if (!Array.isArray(data)) {
|
||||
this.addError(path, '', 'Prayer must be an array');
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const node = data[i];
|
||||
if (typeof node !== 'object' || node === null) {
|
||||
this.addError(path, `[${i}]`, 'Node must be an object');
|
||||
continue;
|
||||
}
|
||||
const obj = node as Record<string, unknown>;
|
||||
if (!obj.type || !['TEXT', 'HEADER'].includes(obj.type as string)) {
|
||||
this.addError(path, `[${i}].type`, 'Type must be TEXT or HEADER');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateCommand(data: unknown, path: string): void {
|
||||
if (!Array.isArray(data)) {
|
||||
this.addError(path, '', 'Command must be an array');
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const node = data[i];
|
||||
if (typeof node !== 'object' || node === null) {
|
||||
this.addError(path, `[${i}]`, 'Node must be an object');
|
||||
continue;
|
||||
}
|
||||
const obj = node as Record<string, unknown>;
|
||||
if (!obj.type || !['TEXT', 'text'].includes(obj.type as string)) {
|
||||
this.addError(path, `[${i}].type`, 'Type must be TEXT or text');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateCommemoration(data: unknown, path: string): void {
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
this.addError(path, '', 'Commemoration must be an object');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
validateFasting(data: unknown, path: string): void {
|
||||
if (!Array.isArray(data)) {
|
||||
this.addError(path, '', 'Fasting rules must be an array');
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const period = data[i];
|
||||
if (typeof period !== 'object' || period === null) {
|
||||
this.addError(path, `[${i}]`, 'Period must be an object');
|
||||
continue;
|
||||
}
|
||||
const obj = period as Record<string, unknown>;
|
||||
if (!obj.cmd || typeof obj.cmd !== 'string') {
|
||||
this.addError(path, `[${i}].cmd`, 'Missing cmd');
|
||||
}
|
||||
if (!Array.isArray(obj.rules)) {
|
||||
this.addError(path, `[${i}].rules`, 'Missing rules array');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addError(file: string, path: string, message: string): void {
|
||||
this.errors.push({ file, path, message });
|
||||
}
|
||||
|
||||
addWarning(file: string, path: string, message: string): void {
|
||||
this.warnings.push({ file, path, message });
|
||||
}
|
||||
|
||||
printReport(): void {
|
||||
console.log(`\nValidated ${this.filesChecked} files.\n`);
|
||||
|
||||
if (this.errors.length > 0) {
|
||||
console.log(`ERRORS (${this.errors.length}):`);
|
||||
for (const err of this.errors) {
|
||||
console.log(` ${err.file}${err.path ? ':' + err.path : ''} - ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.warnings.length > 0) {
|
||||
console.log(`\nWARNINGS (${this.warnings.length}):`);
|
||||
for (const warn of this.warnings) {
|
||||
console.log(` ${warn.file}${warn.path ? ':' + warn.path : ''} - ${warn.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.errors.length === 0 && this.warnings.length === 0) {
|
||||
console.log('All files valid!');
|
||||
}
|
||||
|
||||
process.exit(this.errors.length > 0 ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const targetPath = process.argv[2] || DATA_DIR;
|
||||
const validator = new Validator(DATA_DIR);
|
||||
|
||||
console.log(`Validating data in: ${targetPath}\n`);
|
||||
|
||||
// Validate shared data
|
||||
const sharedDir = join(DATA_DIR, 'shared');
|
||||
if (existsSync(sharedDir)) {
|
||||
console.log('Validating shared data...');
|
||||
validator.validateDir(join(sharedDir, 'lives'), 'lives-bundle');
|
||||
validator.validateDir(join(sharedDir, 'commemorations'), 'commemoration');
|
||||
validator.validateFile(join(sharedDir, 'fasting.json'), 'fasting');
|
||||
}
|
||||
|
||||
// Validate language data (start with en)
|
||||
const enDir = join(DATA_DIR, 'en');
|
||||
if (existsSync(enDir)) {
|
||||
console.log('Validating English data...');
|
||||
validator.validateDir(join(enDir, 'lives'), 'lives-bundle');
|
||||
validator.validateDir(join(enDir, 'services', 'templates'), 'service-template');
|
||||
validator.validateDir(join(enDir, 'services', 'prayers'), 'prayer');
|
||||
validator.validateDir(join(enDir, 'services', 'commands'), 'command');
|
||||
}
|
||||
|
||||
validator.printReport();
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in new issue
Block a user