Skip to content

Commit

Permalink
Add address.FormatHandler for serving address formats via JSON.
Browse files Browse the repository at this point in the history
  • Loading branch information
bojanz committed Oct 29, 2020
1 parent ccc46f9 commit 51ad896
Show file tree
Hide file tree
Showing 2 changed files with 223 additions and 0 deletions.
79 changes: 79 additions & 0 deletions http.go
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
}
144 changes: 144 additions & 0 deletions http_test.go
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)
}
}

0 comments on commit 51ad896

Please sign in to comment.