Working examples for presentation
For most of the files:
go run FILENAME
If something will require dependencies, check impoert
section and do
go get PACKAGE_IN_IMPORT_SECTION
Aurelijus Banelis
{ | |
"items": [ | |
{ | |
"version": "v3", | |
"title": "Google Analytics API", | |
"description": "Views and manages your Google Analytics data.", | |
"documentationLink": "https://developers.google.com/analytics/" | |
}, | |
{ | |
"title": "Google Analytics API", | |
"description": "Views and manages your Google Analytics data.", | |
"documentationLink": "https://developers.google.com/analytics/", | |
"version": "v2.4" | |
} | |
] | |
} |
package main | |
import ( | |
. "github.com/onsi/ginkgo" | |
. "github.com/onsi/gomega" | |
"testing" | |
"net/http" | |
"io/ioutil" | |
"time" | |
"encoding/json" | |
"fmt" | |
) | |
type ( | |
FirstLastName string | |
Date string | |
Url string | |
HTML string | |
Address string | |
Company struct { | |
Name string `json:"name"` | |
Url Url `json:"url"` | |
} | |
Topic struct { | |
Title string `json:"title"` | |
Description HTML `json:"description"` | |
} | |
Speaker struct { | |
Name FirstLastName `json:"name"` | |
Topic Topic `json:"topic"` | |
Company *Company `json:"company,omitempty"` | |
} | |
Place struct { | |
Name string `json:"name"` | |
Address Address `json:"address"` | |
} | |
Meetup struct { | |
Session *uint `json:"session"` | |
Episode *uint `json:"episode"` | |
Name string `json:"name"` | |
Speakers []Speaker | |
Date Date `json:"date"` | |
Venue Place `json:"venue"` | |
Sponsor Company `json:"sponsor"` | |
Description HTML `json:"description"` | |
} | |
Meetups []Meetup | |
) | |
var _ = Describe("Examples of usefulnes of running tests", func() { | |
It("Should be easy to copy expected values from curl response", func() { | |
response := readAll("https://www.googleapis.com/discovery/v1/apis?name=analytics&fields=items(description%2CdocumentationLink%2Ctitle%2Cversion)") | |
// Note "version" and "title" are not ordered | |
expected := `{ | |
"items": [ | |
{ | |
"title": "Google Analytics API", | |
"description": "Views and manages your Google Analytics data.", | |
"documentationLink": "https://developers.google.com/analytics/", | |
"version": "v2.4" | |
}, | |
{ | |
"version": "v3", | |
"title": "Google Analytics API", | |
"description": "Views and manages your Google Analytics data.", | |
"documentationLink": "https://developers.google.com/analytics/" | |
} | |
] | |
}` | |
Expect(response).Should(MatchJSON(expected)) | |
}) | |
It("Is hard to write tests with deep unless you have good IDE and write by position", func() { | |
meetups := Meetups{ | |
Meetup{ | |
Name: "Golang Meetup S02E01", | |
Session: intPtr(2), | |
Episode: intPtr(1), | |
Speakers:[]Speaker{ | |
{ | |
FirstLastName("Aurelijus Banelis"), | |
Topic{ | |
"JSON+Go in practice", | |
HTML("Practical tips and tricks ..."), | |
}, | |
&Company{ | |
"NFQ", | |
Url("http://www.nfq.lt/"), | |
}, | |
}, | |
{ | |
FirstLastName("Valdas Petrulis"), | |
Topic{ | |
"NATS pub-sub daemon packed inside your application ", | |
HTML("Here at www.mysterium.network ..."), | |
}, | |
&Company{ | |
"MysteriumNetwork", | |
Url("http://www.mysterium.network/"), | |
}, | |
}, | |
}, | |
Date: Date("2017-09-27"), | |
Description: HTML("Arriving: Call Povilas if you'll face any problems reaching the place."), | |
Sponsor: Company{ | |
"Uber", | |
Url("https://www.uber.com/en-LT/"), | |
}, | |
Venue: Place{ | |
Name: "Uber", | |
Address: Address("Lvovo g. 25, Vilnius"), | |
}, | |
}, | |
Meetup{ | |
Name: "Golang Meetup #5", | |
Speakers:[]Speaker{ | |
{ | |
"Martynas Pumputis", | |
Topic{ | |
"Go and Linux Namespaces: Better Don't Mix", | |
"This talk is about discovering Go runtime limitations which make us reconsider its adoption within container software", | |
}, | |
&Company{ | |
"Weaveworks", | |
"https://www.weave.works/", | |
}, | |
}, | |
{ | |
"Mike Kabischev", | |
Topic{ | |
"Instrumenting Go application", | |
"Instrumentation ...", | |
}, | |
nil, | |
}, | |
{ | |
"Max Chechel", | |
Topic{ | |
"Code generation in go", | |
"Why do we need to generate Go ...", | |
}, | |
nil, | |
}, | |
}, | |
Date: "2017-05-24", | |
Description: "Arriving: Call Povilas if you'll face any problems reaching the place.", | |
Sponsor: Company{ | |
"Uber", | |
"https://www.uber.com/en-LT/", | |
}, | |
Venue: Place{ | |
Name: "Uber", | |
Address: "Lvovo g. 25, Vilnius", | |
}, | |
}, | |
} | |
Expect(nextSpeaker(mockedClock, meetups)).Should(Equal("Aurelijus Banelis")) | |
}) | |
It("JSON can be used for deep structures and no fear of modification by pointers", func() { | |
meetups := Meetups{} | |
input := `[ | |
{ | |
"name": "Golang Meetup S02E01", | |
"session": 2, | |
"episode": 1, | |
"Speakers": [ | |
{ | |
"name": "Aurelijus Banelis", | |
"topic": { | |
"title": "JSON+Go in practice", | |
"description": "Practical tips and tricks ..." | |
}, | |
"company": { | |
"name": "NFQ", | |
"url": "http://www.nfq.lt/" | |
} | |
}, | |
{ | |
"name": "Valdas Petrulis", | |
"topic": { | |
"title": "NATS pub-sub daemon packed inside your application ", | |
"description": "Here at www.mysterium.network ..." | |
}, | |
"company": { | |
"name": "MysteriumNetwork", | |
"url": "http://www.mysterium.network/" | |
} | |
} | |
], | |
"date": "2017-09-27", | |
"venue": { | |
"name": "Uber", | |
"address": "Lvovo g. 25, Vilnius" | |
}, | |
"sponsor": { | |
"name": "Uber", | |
"url": "https://www.uber.com/en-LT/" | |
}, | |
"description": "Arriving: Call Povilas if you'll face any problems reaching the place." | |
}, | |
{ | |
"name": "Golang Meetup #5", | |
"Speakers": [ | |
{ | |
"name": "Martynas Pumputis", | |
"topic": { | |
"title": "Go and Linux Namespaces: Better Don't Mix", | |
"description": "This talk is about discovering Go runtime limitations which make us reconsider its adoption within container software" | |
}, | |
"company": { | |
"name": "Weaveworks", | |
"url": "https://www.weave.works/" | |
} | |
}, | |
{ | |
"name": "Mike Kabischev", | |
"topic": { | |
"title": "Instrumenting Go application", | |
"description": "Instrumentation ..." | |
} | |
}, | |
{ | |
"name": "Max Chechel", | |
"topic": { | |
"title": "Code generation in go", | |
"description": "Why do we need to generate Go ..." | |
} | |
} | |
], | |
"date": "2017-05-24", | |
"venue": { | |
"name": "Uber", | |
"address": "Lvovo g. 25, Vilnius" | |
}, | |
"sponsor": { | |
"name": "Uber", | |
"url": "https://www.uber.com/en-LT/" | |
}, | |
"description": "Arriving: Call Povilas if you'll face any problems reaching the place." | |
} | |
]` | |
json.Unmarshal([]byte(input), &meetups) | |
Expect(nextSpeaker(mockedClock, meetups)).Should(Equal("Aurelijus Banelis")) | |
}) | |
It("Fmt prints badly deep structures of pointers", func() { | |
meetups := Meetups{ | |
Meetup{ | |
Name: "Golang Meetup S02E01", | |
Session: intPtr(2), | |
Episode: intPtr(1), | |
Speakers: []Speaker{ | |
{ | |
Company: &Company{ | |
Name: "NFQ", | |
}, | |
}, | |
}, | |
}, | |
} | |
representation := fmt.Sprintf("%#v", meetups) | |
// Representation: test.Meetups{test.Meetup{Session:(*uint)(0xc42028c5d0), Episode:(*uint)(0xc42028c5d8) ... | |
Expect(representation).ShouldNot(BeEmpty()) | |
}) | |
}) | |
func intPtr(value uint) *uint { | |
return &value | |
} | |
var mockedClock, _ = time.Parse(time.RFC3339, "2017-09-27T19:10:00+03:00") | |
func nextSpeaker(new time.Time, meetups Meetups) string { | |
// Skipping real implementation just for the sake of example | |
return string(meetups[0].Speakers[0].Name) | |
} | |
func readAll(url string) string { | |
resp, err := http.Get(url) | |
defer resp.Body.Close() | |
if err != nil { | |
panic(err) | |
} | |
body, err := ioutil.ReadAll(resp.Body) | |
if err != nil { | |
panic(err) | |
} | |
return string(body) | |
} | |
func TestGoJson(t *testing.T) { | |
RegisterFailHandler(Fail) | |
RunSpecs(t, "Go+Json Example Suite") | |
} | |
default: | |
golint *.go | |
go vet *.go | |
go fmt *.go | |
run: | |
go run naming.go | |
go run naming2.go | |
go run naming-unmarshal.go | |
go run naming-polymorphism.go |
package main | |
// THIS IS AUTO GENERATED FILE - DO NOT EDIT! | |
// SomeGeneratedDTO type | |
type SomeGeneratedDTO struct { | |
ID CommonGenerated.ID `json:"id"` | |
ParentID CommonGenerated.ID `json:"parentId"` | |
Title CommonGenerated.Translation `json:"title"` | |
} |
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
) | |
// Main structure | |
type polymorphic struct { | |
Type string `json:"type"` | |
Attributes iAttribute `json:"attributes"` | |
} | |
type iAttribute interface { | |
Type() string | |
} | |
// Image type | |
var _ iAttribute = (*image)(nil) | |
func (i *image) Type() string { | |
return "image" | |
} | |
type image struct { | |
Path string `json:"path"` | |
} | |
// Meetup type | |
var _ iAttribute = (*meetup)(nil) | |
type meetup struct { | |
Language string `json:"language"` | |
Date string `json:"date"` | |
} | |
func (i *meetup) Type() string { | |
return "meetup" | |
} | |
// Testing example | |
func main() { | |
data := []polymorphic{ | |
{ | |
Type: "image", | |
Attributes: &image{ | |
Path: "http://tny.im/ajb", | |
}, | |
}, | |
{ | |
Type: "meetup", | |
Attributes: &meetup{ | |
Language: "go", | |
Date: "2017-09-27", | |
}, | |
}, | |
} | |
api, err := json.MarshalIndent(&data, "", " ") | |
if err != nil { | |
panic(err.Error()) | |
} | |
fmt.Printf("%s\n", api) | |
} |
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
"github.com/segmentio/go-camelcase" | |
"reflect" | |
) | |
type structureDynamic struct { | |
NetworkID string | |
EndpointID string | |
Gateway string | |
IPAddress string | |
IPPrefixLen int | |
} | |
func (s *structureDynamic) MarshalJSON() ([]byte, error) { | |
keyValues := map[string]interface{}{} | |
value := reflect.ValueOf(s).Elem() | |
for i := 0; i < value.NumField(); i++ { | |
valueField := value.Field(i) | |
typeField := value.Type().Field(i) | |
name := camelcase.Camelcase(typeField.Name) | |
keyValues[name] = valueField.Interface() | |
} | |
return json.Marshal(keyValues) | |
} | |
func main() { | |
data := structureDynamic{ | |
NetworkID: "b5a465c4ddf1", | |
EndpointID: "5483c7bbf04", | |
Gateway: "172.20.0.1", | |
IPAddress: "172.20.0.4", | |
IPPrefixLen: 16, | |
} | |
api, err := json.MarshalIndent(&data, "", " ") | |
if err != nil { | |
panic(err.Error()) | |
} | |
fmt.Printf("%s\n", api) | |
} | |
// Adapted from https://gist.github.com/drewolson/4771479 |
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
) | |
type structureGo struct { | |
NetworkID string | |
EndpointID string | |
Gateway string | |
IPAddress string | |
IPPrefixLen int | |
} | |
func main() { | |
data := structureGo{ | |
NetworkID: "b5a465c4ddf1", | |
EndpointID: "5483c7bbf04", | |
Gateway: "172.20.0.1", | |
IPAddress: "172.20.0.4", | |
IPPrefixLen: 16, | |
} | |
api, err := json.MarshalIndent(&data, "", " ") | |
if err != nil { | |
panic(err.Error()) | |
} | |
fmt.Printf("%s\n", api) | |
} | |
// Go lint: struct field NetworkID should be NetworkID |
{ | |
"NetworkID": "b5a465c4ddf1", | |
"EndpointID": "5483c7bbf04", | |
"Gateway": "172.20.0.1", | |
"IPAddress": "172.20.0.4", | |
"IPPrefixLen": 16 | |
} |
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
) | |
type structureJs struct { | |
NetworkID string `json:"networkId"` | |
EndpointID string `json:"endpointId"` | |
Gateway string `json:"gateway"` | |
IPAddress string `json:"ipAddress"` | |
IPPrefixLen int `json:"ipPrefixLen"` | |
} | |
func main() { | |
data := structureJs{ | |
NetworkID: "b5a465c4ddf1", | |
EndpointID: "5483c7bbf04", | |
Gateway: "172.20.0.1", | |
IPAddress: "172.20.0.4", | |
IPPrefixLen: 16, | |
} | |
api, err := json.MarshalIndent(&data, "", " ") | |
if err != nil { | |
panic(err.Error()) | |
} | |
fmt.Printf("%s\n", api) | |
} |
{ | |
"networkId": "b5a465c4ddf1", | |
"endpointId": "5483c7bbf04", | |
"gateway": "172.20.0.1", | |
"IpAddress": "172.20.0.4", | |
"IpPrefixLen": 16 | |
} |
[ | |
{ | |
"type": "image", | |
"attributes": { | |
"path": "http://tny.im/ajb" | |
} | |
}, | |
{ | |
"type": "meetup", | |
"attributes": { | |
"language": "Go", | |
"date": "2017-09-27" | |
} | |
} | |
] |
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
"github.com/juju/errors" | |
) | |
type ( | |
Image struct { | |
Path string `json:"path"` | |
} | |
Meetup struct { | |
Language string `json:"language"` | |
Date string `json:"date"` | |
} | |
IAttribute interface { | |
Type() string | |
} | |
Element struct { | |
attribute IAttribute | |
} | |
) | |
func (i *Element) UnmarshalJSON(data []byte) error { | |
intermediate := struct { | |
Type string `json:"type"` | |
Attributes json.RawMessage `json:"attributes"` | |
}{} | |
err := json.Unmarshal(data, &intermediate) | |
if err != nil { | |
return errors.Annotate(err, "Invalid Element") | |
} | |
switch intermediate.Type { | |
case "image": | |
img := Image{} | |
err = json.Unmarshal(intermediate.Attributes, &img) | |
if err == nil { | |
i.attribute = &img | |
} | |
break | |
case "meetup": | |
meetup := Meetup{} | |
err = json.Unmarshal(intermediate.Attributes, &meetup) | |
if err == nil { | |
i.attribute = &meetup | |
} | |
break | |
default: | |
err = fmt.Errorf("Unexpected element type %s", intermediate.Type) | |
} | |
return errors.Trace(err) | |
} | |
func (p *Image) Type() string { | |
return "image" | |
} | |
func (p Meetup) Type() string { | |
return "meetup" | |
} | |
type Elements []Element | |
type GroupedElements map[string]Elements | |
func main() { | |
var elements []Element | |
err := json.Unmarshal([]byte(` | |
[ | |
{ | |
"type": "image", | |
"attributes": { | |
"path": "http://tny.im/ajb" | |
} | |
}, | |
{ | |
"type": "meetup", | |
"attributes": { | |
"language": "Go", | |
"date": "2017-09-27" | |
} | |
} | |
] | |
`), &elements) | |
if err != nil { | |
fmt.Printf("Validation: %s\n", err.Error()) | |
} | |
fmt.Printf("Data: %#v\n", elements) | |
var grouped GroupedElements | |
err = json.Unmarshal([]byte(` | |
{ | |
"images": [ | |
{ | |
"type": "image", | |
"attributes": { | |
"path": "http://tny.im/ajb" | |
} | |
} | |
], | |
"meetups": [ | |
{ | |
"type": "meetup", | |
"attributes": { | |
"language": "Go", | |
"date": "2017-09-27" | |
} | |
} | |
] | |
} | |
`), &grouped) | |
if err != nil { | |
fmt.Printf("Validation: %s\n", err.Error()) | |
} | |
fmt.Printf("Data: %#v\n", grouped) | |
var grouped2 GroupedElements | |
err = json.Unmarshal([]byte(` | |
{ | |
"images": [ | |
{ | |
"type": "image", | |
"attributes": { | |
"path": "http://tny.im/ajb" | |
} | |
} | |
], | |
"meetups": null | |
} | |
`), &grouped2) | |
if err != nil { | |
fmt.Printf("Validation: %s\n", err.Error()) | |
} | |
fmt.Printf("Data: %#v\n", grouped2) | |
} |
{ | |
"images": [ | |
{ | |
"type": "image", | |
"attributes": { | |
"path": "http://tny.im/ajb" | |
} | |
} | |
], | |
"meetups": null | |
} |
{ | |
"images": [ | |
{ | |
"type": "image", | |
"attributes": { | |
"path": "http://tny.im/ajb" | |
} | |
} | |
], | |
"meetups": [ | |
{ | |
"type": "meetup", | |
"attributes": { | |
"language": "Go", | |
"date": "2017-09-27" | |
} | |
} | |
] | |
} |
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
) | |
type LocationNumber uint64 | |
type LocationData struct { | |
Number LocationNumber `json:"number"` | |
} | |
func (i *LocationNumber) UnmarshalJSON(data []byte) error { | |
var longInteger uint64 | |
var longFloat float64 | |
errLong := json.Unmarshal(data, &longInteger) | |
errFloat := json.Unmarshal(data, &longFloat) | |
if errLong != nil && errFloat == nil { | |
*i = LocationNumber(longFloat) | |
return nil | |
} | |
if errLong != nil { | |
return errLong | |
} | |
*i = LocationNumber(longInteger) | |
return nil | |
} | |
func main() { | |
// 1.0e+19 | |
data1 := LocationData{} | |
err := json.Unmarshal([]byte(`{"number": 100}`), &data1) | |
if err != nil { | |
fmt.Printf("Validation: %s\n", err.Error()) | |
} | |
fmt.Printf("Data: %#v\n", data1) | |
data2 := LocationData{} | |
err = json.Unmarshal([]byte(`{"number": 1.0e+8}`), &data2) | |
if err != nil { | |
fmt.Printf("Validation: %s\n", err.Error()) | |
} | |
fmt.Printf("Data: %#v %d\n", data2, data2.Number) | |
} |
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
"sort" | |
) | |
func main() { | |
set1 := []string{"a", "b", "c"} | |
set2 := map[string]string{"a": "a", "b": "b", "c": "c"} | |
sort.Strings(set1) | |
setJson1, _ := json.MarshalIndent(set1, "", " ") | |
setJson2, _ := json.MarshalIndent(set2, "", " ") | |
fmt.Printf("Set as array:\n%s\n", setJson1) | |
fmt.Printf("Set as objects:\n%s\n", setJson2) | |
} |
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
"gopkg.in/go-playground/validator.v9" | |
"strings" | |
) | |
type DataExample struct { | |
Email string `json:"email" validate:"email,isExampleDomain"` | |
Gln string `json:"gln" validate:"numeric,len=13"` | |
} | |
func main() { | |
validate := validator.New() | |
validate.RegisterValidation("isExampleDomain", func(fl validator.FieldLevel) bool { | |
return strings.HasSuffix(fl.Field().String(), "@example.com") | |
}) | |
data1 := DataExample{} | |
json.Unmarshal([]byte(`{"email":"aurelijus@example.com", "gln":"1234567890123"}`), &data1) | |
err := validate.Struct(data1) | |
if err == nil { | |
fmt.Printf("Data: %#v\n", data1) | |
} | |
data2 := DataExample{} | |
json.Unmarshal([]byte(`{"email":"aurelijus@example.com", "gln":"123"}`), &data2) | |
err = validate.Struct(data2) | |
if err != nil { | |
fmt.Printf("Validation: %s\n", err.Error()) | |
} | |
data3 := DataExample{} | |
json.Unmarshal([]byte(`{"email":"aurelijus@google.com", "gln":"1234567890123"}`), &data3) | |
err = validate.Struct(data3) | |
if err != nil { | |
fmt.Printf("Validation: %s\n", err.Error()) | |
} | |
} |
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
"github.com/juju/errors" | |
"strings" | |
"time" | |
) | |
// Validation with builtin JSON types | |
type Data struct { | |
ID string `json:"id"` | |
Location uint64 `json:"location"` | |
} | |
// Validation as custom type example | |
type ID string | |
type Data2 struct { | |
ID ID `json:"id"` | |
Location uint64 `json:"location"` | |
} | |
func (i *ID) UnmarshalJSON(data []byte) error { | |
var parsed string = "" | |
err := json.Unmarshal(data, &parsed) | |
errorMessage := "Invalid input %s. Expected ID need to be 6 digits uppercase string" | |
if err != nil { | |
return errors.Annotatef(err, errorMessage) | |
} | |
if len(parsed) != 6 || strings.ToUpper(parsed) != parsed { | |
return fmt.Errorf(errorMessage, data) | |
} | |
// Common mistake to forgot to update and not to confuse pointer and pointed value | |
*i = ID(parsed) | |
return nil | |
} | |
// Date type example | |
type Date time.Time | |
func (d *Date) UnmarshalJSON(data []byte) error { | |
var parsed string = "" | |
err := json.Unmarshal(data, &parsed) | |
errorMessage := "Invalid input %s. Expected Date in format 2006-01-02" | |
if err != nil { | |
return errors.Annotatef(err, errorMessage, data) | |
} | |
var parsedTime time.Time = time.Now() | |
err = json.Unmarshal([]byte(`"`+parsed+`T00:00:00Z"`), &parsedTime) | |
if err != nil { | |
return errors.Annotatef(err, errorMessage, data) | |
} | |
// Common mistake to forgot to update and not to confuse pointer and pointed value | |
*d = Date(parsedTime) | |
return nil | |
} | |
func (d Date) MarshalJSON() ([]byte, error) { | |
date := time.Time(d).Format("2006-01-02") | |
return json.Marshal(&date) | |
} | |
type DateExample struct { | |
Date Date `json:"date"` | |
} | |
// Manual testing | |
func main() { | |
data := &Data{} | |
err := json.Unmarshal([]byte(`{"id":"testas", "Location":"Vilnius"}`), &data) | |
fmt.Printf("Validation error: %s\n", err.Error()) | |
data2 := &Data2{} | |
err = json.Unmarshal([]byte(`{"id":"testas", "Location":123456}`), &data2) | |
if err != nil { | |
fmt.Printf("Validation error: %s\n", err.Error()) | |
} | |
data2valid := &Data2{} | |
err = json.Unmarshal([]byte(`{"id":"ABCDEF", "Location":123456}`), &data2valid) | |
fmt.Printf("DateExample %#v\n", data2valid) | |
data3 := &DateExample{} | |
err = json.Unmarshal([]byte(`{"date":"2017 September 27th"}`), &data3) | |
if err != nil { | |
fmt.Printf("Validation error: %s\n", err.Error()) | |
} | |
data3valid := &DateExample{} | |
err = json.Unmarshal([]byte(`{"date":"2017-09-27"}`), &data3valid) | |
fmt.Printf("DateExample %#v\n", data3) | |
jsonData3, _ := json.Marshal(data3valid) | |
fmt.Printf("DateExample %s\n", jsonData3) | |
} |
{"number": 1.0e+8} |
{ | |
"set": { | |
"a": "a", | |
"b": "b", | |
"c": "c" | |
} | |
} |
package main | |
import ( | |
"encoding/json" | |
"github.com/ant0ine/go-json-rest/rest" | |
"io/ioutil" | |
"log" | |
"net/http" | |
"strings" | |
) | |
type speakerLatest struct { | |
Name string `json:"name"` | |
Topic string `json:"topic"` | |
LinkedIn string `json:"linkedIn"` | |
} | |
func writeValidationFailed(message string, w rest.ResponseWriter) { | |
w.WriteHeader(http.StatusBadRequest) | |
var errorResponse = struct { | |
Error string `json:"error"` | |
}{message} | |
w.WriteJson(&errorResponse) | |
} | |
func writeSystemError(err error, w rest.ResponseWriter) { | |
w.WriteHeader(http.StatusInternalServerError) | |
var errorResponse = struct { | |
Error string `json:"error"` | |
}{err.Error()} | |
w.WriteJson(&errorResponse) | |
} | |
func main() { | |
api := rest.NewApi() | |
api.Use(rest.DefaultDevStack...) | |
router, err := rest.MakeRouter( | |
rest.Post("/speaker", func(w rest.ResponseWriter, req *rest.Request) { | |
// Request is read only once | |
payload, err := ioutil.ReadAll(req.Body) | |
if err != nil { | |
writeSystemError(err, w) | |
return | |
} | |
compatiblity := struct { | |
HomePage string `json:"homePage"` | |
}{} | |
fields := map[string]json.RawMessage{} | |
latest := speakerLatest{} | |
err1 := json.Unmarshal(payload, &latest) | |
err2 := json.Unmarshal(payload, &compatiblity) | |
err3 := json.Unmarshal(payload, &fields) | |
if err1 != nil { | |
writeSystemError(err1, w) | |
return | |
} | |
if err2 != nil { | |
writeSystemError(err2, w) | |
return | |
} | |
if err3 != nil { | |
writeSystemError(err3, w) | |
return | |
} | |
linkedInPrefix := "https://www.linkedin.com/in/" | |
if _, exists := fields["linkedIn"]; !exists && strings.HasPrefix(compatiblity.HomePage, linkedInPrefix) { | |
latest.LinkedIn = (compatiblity.HomePage)[len(linkedInPrefix):] | |
} | |
if _, exists := fields["topic"]; !exists { | |
writeValidationFailed("topic property must be provided", w) | |
return | |
} | |
if strings.TrimSpace(latest.Topic) == "" { | |
writeValidationFailed("topic property cannot be empty", w) | |
return | |
} | |
// Assuming everything was ok | |
w.WriteHeader(201) | |
w.WriteJson(&latest) | |
}), | |
) | |
if err != nil { | |
log.Fatal(err) | |
} | |
api.SetApp(router) | |
log.Fatal(http.ListenAndServe(":8082", api.MakeHandler())) | |
} | |
// Usage: | |
// curl -XPOST -H 'Content-type: application/json' -d '{"name":"Aurelijus Banelis"}' http://127.0.0.1:8082/speaker | |
// curl -XPOST -H 'Content-type: application/json' -d '{"name":"Aurelijus Banelis", "topic": "JSON+Go in practice"}' http://127.0.0.1:8082/speaker | |
// curl -XPOST -H 'Content-type: application/json' -d '{"name":"Aurelijus Banelis", "topic": "JSON+Go in practice", "homePage": "https://aurelijus.banelis.lt"}' http://127.0.0.1:8082/speaker | |
// curl -XPOST -H 'Content-type: application/json' -d '{"name":"Aurelijus Banelis", "topic": "JSON+Go in practice", "homePage": "https://www.linkedin.com/in/aurelijusbanelis"}' http://127.0.0.1:8082/speaker | |
// curl -XPOST -H 'Content-type: application/json' -d '{"name":"Aurelijus Banelis", "topic": "JSON+Go in practice", "linkedIn": "aurelijusbanelis"}' http://127.0.0.1:8082/speaker | |
// curl -XPOST -H 'Content-type: application/json' -d '{"topic": "JSON+Go in practice", "linkedIn": "aurelijusbanelis"}' http://127.0.0.1:8082/speaker | |
// curl -XPOST -H 'Content-type: application/json' -d '{"name":"", "topic": "JSON+Go in practice", "linkedIn": "aurelijusbanelis"}' http://127.0.0.1:8082/speaker |
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
) | |
type optionalPointers struct { | |
Attributes map[string]string | |
} | |
func main() { | |
defaultValues, _ := json.Marshal(&optionalPointers{}) | |
defaultInitialised, _ := json.Marshal(&optionalPointers{map[string]string{}}) | |
normalValues, _ := json.Marshal(&optionalPointers{map[string]string{"field": "value"}}) | |
fmt.Printf("%s\n", defaultValues) | |
fmt.Printf("%s\n", defaultInitialised) | |
fmt.Printf("%s\n", normalValues) | |
payload := &struct{ Attributes map[string]string }{} | |
json.Unmarshal([]byte("{}"), &payload) | |
payload.Attributes["new"] = "value" | |
} |
package main | |
import ( | |
"github.com/ant0ine/go-json-rest/rest" | |
"log" | |
"net/http" | |
"strings" | |
) | |
type speakerData struct { | |
Name string `json:"name"` | |
Topic *string `json:"topic,omitempty"` | |
HomePage *string `json:"homePage,omitempty"` | |
LinkedIn string `json:"linkedIn,omitempty"` | |
} | |
func writeInvalid(message string, w rest.ResponseWriter) { | |
w.WriteHeader(http.StatusBadRequest) | |
var errorResponse = struct { | |
Error string `json:"error"` | |
}{message} | |
w.WriteJson(&errorResponse) | |
} | |
func main() { | |
api := rest.NewApi() | |
api.Use(rest.DefaultDevStack...) | |
router, err := rest.MakeRouter( | |
rest.Post("/speaker", func(w rest.ResponseWriter, req *rest.Request) { | |
markerForNotProvided := "\x00" | |
data := speakerData{ | |
Name: markerForNotProvided, | |
} | |
req.DecodeJsonPayload(&data) | |
// Using pointer to distinguish provided and not provided field | |
if data.Topic == nil || strings.TrimSpace(*data.Topic) == "" { | |
writeInvalid("Old API is deprecated. Please provide topic", w) | |
return | |
} | |
// Too many pointers are error prone | |
linkedInPrefix := "https://www.linkedin.com/in/" | |
if data.HomePage != nil && strings.HasPrefix(*data.HomePage, linkedInPrefix) { | |
data.LinkedIn = (*data.HomePage)[len(linkedInPrefix):] | |
data.HomePage = nil | |
} | |
if data.HomePage != nil && data.LinkedIn == "" { | |
writeInvalid("Old API is deprecated. Please provide linkedIn instead of homePage", w) | |
return | |
} | |
// Too many optional fields increase the number of checks | |
if data.LinkedIn == "" { | |
writeInvalid("Old API is deprecated. Please provide linkedIn field", w) | |
return | |
} | |
// One of the hackish way to solve default values is to provide uncommon value before | |
if data.Name == markerForNotProvided { | |
writeInvalid("Validation failed. Please provide name field", w) | |
return | |
} | |
if data.Name == "" { | |
writeInvalid("Validation failed: name field cannot be empty", w) | |
return | |
} | |
// Assuming everything was ok | |
w.WriteHeader(201) | |
w.WriteJson(&data) | |
}), | |
) | |
if err != nil { | |
log.Fatal(err) | |
} | |
api.SetApp(router) | |
log.Fatal(http.ListenAndServe(":8081", api.MakeHandler())) | |
} | |
// Usage: | |
// curl -XPOST -H 'Content-type: application/json' -d '{"name":"Aurelijus Banelis"}' http://127.0.0.1:8081/speaker | |
// curl -XPOST -H 'Content-type: application/json' -d '{"name":"Aurelijus Banelis", "topic": "JSON+Go in practice"}' http://127.0.0.1:8081/speaker | |
// curl -XPOST -H 'Content-type: application/json' -d '{"name":"Aurelijus Banelis", "topic": "JSON+Go in practice", "homePage": "https://aurelijus.banelis.lt"}' http://127.0.0.1:8081/speaker | |
// curl -XPOST -H 'Content-type: application/json' -d '{"name":"Aurelijus Banelis", "topic": "JSON+Go in practice", "homePage": "https://www.linkedin.com/in/aurelijusbanelis"}' http://127.0.0.1:8081/speaker | |
// curl -XPOST -H 'Content-type: application/json' -d '{"name":"Aurelijus Banelis", "topic": "JSON+Go in practice", "linkedIn": "aurelijusbanelis"}' http://127.0.0.1:8081/speaker | |
// curl -XPOST -H 'Content-type: application/json' -d '{"topic": "JSON+Go in practice", "linkedIn": "aurelijusbanelis"}' http://127.0.0.1:8081/speaker | |
// curl -XPOST -H 'Content-type: application/json' -d '{"name":"", "topic": "JSON+Go in practice", "linkedIn": "aurelijusbanelis"}' http://127.0.0.1:8081/speaker | |
type a struct { | |
A *[]map[string]string | |
} |
package main | |
import ( | |
"errors" | |
"github.com/ant0ine/go-json-rest/rest" | |
"log" | |
"net/http" | |
"strings" | |
) | |
// Input models | |
type speaker0_0_1 struct { | |
Name string `json:"name"` | |
} | |
type speaker0_1_0 struct { | |
Name string `json:"name"` | |
Topic string `json:"topic"` | |
} | |
type speaker0_2_0 struct { | |
Name string `json:"name"` | |
Topic string `json:"topic"` | |
HomePage string `json:"homePage"` | |
} | |
type speaker0_3_0 struct { | |
Name string `json:"name"` | |
Topic string `json:"topic"` | |
LinkedIn string `json:"linkedIn"` | |
} | |
// Controllers | |
func sepakerController0_0_1(w rest.ResponseWriter, req *rest.Request) { | |
writeBadRequest("API deprecated. Use /v0.3.0/speaker", w) | |
} | |
func speakerController0_1_0(w rest.ResponseWriter, req *rest.Request) { | |
writeBadRequest("API deprecated. Use /v0.3.0/speaker", w) | |
} | |
func speakerController0_2_0(w rest.ResponseWriter, req *rest.Request) { | |
decodeSpeaker(&speaker0_2_0{}, req, w, meetupController) | |
} | |
func speakerController0_3_0(w rest.ResponseWriter, req *rest.Request) { | |
decodeSpeaker(&speaker0_3_0{}, req, w, meetupController) | |
} | |
// Converters to domain objects | |
type speakerInputToDomain interface { | |
ToDomainObject() (speaker, error) | |
} | |
func (s *speaker0_2_0) ToDomainObject() (speaker, error) { | |
linkedinPrefix := "https://www.linkedin.com/in/" | |
if strings.HasPrefix(s.HomePage, linkedinPrefix) { | |
return speaker{ | |
Name: s.Name, | |
Topic: s.Topic, | |
LinkedIn: s.HomePage[len(linkedinPrefix):], | |
}, nil | |
} | |
return speaker{}, errors.New("Only Linkedin usrs are supported. Use /v0.3.0/speaker") | |
} | |
func (s *speaker0_3_0) ToDomainObject() (speaker, error) { | |
return speaker{ | |
Name: s.Name, | |
Topic: s.Topic, | |
LinkedIn: s.LinkedIn, | |
}, nil | |
} | |
// Domain objects | |
type speaker struct { | |
Name string | |
Topic string | |
LinkedIn string | |
} | |
// Domain logic | |
func meetupController(speaker speaker, w rest.ResponseWriter) { | |
w.WriteHeader(201) | |
w.WriteJson(&speaker) | |
} | |
// Representation helper functions | |
func decodeSpeaker(speaker speakerInputToDomain, req *rest.Request, w rest.ResponseWriter, controller func(speaker, rest.ResponseWriter)) { | |
if err := req.DecodeJsonPayload(&speaker); err != nil { | |
w.WriteHeader(http.StatusInternalServerError) | |
log.Fatal(err) | |
return | |
} | |
latest, err := speaker.ToDomainObject() | |
if err != nil { | |
writeBadRequest(err.Error(), w) | |
return | |
} | |
controller(latest, w) | |
} | |
type errorResponse struct { | |
Error string `json:"error"` | |
} | |
func writeBadRequest(message string, w rest.ResponseWriter) { | |
w.WriteHeader(http.StatusBadRequest) | |
w.WriteJson(&errorResponse{message}) | |
} | |
// Initiating server | |
func main() { | |
api := rest.NewApi() | |
api.Use(rest.DefaultDevStack...) | |
router, err := rest.MakeRouter( | |
rest.Post("/v0.0.1/speaker", sepakerController0_0_1), | |
rest.Post("/v0.1.0/speaker", speakerController0_1_0), | |
rest.Post("/v0.2.0/speaker", speakerController0_2_0), | |
rest.Post("/v0.3.0/speaker", speakerController0_3_0), | |
) | |
if err != nil { | |
log.Fatal(err) | |
} | |
api.SetApp(router) | |
log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) | |
} | |
// Usage: | |
// curl -v -XPOST -H 'Content-type: application/json' -d '{"name":"Aurelijus Banelis"}' http://127.0.0.1:8080/v0.0.1/speaker | |
// curl -v -XPOST -H 'Content-type: application/json' -d '{"name":"Aurelijus Banelis", "topic": "JSON+Go in practice"}' http://127.0.0.1:8080/v0.1.0/speaker | |
// curl -v -XPOST -H 'Content-type: application/json' -d '{"name":"Aurelijus Banelis", "topic": "JSON+Go in practice", "homePage": "https://aurelijus.banelis.lt"}' http://127.0.0.1:8080/v0.2.0/speaker | |
// curl -v -XPOST -H 'Content-type: application/json' -d '{"name":"Aurelijus Banelis", "topic": "JSON+Go in practice", "homePage": "https://www.linkedin.com/in/aurelijusbanelis"}' http://127.0.0.1:8080/v0.2.0/speaker | |
// curl -v -XPOST -H 'Content-type: application/json' -d '{"name":"Aurelijus Banelis", "topic": "JSON+Go in practice", "linkedIn": "aurelijusbanelis"}' http://127.0.0.1:8080/v0.3.0/speaker |
{"topic":""} | |
{"Attributes":{}} | |
{"Attributes":{"field":"value"}} | |
{} |