Модель данных и назначение
Создайте на Go REST API личного списка чтения. Состояние существует только в памяти процесса и теряется при перезапуске. Запись закладки состоит из id, url, title, status, createdAt и updatedAt. status принимает ровно значения unread и read. Сервер задаёт id и временные метки; клиент не может подменить их. Внешняя аутентификация и несколько пользователей в рамках этого задания не требуются.
API, проверки и эксплуатация
POST /api/bookmarks создаёт закладку и отвечает 201. url обязателен, разбирается стандартным URL-парсером, использует только http или https, содержит hostname и не содержит userinfo; после нормализации схемы и hostname сравниваются без учёта регистра, fragment полностью исключается из сравнения. Путь и query сохраняют семантику URL, поэтому разные query считаются разными адресами. Дубликат нормализованного URL запрещён: сервер отвечает 409 с кодом duplicate_url и не создаёт вторую запись. title обязателен, после trim содержит 1–200 символов; status при создании необязателен и по умолчанию unread. Неправильное поле возвращает 400 с указанием поля.
GET /api/bookmarks поддерживает limit и offset с теми же пределами: default limit 20, диапазон 1–100, offset от нуля. Необязательный status фильтрует по одному из двух допустимых значений, а q выполняет регистронезависимый поиск по title и hostname; пустой q трактуется как отсутствие фильтра. Ответ содержит items, limit, offset и total. Порядок — createdAt по убыванию, затем id по возрастанию. GET /api/bookmarks/{id} возвращает запись или 404. PATCH /api/bookmarks/{id} принимает одно или оба поля title и status, отклоняет неизвестные поля, пустой патч и некорректные значения; при успехе обновляет updatedAt и возвращает 200. URL после создания неизменяем. DELETE отвечает 204 либо 404. Ошибки имеют единый JSON-конверт, неизвестный маршрут даёт 404, неподдерживаемый метод — 405. Ограничьте тело запроса, установите HTTP-таймауты и корректно прекращайте работу при отмене контекста. Общие проверки уникальности URL и запись должны выполняться как одна синхронизированная операция, в том числе при параллельных POST. Не записывайте URL целиком в логи, поскольку query может содержать личные данные.
Структура программы
Небольшой internal/httpapi содержит обработчики и DTO, internal/bookmarks — нормализацию URL, уникальность и правила закладок, internal/bookmarkmemory — хранилище в памяти. main.go связывает пакеты, запускает сервер и выполняет shutdown; правила URL тестируются без HTTP.