پارس کردن پارامترهای رشته کوئری
در فصلهای بعدی، ما اندپوینت GET /v1/movies را به گونهای پیکربندی میکنیم که یک کلاینت بتواند از طریق پارامترهای رشته کوئری کنترل کند که کدام رکوردهای فیلم برگردانده شوند. به عنوان مثال:
/v1/movies?title=godfather&genres=crime,drama&page=1&page_size=5&sort=-year
اگر یک کلاینت چنین رشته کوئریای ارسال کند، در اصل به API ما میگوید: “لطفاً اولین ۵ رکوردی که عنوان فیلم شامل godfather است و ژانرها شامل crime و drama هستند، بر اساس سال انتشار نزولی مرتب شدهاند را برگردان”.
پس اولین چیزی که بررسی خواهیم کرد نحوه پارس کردن این پارامترهای رشته کوئری در کد Go ما است.
همانطور که امیدوارم از Let's Go به یاد دارید، ما میتوانیم دادههای رشته کوئری را از یک درخواست با فراخوانی متد r.URL.Query() بازیابی کنیم. این متد یک نوع url.Values برمیگرداند که در اصل یک نقشه حاوی دادههای رشته کوئری است.
سپس میتوانیم مقادیر را از این نقشه با استفاده از متد Get() استخراج کنیم، که مقدار یک کلید خاص را به عنوان یک نوع string برمیگرداند، یا اگر کلید متناظری در رشته کوئری وجود نداشته باشد، رشته خالی "" را برمیگرداند.
در مورد ما، باید پردازش اضافی روی برخی از این مقادیر رشته کوئری نیز انجام دهیم. به طور خاص:
- پارامتر
genresممکن است شامل مقادیر جدا شده با کاما باشد — مانندgenres=crime,drama. ما میخواهیم این مقادیر را جدا کرده و در یک برش[]stringذخیره کنیم. - پارامترهای
pageوpage_sizeشامل اعداد هستند و ما میخواهیم این مقادیر رشته کوئری را به انواعintGo تبدیل کنیم.
علاوه بر این:
- برخی بررسیهای اعتبارسنجی وجود دارد که باید روی مقادیر رشته کوئری اعمال کنیم، مانند اطمینان از اینکه
pageوpage_sizeاعداد منفی نباشند. - میخواهیم برنامه ما برخی مقادیر پیشفرض منطقی در صورتی که پارامترهایی مانند
page،page_sizeوsortتوسط کلاینت ارائه نشوند، تنظیم کند.
ایجاد توابع کمکی
برای کمک به این کار، سه تابع کمکی جدید ایجاد خواهیم کرد: readString()، readInt() و readCSV(). ما از این توابع کمکی برای استخراج و پارس کردن مقادیر از رشته کوئری، یا برگرداندن یک مقدار پیشفرض 'جایگزین' در صورت لزوم استفاده خواهیم کرد.
به فایل cmd/api/helpers.go خود بروید و کد زیر را اضافه کنید:
package main import ( "encoding/json" "errors" "fmt" "io" "net/http" "net/url" // New import "strconv" "strings" "greenlight.alexedwards.net/internal/validator" // New import "github.com/julienschmidt/httprouter" ) ... // The readString() helper returns a string value from the query string, or the provided // default value if no matching key could be found. func (app *application) readString(qs url.Values, key string, defaultValue string) string { // Extract the value for a given key from the query string. If no key exists this // will return the empty string "". s := qs.Get(key) // If no key exists (or the value is empty) then return the default value. if s == "" { return defaultValue } // Otherwise return the string. return s } // The readCSV() helper reads a string value from the query string and then splits it // into a slice on the comma character. If no matching key could be found, it returns // the provided default value. func (app *application) readCSV(qs url.Values, key string, defaultValue []string) []string { // Extract the value from the query string. csv := qs.Get(key) // If no key exists (or the value is empty) then return the default value. if csv == "" { return defaultValue } // Otherwise parse the value into a []string slice and return it. return strings.Split(csv, ",") } // The readInt() helper reads a string value from the query string and converts it to an // integer before returning. If no matching key could be found it returns the provided // default value. If the value couldn't be converted to an integer, then we record an // error message in the provided Validator instance. func (app *application) readInt(qs url.Values, key string, defaultValue int, v *validator.Validator) int { // Extract the value from the query string. s := qs.Get(key) // If no key exists (or the value is empty) then return the default value. if s == "" { return defaultValue } // Try to convert the value to an int. If this fails, add an error message to the // validator instance and return the default value. i, err := strconv.Atoi(s) if err != nil { v.AddError(key, "must be an integer value") return defaultValue } // Otherwise, return the converted integer value. return i }
افزودن هندلر و مسیر API
در مرحله بعد، بیایید یک listMoviesHandler جدید برای اندپوینت GET /v1/movies ایجاد کنیم. در حال حاضر، این هندلر به سادگی رشته کوئری درخواست را با استفاده از توابع کمکی که تازه ایجاد کردیم پارس میکند و سپس محتوای آن را در یک پاسخ HTTP خروجی میدهد.
اگر دنبال میکنید، بروید و listMoviesHandler را به این صورت ایجاد کنید:
package main ... func (app *application) listMoviesHandler(w http.ResponseWriter, r *http.Request) { // To keep things consistent with our other handlers, we'll define an input struct // to hold the expected values from the request query string. var input struct { Title string Genres []string Page int PageSize int Sort string } // Initialize a new Validator instance. v := validator.New() // Call r.URL.Query() to get the url.Values map containing the query string data. qs := r.URL.Query() // Use our helpers to extract the title and genres query string values, falling back // to defaults of an empty string and an empty slice respectively if they are not // provided by the client. input.Title = app.readString(qs, "title", "") input.Genres = app.readCSV(qs, "genres", []string{}) // Get the page and page_size query string values as integers. Notice that we set // the default page value to 1 and default page_size to 20, and that we pass the // validator instance as the final argument here. input.Page = app.readInt(qs, "page", 1, v) input.PageSize = app.readInt(qs, "page_size", 20, v) // Extract the sort query string value, falling back to "id" if it is not provided // by the client (which will imply an ascending sort on movie ID). input.Sort = app.readString(qs, "sort", "id") // Check the Validator instance for any errors and use the failedValidationResponse() // helper to send the client a response if necessary. if !v.Valid() { app.failedValidationResponse(w, r, v.Errors) return } // Dump the contents of the input struct in an HTTP response. fmt.Fprintf(w, "%+v\n", input) }
سپس باید مسیر GET /v1/movies را در فایل cmd/api/routes.go خود ایجاد کنیم، به این صورت:
package main ... func (app *application) routes() http.Handler { router := httprouter.New() router.NotFound = http.HandlerFunc(app.notFoundResponse) router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse) router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthcheckHandler) // Add the route for the GET /v1/movies endpoint. router.HandlerFunc(http.MethodGet, "/v1/movies", app.listMoviesHandler) router.HandlerFunc(http.MethodPost, "/v1/movies", app.createMovieHandler) router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.showMovieHandler) router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.updateMovieHandler) router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.deleteMovieHandler) return app.recoverPanic(router) }
و با این کار، آمادهایم این را در عمل ببینیم!
بروید و درخواستی به اندپوینت GET /v1/movies ارسال کنید که شامل پارامترهای رشته کوئری مورد انتظار باشد، مانند زیر.
$ curl "localhost:4000/v1/movies?title=godfather&genres=crime,drama&page=1&page_size=5&sort=year"
{Title:godfather Genres:[crime drama] Page:1 PageSize:5 Sort:year}
خوب به نظر میرسد — میتوانیم ببینیم که مقادیر ارائه شده در رشته کوئری ما همگی به درستی پارس شدهاند و در ساختار input گنجانده شدهاند.
اگر میخواهید، سعی کنید درخواستی با هیچ پارامتر رشته کوئری ارسال کنید. در این مورد، باید ببینید که مقادیر در ساختار input مقادیر پیشفرضی را که در کد listMoviesHandler خود مشخص کردهایم، به خود میگیرند. به این صورت:
$ curl localhost:4000/v1/movies
{Title: Genres:[] Page:1 PageSize:20 Sort:id}
ایجاد یک ساختار Filters
پارامترهای رشته کوئری page، page_size و sort چیزهایی هستند که احتمالاً میخواهید در اندپوینتهای دیگر API خود نیز استفاده کنید. بنابراین، برای کمک به تسهیل این کار، بیایید به سرعت آنها را در یک ساختار Filters قابل استفاده مجدد جدا کنیم.
اگر دنبال میکنید، بروید و فایل جدید internal/data/filters.go را ایجاد کنید:
$ touch internal/data/filters.go
و سپس کد زیر را اضافه کنید:
package data type Filters struct { Page int PageSize int Sort string }
پس از اتمام کار، به listMoviesHandler خود برگردید و آن را به این صورت برای استفاده از ساختار Filters جدید به روز کنید:
package main ... func (app *application) listMoviesHandler(w http.ResponseWriter, r *http.Request) { // Embed the new Filters struct. var input struct { Title string Genres []string data.Filters } v := validator.New() qs := r.URL.Query() input.Title = app.readString(qs, "title", "") input.Genres = app.readCSV(qs, "genres", []string{}) // Read the page and page_size query string values into the embedded struct. input.Filters.Page = app.readInt(qs, "page", 1, v) input.Filters.PageSize = app.readInt(qs, "page_size", 20, v) // Read the sort query string value into the embedded struct. input.Filters.Sort = app.readString(qs, "sort", "id") if !v.Valid() { app.failedValidationResponse(w, r, v.Errors) return } fmt.Fprintf(w, "%+v\n", input) }
در این مرحله، باید بتوانید API را دوباره اجرا کنید و همه چیز باید مانند قبل به کار خود ادامه دهد.