input-spec
Version: 
Zero-dependency TypeScript implementation of the Dynamic Input Field Specification Protocol with framework integration support
936 lines (930 loc) • 31.5 kB
JavaScript
// src/types/index.ts
function isInputFieldSpec(obj) {
  if (!obj || typeof obj !== "object")
    return false;
  if (!["STRING", "NUMBER", "DATE", "BOOLEAN"].includes(obj.dataType))
    return false;
  if (!Array.isArray(obj.constraints))
    return false;
  return typeof obj.displayName === "string" && typeof obj.expectMultipleValues === "boolean" && typeof obj.required === "boolean";
}
function isValueAlias(obj) {
  if (!obj || typeof obj !== "object") {
    return false;
  }
  return obj.value !== void 0 && typeof obj.label === "string";
}
function createDefaultValuesEndpoint(uri) {
  return {
    protocol: "HTTPS",
    uri,
    method: "GET",
    debounceMs: 300,
    minSearchLength: 0,
    // Provide a minimal default response mapping to satisfy tests & typical usage
    responseMapping: { dataField: "data" }
  };
}
function buildInlineValuesEndpoint(items, mode = "CLOSED") {
  return { protocol: "INLINE", items, mode };
}
function isAtomicConstraint(c) {
  return c.type !== void 0 && c.params !== void 0;
}
// src/validation/index.ts
var FieldValidator = class {
  constructor(options = {}) {
    this.options = options;
  }
  async validate(fieldSpec, value, specificConstraintName) {
    const errors = [];
    value = this.applyCoercion(fieldSpec, value);
    if (fieldSpec.required && this.isEmpty(value)) {
      return { isValid: false, errors: [{ constraintName: "required", message: "This field is required", value }] };
    }
    if (this.isEmpty(value)) {
      return { isValid: true, errors: [] };
    }
    const typeErrors = this.typePhase(fieldSpec, value);
    if (typeErrors.length) {
      return { isValid: false, errors: typeErrors };
    }
    const domain = await this.resolveMembership(fieldSpec.valuesEndpoint);
    if (domain && domain.closed) {
      this.checkMembership(fieldSpec, value, domain, errors);
    }
    for (const c of fieldSpec.constraints) {
      if (specificConstraintName && c.name !== specificConstraintName)
        continue;
      if (!isAtomicConstraint(c)) {
        this.applyLegacyConstraint(fieldSpec, value, c, errors);
        continue;
      }
      await this.applyAtomicConstraint(fieldSpec, value, c, errors);
    }
    return { isValid: errors.length === 0, errors };
  }
  async validateAll(fieldSpec, value) {
    return this.validate(fieldSpec, value);
  }
  // --- Membership handling ---
  async resolveMembership(endpoint) {
    if (!endpoint)
      return void 0;
    const closed = endpoint.mode !== "SUGGESTIONS";
    if (endpoint.protocol === "INLINE") {
      const set = new Set((endpoint.items || []).map((i) => i.value));
      return { closed, values: set };
    }
    return { closed, unresolved: true };
  }
  checkMembership(fieldSpec, value, domain, errors) {
    if (domain.unresolved) {
      return;
    }
    const allowed = domain.values || /* @__PURE__ */ new Set();
    if (fieldSpec.expectMultipleValues) {
      if (!Array.isArray(value)) {
        errors.push({ constraintName: "membership", message: "Expected array for multi-value field", value });
        return;
      }
      value.forEach((v, idx) => {
        if (!allowed.has(v) && !this.looseMembershipMatch(v, allowed)) {
          errors.push({ constraintName: "membership", message: "Value not allowed", value: v, index: idx });
        }
      });
    } else {
      if (!allowed.has(value) && !this.looseMembershipMatch(value, allowed)) {
        errors.push({ constraintName: "membership", message: "Value not allowed", value });
      }
    }
  }
  // Loose membership second-pass for coerced cases (string->number, string->boolean)
  looseMembershipMatch(value, allowed) {
    if (typeof value === "number") {
      for (const a of allowed) {
        if (typeof a === "string" && this.numericPattern().test(a) && Number(a) === value)
          return true;
      }
    }
    if (typeof value === "boolean") {
      for (const a of allowed) {
        if (typeof a === "string") {
          const lower = a.toLowerCase();
          if (lower === "true" && value === true)
            return true;
          if (lower === "false" && value === false)
            return true;
        }
      }
    }
    return false;
  }
  applyCoercion(fieldSpec, value) {
    const fieldCfg = fieldSpec.coercion || {};
    const coerceFlag = fieldCfg.coerce ?? this.options.coerce ?? false;
    const effective = {
      coerce: coerceFlag,
      trimStrings: this.options.trimStrings ?? fieldCfg.trimStrings ?? true,
      acceptNumericBoolean: this.options.acceptNumericBoolean ?? fieldCfg.acceptNumericBoolean ?? false,
      extraTrueValues: (this.options.extraTrueValues || []).concat(fieldCfg.extraTrueValues || []),
      extraFalseValues: (this.options.extraFalseValues || []).concat(fieldCfg.extraFalseValues || []),
      numberPattern: this.options.numberPattern || fieldCfg.numberPattern || this.numericPattern(),
      dateEpochSupport: this.options.dateEpochSupport ?? fieldCfg.dateEpochSupport ?? false
    };
    if (!effective.coerce)
      return value;
    if (fieldSpec.expectMultipleValues) {
      if (!Array.isArray(value))
        return value;
      return value.map((v) => this.coerceScalar(v, fieldSpec.dataType, effective));
    }
    return this.coerceScalar(value, fieldSpec.dataType, effective);
  }
  numericPattern() {
    return /^[+-]?(\d+)(\.\d+)?$/;
  }
  coerceScalar(v, dataType, opt) {
    if (v == null)
      return v;
    if (typeof v === "string" && opt.trimStrings)
      v = v.trim();
    switch (dataType) {
      case "NUMBER":
        if (typeof v === "string" && opt.numberPattern.test(v)) {
          const normalized = v.replace(/_/g, "");
          const n = Number(normalized);
          return isNaN(n) ? v : n;
        }
        return v;
      case "BOOLEAN":
        if (typeof v === "string") {
          const lower = v.toLowerCase();
          if (lower === "true")
            return true;
          if (lower === "false")
            return false;
          if (opt.acceptNumericBoolean) {
            if (lower === "1")
              return true;
            if (lower === "0")
              return false;
          }
          if (opt.extraTrueValues.includes(lower))
            return true;
          if (opt.extraFalseValues.includes(lower))
            return false;
        }
        return v;
      case "DATE":
        if (typeof v === "string" && opt.dateEpochSupport && /^\d+$/.test(v)) {
          const num = Number(v);
          const ms = v.length <= 10 ? num * 1e3 : num;
          const d = new Date(ms);
          if (!isNaN(d.getTime()))
            return d.toISOString();
        }
        return v;
      case "STRING":
      default:
        return v;
    }
  }
  // --- Atomic constraint evaluation ---
  async applyAtomicConstraint(fieldSpec, rawValue, c, errors) {
    if (fieldSpec.expectMultipleValues) {
      if (!Array.isArray(rawValue)) {
        errors.push({ constraintName: c.name, message: "Expected array", value: rawValue });
        return;
      }
      if ((c.type === "minValue" || c.type === "maxValue") && fieldSpec.dataType === "STRING") {
        const count = rawValue.length;
        if (c.type === "minValue" && typeof c.params?.value === "number" && count < c.params.value) {
          errors.push({ constraintName: c.name, message: c.errorMessage || `Minimum ${c.params.value} items`, value: rawValue });
        }
        if (c.type === "maxValue" && typeof c.params?.value === "number" && count > c.params.value) {
          errors.push({ constraintName: c.name, message: c.errorMessage || `Maximum ${c.params.value} items`, value: rawValue });
        }
        return;
      }
      rawValue.forEach((v, idx) => {
        const msgs2 = this.evaluateAtomic(v, c, fieldSpec.dataType);
        msgs2.forEach((m) => errors.push({ constraintName: c.name, message: m, value: v, index: idx }));
      });
      return;
    }
    const msgs = this.evaluateAtomic(rawValue, c, fieldSpec.dataType);
    msgs.forEach((m) => errors.push({ constraintName: c.name, message: m, value: rawValue }));
  }
  evaluateAtomic(value, c, dataType) {
    switch (c.type) {
      case "pattern":
        if (typeof value !== "string")
          return [];
        try {
          const { regex, flags } = c.params || {};
          if (!regex)
            return [];
          const r = new RegExp(regex, flags);
          if (!r.test(value))
            return [c.errorMessage || "Invalid format"];
          return [];
        } catch {
          return [c.errorMessage || "Invalid pattern"];
        }
      case "minLength":
        if (typeof value !== "string")
          return [];
        if (value.length < c.params?.value)
          return [c.errorMessage || `Minimum ${c.params.value} characters`];
        return [];
      case "maxLength":
        if (typeof value !== "string")
          return [];
        if (value.length > c.params?.value)
          return [c.errorMessage || `Maximum ${c.params.value} characters`];
        return [];
      case "minValue":
        if (dataType !== "NUMBER" || typeof value !== "number")
          return [];
        if (value < c.params?.value)
          return [c.errorMessage || `Minimum value is ${c.params.value}`];
        return [];
      case "maxValue":
        if (dataType !== "NUMBER" || typeof value !== "number")
          return [];
        if (value > c.params?.value)
          return [c.errorMessage || `Maximum value is ${c.params.value}`];
        return [];
      case "minDate":
        if (dataType !== "DATE")
          return [];
        if (this.invalidDate(value))
          return [c.errorMessage || "Invalid date"];
        if (new Date(value) < new Date(c.params?.iso))
          return [c.errorMessage || `Date must be after ${c.params.iso}`];
        return [];
      case "maxDate":
        if (dataType !== "DATE")
          return [];
        if (this.invalidDate(value))
          return [c.errorMessage || "Invalid date"];
        if (new Date(value) > new Date(c.params?.iso))
          return [c.errorMessage || `Date must be before ${c.params.iso}`];
        return [];
      case "range":
        if (dataType === "NUMBER" && typeof value === "number") {
          const { min, max } = c.params || {};
          if (min !== void 0 && value < min)
            return [c.errorMessage || `Must be \u2265 ${min}`];
          if (max !== void 0 && value > max)
            return [c.errorMessage || `Must be \u2264 ${max}`];
          return [];
        }
        if (dataType === "DATE") {
          if (this.invalidDate(value))
            return [c.errorMessage || "Invalid date"];
          const { min, max } = c.params || {};
          const d = new Date(value);
          if (min && d < new Date(min))
            return [c.errorMessage || `Date must be after ${min}`];
          if (max && d > new Date(max))
            return [c.errorMessage || `Date must be before ${max}`];
          return [];
        }
        return [];
      case "custom":
        return [];
      default:
        return [];
    }
  }
  // Legacy descriptor support (best-effort)
  /**
   * @deprecated Legacy composite constraint + enumValues adapter. This exists solely to support
   * migration tests for v1 -> v2. It will be removed in the next major (3.0.0) once downstream
   * projects have adopted pure atomic constraints plus `valuesEndpoint` membership.
   *
   * Replacement strategy:
   *  - Replace legacy `enumValues` with an INLINE `valuesEndpoint` definition.
   *  - Split combined min/max or pattern properties into discrete atomic constraint descriptors
   *    (e.g. { type: 'minLength' }, { type: 'maxLength' }, { type: 'pattern' }).
   *  - Move any legacy `format` field to the field-level `formatHint` (non-failing advisory).
   *  - Use the exported `migrateV1Spec` helper for automated transformation when feasible.
   */
  applyLegacyConstraint(fieldSpec, value, legacy, errors) {
    const anyLegacy = legacy;
    if (anyLegacy.enumValues && Array.isArray(anyLegacy.enumValues)) {
      const set = new Set(anyLegacy.enumValues.map((v) => v.value));
      const valuesToCheck = fieldSpec.expectMultipleValues && Array.isArray(value) ? value : [value];
      valuesToCheck.forEach((v, idx) => {
        if (!set.has(v)) {
          if (fieldSpec.expectMultipleValues) {
            errors.push({ constraintName: legacy.name, message: anyLegacy.errorMessage || "Value not allowed", value: v, index: idx });
          } else {
            errors.push({ constraintName: legacy.name, message: anyLegacy.errorMessage || "Value not allowed", value: v });
          }
        }
      });
    }
    if (legacy.pattern) {
      const pattern = legacy.pattern;
      try {
        const r = new RegExp(pattern);
        const valuesToCheck = fieldSpec.expectMultipleValues && Array.isArray(value) ? value : [value];
        valuesToCheck.forEach((v, idx) => {
          if (typeof v !== "string" || !r.test(v)) {
            const constraintName = fieldSpec.expectMultipleValues ? `${legacy.name}[${idx}]` : legacy.name;
            if (fieldSpec.expectMultipleValues) {
              errors.push({ constraintName, message: legacy.errorMessage || "Invalid format", value: v, index: idx });
            } else {
              errors.push({ constraintName, message: legacy.errorMessage || "Invalid format", value: v });
            }
          }
        });
      } catch {
        errors.push({ constraintName: legacy.name, message: legacy.errorMessage || "Invalid pattern", value });
      }
    }
    if (legacy.min !== void 0 || legacy.max !== void 0) {
      const min = legacy.min;
      const max = legacy.max;
      if (fieldSpec.expectMultipleValues) {
        if (!Array.isArray(value)) {
          errors.push({ constraintName: legacy.name, message: "Expected an array", value });
        } else {
          if (typeof min === "number" && value.length < min) {
            errors.push({ constraintName: legacy.name, message: legacy.errorMessage || `Minimum ${min} items`, value });
          }
          if (typeof max === "number" && value.length > max) {
            errors.push({ constraintName: legacy.name, message: legacy.errorMessage || `Maximum ${max} items`, value });
          }
        }
      } else if (fieldSpec.dataType === "STRING") {
        const combinedMessage = legacy.errorMessage;
        const violations = [];
        if (typeof min === "number" && typeof value === "string" && value.length < min)
          violations.push("min");
        if (typeof max === "number" && typeof value === "string" && value.length > max)
          violations.push("max");
        if (violations.length) {
          errors.push({ constraintName: legacy.name, message: combinedMessage || (violations[0] === "min" ? `Minimum ${min} characters` : `Maximum ${max} characters`), value });
        }
        if (!combinedMessage) {
          if (typeof min === "number")
            this.applyAtomicConstraint(fieldSpec, value, { name: legacy.name + "_minLength", type: "minLength", params: { value: min } }, errors);
          if (typeof max === "number")
            this.applyAtomicConstraint(fieldSpec, value, { name: legacy.name + "_maxLength", type: "maxLength", params: { value: max } }, errors);
        }
      } else if (fieldSpec.dataType === "NUMBER") {
        if (typeof min === "number")
          this.applyAtomicConstraint(fieldSpec, value, { name: legacy.name + "_minValue", type: "minValue", params: { value: min } }, errors);
        if (typeof max === "number")
          this.applyAtomicConstraint(fieldSpec, value, { name: legacy.name + "_maxValue", type: "maxValue", params: { value: max } }, errors);
      } else if (fieldSpec.dataType === "DATE") {
        if (min !== void 0)
          this.applyAtomicConstraint(fieldSpec, value, { name: legacy.name + "_minDate", type: "minDate", params: { iso: min } }, errors);
        if (max !== void 0)
          this.applyAtomicConstraint(fieldSpec, value, { name: legacy.name + "_maxDate", type: "maxDate", params: { iso: max } }, errors);
      }
    }
  }
  typePhase(fieldSpec, value) {
    const errs = [];
    const { dataType, expectMultipleValues } = fieldSpec;
    if (expectMultipleValues) {
      if (!Array.isArray(value)) {
        errs.push({ constraintName: "type", message: "Expected an array", value });
        return errs;
      }
      const base = fieldSpec.displayName.split(/\s+/)[0].toLowerCase();
      value.forEach((v, idx) => {
        if (!this.scalarTypeOk(v, dataType)) {
          errs.push({ constraintName: `${base}[${idx}]`, message: `${dataType.toLowerCase()} type expected`, value: v, index: idx });
        }
      });
      return errs;
    }
    if (!this.scalarTypeOk(value, dataType)) {
      errs.push({ constraintName: "type", message: `${dataType.toLowerCase()} type expected`, value });
    }
    return errs;
  }
  scalarTypeOk(value, dataType) {
    switch (dataType) {
      case "STRING":
        return typeof value === "string";
      case "NUMBER":
        return typeof value === "number" && !isNaN(value);
      case "BOOLEAN":
        return typeof value === "boolean";
      case "DATE":
        return !isNaN(new Date(value).getTime());
      default:
        return false;
    }
  }
  isEmpty(value) {
    return value === null || value === void 0 || value === "" || Array.isArray(value) && value.length === 0;
  }
  invalidDate(value) {
    return isNaN(new Date(value).getTime());
  }
};
async function validateField(fieldSpec, value, constraintName) {
  const validator = new FieldValidator();
  return validator.validate(fieldSpec, value, constraintName);
}
async function validateAllConstraints(fieldSpec, value) {
  const validator = new FieldValidator();
  return validator.validateAll(fieldSpec, value);
}
// src/client/implementations.ts
var FetchHttpClient = class {
  constructor(baseTimeout = 5e3) {
    this.baseTimeout = baseTimeout;
  }
  async request(url, options) {
    const controller = new AbortController();
    const timeout = options.timeout || this.baseTimeout;
    const timeoutId = setTimeout(() => controller.abort(), timeout);
    try {
      const requestUrl = this.buildUrl(url, options.params);
      const fetchOptions = {
        method: options.method,
        signal: controller.signal
      };
      if (options.headers) {
        fetchOptions.headers = options.headers;
      }
      if (options.body) {
        fetchOptions.body = JSON.stringify(options.body);
        fetchOptions.headers = {
          "Content-Type": "application/json",
          ...options.headers
        };
      }
      const response = await fetch(requestUrl, fetchOptions);
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
      return await response.json();
    } finally {
      clearTimeout(timeoutId);
    }
  }
  buildUrl(baseUrl, params) {
    if (!params || Object.keys(params).length === 0) {
      return baseUrl;
    }
    const url = new URL(baseUrl);
    Object.entries(params).forEach(([key, value]) => {
      if (value !== void 0 && value !== null) {
        url.searchParams.append(key, String(value));
      }
    });
    return url.toString();
  }
};
var NodeHttpClient = class {
  async request(url, options) {
    const fetchClient = new FetchHttpClient();
    return fetchClient.request(url, options);
  }
};
var MemoryCacheProvider = class {
  constructor() {
    this.cache = /* @__PURE__ */ new Map();
  }
  get(key) {
    const item = this.cache.get(key);
    if (!item) {
      return null;
    }
    if (item.expiry && Date.now() > item.expiry) {
      this.cache.delete(key);
      return null;
    }
    return item.value;
  }
  set(key, value, ttlMs) {
    const item = { value };
    if (ttlMs !== void 0) {
      item.expiry = Date.now() + ttlMs;
    }
    this.cache.set(key, item);
  }
  delete(key) {
    this.cache.delete(key);
  }
  clear() {
    this.cache.clear();
  }
  // Helper method for pattern-based clearing
  clearByPattern(pattern) {
    const keys = Array.from(this.cache.keys());
    keys.filter((key) => key.includes(pattern)).forEach((key) => this.cache.delete(key));
  }
};
var NullCacheProvider = class {
  get() {
    return null;
  }
  set(_key, _value, _ttlMs) {
  }
  delete() {
  }
  clear() {
  }
};
function createValuesResolver(config = {}) {
  let httpClient;
  switch (config.httpImplementation) {
    case "node":
      httpClient = new NodeHttpClient();
      break;
    case "fetch":
    default:
      httpClient = new FetchHttpClient(config.httpTimeout);
      break;
  }
  let cacheProvider;
  switch (config.cacheStrategy) {
    case "null":
      cacheProvider = new NullCacheProvider();
      break;
    case "memory":
    default:
      cacheProvider = new MemoryCacheProvider();
      break;
  }
  return { httpClient, cacheProvider };
}
// src/client/framework-adapters.ts
var AngularHttpClientAdapter = class {
  constructor(angularHttpClient) {
    this.angularHttpClient = angularHttpClient;
  }
  async request(url, options) {
    const angularOptions = {
      headers: options.headers || {},
      params: options.params || {}
    };
    const observable = this.angularHttpClient.request(
      options.method,
      url,
      {
        ...angularOptions,
        body: options.body,
        responseType: "json"
      }
    );
    return new Promise((resolve, reject) => {
      const subscription = observable.subscribe({
        next: (data) => {
          subscription.unsubscribe();
          resolve(data);
        },
        error: (error) => {
          subscription.unsubscribe();
          reject(error);
        }
      });
    });
  }
};
var AxiosHttpClientAdapter = class {
  constructor(axiosInstance) {
    this.axiosInstance = axiosInstance;
  }
  async request(url, options) {
    const axiosConfig = {
      method: options.method.toLowerCase(),
      url,
      headers: options.headers,
      params: options.params,
      data: options.body,
      timeout: options.timeout
    };
    const response = await this.axiosInstance.request(axiosConfig);
    return response.data;
  }
};
var HttpClientFactory = class {
  /**
   * Create an HTTP client using Angular's HttpClient
   * Perfect for Angular applications with interceptors
   */
  static createAngularAdapter(angularHttpClient) {
    return new AngularHttpClientAdapter(angularHttpClient);
  }
  /**
   * Create an HTTP client using an Axios instance
   * Perfect for applications already using Axios with custom configuration
   */
  static createAxiosAdapter(axiosInstance) {
    return new AxiosHttpClientAdapter(axiosInstance);
  }
  /**
   * Create a native fetch-based HTTP client
   * Good for applications without specific HTTP client requirements
   */
  static createFetchAdapter(baseConfig) {
    return new ConfigurableFetchHttpClient(baseConfig);
  }
  /**
   * Auto-detect and create appropriate HTTP client
   * Tries to detect existing HTTP client instances in the environment
   */
  static createAuto() {
    if (typeof window !== "undefined" && window.ng) {
      console.warn("Angular detected but HttpClient not provided. Use createAngularAdapter() for better integration.");
    }
    return new ConfigurableFetchHttpClient();
  }
};
var ConfigurableFetchHttpClient = class {
  constructor(baseConfig = {}, options = {}) {
    this.baseConfig = baseConfig;
    this.defaultHeaders = options.defaultHeaders || {};
    this.baseTimeout = options.timeout || 3e4;
    this.interceptors = options.interceptors || [];
    this.errorHandlers = options.errorHandlers || [];
  }
  async request(url, options) {
    let finalOptions = { ...options };
    for (const interceptor of this.interceptors) {
      finalOptions = await interceptor(url, finalOptions);
    }
    const requestInit = {
      ...this.baseConfig,
      method: finalOptions.method,
      headers: {
        "Content-Type": "application/json",
        ...this.defaultHeaders,
        ...finalOptions.headers
      }
    };
    if (finalOptions.body && ["POST", "PUT"].includes(finalOptions.method)) {
      requestInit.body = JSON.stringify(finalOptions.body);
    }
    const finalUrl = this.buildUrlWithParams(url, finalOptions.params);
    const controller = new AbortController();
    const timeout = finalOptions.timeout || this.baseTimeout;
    const timeoutId = setTimeout(() => controller.abort(), timeout);
    try {
      const response = await fetch(finalUrl, {
        ...requestInit,
        signal: controller.signal
      });
      clearTimeout(timeoutId);
      if (!response.ok) {
        const error = new HttpError(
          `HTTP ${response.status}: ${response.statusText}`,
          response.status,
          response.statusText
        );
        for (const handler of this.errorHandlers) {
          await handler(error, response);
        }
        throw error;
      }
      const data = await response.json();
      return data;
    } catch (error) {
      clearTimeout(timeoutId);
      if (error instanceof HttpError) {
        throw error;
      }
      const httpError = new HttpError(
        error instanceof Error ? error.message : "Network error",
        0,
        "Network Error"
      );
      for (const handler of this.errorHandlers) {
        await handler(httpError);
      }
      throw httpError;
    }
  }
  /**
   * Add a request interceptor
   * Useful for adding authentication tokens, logging, etc.
   */
  addInterceptor(interceptor) {
    this.interceptors.push(interceptor);
  }
  /**
   * Add an error handler
   * Useful for global error handling, logging, retries, etc.
   */
  addErrorHandler(handler) {
    this.errorHandlers.push(handler);
  }
  buildUrlWithParams(url, params) {
    if (!params || Object.keys(params).length === 0) {
      return url;
    }
    const urlObj = new URL(url);
    Object.entries(params).forEach(([key, value]) => {
      if (value !== void 0 && value !== null) {
        urlObj.searchParams.append(key, String(value));
      }
    });
    return urlObj.toString();
  }
};
var HttpError = class extends Error {
  constructor(message, status, statusText) {
    super(message);
    this.status = status;
    this.statusText = statusText;
    this.name = "HttpError";
  }
};
var FrameworkCacheAdapter = class {
  constructor(cacheImplementation) {
    this.cacheImplementation = cacheImplementation;
  }
  get(key) {
    return this.cacheImplementation.get ? this.cacheImplementation.get(key) : null;
  }
  set(key, value, ttlMs) {
    if (this.cacheImplementation.set) {
      this.cacheImplementation.set(key, value, ttlMs);
    }
  }
  delete(key) {
    if (this.cacheImplementation.delete) {
      this.cacheImplementation.delete(key);
    }
  }
  clear() {
    if (this.cacheImplementation.clear) {
      this.cacheImplementation.clear();
    }
  }
};
// src/client/index.ts
var ValuesResolver = class {
  constructor(httpClient, cache) {
    this.httpClient = httpClient;
    this.cache = cache;
  }
  /**
   * Résout les valeurs pour un endpoint donné
   * Gère: debouncing, cache, pagination, search
   */
  async resolveValues(endpoint, options = {}) {
    const debounceMs = endpoint.debounceMs ?? 0;
    if (options.search !== void 0 && debounceMs > 0) {
      return this.debouncedResolve(endpoint, options);
    }
    return this.performResolve(endpoint, options);
  }
  debouncedResolve(endpoint, options) {
    return this.performResolve(endpoint, options);
  }
  async performResolve(endpoint, options) {
    if (endpoint.protocol === "INLINE") {
      const items = endpoint.items || [];
      return {
        values: items,
        hasNext: false,
        total: items.length,
        page: 1
      };
    }
    if (!endpoint.uri) {
      throw new Error("ValuesEndpoint uri is required for remote protocols");
    }
    const cacheKey = this.buildCacheKey(endpoint, options);
    const cached = this.getFromCache(cacheKey, endpoint.cacheStrategy);
    if (cached) {
      return cached;
    }
    if (options.search !== void 0 && options.search.length < (endpoint.minSearchLength ?? 0)) {
      return {
        values: [],
        hasNext: false,
        total: 0
      };
    }
    try {
      const params = this.buildRequestParams(endpoint, options);
      const response = await this.httpClient.request(endpoint.uri, {
        method: endpoint.method,
        params
      });
      const result = this.parseResponse(response, endpoint);
      this.setCache(cacheKey, result, endpoint.cacheStrategy);
      return result;
    } catch (error) {
      throw new Error(`Failed to resolve values: ${error}`);
    }
  }
  buildCacheKey(endpoint, options) {
    const parts = [
      endpoint.uri,
      options.page || 1,
      options.search || "",
      options.limit || endpoint.requestParams?.defaultLimit || 50
    ];
    return parts.join("|");
  }
  buildRequestParams(endpoint, options) {
    const params = {};
    if (!endpoint.requestParams) {
      return params;
    }
    if (endpoint.paginationStrategy === "PAGE_NUMBER" && options.page !== void 0) {
      if (endpoint.requestParams.pageParam) {
        params[endpoint.requestParams.pageParam] = options.page;
      }
    }
    if (endpoint.requestParams.limitParam) {
      params[endpoint.requestParams.limitParam] = options.limit || endpoint.requestParams.defaultLimit || 50;
    }
    if (endpoint.requestParams.searchParam && options.search) {
      params[endpoint.requestParams.searchParam] = options.search;
    }
    return params;
  }
  parseResponse(data, endpoint) {
    const mapping = endpoint.responseMapping || { dataField: "data" };
    let values = [];
    if (mapping.dataField && data && typeof data === "object" && data[mapping.dataField] !== void 0) {
      values = data[mapping.dataField] || [];
    } else if (Array.isArray(data)) {
      values = data;
    }
    const hasNext = mapping.hasNextField && data ? Boolean(data[mapping.hasNextField]) : false;
    const total = mapping.totalField && data ? data[mapping.totalField] : void 0;
    const page = mapping.pageField && data ? data[mapping.pageField] : void 0;
    return {
      values,
      hasNext,
      total,
      page
    };
  }
  getFromCache(key, strategy) {
    if (!strategy || strategy === "NONE") {
      return null;
    }
    return this.cache.get(key);
  }
  setCache(key, value, strategy) {
    if (!strategy || strategy === "NONE") {
      return;
    }
    let ttlMs;
    switch (strategy) {
      case "SESSION":
        ttlMs = void 0;
        break;
      case "SHORT_TERM":
        ttlMs = 5 * 60 * 1e3;
        break;
      case "LONG_TERM":
        ttlMs = 60 * 60 * 1e3;
        break;
      default:
        return;
    }
    this.cache.set(key, value, ttlMs);
  }
  /**
   * Clear cache for a specific endpoint
   */
  clearCacheForEndpoint(_endpoint) {
    this.cache.clear();
  }
};
// src/index.ts
var PROTOCOL_VERSION = "2.0.0";
var LIBRARY_VERSION = "2.0.0";
export {
  AngularHttpClientAdapter,
  AxiosHttpClientAdapter,
  ConfigurableFetchHttpClient,
  FetchHttpClient,
  FieldValidator,
  FrameworkCacheAdapter,
  HttpClientFactory,
  HttpError,
  LIBRARY_VERSION,
  MemoryCacheProvider,
  NodeHttpClient,
  NullCacheProvider,
  PROTOCOL_VERSION,
  ValuesResolver,
  buildInlineValuesEndpoint,
  createDefaultValuesEndpoint,
  createValuesResolver,
  isAtomicConstraint,
  isInputFieldSpec,
  isValueAlias,
  validateAllConstraints,
  validateField
};