Implement CE-WP-0012 interim catalog (accepted defaults): widgets, options editor, amount JSON encoding, textarea→text-long normalize.
220 lines
5.9 KiB
TypeScript
220 lines
5.9 KiB
TypeScript
/**
|
|
* Attribute value types for Capture (CE-WP-0012).
|
|
*
|
|
* Interim consumer catalog aligned with
|
|
* `citation-evidence/wiki/AttributeValueTypes-proposal.md` (accepted defaults).
|
|
* Long-term owner: InfoTechCanon AttributeValueType catalog (ITC-WP-0013).
|
|
*/
|
|
|
|
/** Implemented MVP types + legacy alias `textarea`. */
|
|
export type AttributeValueType =
|
|
| "text"
|
|
| "text-long"
|
|
| "textarea" // legacy alias of text-long
|
|
| "date"
|
|
| "time"
|
|
| "datetime"
|
|
| "amount"
|
|
| "one-of"
|
|
| "some-of";
|
|
|
|
/** Reserved in the proposal; not rendered yet. */
|
|
export type ReservedAttributeValueType =
|
|
| "boolean"
|
|
| "integer"
|
|
| "number"
|
|
| "uri"
|
|
| "identifier";
|
|
|
|
export interface AttributeOption {
|
|
readonly id: string;
|
|
readonly label: string;
|
|
}
|
|
|
|
export interface FormFieldSchema {
|
|
readonly type: AttributeValueType;
|
|
readonly id: string;
|
|
readonly label: string;
|
|
/** CodeList options for one-of / some-of. */
|
|
readonly options?: readonly AttributeOption[];
|
|
/** Default ISO 4217 code for amount attributes. */
|
|
readonly defaultCurrency?: string;
|
|
}
|
|
|
|
export interface FormSchema {
|
|
readonly id: string;
|
|
readonly title: string;
|
|
readonly fields: readonly FormFieldSchema[];
|
|
}
|
|
|
|
export const IMPLEMENTED_FIELD_TYPES: readonly {
|
|
value: AttributeValueType;
|
|
label: string;
|
|
}[] = [
|
|
{ value: "text", label: "Text" },
|
|
{ value: "text-long", label: "Long text" },
|
|
{ value: "date", label: "Date" },
|
|
{ value: "time", label: "Time" },
|
|
{ value: "datetime", label: "Date & time" },
|
|
{ value: "amount", label: "Amount" },
|
|
{ value: "one-of", label: "One of" },
|
|
{ value: "some-of", label: "Some of" },
|
|
];
|
|
|
|
const KNOWN_TYPES = new Set<string>([
|
|
...IMPLEMENTED_FIELD_TYPES.map((t) => t.value),
|
|
"textarea",
|
|
]);
|
|
|
|
export function isAttributeValueType(value: unknown): value is AttributeValueType {
|
|
return typeof value === "string" && KNOWN_TYPES.has(value);
|
|
}
|
|
|
|
/** Normalize legacy wire ids. */
|
|
export function normalizeAttributeType(type: AttributeValueType): AttributeValueType {
|
|
return type === "textarea" ? "text-long" : type;
|
|
}
|
|
|
|
export function displayTypeLabel(type: AttributeValueType): string {
|
|
const n = normalizeAttributeType(type);
|
|
return IMPLEMENTED_FIELD_TYPES.find((t) => t.value === n)?.label ?? n;
|
|
}
|
|
|
|
export function typeNeedsOptions(type: AttributeValueType): boolean {
|
|
const n = normalizeAttributeType(type);
|
|
return n === "one-of" || n === "some-of";
|
|
}
|
|
|
|
export interface AmountValue {
|
|
readonly value: string;
|
|
readonly currency: string;
|
|
}
|
|
|
|
export const DEFAULT_CURRENCY = "EUR";
|
|
|
|
export function parseAmountValue(raw: string | undefined): AmountValue {
|
|
if (!raw || raw.trim().length === 0) {
|
|
return { value: "", currency: DEFAULT_CURRENCY };
|
|
}
|
|
try {
|
|
const parsed = JSON.parse(raw) as unknown;
|
|
if (typeof parsed === "object" && parsed !== null) {
|
|
const o = parsed as Record<string, unknown>;
|
|
return {
|
|
value: typeof o.value === "string" ? o.value : String(o.value ?? ""),
|
|
currency:
|
|
typeof o.currency === "string" && o.currency.length > 0
|
|
? o.currency
|
|
: DEFAULT_CURRENCY,
|
|
};
|
|
}
|
|
} catch {
|
|
// plain "1500 EUR" or bare number
|
|
const m = raw.trim().match(/^([0-9]+(?:\.[0-9]+)?)\s*([A-Za-z]{3})?$/);
|
|
if (m) {
|
|
return {
|
|
value: m[1] ?? "",
|
|
currency: (m[2] ?? DEFAULT_CURRENCY).toUpperCase(),
|
|
};
|
|
}
|
|
}
|
|
return { value: raw, currency: DEFAULT_CURRENCY };
|
|
}
|
|
|
|
export function serializeAmountValue(amount: AmountValue): string {
|
|
return JSON.stringify({
|
|
value: amount.value,
|
|
currency: amount.currency || DEFAULT_CURRENCY,
|
|
});
|
|
}
|
|
|
|
export function parseSomeOfValue(raw: string | undefined): readonly string[] {
|
|
if (!raw || raw.trim().length === 0) return [];
|
|
try {
|
|
const parsed = JSON.parse(raw) as unknown;
|
|
if (Array.isArray(parsed)) {
|
|
return parsed.filter((x): x is string => typeof x === "string");
|
|
}
|
|
} catch {
|
|
// single id without JSON
|
|
return [raw];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
export function serializeSomeOfValue(ids: readonly string[]): string {
|
|
return JSON.stringify([...ids]);
|
|
}
|
|
|
|
/** slug-ish option id from label */
|
|
export function optionIdFromLabel(label: string, used: ReadonlySet<string>): string {
|
|
let base = label
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-+|-+$/g, "");
|
|
if (!base) base = "opt";
|
|
let id = base;
|
|
let n = 2;
|
|
while (used.has(id)) {
|
|
id = `${base}-${n}`;
|
|
n += 1;
|
|
}
|
|
return id;
|
|
}
|
|
|
|
/**
|
|
* Parse options editor text: one option per line.
|
|
* `id|label` or bare `label` (id auto-derived).
|
|
*/
|
|
export function parseOptionsText(text: string): readonly AttributeOption[] {
|
|
const used = new Set<string>();
|
|
const out: AttributeOption[] = [];
|
|
for (const line of text.split("\n")) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) continue;
|
|
const pipe = trimmed.indexOf("|");
|
|
let id: string;
|
|
let label: string;
|
|
if (pipe >= 0) {
|
|
id = trimmed.slice(0, pipe).trim();
|
|
label = trimmed.slice(pipe + 1).trim();
|
|
if (!id) id = optionIdFromLabel(label, used);
|
|
} else {
|
|
label = trimmed;
|
|
id = optionIdFromLabel(label, used);
|
|
}
|
|
if (!label) continue;
|
|
used.add(id);
|
|
out.push({ id, label });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function formatOptionsText(options: readonly AttributeOption[] | undefined): string {
|
|
if (!options || options.length === 0) return "";
|
|
return options.map((o) => `${o.id}|${o.label}`).join("\n");
|
|
}
|
|
|
|
export function normalizeFormField(field: FormFieldSchema): FormFieldSchema {
|
|
const type = normalizeAttributeType(field.type);
|
|
const base: FormFieldSchema = {
|
|
id: field.id,
|
|
label: field.label,
|
|
type,
|
|
};
|
|
if (typeNeedsOptions(type) && field.options) {
|
|
return { ...base, options: field.options };
|
|
}
|
|
if (type === "amount" && field.defaultCurrency) {
|
|
return { ...base, defaultCurrency: field.defaultCurrency };
|
|
}
|
|
return base;
|
|
}
|
|
|
|
export function normalizeFormSchema(schema: FormSchema): FormSchema {
|
|
return {
|
|
...schema,
|
|
fields: schema.fields.map(normalizeFormField),
|
|
};
|
|
}
|