Suche und Fehler vorhersehbar machen

GolangBeginner
Jetzt üben

Einführung

Die Fundsachenseite benötigt jetzt eine sinnvolle Suche. Wer nach einer blauen Tasche sucht, sollte weder sämtliche blauen Gegenstände noch sämtliche Taschen erhalten. Auch ein fehlender Gegenstand, eine ungültige Kategorie und eine gültige Suche ohne Treffer benötigen unterschiedliche Antworten.

Dieser unabhängige Arbeitsbereich beginnt mit dem Listen- und Detail-Handler aus dem vorherigen geführten Lab. Du benötigst dessen Kenntnisse zu HTTP, JSON und httptest oder gleichwertige Erfahrung. Du kombinierst Filter, ergänzt eindeutige Fehler und verwendest die echte Webseite. Plane einschließlich Lesen und gewöhnlicher Fehler etwa 55 Minuten ein; dies ist eine Schätzung des Autors.

Namens- und Kategoriefilter kombinieren

In diesem Schritt sorgst du dafür, dass beide Suchbedingungen auf denselben Datensatz zutreffen müssen. Beginne in Workspace. Stelle das Arbeitsverzeichnis wieder her und ergänze die vorbereitete Go-Installation im Suchpfad dieses Terminals. export wirkt nur auf die aktuelle Shell; $PATH behält deren bestehende Einträge bei.

cd /home/labex/project/lost-found
export PATH=/usr/local/go/bin:$PATH

Verwende zuerst die funktionierende Liste und lies ihre Daten:

curl -i 'http://127.0.0.1:8080/api/items'
cat items.json

Erwarte alle sechs Datensätze. Wechsle zu Lost and Found, gib blue ein, wähle bags und klicke auf Search collection. Der aktuelle Handler ignoriert diese Eingaben und zeigt weiterhin alle sechs Gegenstände. Kehre zu Workspace zurück, um die fehlende Entscheidung zu ergänzen.

Die Abfragezeichenfolge einer URL folgt auf ?. category=bags&q=blue übergibt zwei benannte Parameter, getrennt durch &. Eine Suche in der API-Liste akzeptiert nur category und q, jeweils höchstens einmal. Dieser Schritt implementiert zunächst gültige Suchen; der nächste behandelt ungültige Eingaben.

Der Vertrag ist präzise: Eine fehlende oder leere Kategorie bedeutet alle Kategorien. Andernfalls müssen Schreibweise und Groß-/Kleinschreibung einer Kategorie in items.json entsprechen. Das Schlüsselwort q sucht eine Teilzeichenkette im Namen ohne Unterscheidung der Groß-/Kleinschreibung; umgebender Leerraum wird ignoriert. Ein fehlendes, leeres oder nur aus Leerraum bestehendes Schlüsselwort passt zu jedem Namen. Werden beide Filter angegeben, müssen beide zutreffen (AND). Behalte die vollständigen Felder jeder passenden ID ohne Duplikate bei; jede Ergebnisreihenfolge ist erlaubt. Eine gültige Suche ohne Treffer liefert HTTP 200 und [].

r.URL.Query().Get liest einen decodierten Parameter. strings.TrimSpace entfernt umgebenden Leerraum, strings.ToLower wendet die Unicode-Umwandlung in Kleinbuchstaben an und strings.Contains prüft eine Teilzeichenkette. Dies ist ein einfacher Vergleich in Kleinbuchstaben, keine Entfernung von Akzenten und keine sprachabhängige Suchmaschine. make([]Item, 0) erstellt einen leeren Slice, der nicht nil ist, damit JSON fehlende Treffer als [] codiert. && verbindet beide erforderlichen Bedingungen; || würde hier fälschlich auch Datensätze einschließen, die nur eine Bedingung erfüllen.

Sage vor dem Bearbeiten drei Ergebnisse anhand von items.json voraus: nur das Schlüsselwort blue, nur die Kategorie bags und beide gemeinsam. Nur LF-106 gehört zu ihrer Schnittmenge. Die beiden Regenschirme zeigen, warum die Rückgabe von Treffern irgendeiner der Bedingungen falsch wäre.

Behalte den geerbten Detailcode. Verwende den nano-Ablauf aus dem ersten Lab: Ctrl+W findet Text, Ctrl+O und Enter speichern unter dem vorhandenen Dateinamen, und Ctrl+X beendet den Editor. Eine Sicherung muss eine andere Endung als .go haben, damit Go keinen zweiten Handler kompiliert.

cp handler.go handler.go.backup
LC_ALL=C.UTF-8 nano handler.go

Finde den Sammlungszweig /api/items und ersetze nur seine Logik zur Auswahl der Sammlung. Lass Antwortheader, Detailschleife und Antwort für fehlende IDs unverändert. Implementiere diese Entscheidungen anhand des Arguments items:

  • Lies die decodierten Parameter category und q mit r.URL.Query().Get. Behalte die Kategorie exakt wie angegeben. Wende vor der Schleife strings.TrimSpace und dann strings.ToLower auf das Schlüsselwort an.
  • Beginne den Ergebnis-Slice mit make([]Item, 0). Verwende den ursprünglichen vollständigen Slice nicht als Ergebnis weiter, da sonst nicht passende Datensätze erhalten blieben.
  • Durchlaufe die Datensätze mit for _, item := range items. Entscheide, ob die Kategorie fehlt/leer ist oder item.Category entspricht, und unabhängig davon, ob der kleingeschriebene Name das normalisierte Schlüsselwort enthält. Verbinde beide Entscheidungen mit &&. Hänge mit append das gesamte passende item an, nicht nur seinen Namen oder eine ID aus einem anderen Datensatz.
  • Codiere diesen Ergebnis-Slice im Sammlungszweig und kehre zurück. Wähle Details weiterhin nach ID aus; wende Sammlungsfilter nicht auf Detailanfragen an.

Zum Vergleich kann eine Kategoriebedingung als category == "" || item.Category == category ausgedrückt werden: Das || steht hier entweder für keine Einschränkung oder für die angefragte Kategorie. Es ersetzt nicht das AND zwischen Kategorie- und Namensbedingung. Wähle lokale Variablennamen, die diese getrennten Entscheidungen lesbar machen.

Speichere, beende und prüfe die tatsächlich gespeicherte Datei:

cat handler.go

Formatiere und baue neu. Mit -w darf gofmt die Datei aktualisieren. serve.py baut deinen Code und ersetzt erst nach einem erfolgreichen Build den aufgezeichneten Server dieses Arbeitsverzeichnisses. Bei Kompilierungsfehlern läuft die alte Version weiter; korrigiere die gemeldete Zeile und führe beide Befehle erneut aus.

gofmt -w handler.go
python3 serve.py

Sende nun eine kombinierte Suche. Die Anführungszeichen halten & innerhalb der URL, statt es als Shell-Operator zu behandeln:

curl -i 'http://127.0.0.1:8080/api/items?category=bags&q=blue'

Erwarte ausschließlich LF-106, Blue sports bag, bei Sports hall. Würden auch die Regenschirme zurückgegeben, wäre OR verwendet worden. Ein gültiges Schlüsselwort ohne Treffer muss ein leeres Array erzeugen:

curl -i 'http://127.0.0.1:8080/api/items?q=not-in-the-collection'

Lass curl Texte mit Leerzeichen, + oder Nicht-ASCII-Zeichen codieren. -G sendet die angegebenen Daten in einer GET-Abfragezeichenfolge, und --data-urlencode codiert diesen Parameter mit Prozentcodierung. Ohne Codierung kann ein wörtliches Pluszeichen in Abfragedaten als Leerzeichen decodiert werden.

curl -G -i --data-urlencode 'q=  CAFÉ +  ' 'http://127.0.0.1:8080/api/items'

Erwarte LF-104, Café + Map, bei Welcome booth. Das großgeschriebene Schlüsselwort und der umgebende Leerraum werden normalisiert, während das wörtliche Pluszeichen weiterhin zum Namen gehört. Falls du einen Pfad falsch eingegeben hast, stelle cd /home/labex/project/lost-found wieder her; ist der Dienst nicht verfügbar, lies .work/server.log mit cat und baue nach der Fehlerkorrektur neu.

Ungültige Anfragen von leeren Suchergebnissen unterscheiden

In diesem Schritt machst du Fehler sowohl durch den Status als auch durch JSON erkennbar und verwendest anschließend die fertige Seite. Beginne in Workspace im selben Verzeichnis.

Verwende 400 für eine Sammlungsabfrage mit einer unbekannten Kategorie, einem unbekannten Parameter oder einem wiederholten Parameter. Die Schreibweise der Kategorie ist exakt; Bags unterscheidet sich von bags. Leere Werte für category und q bleiben gültig. Verwende 404 für eine unbekannte Gegenstands-ID oder einen unbekannten API-Pfad. Verwende 405 für andere Methoden als GET auf /api/items und /api/items/<id>, mit Allow: GET. Fehlerbodys sind Objekte mit einer nicht leeren error-Zeichenkette; der Wortlaut ist frei. Eine gültige Suche ohne Treffer bleibt bei 200 mit []. Detailanfragen wählen nach ID aus; die Abfragefilter der Sammlung gelten nicht für Details. Die vorbereitete Startseite / bleibt verfügbar.

url.ParseQuery(r.URL.RawQuery) analysiert die Abfrage und liefert sowohl Werte als auch einen Fehler, sodass fehlerhafte Abfragecodierung abgelehnt werden kann. Jeder Schlüssel verweist auf einen Slice von Werten: Dessen Länge macht wiederholte Parameter erkennbar. Dieses Beispiel prüft eine Kategorie anhand der aktuellen Quelldatensätze statt einer festen Kategorienliste, sodass ein anderes Fundbüro den Handler wiederverwenden kann.

Setze Header vor dem Schreiben des Bodys. Frühe Rückgaben verhindern, dass ein Fehler in ein normales erfolgreiches Ergebnis übergeht. Vergleiche vor dem Bearbeiten die gültige Suche ohne Treffer aus dem vorherigen Schritt mit den folgenden abgelehnten Anfragen. Sage voraus, warum ihre Statuswerte unterschiedlich sind. Eine fehlende Gegenstands-ID ist ein anderer Fehler als eine fehlerhafte Sammlungsabfrage. Behalte die Suchentscheidungen des vorherigen Schritts; diese Änderung ergänzt Prüfungen darum herum.

LC_ALL=C.UTF-8 nano handler.go

Finde mit Ctrl+W die zurückgegebene Handler-Funktion. Ergänze net/url in deinen Imports für url.ParseQuery. Implementiere diese Prüfungen, ohne die gesamte Datei zu ersetzen:

Prüfe zuerst den Pfad. Der Sammlungspfad ist genau /api/items; ein Detail beginnt mit /api/items/. Jeder andere API-Pfad muss 404 zurückgeben, statt in die normale Seite oder Liste überzugehen. Prüfe dann die Methode auf diesen erkannten Routen: Ist sie nicht GET, setze Allow auf GET und liefere einen JSON-Fehler mit 405.

Verwende anschließend bei Bedarf eine gemeinsame lokale Funktion zum Fehlerschreiben. Sie kann einen ganzzahligen Status und eine Nachrichtenzeichenkette erhalten, w.WriteHeader(status) aufrufen und map[string]string{"error": message} codieren. Der Content-Type-Header wird weiterhin zuerst gesetzt. Wähle nicht leere Nachrichten; ihr Wortlaut wird nicht bewertet.

Lass dann die Detailauswahl vor der Verarbeitung der Sammlungsabfrage, damit Abfragefilter keine ID-Suche verändern. Ein fehlendes Detail liefert weiterhin 404 mit einem JSON-Fehler.

Analysiere danach im Sammlungszweig r.URL.RawQuery mit url.ParseQuery. Prüfe dessen zurückgegebenen Fehler, bevor du Werte verwendest; fehlerhafte Codierung muss 400 liefern. Eine Prozent-Escape-Sequenz benötigt zwei hexadezimale Ziffern; /api/items?q=%ZZ ist daher eine konkrete fehlerhafte Eingabe für einen Test. Durchlaufe jeden Schlüssel und seinen Werte-Slice. Lehne andere Schlüssel als category oder q sowie Slices ab, deren Länge nicht eins ist. Kehre nach jeder Ablehnung sofort zurück.

Lies als Nächstes die akzeptierten Parameter aus den analysierten Werten. Durchlaufe bei einer nicht leeren Kategorie die übergebenen Datensätze, um festzustellen, ob genau diese Kategorie existiert; lehne sie andernfalls mit 400 ab. Kopiere keine feste Kategorienliste aus items.json in das Programm. Ein anderes Fundbüro könnte vollständig neue Kategorien haben.

Führe schließlich nach erfolgreichen Prüfungen deine vorhandene AND-Suche aus und codiere das Ergebnisarray. Leere Kategorie- und Schlüsselwortwerte bleiben gültig. Behalte für fehlende Treffer [], statt daraus einen Fehler zu machen.

Speichere mit Ctrl+O und Enter und beende mit Ctrl+X. Meldet der Editor einen unbeabsichtigten Dateinamen, brich die Abfrage mit Ctrl+C ab und prüfe den Pfad. Erhalte beim Korrigieren von Compilerfehlern den bereits funktionierenden Such- und Detailcode.

gofmt -w handler.go
python3 serve.py

Die folgenden Anfragen werden absichtlich abgelehnt. Erwarte der Reihe nach 400, 400, 404 und 405. -X POST wählt die POST-Methode; dies ist ein schreibgeschützter Dienst und darf nichts anlegen.

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'

Jede Antwort enthält ein JSON-Feld error; die POST-Antwort enthält außerdem Allow: GET. Wenn curl diese Antworten empfängt, unterscheidet sich das von einem Verbindungsfehler, bei dem weder HTTP-Status noch JSON-Antwort eintreffen. Nimm erwartete HTTP-Fehler nicht zum Anlass, das Arbeitsverzeichnis zurückzusetzen.

Verwende eine Tabelle mit Testfällen, um zusammengehörende Erwartungen zu bündeln. Das wiederholt die Recorder-/Request-Technik des vorherigen Labs. url.Values.Encode() codiert die Unicode-/Plus-Abfrage in Go. Decodiere eine Listenantwort in []Item und prüfe den von json.Unmarshal zurückgegebenen Fehler, bevor du ihren Inhalt untersuchst. Bei einem leeren Ergebnis bedeutet ein Slice ungleich nil mit Länge null []; ein nil-Slice würde null bedeuten. Die bereits ausgeführte Suche am laufenden Dienst prüft außerdem die tatsächlichen Gegenstandsinhalte.

Erstelle einen eigenen tabellengesteuerten Test mit unabhängigen Datensätzen statt der laufenden Sammlung. Wähle eine kleine Testeingabe mit demselben Namen in zwei Kategorien und einem dritten nicht passenden Gegenstand. Sage zuerst die exakte ID-Menge einer kombinierten Suche voraus; das erkennt eine OR-Implementierung, die eine reine Statusprüfung übersehen würde.

LC_ALL=C.UTF-8 nano handler_test.go

Beginne wie im ersten Lab mit package main, importiere die verwendeten Pakete und schreibe den Test in eine Test-Funktion mit *testing.T. Verwende net/http/httptest für Anfragen/Recorder, encoding/json zum Decodieren, testing für Prüfungen und net/url, falls du eine Abfrage codierst. Erstelle innerhalb des Tests einmal einen Handler aus deinen Testdaten.

Stelle verwandte Anfragen durch einen Slice anonymer Strukturen dar. Beispielsweise geben die Felder name string, path string und status int jedem Fall eine Bezeichnung, einen Anfragepfad und einen erwarteten Status. Erstelle eigene Zeilen: eine erfolgreiche kombinierte Suche, eine leere Suche, eine ungültige Kategorie, einen wiederholten Schlüssel, fehlerhafte Codierung und eine fehlende ID. Eine Schleife for _, tc := range cases wendet die Anfrage jedes Falls auf einen neuen Recorder an. Verwende tc.path in httptest.NewRequest("GET", tc.path, nil); vergleiche recorder.Code mit tc.status und gib bei Fehlern den Fallnamen aus. Ein wiederverwendeter Recorder würde Antwortbodys vermischen; erstelle daher für jeden Fall einen neuen.

Mit Gos t.Run(tc.name, func(t *testing.T) { ... }) kann jede Zeile optional einen benannten Untertest erhalten. Ersetze die Punkte durch deine Recorder-, Anfrage- und Prüfanweisungen. Ein fehlgeschlagenes t.Fatalf sollte erwarteten und beobachteten Status melden.

Der Status allein belegt die Suche nicht. Decodiere den erfolgreichen kombinierten Fall in []Item, prüfe den Decodierungsfehler und vergleiche zurückgegebene IDs und Felder mit den passenden Testdatensätzen. Deklariere für den leeren Fall var got []Item, prüfe den Decodierungsfehler und fordere dann got != nil und len(got) == 0. Das unterscheidet ein leeres Array von null, ohne Typen umzuwandeln. Decodiere abgelehnte Fälle in map[string]string und fordere ein nicht leeres error. Ergänze eine separate POST-Anfrage für eine erkannte Route und prüfe sowohl 405 als auch recorder.Header().Get("Allow") == "GET". Diese verwenden dieselbe Recorder-Technik mit anderen erwarteten Ergebnissen.

Du entscheidest über Testdaten, Fallnamen und genaue Prüfungen. Schreibe mindestens eine Prüfung, die bei einem zusätzlichen gleichnamigen Gegenstand im Ergebnis fehlschlagen würde. Speichere mit Ctrl+O und Enter und beende mit Ctrl+X.

gofmt -w handler_test.go
go test -v .

Erwarte PASS. Wenn ein Test fehlschlägt, vergleiche den angezeigten Pfad und Status mit dem obigen Vertrag. Korrigiere den Handler, führe die Tests erneut aus und baue mit python3 serve.py neu, damit der Browser deinen aktuellen Code verwendet. Halte die unterstützenden Dateien main.go, go.mod, items.json, serve.py und web/ unverändert. Du darfst handler.go bearbeiten und eigenen Go-Quellcode sowie Tests ergänzen oder bearbeiten, ohne die Quellsammlung oder Webseite zu ersetzen.

Wechsle zu Lost and Found und lade neu. Suche nach blue in der Kategorie bags; erwarte eine Karte mit der Referenz LF-106. Öffne View item, lies Sports hall und schließe dann mit Back to collection. Suche nach not-in-the-collection; erwarte 0 items found und No matching items. Leere das Schlüsselwort, wähle All categories und suche erneut; alle sechs Datensätze müssen wieder erscheinen. Erreiche Felder und Schaltflächen mit der Tabulatortaste; Enter sendet das Formular ab, und Escape schließt einen geöffneten Detaildialog.

Eine kombinierte Suche zeigt nur die passende blaue Tasche

Achte auf die ausgewählte Kategorie, den Zähler für ein einzelnes Ergebnis und die passende Referenz. Die Seite verwendet deine API für jede Suche und jedes Detail. Ihre Meldung zur Nichtverfügbarkeit zeigt ein tatsächliches Anfrageproblem an; sie ersetzt die Daten niemals durch eine eingebaute Sammlung.

Zusammenfassung

Du hast normalisierte Namensfilter und exakte Kategoriefilter kombiniert, die Identität der Gegenstände erhalten, für gültige Suchen ohne Treffer [] zurückgegeben und die Fehler 400, 404 und 405 unterschieden. Dein tabellengesteuerter Test und die echte Webseite verwenden den Handler. Die Challenge überträgt denselben Vertrag auf die Daten eines anderen Fundbüros.