-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add address.FormatHandler for serving address formats via JSON.
- Loading branch information
Showing
2 changed files
with
223 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
// Copyright (c) 2020 Bojan Zivanovic and contributors | ||
// SPDX-License-Identifier: MIT | ||
|
||
package address | ||
|
||
import ( | ||
"encoding/json" | ||
"net/http" | ||
"strings" | ||
) | ||
|
||
// FormatHandler is an HTTP handler for serving address formats. | ||
// Response size is ~45kb, or ~14kb if gzip compression is used. | ||
type FormatHandler struct{} | ||
|
||
// ServeHTTP implements the http.Handler interface. | ||
func (h FormatHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
locale := h.getLocale(r) | ||
// Preselecting the layout and regions reduces HTTP request size by ~20%. | ||
type localizedFormat struct { | ||
Locale string `json:"locale,omitempty"` | ||
Layout string `json:"layout,omitempty"` | ||
Required []Field `json:"required,omitempty"` | ||
SublocalityType SublocalityType `json:"sublocality_type,omitempty"` | ||
LocalityType LocalityType `json:"locality_type,omitempty"` | ||
RegionType RegionType `json:"region_type,omitempty"` | ||
PostalCodeType PostalCodeType `json:"postal_code_type,omitempty"` | ||
PostalCodePattern string `json:"postal_code_pattern,omitempty"` | ||
ShowRegionID bool `json:"show_region_id,omitempty"` | ||
Regions map[string]string `json:"regions,omitempty"` | ||
} | ||
data := make(map[string]localizedFormat, len(formats)) | ||
for countryCode, format := range formats { | ||
data[countryCode] = localizedFormat{ | ||
Locale: format.Locale.String(), | ||
Layout: format.SelectLayout(locale), | ||
Required: format.Required, | ||
SublocalityType: format.SublocalityType, | ||
LocalityType: format.LocalityType, | ||
RegionType: format.RegionType, | ||
PostalCodeType: format.PostalCodeType, | ||
PostalCodePattern: format.PostalCodePattern, | ||
ShowRegionID: format.ShowRegionID, | ||
Regions: format.SelectRegions(locale), | ||
} | ||
} | ||
|
||
jsonData, _ := json.Marshal(data) | ||
w.Header().Set("Content-Type", "application/json") | ||
w.Header().Set("Content-Language", locale.String()) | ||
w.WriteHeader(http.StatusOK) | ||
w.Write(jsonData) | ||
} | ||
|
||
// getLocale returns the locale to use. | ||
// | ||
// Priority: | ||
// 1) Query string (?locale=fr) | ||
// 2) Header (Accept-Language=fr) | ||
// 3) English | ||
// | ||
func (h FormatHandler) getLocale(r *http.Request) Locale { | ||
var locale Locale | ||
if param := r.URL.Query().Get("locale"); param != "" { | ||
locale = NewLocale(param) | ||
} else if accept := r.Header.Get("Accept-Language"); accept != "" { | ||
for _, sep := range []string{",", ";"} { | ||
if strings.Index(accept, sep) != -1 { | ||
acceptParts := strings.Split(accept, sep) | ||
accept = acceptParts[0] | ||
} | ||
} | ||
locale = NewLocale(strings.TrimSpace(accept)) | ||
} else { | ||
locale = Locale{Language: "en"} | ||
} | ||
|
||
return locale | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
// Copyright (c) 2020 Bojan Zivanovic and contributors | ||
// SPDX-License-Identifier: MIT | ||
|
||
package address_test | ||
|
||
import ( | ||
"encoding/json" | ||
"net/http" | ||
"net/http/httptest" | ||
"reflect" | ||
"testing" | ||
|
||
"github.com/bojanz/address" | ||
) | ||
|
||
// testFormat is a reduced format for testing purposes. | ||
type testFormat struct { | ||
Locale string `json:"locale"` | ||
Layout string `json:"layout"` | ||
RegionType string `json:"region_type"` | ||
Regions map[string]string `json:"regions"` | ||
} | ||
|
||
func TestFormatHandlerNoLocale(t *testing.T) { | ||
req, err := http.NewRequest("GET", "/address-formats", nil) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
rr := httptest.NewRecorder() | ||
handler := address.FormatHandler{} | ||
handler.ServeHTTP(rr, req) | ||
|
||
if status := rr.Code; status != http.StatusOK { | ||
t.Errorf("got HTTP %v want HTTP %v", status, http.StatusOK) | ||
} | ||
if contentType := rr.Header().Get("Content-Type"); contentType != "application/json" { | ||
t.Errorf("got %v want %v", contentType, "application/json") | ||
} | ||
if contentLanguage := rr.Header().Get("Content-Language"); contentLanguage != "en" { | ||
t.Errorf("got %v want %v", contentLanguage, "en") | ||
} | ||
|
||
var data map[string]testFormat | ||
err = json.Unmarshal(rr.Body.Bytes(), &data) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
format, ok := data["TW"] | ||
wantFormat := address.GetFormat("TW") | ||
if !ok { | ||
t.Errorf(`address format "TW" not found.`) | ||
} | ||
// Confirm that Locale and RegionType were correctly converted to strings. | ||
if format.Locale != wantFormat.Locale.String() { | ||
t.Errorf("got %v, want %v", format.Locale, wantFormat.Locale.String()) | ||
} | ||
if format.RegionType != wantFormat.RegionType.String() { | ||
t.Errorf("got %v, want %v", format.Layout, wantFormat.Layout) | ||
} | ||
// Confirm that the correct layout and regions were selected. | ||
if format.Layout != wantFormat.Layout { | ||
t.Errorf("got %q, want %q", format.Layout, wantFormat.Layout) | ||
} | ||
if !reflect.DeepEqual(format.Regions, wantFormat.Regions) { | ||
t.Errorf("got %v, want %v", format.Regions, wantFormat.Regions) | ||
} | ||
} | ||
|
||
func TestFormatHandlerLocaleQuery(t *testing.T) { | ||
req, err := http.NewRequest("GET", "/address-formats?locale=zh", nil) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
rr := httptest.NewRecorder() | ||
handler := address.FormatHandler{} | ||
handler.ServeHTTP(rr, req) | ||
|
||
if status := rr.Code; status != http.StatusOK { | ||
t.Errorf("got HTTP %v want HTTP %v", status, http.StatusOK) | ||
} | ||
if contentType := rr.Header().Get("Content-Type"); contentType != "application/json" { | ||
t.Errorf("got %v want %v", contentType, "application/json") | ||
} | ||
if contentLanguage := rr.Header().Get("Content-Language"); contentLanguage != "zh" { | ||
t.Errorf("got %v want %v", contentLanguage, "zh") | ||
} | ||
|
||
var data map[string]testFormat | ||
err = json.Unmarshal(rr.Body.Bytes(), &data) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
format, ok := data["TW"] | ||
wantFormat := address.GetFormat("TW") | ||
if !ok { | ||
t.Errorf(`address format "TW" not found.`) | ||
} | ||
// Confirm that the correct layout and regions were selected. | ||
if format.Layout != wantFormat.LocalLayout { | ||
t.Errorf("got %q, want %q", format.Layout, wantFormat.LocalLayout) | ||
} | ||
if !reflect.DeepEqual(format.Regions, wantFormat.LocalRegions) { | ||
t.Errorf("got %v, want %v", format.Regions, wantFormat.LocalRegions) | ||
} | ||
} | ||
|
||
func TestFormatHandlerLocaleHeader(t *testing.T) { | ||
req, err := http.NewRequest("GET", "/address-formats", nil) | ||
req.Header.Add("Accept-Language", "zh-hant, zh, en;q=0.8") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
rr := httptest.NewRecorder() | ||
handler := address.FormatHandler{} | ||
handler.ServeHTTP(rr, req) | ||
|
||
if status := rr.Code; status != http.StatusOK { | ||
t.Errorf("got HTTP %v want HTTP %v", status, http.StatusOK) | ||
} | ||
if contentType := rr.Header().Get("Content-Type"); contentType != "application/json" { | ||
t.Errorf("got %v want %v", contentType, "application/json") | ||
} | ||
if contentLanguage := rr.Header().Get("Content-Language"); contentLanguage != "zh-Hant" { | ||
t.Errorf("got %v want %v", contentLanguage, "zh-Hant") | ||
} | ||
|
||
var data map[string]testFormat | ||
err = json.Unmarshal(rr.Body.Bytes(), &data) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
format, ok := data["TW"] | ||
wantFormat := address.GetFormat("TW") | ||
if !ok { | ||
t.Errorf(`address format "TW" not found.`) | ||
} | ||
// Confirm that the correct layout and regions were selected. | ||
if format.Layout != wantFormat.LocalLayout { | ||
t.Errorf("got %q, want %q", format.Layout, wantFormat.LocalLayout) | ||
} | ||
if !reflect.DeepEqual(format.Regions, wantFormat.LocalRegions) { | ||
t.Errorf("got %v, want %v", format.Regions, wantFormat.LocalRegions) | ||
} | ||
} |