87 lines
3.0 KiB
TypeScript
87 lines
3.0 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import type { ScannerLearningLoadResult, ScannerLearningRepositoryPort, ScannerLearningRules, ScannerLearningSaveResult } from "./contracts.js";
|
|
|
|
export class ScannerLearningRepository implements ScannerLearningRepositoryPort {
|
|
private readonly filePath: string;
|
|
|
|
constructor(userDataPath: string, fileName = "scanner-learning.json") {
|
|
this.filePath = path.join(userDataPath, fileName);
|
|
}
|
|
|
|
async load(): Promise<ScannerLearningLoadResult> {
|
|
try {
|
|
const raw = await fs.readFile(this.filePath, "utf8");
|
|
const parsed = JSON.parse(raw) as ScannerLearningRules;
|
|
return {
|
|
ok: true,
|
|
path: this.filePath,
|
|
rules: parsed && typeof parsed === "object" ? parsed : emptyRules(),
|
|
};
|
|
} catch {
|
|
return { ok: true, path: this.filePath, rules: emptyRules() };
|
|
}
|
|
}
|
|
|
|
async save(rules: ScannerLearningRules): Promise<ScannerLearningSaveResult> {
|
|
const current = await this.load();
|
|
const payload: ScannerLearningRules = mergeRules(current.rules, rules);
|
|
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
|
|
await fs.writeFile(this.filePath, JSON.stringify(payload, null, 2), "utf8");
|
|
return { ok: true, path: this.filePath, rules: payload, total: countRules(payload) };
|
|
}
|
|
}
|
|
|
|
function emptyRules(): ScannerLearningRules {
|
|
return {
|
|
textReplacements: {},
|
|
fieldAliases: {},
|
|
constrainedFixes: {},
|
|
cropAdjustments: {},
|
|
uiProfileAdjustments: {},
|
|
};
|
|
}
|
|
|
|
function mergeRules(current: ScannerLearningRules, incoming: ScannerLearningRules): ScannerLearningRules {
|
|
return {
|
|
textReplacements: {
|
|
...(current.textReplacements ?? {}),
|
|
...(incoming.textReplacements ?? {}),
|
|
},
|
|
fieldAliases: mergeNested(current.fieldAliases, incoming.fieldAliases),
|
|
constrainedFixes: {
|
|
...(current.constrainedFixes ?? {}),
|
|
...(incoming.constrainedFixes ?? {}),
|
|
},
|
|
cropAdjustments: {
|
|
...(current.cropAdjustments ?? {}),
|
|
...(incoming.cropAdjustments ?? {}),
|
|
},
|
|
uiProfileAdjustments: {
|
|
...(current.uiProfileAdjustments ?? {}),
|
|
...(incoming.uiProfileAdjustments ?? {}),
|
|
},
|
|
};
|
|
}
|
|
|
|
function mergeNested(
|
|
current: Record<string, Record<string, string>> | undefined,
|
|
incoming: Record<string, Record<string, string>> | undefined,
|
|
) {
|
|
const merged: Record<string, Record<string, string>> = {};
|
|
for (const [field, values] of Object.entries(current ?? {})) merged[field] = { ...(values ?? {}) };
|
|
for (const [field, values] of Object.entries(incoming ?? {})) merged[field] = { ...(merged[field] ?? {}), ...(values ?? {}) };
|
|
return merged;
|
|
}
|
|
|
|
function countRules(rules: ScannerLearningRules) {
|
|
const fieldAliases = Object.values(rules.fieldAliases ?? {}).reduce((sum, aliases) => sum + Object.keys(aliases ?? {}).length, 0);
|
|
return (
|
|
Object.keys(rules.textReplacements ?? {}).length
|
|
+ fieldAliases
|
|
+ Object.keys(rules.constrainedFixes ?? {}).length
|
|
+ Object.keys(rules.cropAdjustments ?? {}).length
|
|
+ Object.keys(rules.uiProfileAdjustments ?? {}).length
|
|
);
|
|
}
|