All files / src/schema normalizeSchema.ts

97.87% Statements 46/47
87.09% Branches 27/31
100% Functions 14/14
97.87% Lines 46/47

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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                          94x       91x               62x 13x     49x 11x     38x 1x     37x       90x 74x     16x 1x     15x   15x 27x   27x 1x     26x                     29x 29x   29x 94x 3x   3x         12x       3x     91x 1x     90x   90x 94x           94x 94x 94x     29x   82x             21x   21x 68x   68x       68x       21x         1x 3x 3x    
import type {
  FieldSchema,
  FormSchema,
  FormValues,
  NormalizedField,
  NormalizedFormSchema,
  NormalizedSchemaNode,
  NormalizedSection,
  SchemaNode,
  SectionSchema
} from "./types";
 
function isSection(node: SchemaNode): node is SectionSchema {
  return node.type === "section";
}
 
function normalizeId(value: string): string {
  return value
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9_-]+/g, "-")
    .replace(/^-+|-+$/g, "");
}
 
function defaultValueForField(field: FieldSchema) {
  if (field.defaultValue !== undefined) {
    return field.defaultValue;
  }
 
  if (field.type === "checkbox") {
    return false;
  }
 
  if (field.type === "number") {
    return null;
  }
 
  return "";
}
 
function validateOptionField(field: FieldSchema, schemaId: string): void {
  if (field.type !== "select" && field.type !== "radio") {
    return;
  }
 
  if (!field.options || field.options.length === 0) {
    throw new Error(`Field "${field.name}" in schema "${schemaId}" requires at least one option.`);
  }
 
  const optionValues = new Set<string>();
 
  field.options.forEach((option) => {
    const optionValue = String(option.value);
 
    if (optionValues.has(optionValue)) {
      throw new Error(`Field "${field.name}" in schema "${schemaId}" has duplicate option value "${optionValue}".`);
    }
 
    optionValues.add(optionValue);
  });
}
 
/**
 * Prepares a public schema for rendering and validation.
 *
 * Normalization keeps consumer-facing schema data unchanged while deriving
 * stable IDs, traversal paths, field lookup maps, and duplicate/option checks.
 */
export function normalizeSchema(schema: FormSchema): NormalizedFormSchema {
  const fieldMap = new Map<string, NormalizedField>();
  const fieldOrder: string[] = [];
 
  const normalizeNode = (node: SchemaNode, path: string[]): NormalizedSchemaNode => {
    if (isSection(node)) {
      const sectionId = node.id ? normalizeId(node.id) : normalizeId([...path, node.title].join("-"));
 
      const normalizedSection: NormalizedSection = {
        ...node,
        id: sectionId,
        path,
        fields: node.fields.map((child, index) =>
          normalizeNode(child, [...path, sectionId || `section-${index}`])
        )
      };
 
      return normalizedSection;
    }
 
    if (fieldMap.has(node.name)) {
      throw new Error(`Duplicate field name "${node.name}" in schema "${schema.id}".`);
    }
 
    validateOptionField(node, schema.id);
 
    const fieldId = node.id ? normalizeId(node.id) : normalizeId(node.name);
    const normalizedField: NormalizedField = {
      ...node,
      id: fieldId,
      path
    };
 
    fieldMap.set(node.name, normalizedField);
    fieldOrder.push(node.name);
    return normalizedField;
  };
 
  return {
    ...schema,
    fields: schema.fields.map((node, index) => normalizeNode(node, [schema.id, String(index)])),
    fieldMap,
    fieldOrder
  };
}
 
export function getInitialValues(schema: NormalizedFormSchema, values: FormValues = {}): FormValues {
  const initialValues: FormValues = {};
 
  schema.fieldOrder.forEach((fieldName) => {
    const field = schema.fieldMap.get(fieldName);
 
    Iif (!field) {
      return;
    }
 
    initialValues[fieldName] =
      values[fieldName] !== undefined ? values[fieldName] : defaultValueForField(field);
  });
 
  return initialValues;
}
 
/** Return all normalized value-producing fields in schema order. */
export function flattenFields(schema: NormalizedFormSchema): NormalizedField[] {
  return schema.fieldOrder
    .map((fieldName) => schema.fieldMap.get(fieldName))
    .filter((field): field is NormalizedField => Boolean(field));
}