Модель данных и назначение
Реализуйте на Go REST API каталога рецептов без внешней базы: записи живут в памяти и исчезают после перезапуска. Рецепт имеет id, title, portions, ingredients, steps, createdAt и updatedAt. portions — положительное целое число от 1 до 100. Каждый ingredient содержит name и amount; amount — положительное конечное десятичное число, unit — короткая строка единицы измерения. Шаги представляются упорядоченным непустым списком строк и возвращаются в исходном порядке. id и временные метки назначаются сервером.
API, проверки и эксплуатация
POST /api/recipes создаёт запись и отвечает 201. title после trim содержит от 1 до 160 символов. Список ingredients содержит 1–100 элементов; имя каждого ингредиента непустое и не длиннее 80 символов, amount больше нуля, unit содержит 1–24 символа. Повторение ингредиента с одинаковым нормализованным именем в одном рецепте запрещено с 400; регистр и крайние пробелы при этой проверке игнорируются. steps содержит 1–50 непустых строк длиной до 1000 символов каждая. Неизвестные поля, нулевое число порций, NaN/Infinity в количествах и неверные типы отклоняются с 400 и кодом ошибки.
GET /api/recipes поддерживает limit (по умолчанию 20, диапазон 1–100), offset (неотрицательный, по умолчанию 0) и необязательный ingredient. Фильтр совпадает с именем ингредиента без учёта регистра и крайних пробелов. Результаты сортируются по title без учёта регистра, затем по id. Ответ включает items и метаданные limit, offset, total. GET /api/recipes/{id} возвращает запись или 404. PUT /api/recipes/{id} полностью заменяет редактируемые поля после всех тех же проверок, сохраняя id и createdAt и устанавливая новый updatedAt. DELETE возвращает 204 либо 404. Все ошибки имеют единый JSON-формат; неизвестный маршрут — 404, неподдерживаемый метод — 405. Ограничьте тело запроса, задайте конечные HTTP-таймауты и не продолжайте обработку после отмены контекста. Параллельный GET не должен видеть частично заменённый список ингредиентов или шагов: запись целиком проверяется до атомарной замены под синхронизацией. Сервис не логирует содержимое рецептов и не использует внешние провайдеры. Пустая страница при offset больше total возвращает пустой items и исходный total. Для фильтра ингредиента пробелы по краям удаляются до проверки, а пустое после нормализации значение возвращает 400.
Структура программы
Небольшой internal/httpapi содержит обработчики и DTO, internal/recipes — валидацию, замену рецепта и фильтр ингредиентов, internal/recipememory — хранилище в памяти. main.go читает конфигурацию, связывает пакеты, запускает сервер и выполняет shutdown; правила тестируются без HTTP.