Naming, layout and comments: descriptive full-word names, one item per line in wrapped calls and literals, and doc comments on exported names
Short and single-letter names in application code become full words (tests keep theirs). Wrapped calls, signatures and composite literals put one item per line, and one-line function bodies are expanded. Exported names and groups of constants get doc comments, and comments refer to the renamed identifiers.
This commit is contained in:
1 parent
043e307663
commit
465358609a
78 files changed
+2374
-1402
No files matched your search
@@ -14,11 +14,17 @@ import (
|
||||
"git.avsdev.uk/Go/job-search/internal/mock/findatender/ocds"
|
||||
)
|
||||
|
||||
// Paging and request parameters of the release package endpoint.
|
||||
const (
|
||||
// defaultLimit is the page size when the request sets no limit.
|
||||
defaultLimit = 100
|
||||
maxLimit = 100
|
||||
timeLayout = "2006-01-02T15:04:05"
|
||||
BasePath = "/api/1.0/ocdsReleasePackages"
|
||||
// maxLimit is the largest page size a request may ask for.
|
||||
maxLimit = 100
|
||||
// timeLayout is the documented layout of updatedFrom and updatedTo; see
|
||||
// parseTime for the others accepted.
|
||||
timeLayout = "2006-01-02T15:04:05"
|
||||
// BasePath is the path the endpoint is served at.
|
||||
BasePath = "/api/1.0/ocdsReleasePackages"
|
||||
)
|
||||
|
||||
// server serves release packages from a Store; see NewHandler.
|
||||
@@ -36,10 +42,18 @@ type server struct {
|
||||
// Stored release dates are UTC; comparisons are on absolute instants. It
|
||||
// logs to slog.Default() unless given WithLogger.
|
||||
func NewHandler(
|
||||
store *Store, baseURL string, loc *time.Location, opts ...Option,
|
||||
store *Store,
|
||||
baseURL string,
|
||||
location *time.Location,
|
||||
opts ...Option,
|
||||
) http.HandlerFunc {
|
||||
s := &server{store: store, baseURL: baseURL, loc: loc, opts: applyOptions(opts)}
|
||||
return s.handle
|
||||
handler := &server{
|
||||
store: store,
|
||||
baseURL: baseURL,
|
||||
loc: location,
|
||||
opts: applyOptions(opts),
|
||||
}
|
||||
return handler.handle
|
||||
}
|
||||
|
||||
// encodeCursor makes the opaque cursor for a next link that resumes at store
|
||||
@@ -50,18 +64,18 @@ func encodeCursor(i int) string {
|
||||
|
||||
// decodeCursor returns the release index in a cursor made by encodeCursor.
|
||||
// Anything else, including a negative index, is an error.
|
||||
func decodeCursor(c string) (int, error) {
|
||||
b, err := base64.RawURLEncoding.DecodeString(c)
|
||||
if err != nil || !strings.HasPrefix(string(b), "idx:") {
|
||||
func decodeCursor(cursor string) (int, error) {
|
||||
decoded, err := base64.RawURLEncoding.DecodeString(cursor)
|
||||
if err != nil || !strings.HasPrefix(string(decoded), "idx:") {
|
||||
return 0, fmt.Errorf("invalid cursor")
|
||||
}
|
||||
|
||||
n, err := strconv.Atoi(strings.TrimPrefix(string(b), "idx:"))
|
||||
if err != nil || n < 0 {
|
||||
index, err := strconv.Atoi(strings.TrimPrefix(string(decoded), "idx:"))
|
||||
if err != nil || index < 0 {
|
||||
return 0, fmt.Errorf("invalid cursor")
|
||||
}
|
||||
|
||||
return n, nil
|
||||
return index, nil
|
||||
}
|
||||
|
||||
func badRequest(w http.ResponseWriter, msg string) {
|
||||
@@ -72,20 +86,20 @@ func badRequest(w http.ResponseWriter, msg string) {
|
||||
}
|
||||
|
||||
// parseTime reads a query timestamp. Values without an offset (the documented
|
||||
// yyyy-MM-ddTHH:mm:ss, or a plain date) are local time in loc; values with an
|
||||
// yyyy-MM-ddTHH:mm:ss, or a plain date) are local time in location; values with an
|
||||
// offset or Z (RFC 3339) mean exactly what they say.
|
||||
func parseTime(v string, loc *time.Location) (time.Time, error) {
|
||||
if loc == nil {
|
||||
loc = time.UTC
|
||||
func parseTime(value string, location *time.Location) (time.Time, error) {
|
||||
if location == nil {
|
||||
location = time.UTC
|
||||
}
|
||||
|
||||
if t, err := time.Parse(time.RFC3339, v); err == nil {
|
||||
return t, nil
|
||||
if parsed, err := time.Parse(time.RFC3339, value); err == nil {
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
for _, layout := range []string{timeLayout, "2006-01-02"} {
|
||||
if t, err := time.ParseInLocation(layout, v, loc); err == nil {
|
||||
return t, nil
|
||||
if parsed, err := time.ParseInLocation(layout, value, location); err == nil {
|
||||
return parsed, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,15 +121,15 @@ func (s *server) handle(w http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
pq, err := s.parseQuery(q)
|
||||
query := req.URL.Query()
|
||||
page, err := s.parseQuery(query)
|
||||
if err != nil {
|
||||
badRequest(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
releases, next, more := s.store.Page(pq.from, pq.upper, pq.start, pq.limit)
|
||||
pkg := ocds.ReleasePackage{
|
||||
releases, next, more := s.store.Page(page.from, page.upper, page.start, page.limit)
|
||||
releasePackage := ocds.ReleasePackage{
|
||||
URI: s.baseURL + req.URL.RequestURI(),
|
||||
Version: "1.1",
|
||||
Extensions: ocds.DeclaredExtensions,
|
||||
@@ -127,73 +141,78 @@ func (s *server) handle(w http.ResponseWriter, req *http.Request) {
|
||||
Releases: releases,
|
||||
}
|
||||
if more {
|
||||
pkg.Links = &ocds.ReleasePackageLinks{Next: s.nextLink(q, next)}
|
||||
releasePackage.Links = &ocds.ReleasePackageLinks{
|
||||
Next: s.nextLink(query, next),
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", " ")
|
||||
enc.SetEscapeHTML(false)
|
||||
if err := enc.Encode(pkg); err != nil {
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder.SetIndent("", " ")
|
||||
encoder.SetEscapeHTML(false)
|
||||
if err := encoder.Encode(releasePackage); err != nil {
|
||||
s.opts.logger("handler").Warn("writing response failed", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// parseQuery reads and checks the query parameters. Its errors are meant for
|
||||
// the client.
|
||||
func (s *server) parseQuery(q url.Values) (pageQuery, error) {
|
||||
pq := pageQuery{limit: defaultLimit, upper: time.Now().UTC()}
|
||||
|
||||
if v := q.Get("limit"); v != "" {
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil || n < 1 || n > maxLimit {
|
||||
return pq, fmt.Errorf("limit must be an integer between 1 and %d", maxLimit)
|
||||
}
|
||||
pq.limit = n
|
||||
func (s *server) parseQuery(query url.Values) (pageQuery, error) {
|
||||
page := pageQuery{
|
||||
limit: defaultLimit,
|
||||
upper: time.Now().UTC(),
|
||||
}
|
||||
|
||||
if v := q.Get("updatedFrom"); v != "" {
|
||||
t, err := parseTime(v, s.loc)
|
||||
if value := query.Get("limit"); value != "" {
|
||||
number, err := strconv.Atoi(value)
|
||||
if err != nil || number < 1 || number > maxLimit {
|
||||
return page, fmt.Errorf("limit must be an integer between 1 and %d", maxLimit)
|
||||
}
|
||||
page.limit = number
|
||||
}
|
||||
|
||||
if value := query.Get("updatedFrom"); value != "" {
|
||||
from, err := parseTime(value, s.loc)
|
||||
if err != nil {
|
||||
return pq, fmt.Errorf("updatedFrom %w", err)
|
||||
return page, fmt.Errorf("updatedFrom %w", err)
|
||||
}
|
||||
pq.from = t
|
||||
page.from = from
|
||||
}
|
||||
|
||||
if v := q.Get("updatedTo"); v != "" {
|
||||
t, err := parseTime(v, s.loc)
|
||||
if value := query.Get("updatedTo"); value != "" {
|
||||
to, err := parseTime(value, s.loc)
|
||||
if err != nil {
|
||||
return pq, fmt.Errorf("updatedTo %w", err)
|
||||
return page, fmt.Errorf("updatedTo %w", err)
|
||||
}
|
||||
if q.Get("updatedFrom") != "" && t.Before(pq.from) {
|
||||
return pq, errors.New("updatedTo must not be before updatedFrom")
|
||||
if query.Get("updatedFrom") != "" && to.Before(page.from) {
|
||||
return page, errors.New("updatedTo must not be before updatedFrom")
|
||||
}
|
||||
if t.Before(pq.upper) {
|
||||
pq.upper = t
|
||||
if to.Before(page.upper) {
|
||||
page.upper = to
|
||||
}
|
||||
}
|
||||
|
||||
if c := q.Get("cursor"); c != "" {
|
||||
n, err := decodeCursor(c)
|
||||
if cursor := query.Get("cursor"); cursor != "" {
|
||||
index, err := decodeCursor(cursor)
|
||||
if err != nil {
|
||||
return pq, err
|
||||
return page, err
|
||||
}
|
||||
pq.start = n
|
||||
page.start = index
|
||||
}
|
||||
|
||||
return pq, nil
|
||||
return page, nil
|
||||
}
|
||||
|
||||
// nextLink is the URL of the page starting at release index next: the same
|
||||
// query with a new cursor.
|
||||
func (s *server) nextLink(q url.Values, next int) string {
|
||||
nq := url.Values{}
|
||||
for k, vs := range q {
|
||||
if k != "cursor" {
|
||||
nq[k] = vs
|
||||
func (s *server) nextLink(query url.Values, next int) string {
|
||||
nextQuery := url.Values{}
|
||||
for key, values := range query {
|
||||
if key != "cursor" {
|
||||
nextQuery[key] = values
|
||||
}
|
||||
}
|
||||
nq.Set("cursor", encodeCursor(next))
|
||||
return s.baseURL + BasePath + "?" + nq.Encode()
|
||||
nextQuery.Set("cursor", encodeCursor(next))
|
||||
return s.baseURL + BasePath + "?" + nextQuery.Encode()
|
||||
}
|
||||
Reference in new issue
Block a user