Введение
Странице бюро находок теперь нужен осмысленный поиск. Посетитель, который ищет синюю сумку, не должен получать все синие предметы или все сумки. Отсутствующий предмет, недопустимая категория и допустимый поиск без совпадений также требуют разных ответов.
Эта независимая рабочая среда начинает с обработчика списка и отдельных предметов из предыдущей пошаговой лабораторной работы. Вам нужны её понятия HTTP, JSON и httptest либо аналогичный опыт. Вы объедините фильтры, добавите явные ошибки и воспользуетесь настоящей страницей. Отведите около 55 минут с учётом чтения и обычных ошибок; это оценка автора.
Объедините фильтры названия и категории
На этом шаге сделайте так, чтобы оба условия поиска применялись к одной записи. Начните в Workspace. Восстановите рабочий каталог и добавьте подготовленную установку Go в путь поиска этого терминала. export влияет только на текущую оболочку; $PATH сохраняет существующие элементы.
cd /home/labex/project/lost-found
export PATH=/usr/local/go/bin:$PATH
Сначала используйте работающий список и прочитайте его данные:
curl -i 'http://127.0.0.1:8080/api/items'
cat items.json
Ожидайте все шесть записей. Переключитесь на Lost and Found, введите blue, выберите bags и нажмите Search collection. Текущий обработчик игнорирует эти значения и по-прежнему показывает все шесть предметов. Вернитесь в Workspace, чтобы добавить недостающее решение.
Строка запроса URL идёт после ?. category=bags&q=blue передаёт два именованных параметра, разделённых &. Поиск по списку API принимает только category и q, каждый не более одного раза. На этом шаге сначала реализуется допустимый поиск; на следующем — обработка неверного ввода.
Контракт точен: отсутствующая или пустая категория означает все категории. Иначе её написание и регистр должны совпадать с категорией в items.json. Ключевое слово q ищет подстроку названия без учёта регистра; окружающие пробельные символы игнорируются. Отсутствующее, пустое или состоящее только из пробельных символов ключевое слово подходит каждому названию. Если переданы оба фильтра, должны совпасть оба (AND). Сохраните полные поля каждого подходящего ID без повторов; порядок результатов любой. Допустимый поиск без совпадений возвращает HTTP 200 и [].
r.URL.Query().Get читает декодированный параметр. strings.TrimSpace удаляет окружающие пробельные символы, strings.ToLower переводит в нижний регистр по правилам Unicode, а strings.Contains проверяет подстроку. Это простое сравнение в нижнем регистре, а не удаление диакритики или поисковая система с учётом языка. make([]Item, 0) создаёт пустой срез, отличный от nil, чтобы JSON кодировал отсутствие совпадений как []. && объединяет оба обязательных условия; || здесь ошибочно включил бы записи, соответствующие только одному.
Перед редактированием предскажите три результата по items.json: только ключевое слово blue, только категория bags и оба вместе. Только LF-106 входит в их пересечение. Два зонта показывают, почему было бы неверно возвращать совпадения по любому из условий.
Сохраните унаследованный код сведений. Используйте nano, как в первой лабораторной: Ctrl+W находит текст, Ctrl+O и Enter сохраняют существующее имя, Ctrl+X завершает редактор. Копия должна иметь расширение, отличное от .go, чтобы Go не скомпилировал второй обработчик.
cp handler.go handler.go.backup
LC_ALL=C.UTF-8 nano handler.go
Найдите ветку коллекции /api/items и замените только её логику выбора коллекции. Оставьте её заголовок ответа, цикл сведений и ответ отсутствующего ID. Реализуйте следующие решения, используя аргумент items:
- Прочитайте декодированные параметры
categoryиqчерезr.URL.Query().Get. Оставьте категорию ровно в переданном виде. До цикла примените к ключевому словуstrings.TrimSpace, затемstrings.ToLower. - Начните срез результата с
make([]Item, 0). Не используйте исходный полный срез как результат, иначе неподходящие записи сохранятся. - Обойдите записи через
for _, item := range items. Определите, отсутствует ли категория, пуста ли она или равнаitem.Category, а отдельно — содержит ли название в нижнем регистре нормализованное ключевое слово. Объедините эти два решения через&&. Добавляйте весь подходящийitemчерезappend, не только его название или ID от другой записи. - Закодируйте срез результата в ветке коллекции и вернитесь. Сохраните выбор сведений по ID; не применяйте фильтры коллекции к запросу сведений.
Для сравнения, условие категории можно выразить как category == "" || item.Category == category: здесь || означает отсутствие ограничения либо запрошенную категорию. Оно не заменяет AND между условиями категории и названия. Выберите имена локальных переменных, делающие эти отдельные решения понятными.
Сохраните, выйдите и изучите действительно сохранённый файл:
cat handler.go
Отформатируйте и пересоберите. -w позволяет gofmt обновить файл. serve.py собирает ваш код и заменяет только записанный сервер рабочей среды после успешной сборки. При ошибках компиляции прежняя версия продолжает работать; исправьте указанную строку и повторите обе команды.
gofmt -w handler.go
python3 serve.py
Теперь отправьте совместный поиск. Кавычки удерживают & внутри URL, чтобы он не считался оператором оболочки:
curl -i 'http://127.0.0.1:8080/api/items?category=bags&q=blue'
Ожидайте только LF-106, Blue sports bag, в Sports hall. Если возвращаются и зонты, значит использовалось OR. Допустимое ключевое слово без совпадений должно дать пустой массив:
curl -i 'http://127.0.0.1:8080/api/items?q=not-in-the-collection'
Для текста с пробелами, + или символами вне ASCII поручите кодирование curl. -G отправляет переданные данные в строке запроса GET, а --data-urlencode кодирует этот параметр процентным кодированием. Без кодирования буквальный плюс в данных запроса может декодироваться как пробел.
curl -G -i --data-urlencode 'q= CAFÉ + ' 'http://127.0.0.1:8080/api/items'
Ожидайте LF-104, Café + Map, в Welcome booth. Ключевое слово в верхнем регистре и окружающие пробелы нормализуются, а буквальный плюс остаётся частью названия. Если в пути опечатка, восстановите cd /home/labex/project/lost-found; если сервис недоступен, прочитайте .work/server.log через cat и пересоберите после исправления ошибки.
Отличайте неверные запросы от поиска без совпадений
На этом шаге сделайте ошибки наблюдаемыми через статус и JSON, затем воспользуйтесь готовой страницей. Начните в Workspace в том же каталоге.
Используйте 400 для запроса коллекции с неизвестной категорией, неизвестным параметром или повторяющимся параметром. Написание категории должно быть точным; Bags отличается от bags. Пустые category и q остаются допустимыми. Используйте 404 для неизвестного ID предмета или неизвестного пути API. Используйте 405 для методов, отличных от GET, на /api/items и /api/items/<id>, с Allow: GET. Тела ошибок — объекты с непустой строкой error; формулировка свободная. Допустимый поиск без совпадений остаётся 200 с []. Запросы отдельных предметов выбирают по ID; фильтры запроса коллекции на них не действуют. Подготовленная домашняя страница / остаётся доступной.
url.ParseQuery(r.URL.RawQuery) разбирает запрос и возвращает значения и ошибку, поэтому можно отклонять неверное кодирование запроса. Каждый ключ соответствует срезу значений: его длина выявляет повторяющиеся параметры. Этот пример проверяет категорию по текущим исходным записям, а не по фиксированному списку категорий, чтобы другое бюро могло использовать обработчик повторно.
Задавайте заголовки до записи тела. Ранние возвраты не дают после ошибки перейти к обычному успешному результату. Перед редактированием сравните допустимый поиск без совпадений из предыдущего шага с отклонёнными запросами ниже. Предскажите, почему статусы различаются. Отсутствующий ID предмета — другая ошибка, чем неверный запрос коллекции. Сохраните поисковые решения предыдущего шага; эта правка добавляет проверки вокруг них.
LC_ALL=C.UTF-8 nano handler.go
Через Ctrl+W найдите возвращённую функцию обработчика. Добавьте net/url в импорты для url.ParseQuery. Реализуйте проверки, не заменяя весь файл:
Сначала проверьте путь. Путь коллекции — ровно /api/items; сведения начинаются с /api/items/. Любой другой путь API должен вернуть 404, а не перейти к обычной странице или списку. Затем проверьте метод на распознанных маршрутах: если это не GET, задайте Allow равным GET и верните ошибку JSON 405.
Далее при желании повторно используйте одну локальную функцию записи ошибки. Она может принимать целый статус и строку сообщения, вызывать w.WriteHeader(status) и кодировать map[string]string{"error": message}. Заголовок типа содержимого по-прежнему задаётся первым. Выбирайте непустые сообщения; формулировка не оценивается.
Затем оставьте выбор сведений перед разбором запроса коллекции, чтобы фильтры не меняли поиск по ID. Отсутствующие сведения по-прежнему возвращают 404 с ошибкой JSON.
После этого в ветке коллекции разберите r.URL.RawQuery через url.ParseQuery. Проверьте возвращённую ошибку до использования значений; неверное кодирование должно вернуть 400. Процентная escape-последовательность требует двух шестнадцатеричных цифр, поэтому /api/items?q=%ZZ — конкретный неверный ввод для теста. Обойдите каждый ключ и его срез значений. Отклоняйте ключи, отличные от category или q, и срез, длина которого не равна одному. Немедленно возвращайтесь после каждого отказа.
Далее прочитайте принятые параметры из разобранных значений. Для непустой категории обойдите переданные записи и определите, существует ли эта точная категория; если нет, отклоните с 400. Не копируйте фиксированный список категорий из items.json в программу. У другого бюро могут быть совершенно новые категории.
Наконец, после успешных проверок выполните свой существующий поиск AND и закодируйте массив результата. Пустые значения категории и ключевого слова остаются допустимыми. Сохраните результат без совпадений [], не превращая его в ошибку.
Сохраните через Ctrl+O и Enter, затем выйдите через Ctrl+X. Если редактор показывает нежелательное имя файла, отмените запрос через Ctrl+C и проверьте путь. Сохраняйте уже работающий код поиска и сведений при исправлении ошибок компиляции.
gofmt -w handler.go
python3 serve.py
Следующие запросы намеренно отклоняются. Ожидайте соответственно 400, 400, 404 и 405. -X POST выбирает метод POST; этот сервис предназначен только для чтения и не должен ничего создавать.
curl -i 'http://127.0.0.1:8080/api/items?category=unknown'
curl -i 'http://127.0.0.1:8080/api/items?q=blue&q=bag'
curl -i 'http://127.0.0.1:8080/api/not-a-route'
curl -X POST -i 'http://127.0.0.1:8080/api/items'
Каждый ответ содержит поле JSON error; POST также включает Allow: GET. Получение этих ответов через curl отличается от сбоя соединения, когда не приходит ни статус HTTP, ни ответ JSON. Не считайте ожидаемые ошибки HTTP причиной сбрасывать рабочую среду.
Используйте таблицу тестовых случаев, чтобы собрать связанные ожидания вместе. Здесь повторяется приём с рекордером и запросом из предыдущей лабораторной. url.Values.Encode() кодирует запрос с Unicode и плюсом в Go. Декодируйте ответ списка в []Item и проверьте ошибку, возвращённую json.Unmarshal, до изучения содержимого. Для пустого результата срез, отличный от nil, с длиной ноль означает []; срез nil означал бы null. Уже выполненный поиск по работающему сервису также проверяет фактическое содержимое предметов.
Создайте свой табличный тест с независимыми записями вместо коллекции работающего сервиса. Выберите небольшой набор с одним названием в двух категориях и третьим неподходящим предметом. Сначала предскажите точное множество ID для совместного поиска; это выявляет реализацию OR, которую пропустила бы проверка одного статуса.
LC_ALL=C.UTF-8 nano handler_test.go
Как в первой лабораторной, начните с package main, импортируйте используемые пакеты и поместите тест в функцию Test, принимающую *testing.T. Используйте net/http/httptest для запросов и рекордеров, encoding/json для декодирования, testing для проверок и net/url, если кодируете запрос. Создайте обработчик из тестового набора один раз внутри теста.
Представьте связанные запросы срезом анонимных структур. Например, поля name string, path string и status int задают каждому случаю метку, путь запроса и ожидаемый статус. Задайте собственные строки: включите успешный совместный поиск, поиск без совпадений, неверную категорию, повтор ключа, неверное кодирование и отсутствующий ID. Цикл for _, tc := range cases применяет запрос каждого случая к новому рекордеру. Используйте tc.path в httptest.NewRequest("GET", tc.path, nil); сравните recorder.Code с tc.status и включите имя случая в вывод ошибки. Повторное использование рекордера смешало бы тела ответов, поэтому создавайте новый для каждого случая.
t.Run(tc.name, func(t *testing.T) { ... }) в Go при желании даёт каждой строке именованный подтест. Замените точки операторами рекордера, запроса и проверок. Неуспешный t.Fatalf должен сообщать и ожидаемый, и наблюдаемый статус.
Одного статуса недостаточно для доказательства поиска. Для успешного совместного случая декодируйте в []Item, проверьте ошибку декодирования и сравните возвращённые ID и поля с подходящими тестовыми записями. Для пустого случая объявите var got []Item, проверьте ошибку декодирования, затем потребуйте got != nil и len(got) == 0. Это отличает пустой массив от null без преобразования типов. Для отклонённых случаев декодируйте в map[string]string и потребуйте непустое error. Добавьте отдельный запрос POST к распознанному маршруту и проверьте и 405, и recorder.Header().Get("Allow") == "GET". Здесь используется тот же приём рекордера с другими ожидаемыми результатами.
Вы выбираете набор данных, имена случаев и точные проверки. Сделайте хотя бы одну проверку, которая не прошла бы при лишнем одноимённом предмете в результате. Сохраните через Ctrl+O и Enter, затем Ctrl+X.
gofmt -w handler_test.go
go test -v .
Ожидайте PASS. Если тест не проходит, сравните выведенные путь и статус с контрактом выше. Исправьте обработчик, повторите тесты и пересоберите через python3 serve.py, чтобы браузер использовал ваш текущий код. Сохраните вспомогательные файлы main.go, go.mod, items.json, serve.py и web/ без изменений. Можно редактировать handler.go и добавлять или менять собственный код Go и тесты, не заменяя исходную коллекцию или веб-страницу.
Переключитесь на Lost and Found и обновите страницу. Найдите blue в категории bags; ожидайте одну карточку с номером LF-106. Откройте View item, прочитайте Sports hall, затем закройте через Back to collection. Найдите not-in-the-collection; ожидайте 0 items found и No matching items. Очистите ключевое слово, выберите All categories и повторите поиск; должны вернуться все шесть записей. Используйте клавишу Tab для перехода к полям и кнопкам; Enter отправляет форму, а Escape закрывает открытое окно сведений.

Обратите внимание на выбранную категорию, счётчик одного результата и соответствующий номер. Страница использует ваш API для каждого поиска и запроса сведений. Её сообщение о недоступности означает настоящую проблему запроса; встроенная коллекция никогда не подставляется вместо данных.
Резюме
Вы объединили нормализованный фильтр названия и точный фильтр категории, сохранили идентичность предметов, вернули [] для допустимого поиска без совпадений и разделили ошибки 400, 404 и 405. Табличный тест и настоящая веб-страница вызывают обработчик. В задании вы перенесёте тот же контракт на данные другого бюро.



