src/lib/form/adapters/adapter-registry.ts

Total Symbols
6
Lines of Code
213
Avg Complexity
4.8
Avg Coverage
100.0%

File Relationships

graph LR getAdapter["getAdapter"] getAdapterMap["getAdapterMap"] fillCustomComponent["fillCustomComponent"] extractCustomComponentValue["extractCustomComponentValue"] getAdapter -->|calls| getAdapterMap fillCustomComponent -->|calls| getAdapter extractCustomComponentValue -->|calls| getAdapter click getAdapter "../symbols/fb0f4a5fd9ac2a4b.html" click getAdapterMap "../symbols/9972c116451a41d5.html" click fillCustomComponent "../symbols/fb27203fbaa0cd9b.html" click extractCustomComponentValue "../symbols/e6339fe1023108e6.html"

Architecture violations

View all

  • [warning] max-cyclomatic-complexity: 'extractCustomComponentValue' has cyclomatic complexity 13 (max 10)

Symbols by Kind

function 6

All Symbols

Name Kind Visibility Status Lines Signature
getAdapterMap function - 71-76 getAdapterMap(): : Map<AdapterName, CustomComponentAdapter>
registerAdapter function exported- 84-88 registerAdapter(adapter: CustomComponentAdapter): : void
getAdapter function exported- 93-97 getAdapter( name: AdapterName, ): : CustomComponentAdapter | undefined
detectCustomComponents function exported- 105-133 detectCustomComponents(): : FormField[]
fillCustomComponent function exported- 140-168 fillCustomComponent( field: FormField, value: string, ): : Promise<boolean>
extractCustomComponentValue function exported- 175-212 extractCustomComponentValue(field: FormField): : string | null

Full Source

/**
 * Custom Component Adapter Registry
 *
 * Central registry for all custom component adapters (Select2, Ant Design, etc.).
 * Adapters register here and the registry is consumed by:
 *   - form-detector.ts (page-level detection)
 *   - form-filler.ts (custom fill delegation)
 *
 * The registry provides:
 *   - detectAll()  — scans the page for custom components across all adapters
 *   - getAdapter() — retrieves a specific adapter by name for fill-time delegation
 *
 * To add a new adapter:
 *   1. Implement CustomComponentAdapter
 *   2. Import and add to ADAPTER_REGISTRY array below
 */

import type { FormField } from "@/types";
import type { CustomComponentAdapter, AdapterName } from "./adapter.interface";
import { createLogger } from "@/lib/logger";

// ── Concrete Adapters ─────────────────────────────────────────────────────────
import { select2Adapter } from "./select2";
import { reactSelectAdapter } from "./react-select";
import {
  antdAutoCompleteAdapter,
  antdCascaderAdapter,
  antdCheckboxAdapter,
  antdDatepickerAdapter,
  antdInputAdapter,
  antdRadioAdapter,
  antdRateAdapter,
  antdSelectAdapter,
  antdSliderAdapter,
  antdSwitchAdapter,
  antdTransferAdapter,
  antdTreeSelectAdapter,
} from "./antd";

const log = createLogger("AdapterRegistry");

// ── Registry ──────────────────────────────────────────────────────────────────

/**
 * All registered adapters — import concrete adapters and add them here.
 * Order matters: first adapter to claim an element wins.
 *
 * Ant Design adapters are ordered from most specific (cascader, tree-select)
 * to most generic (input, auto-complete) to avoid false positives.
 */
const ADAPTER_REGISTRY: CustomComponentAdapter[] = [
  select2Adapter,
  reactSelectAdapter,
  antdCascaderAdapter,
  antdTreeSelectAdapter,
  antdSelectAdapter,
  antdAutoCompleteAdapter,
  antdDatepickerAdapter,
  antdInputAdapter,
  antdRadioAdapter,
  antdCheckboxAdapter,
  antdSwitchAdapter,
  antdSliderAdapter,
  antdRateAdapter,
  antdTransferAdapter,
];

/** Fast lookup by adapter name. Built lazily from ADAPTER_REGISTRY. */
let _adapterMap: Map<AdapterName, CustomComponentAdapter> | null = null;

function getAdapterMap(): Map<AdapterName, CustomComponentAdapter> {
  if (!_adapterMap) {
    _adapterMap = new Map(ADAPTER_REGISTRY.map((a) => [a.name, a]));
  }
  return _adapterMap;
}

// ── Public API ────────────────────────────────────────────────────────────────

/**
 * Registers a new adapter at runtime.
 * Useful for lazy-loading or conditionally adding adapters.
 */
export function registerAdapter(adapter: CustomComponentAdapter): void {
  ADAPTER_REGISTRY.push(adapter);
  _adapterMap = null; // invalidate cache
  log.debug(`Adapter registrado: "${adapter.name}"`);
}

/**
 * Returns the adapter matching the given name, or undefined.
 */
export function getAdapter(
  name: AdapterName,
): CustomComponentAdapter | undefined {
  return getAdapterMap().get(name);
}

/**
 * Scans the page for all custom components across registered adapters.
 * Returns FormField stubs (fieldType = "unknown") ready for classification.
 *
 * Each element is claimed by the first matching adapter only (no duplicates).
 */
export function detectCustomComponents(): FormField[] {
  if (ADAPTER_REGISTRY.length === 0) return [];

  const claimed = new WeakSet<HTMLElement>();
  const fields: FormField[] = [];

  for (const adapter of ADAPTER_REGISTRY) {
    const candidates = document.querySelectorAll<HTMLElement>(adapter.selector);

    for (const el of candidates) {
      if (claimed.has(el)) continue;

      if (!adapter.matches(el)) continue;

      claimed.add(el);

      try {
        const field = adapter.buildField(el);
        fields.push(field);
        log.debug(`[${adapter.name}] campo detectado: ${field.selector}`);
      } catch (err) {
        log.warn(`[${adapter.name}] Erro ao construir campo:`, err);
      }
    }
  }

  log.info(`${fields.length} componente(s) customizado(s) detectado(s)`);
  return fields;
}

/**
 * Fills a custom component field using its adapter.
 * Returns true if the adapter handled the fill, false otherwise.
 * Supports both sync and async adapters.
 */
export async function fillCustomComponent(
  field: FormField,
  value: string,
): Promise<boolean> {
  const adapterName = field.adapterName as AdapterName | undefined;
  if (!adapterName) return false;

  const adapter = getAdapter(adapterName);
  if (!adapter) {
    log.warn(`Adapter "${adapterName}" não encontrado para preenchimento`);
    return false;
  }

  try {
    const result = await adapter.fill(field.element as HTMLElement, value);
    if (!result) {
      log.warn(
        `[${adapter.name}] fill() retornou false para: ${field.selector}`,
      );
    }
    return result;
  } catch (err) {
    log.warn(
      `[${adapter.name}] Erro ao preencher campo ${field.selector}:`,
      err,
    );
    return false;
  }
}

/**
 * Attempts to extract a string value from a custom component field.
 * Returns `null` if the adapter does not provide an extractor or if
 * extraction failed.
 */
export function extractCustomComponentValue(field: FormField): string | null {
  const adapterName = field.adapterName as AdapterName | undefined;
  if (!adapterName) return null;

  const adapter = getAdapter(adapterName);
  if (!adapter) return null;

  // Try adapter-provided extractor first
  if (typeof adapter.extractValue === "function") {
    try {
      const result = adapter.extractValue(field.element as HTMLElement);
      if (result !== null && result !== undefined) return result;
    } catch (err) {
      log.warn(
        `[${adapter.name}] Erro ao extrair valor do campo ${field.selector}:`,
        err,
      );
      // fallthrough to generic fallback
    }
  }

  // Generic fallback: look for a native input/select/textarea inside the wrapper.
  const native = field.element.querySelector<
    HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
  >("input,textarea,select");
  if (native) {
    if (native instanceof HTMLSelectElement) return native.value;
    if (native instanceof HTMLInputElement) {
      if (native.type === "checkbox" || native.type === "radio") {
        return native.checked ? "true" : "false";
      }
      return native.value;
    }
    return native.value;
  }

  return null;
}