Wrapper функции для YupResolver

YupResolver чаще всего используется совместно с библиотеками управления формами, особенно с React Hook Form. Однако прямое подключение резолвера подходит не для всех сценариев. В реальных проектах появляются дополнительные требования:

  • централизованная обработка ошибок;
  • автоматическая локализация сообщений;
  • логирование;
  • переключение схем в зависимости от контекста;
  • динамическая генерация схем;
  • модификация поведения валидации;
  • переиспользование типовой логики.

Для решения подобных задач создаются wrapper-функции — функции-обёртки над yupResolver.

Базовая идея выглядит так:

const resolver = customYupResolver(schema);

Вместо прямого:

const resolver = yupResolver(schema);

Базовая структура wrapper-функции

Минимальная обёртка:

import { yupResolver } from "@hookform/resolvers/yup";

export function createResolver(schema) {
  return yupResolver(schema);
}

На первый взгляд такая конструкция бесполезна, однако именно она становится точкой расширения.


Добавление глобальных настроек

Во многих приложениях требуется единое поведение для всех схем.

Пример:

import { yupResolver } from "@hookform/resolvers/yup";

export function createResolver(schema) {
  return yupResolver(schema, {
    abortEarly: false,
    stripUnknown: true,
  });
}

Теперь каждая форма автоматически:

  • собирает все ошибки;
  • удаляет лишние поля;
  • использует одинаковые правила обработки данных.

Централизация сообщений об ошибках

Wrapper удобно использовать для подключения переводов.

Пример локализации

import * as yup from "yup";

yup.setLocale({
  mixed: {
    required: "Поле обязательно",
  },
  string: {
    email: "Некорректный email",
    min: "Слишком короткое значение",
  },
});

Обёртка:

import { yupResolver } from "@hookform/resolvers/yup";

export function localizedResolver(schema) {
  return yupResolver(schema);
}

После этого все формы автоматически получают локализованные сообщения.


Wrapper для логирования ошибок

Во время разработки полезно видеть ошибки валидации централизованно.

import { yupResolver } from "@hookform/resolvers/yup";

export function debugResolver(schema) {
  const resolver = yupResolver(schema);

  return async (data, context, options) => {
    const result = await resolver(data, context, options);

    if (Object.keys(result.errors).length > 0) {
      console.log("Validation errors:", result.errors);
    }

    return result;
  };
}

Использование:

const resolver = debugResolver(schema);

Wrapper для трансформации ошибок

Иногда стандартный формат ошибок неудобен.

Например, сервер ожидает:

{
  "field": "email",
  "message": "Invalid email"
}

Вместо вложенного объекта.

Пример преобразования

import { yupResolver } from "@hookform/resolvers/yup";

export function flatErrorResolver(schema) {
  const resolver = yupResolver(schema);

  return async (data, context, options) => {
    const result = await resolver(data, context, options);

    const flatErrors = Object.entries(result.errors).map(
      ([field, error]) => ({
        field,
        message: error.message,
      })
    );

    return {
      values: result.values,
      errors: flatErrors,
    };
  };
}

Wrapper с динамической схемой

Иногда схема зависит от роли пользователя.

Пример

import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";

export function roleBasedResolver(role) {
  let schema;

  if (role === "admin") {
    schema = yup.object({
      name: yup.string().required(),
      accessLevel: yup.number().required(),
    });
  } else {
    schema = yup.object({
      name: yup.string().required(),
    });
  }

  return yupResolver(schema);
}

Wrapper для условной валидации

Обёртка может менять поведение в зависимости от окружения.

Валидация только в production

import { yupResolver } from "@hookform/resolvers/yup";

export function productionResolver(schema) {
  if (process.env.NODE_ENV !== "production") {
    return async (data) => ({
      values: data,
      errors: {},
    });
  }

  return yupResolver(schema);
}

Подобный подход иногда применяется при тестировании интерфейсов.


Wrapper с предварительной обработкой данных

До запуска схемы данные можно модифицировать.

Очистка строк

import { yupResolver } from "@hookform/resolvers/yup";

export function sanitizeResolver(schema) {
  const resolver = yupResolver(schema);

  return async (data, context, options) => {
    const sanitizedData = Object.fromEntries(
      Object.entries(data).map(([key, value]) => [
        key,
        typeof value === "string"
          ? value.trim()
          : value,
      ])
    );

    return resolver(sanitizedData, context, options);
  };
}

Wrapper для асинхронной подготовки данных

Иногда перед валидацией необходимо получить внешние данные.

Пример проверки blacklist

import { yupResolver } from "@hookform/resolvers/yup";

export function blacklistResolver(schema, blacklistService) {
  const resolver = yupResolver(schema);

  return async (data, context, options) => {
    const blacklist = await blacklistService.getEmails();

    if (blacklist.includes(data.email)) {
      return {
        values: {},
        errors: {
          email: {
            type: "manual",
            message: "Email запрещён",
          },
        },
      };
    }

    return resolver(data, context, options);
  };
}

Wrapper для объединения нескольких схем

Крупные формы часто собираются из модулей.

Композиция схем

import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";

export function mergeSchemas(...schemas) {
  const merged = schemas.reduce(
    (acc, schema) => acc.concat(schema),
    yup.object()
  );

  return yupResolver(merged);
}

Использование:

const resolver = mergeSchemas(
  profileSchema,
  addressSchema,
  securitySchema
);

Wrapper для feature flags

В enterprise-приложениях часть полей может зависеть от фич.

import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";

export function featureResolver(flags) {
  let schema = yup.object({
    name: yup.string().required(),
  });

  if (flags.enablePhone) {
    schema = schema.shape({
      phone: yup.string().required(),
    });
  }

  return yupResolver(schema);
}

Wrapper с кэшированием схем

Создание сложных схем может быть дорогой операцией.

Memoization

const cache = new Map();

export function cachedResolver(key, schemaFactory) {
  if (!cache.has(key)) {
    cache.set(key, schemaFactory());
  }

  return cache.get(key);
}

Использование:

const resolver = cachedResolver(
  "registration",
  () => yupResolver(createSchema())
);

Wrapper для строгой типизации

В TypeScript wrapper-функции особенно полезны.

Generic-обёртка

import { yupResolver } from "@hookform/resolvers/yup";
import { AnyObjectSchema } from "yup";

export function createTypedResolver<T>(
  schema: AnyObjectSchema
) {
  return yupResolver<T>(schema);
}

Wrapper с автоматическим преобразованием ошибок API

Часто требуется объединить серверные ошибки и ошибки Yup.

Пример

import { yupResolver } from "@hookform/resolvers/yup";

export function apiAwareResolver(schema, apiErrors) {
  const resolver = yupResolver(schema);

  return async (data, context, options) => {
    const result = await resolver(data, context, options);

    for (const error of apiErrors) {
      result.errors[error.field] = {
        type: "server",
        message: error.message,
      };
    }

    return result;
  };
}

Wrapper для multi-step форм

Многошаговые формы часто требуют отдельную схему на каждый этап.

Пример

import { yupResolver } from "@hookform/resolvers/yup";

export function stepResolver(step, schemas) {
  return yupResolver(schemas[step]);
}

Использование:

const resolver = stepResolver(currentStep, {
  1: userSchema,
  2: addressSchema,
  3: paymentSchema,
});

Wrapper для fallback-схем

Иногда схема может отсутствовать.

import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";

export function safeResolver(schema) {
  const fallback = yup.object({});

  return yupResolver(schema || fallback);
}

Wrapper для расширенной диагностики

При сложной валидации полезно измерять производительность.

import { yupResolver } from "@hookform/resolvers/yup";

export function profilingResolver(schema) {
  const resolver = yupResolver(schema);

  return async (data, context, options) => {
    const start = performance.now();

    const result = await resolver(data, context, options);

    const end = performance.now();

    console.log(`Validation time: ${end - start}ms`);

    return result;
  };
}

Wrapper для интеграции с аналитикой

Ошибки формы могут отправляться в систему мониторинга.

import { yupResolver } from "@hookform/resolvers/yup";

export function analyticsResolver(schema, analytics) {
  const resolver = yupResolver(schema);

  return async (data, context, options) => {
    const result = await resolver(data, context, options);

    if (Object.keys(result.errors).length > 0) {
      analytics.track("validation_error", result.errors);
    }

    return result;
  };
}

Wrapper для автоматической нормализации данных

Пример нормализации email

import { yupResolver } from "@hookform/resolvers/yup";

export function normalizeResolver(schema) {
  const resolver = yupResolver(schema);

  return async (data, context, options) => {
    const normalized = {
      ...data,
      email: data.email?.toLowerCase(),
    };

    return resolver(normalized, context, options);
  };
}

Композиция wrapper-функций

Обёртки можно объединять.

Пример compose

export function composeResolvers(...wrappers) {
  return (schema) =>
    wrappers.reduce(
      (acc, wrapper) => wrapper(acc),
      schema
    );
}

Использование:

const resolver = composeResolvers(
  sanitizeResolver,
  debugResolver,
  profilingResolver
)(schema);

Wrapper как middleware

Иногда обёртки реализуются по принципу middleware.

Пример архитектуры

export function middlewareResolver(schema, middlewares) {
  let resolver = yupResolver(schema);

  for (const middleware of middlewares.reverse()) {
    resolver = middleware(resolver);
  }

  return resolver;
}

Middleware:

function loggerMiddleware(resolver) {
  return async (data, context, options) => {
    console.log("Before validation");

    const result = await resolver(
      data,
      context,
      options
    );

    console.log("After validation");

    return result;
  };
}

Ошибки при создании wrapper-функций

Потеря сигнатуры resolver

Неправильно:

return async (data) => {
  return resolver(data);
};

Правильно:

return async (data, context, options) => {
  return resolver(data, context, options);
};

Изменение структуры ошибок

Некоторые библиотеки ожидают стандартный формат ошибок React Hook Form.

Опасный вариант:

return {
  errors: [],
};

Без совместимого интерфейса форма перестанет работать.


Повторное создание схем

Плохая практика:

return yupResolver(createSchema());

при каждом рендере компонента.

Лучше:

const schema = createSchema();

const resolver = yupResolver(schema);

Смешивание бизнес-логики и валидации

Wrapper не должен превращаться в сервис приложения.

Плохо:

if (user.balance < 0) {
  banUser();
}

Резолвер отвечает только за подготовку и проверку данных.


Архитектурные рекомендации

Выделение фабрики резолверов

export function createAppResolver(schema) {
  return profilingResolver(
    sanitizeResolver(
      debugResolver(schema)
    )
  );
}

Разделение инфраструктурных слоёв

Полезно разделять:

  • валидацию;
  • логирование;
  • аналитику;
  • локализацию;
  • серверную обработку.

Wrapper-функции позволяют сохранять такую архитектуру чистой.


Минимизация побочных эффектов

Желательно избегать:

  • изменения глобального состояния;
  • сетевых запросов внутри каждой проверки;
  • мутаций исходных данных;
  • тяжёлых синхронных операций.

Паттерн resolver factory

Один из самых распространённых подходов.

Пример

export function createResolverFactory(config) {
  return function(schema) {
    return yupResolver(schema, {
      abortEarly: config.abortEarly,
      stripUnknown: config.stripUnknown,
    });
  };
}

Использование:

const resolverFactory = createResolverFactory({
  abortEarly: false,
  stripUnknown: true,
});

const resolver = resolverFactory(schema);

Wrapper для lazy schema

Yup поддерживает lazy.

import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";

export function lazyResolver(factory) {
  const schema = yup.lazy(factory);

  return yupResolver(schema);
}

Wrapper для переключения режимов валидации

import { yupResolver } from "@hookform/resolvers/yup";

export function modeResolver(schema, mode) {
  const strict = mode === "strict";

  return yupResolver(schema, {
    strict,
  });
}

Wrapper с fallback на manual validation

import { yupResolver } from "@hookform/resolvers/yup";

export function hybridResolver(schema, manualValidate) {
  const resolver = yupResolver(schema);

  return async (data, context, options) => {
    const result = await resolver(data, context, options);

    const manualErrors = manualValidate(data);

    return {
      values: result.values,
      errors: {
        ...result.errors,
        ...manualErrors,
      },
    };
  };
}