Spaces:
Running
Running
File size: 5,219 Bytes
aa89cb5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | import {
JsonFieldNamingProfile,
renderJsonFieldName,
} from './object-location';
import type { CheckResult } from './i18n';
export interface WireFieldMetadata {
readonly canonicalName: string;
readonly wireName: string;
readonly aliases: readonly string[];
}
export interface WireEntityMetadata {
readonly entityType: string;
readonly profile: JsonFieldNamingProfile;
readonly fields: Readonly<Record<string, WireFieldMetadata>>;
}
export interface NormalizedWireInput {
/** Values keyed only by canonical KSML property names. */
readonly values: Readonly<Record<string, unknown>>;
/** Exact submitted RFC 6901 pointer, retained only for accepted aliases. */
readonly sourceInstancePaths: Readonly<Record<string, string>>;
}
export class WireInputError extends Error {
constructor(
public readonly code: 'WIRE_UNKNOWN_FIELD' | 'WIRE_FIELD_COLLISION',
public readonly instancePath: string,
message: string,
) {
super(message);
this.name = 'WireInputError';
}
}
export function createWireEntityMetadata(
entityType: string,
canonicalFields: readonly string[],
profile: JsonFieldNamingProfile = 'camelCase',
aliases: Readonly<Record<string, readonly string[]>> = {},
): WireEntityMetadata {
const fields: Record<string, WireFieldMetadata> = {};
const spellings = new Map<string, string>();
for (const canonicalName of canonicalFields) {
const wireName = renderJsonFieldName(canonicalName, profile);
const fieldAliases = [...(aliases[canonicalName] ?? [])];
for (const spelling of [wireName, ...fieldAliases]) {
const existing = spellings.get(spelling);
if (existing && existing !== canonicalName) {
throw new Error(
`Wire field spelling '${spelling}' maps to both '${existing}' and '${canonicalName}'`,
);
}
spellings.set(spelling, canonicalName);
}
fields[canonicalName] = Object.freeze({
canonicalName,
wireName,
aliases: Object.freeze(fieldAliases),
});
}
return Object.freeze({ entityType, profile, fields: Object.freeze(fields) });
}
/**
* Normalize one JSON object before checker or mutation execution.
*
* No serializer naming policy is consulted. The generated field map is the
* complete authority, and two submitted spellings for one canonical field are
* always rejected, even when their values happen to be equal.
*/
export function normalizeWireInput(
input: Readonly<Record<string, unknown>>,
metadata: WireEntityMetadata,
parentPointer = '',
): NormalizedWireInput {
const lookup = new Map<string, WireFieldMetadata>();
for (const field of Object.values(metadata.fields)) {
lookup.set(field.wireName, field);
for (const alias of field.aliases) lookup.set(alias, field);
}
const values: Record<string, unknown> = {};
const sourceInstancePaths: Record<string, string> = {};
const submitted = new Map<string, string>();
for (const [submittedName, value] of Object.entries(input)) {
const pointer = `${parentPointer}/${escapeJsonPointer(submittedName)}`;
const field = lookup.get(submittedName);
if (!field) {
throw new WireInputError(
'WIRE_UNKNOWN_FIELD', pointer,
`Unknown ${metadata.entityType} field '${submittedName}'`,
);
}
const previous = submitted.get(field.canonicalName);
if (previous !== undefined) {
throw new WireInputError(
'WIRE_FIELD_COLLISION', pointer,
`Fields '${previous}' and '${submittedName}' both map to canonical field '${field.canonicalName}'`,
);
}
submitted.set(field.canonicalName, submittedName);
values[field.canonicalName] = value;
if (submittedName !== field.wireName) {
sourceInstancePaths[field.canonicalName] = pointer;
}
}
return Object.freeze({
values: Object.freeze(values),
sourceInstancePaths: Object.freeze(sourceInstancePaths),
});
}
/** Render canonical mutation values to the selected external profile. */
export function encodeWireOutput(
values: Readonly<Record<string, unknown>>,
metadata: WireEntityMetadata,
): Readonly<Record<string, unknown>> {
const output: Record<string, unknown> = {};
for (const [canonicalName, value] of Object.entries(values)) {
const field = metadata.fields[canonicalName];
if (!field) {
throw new Error(`Unknown canonical ${metadata.entityType} field '${canonicalName}'`);
}
output[field.wireName] = value;
}
return Object.freeze(output);
}
/** Add submitted alias provenance to checker results without changing location identity. */
export function retainSubmittedPaths(
results: readonly CheckResult[],
normalized: NormalizedWireInput,
): CheckResult[] {
return results.map(result => {
const firstProperty = result.location.segments.find(segment => segment.kind === 'property');
if (!firstProperty || firstProperty.kind !== 'property') return { ...result };
const sourceInstancePath = normalized.sourceInstancePaths[firstProperty.name];
return sourceInstancePath === undefined ? { ...result } : { ...result, sourceInstancePath };
});
}
function escapeJsonPointer(value: string): string {
return value.replace(/~/g, '~0').replace(/\//g, '~1');
}
|