DSCGS-17 Use github.com/google/go-querystring/

This commit is contained in:
irlndts 2018-03-17 21:58:56 +03:00
parent 2bc2347afc
commit b2674eb55e
7 changed files with 726 additions and 36 deletions

View File

@ -6,8 +6,6 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"github.com/irlndts/go-apirequest"
) )
const ( const (
@ -35,13 +33,11 @@ var header *http.Header
// NewClient returns a new Client. // NewClient returns a new Client.
func NewClient(o *Options) (*Client, error) { func NewClient(o *Options) (*Client, error) {
header = &http.Header{} header = &http.Header{}
base := apirequest.New().Client(&http.Client{}).Base(discogsAPI)
if o == nil || o.UserAgent == "" { if o == nil || o.UserAgent == "" {
return nil, fmt.Errorf("failed to set user-agent") return nil, fmt.Errorf("failed to set user-agent")
} }
base.Set("User-Agent", o.UserAgent)
header.Add("User-Agent", o.UserAgent) header.Add("User-Agent", o.UserAgent)
cur, err := currency(o.Currency) cur, err := currency(o.Currency)
@ -51,7 +47,6 @@ func NewClient(o *Options) (*Client, error) {
// set token, it's required for some queries like search // set token, it's required for some queries like search
if o.Token != "" { if o.Token != "" {
base.Set("Authorization", "Discogs token="+o.Token)
header.Add("Authorization", "Discogs token="+o.Token) header.Add("Authorization", "Discogs token="+o.Token)
} }
@ -60,11 +55,11 @@ func NewClient(o *Options) (*Client, error) {
} }
return &Client{ return &Client{
Release: newReleaseService(o.URL+"/releases/", cur), Release: newReleaseService(o.URL+"releases/", cur),
Artist: newArtistService(o.URL + "/artists/"), Artist: newArtistService(o.URL + "artists/"),
Label: newLabelService(o.URL + "/labels/"), Label: newLabelService(o.URL + "labels/"),
Master: newMasterService(o.URL + "/masters/"), Master: newMasterService(o.URL + "masters/"),
Search: newSearchService(o.URL + "/database/search"), Search: newSearchService(o.URL + "database/search"),
}, nil }, nil
} }
@ -83,6 +78,7 @@ func currency(c string) (string, error) {
} }
func request(path string, params url.Values, resp interface{}) error { func request(path string, params url.Values, resp interface{}) error {
fmt.Println(path + "?" + params.Encode())
r, err := http.NewRequest("GET", path+"?"+params.Encode(), nil) r, err := http.NewRequest("GET", path+"?"+params.Encode(), nil)
if err != nil { if err != nil {
return err return err

View File

@ -2,7 +2,6 @@ package main
import ( import (
"fmt" "fmt"
"net/url"
"github.com/irlndts/go-discogs" "github.com/irlndts/go-discogs"
) )
@ -18,9 +17,9 @@ func main() {
return return
} }
params := url.Values{} release, err := d.Search.Search(discogs.SearchRequest{
params.Set("q", "Ska-Jazz Review") Q: "Ska-Jazz Review",
release, err := d.Search.Search(params) })
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return return

View File

@ -1,6 +1,8 @@
package discogs package discogs
import "net/url" import (
"github.com/google/go-querystring/query"
)
// SearchService ... // SearchService ...
type SearchService struct { type SearchService struct {
@ -15,27 +17,27 @@ func newSearchService(url string) *SearchService {
// SerachRequest describes search request // SerachRequest describes search request
type SearchRequest struct { type SearchRequest struct {
Q string // search query (optional) Q string `url:"q,omitempty"` // search query
Type string // one of release, master, artist, label (optional) Type string `url:"type,omitempty"` // one of release, master, artist, label
Title string // search by combined “Artist Name - Release Title” title field (optional) Title string `url:"title,omitempty"` // search by combined “Artist Name - Release Title” title field
Release_title string // search release titles (optional) ReleaseTitle string `url:"release_title,omitempty"` // search release titles
Credit string // search release credits (optional) Credit string `url:"credit,omitempty"` // search release credits
Artist string // search artist names (optional) Artist string `url:"artist,omitempty"` // search artist names
Anv string // search artist ANV (optional) Anv string `url:"anv,omitempty"` // search artist ANV
Label string // search label names (optional) Label string `url:"label,omitempty"` // search label names
Genre string // search genres (optional) Genre string `url:"genre,omitempty"` // search genres
Style string // search styles (optional) Style string `url:"style,omitempty"` // search styles
Country string // search release country (optional) Country string `url:"country,omitempty"` // search release country
Year string // search release year (optional) Year string `url:"year,omitempty"` // search release year
Format string // search formats (optional) Format string `url:"format,omitempty"` // search formats
Catno string // search catalog number (optional) Catno string `url:"catno,omitempty"` // search catalog number
Barcode string // search barcodes (optional) Barcode string `url:"barcode,omitempty"` // search barcodes
Track string // search track titles (optional) Track string `url:"track,omitempty"` // search track titles
Submitter string // search submitter username (optional) Submitter string `url:"submitter,omitempty"` // search submitter username
Contributer string // search contributor usernames (optional) Contributer string `url:"contributer,omitempty"` // search contributor usernames
Page int // optional Page int `url:"page,omitempty"`
PerPage int // optional PerPage int `url:"per_page,omitempty"`
} }
// Search describes search response // Search describes search response
@ -67,7 +69,12 @@ type Result struct {
// Authentication (as any user) is required. // Authentication (as any user) is required.
// https://www.discogs.com/developers/#page:database,header:database-search // https://www.discogs.com/developers/#page:database,header:database-search
// TODO(irlndts): improve params to pass // TODO(irlndts): improve params to pass
func (s *SearchService) Search(params url.Values) (*Search, error) { func (s *SearchService) Search(req SearchRequest) (*Search, error) {
params, err := query.Values(req)
if err != nil {
return nil, err
}
var search *Search var search *Search
if err := request(s.url, params, &search); err != nil { if err := request(s.url, params, &search); err != nil {
return nil, err return nil, err

27
vendor/github.com/google/go-querystring/LICENSE generated vendored Normal file
View File

@ -0,0 +1,27 @@
Copyright (c) 2013 Google. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

320
vendor/github.com/google/go-querystring/query/encode.go generated vendored Normal file
View File

@ -0,0 +1,320 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package query implements encoding of structs into URL query parameters.
//
// As a simple example:
//
// type Options struct {
// Query string `url:"q"`
// ShowAll bool `url:"all"`
// Page int `url:"page"`
// }
//
// opt := Options{ "foo", true, 2 }
// v, _ := query.Values(opt)
// fmt.Print(v.Encode()) // will output: "q=foo&all=true&page=2"
//
// The exact mapping between Go values and url.Values is described in the
// documentation for the Values() function.
package query
import (
"bytes"
"fmt"
"net/url"
"reflect"
"strconv"
"strings"
"time"
)
var timeType = reflect.TypeOf(time.Time{})
var encoderType = reflect.TypeOf(new(Encoder)).Elem()
// Encoder is an interface implemented by any type that wishes to encode
// itself into URL values in a non-standard way.
type Encoder interface {
EncodeValues(key string, v *url.Values) error
}
// Values returns the url.Values encoding of v.
//
// Values expects to be passed a struct, and traverses it recursively using the
// following encoding rules.
//
// Each exported struct field is encoded as a URL parameter unless
//
// - the field's tag is "-", or
// - the field is empty and its tag specifies the "omitempty" option
//
// The empty values are false, 0, any nil pointer or interface value, any array
// slice, map, or string of length zero, and any time.Time that returns true
// for IsZero().
//
// The URL parameter name defaults to the struct field name but can be
// specified in the struct field's tag value. The "url" key in the struct
// field's tag value is the key name, followed by an optional comma and
// options. For example:
//
// // Field is ignored by this package.
// Field int `url:"-"`
//
// // Field appears as URL parameter "myName".
// Field int `url:"myName"`
//
// // Field appears as URL parameter "myName" and the field is omitted if
// // its value is empty
// Field int `url:"myName,omitempty"`
//
// // Field appears as URL parameter "Field" (the default), but the field
// // is skipped if empty. Note the leading comma.
// Field int `url:",omitempty"`
//
// For encoding individual field values, the following type-dependent rules
// apply:
//
// Boolean values default to encoding as the strings "true" or "false".
// Including the "int" option signals that the field should be encoded as the
// strings "1" or "0".
//
// time.Time values default to encoding as RFC3339 timestamps. Including the
// "unix" option signals that the field should be encoded as a Unix time (see
// time.Unix())
//
// Slice and Array values default to encoding as multiple URL values of the
// same name. Including the "comma" option signals that the field should be
// encoded as a single comma-delimited value. Including the "space" option
// similarly encodes the value as a single space-delimited string. Including
// the "semicolon" option will encode the value as a semicolon-delimited string.
// Including the "brackets" option signals that the multiple URL values should
// have "[]" appended to the value name. "numbered" will append a number to
// the end of each incidence of the value name, example:
// name0=value0&name1=value1, etc.
//
// Anonymous struct fields are usually encoded as if their inner exported
// fields were fields in the outer struct, subject to the standard Go
// visibility rules. An anonymous struct field with a name given in its URL
// tag is treated as having that name, rather than being anonymous.
//
// Non-nil pointer values are encoded as the value pointed to.
//
// Nested structs are encoded including parent fields in value names for
// scoping. e.g:
//
// "user[name]=acme&user[addr][postcode]=1234&user[addr][city]=SFO"
//
// All other values are encoded using their default string representation.
//
// Multiple fields that encode to the same URL parameter name will be included
// as multiple URL values of the same name.
func Values(v interface{}) (url.Values, error) {
values := make(url.Values)
val := reflect.ValueOf(v)
for val.Kind() == reflect.Ptr {
if val.IsNil() {
return values, nil
}
val = val.Elem()
}
if v == nil {
return values, nil
}
if val.Kind() != reflect.Struct {
return nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind())
}
err := reflectValue(values, val, "")
return values, err
}
// reflectValue populates the values parameter from the struct fields in val.
// Embedded structs are followed recursively (using the rules defined in the
// Values function documentation) breadth-first.
func reflectValue(values url.Values, val reflect.Value, scope string) error {
var embedded []reflect.Value
typ := val.Type()
for i := 0; i < typ.NumField(); i++ {
sf := typ.Field(i)
if sf.PkgPath != "" && !sf.Anonymous { // unexported
continue
}
sv := val.Field(i)
tag := sf.Tag.Get("url")
if tag == "-" {
continue
}
name, opts := parseTag(tag)
if name == "" {
if sf.Anonymous && sv.Kind() == reflect.Struct {
// save embedded struct for later processing
embedded = append(embedded, sv)
continue
}
name = sf.Name
}
if scope != "" {
name = scope + "[" + name + "]"
}
if opts.Contains("omitempty") && isEmptyValue(sv) {
continue
}
if sv.Type().Implements(encoderType) {
if !reflect.Indirect(sv).IsValid() {
sv = reflect.New(sv.Type().Elem())
}
m := sv.Interface().(Encoder)
if err := m.EncodeValues(name, &values); err != nil {
return err
}
continue
}
if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array {
var del byte
if opts.Contains("comma") {
del = ','
} else if opts.Contains("space") {
del = ' '
} else if opts.Contains("semicolon") {
del = ';'
} else if opts.Contains("brackets") {
name = name + "[]"
}
if del != 0 {
s := new(bytes.Buffer)
first := true
for i := 0; i < sv.Len(); i++ {
if first {
first = false
} else {
s.WriteByte(del)
}
s.WriteString(valueString(sv.Index(i), opts))
}
values.Add(name, s.String())
} else {
for i := 0; i < sv.Len(); i++ {
k := name
if opts.Contains("numbered") {
k = fmt.Sprintf("%s%d", name, i)
}
values.Add(k, valueString(sv.Index(i), opts))
}
}
continue
}
for sv.Kind() == reflect.Ptr {
if sv.IsNil() {
break
}
sv = sv.Elem()
}
if sv.Type() == timeType {
values.Add(name, valueString(sv, opts))
continue
}
if sv.Kind() == reflect.Struct {
reflectValue(values, sv, name)
continue
}
values.Add(name, valueString(sv, opts))
}
for _, f := range embedded {
if err := reflectValue(values, f, scope); err != nil {
return err
}
}
return nil
}
// valueString returns the string representation of a value.
func valueString(v reflect.Value, opts tagOptions) string {
for v.Kind() == reflect.Ptr {
if v.IsNil() {
return ""
}
v = v.Elem()
}
if v.Kind() == reflect.Bool && opts.Contains("int") {
if v.Bool() {
return "1"
}
return "0"
}
if v.Type() == timeType {
t := v.Interface().(time.Time)
if opts.Contains("unix") {
return strconv.FormatInt(t.Unix(), 10)
}
return t.Format(time.RFC3339)
}
return fmt.Sprint(v.Interface())
}
// isEmptyValue checks if a value should be considered empty for the purposes
// of omitting fields with the "omitempty" option.
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
}
if v.Type() == timeType {
return v.Interface().(time.Time).IsZero()
}
return false
}
// tagOptions is the string following a comma in a struct field's "url" tag, or
// the empty string. It does not include the leading comma.
type tagOptions []string
// parseTag splits a struct field's url tag into its name and comma-separated
// options.
func parseTag(tag string) (string, tagOptions) {
s := strings.Split(tag, ",")
return s[0], s[1:]
}
// Contains checks whether the tagOptions contains the specified option.
func (o tagOptions) Contains(option string) bool {
for _, s := range o {
if s == option {
return true
}
}
return false
}

View File

@ -0,0 +1,328 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package query
import (
"fmt"
"net/url"
"reflect"
"testing"
"time"
)
type Nested struct {
A SubNested `url:"a"`
B *SubNested `url:"b"`
Ptr *SubNested `url:"ptr,omitempty"`
}
type SubNested struct {
Value string `url:"value"`
}
func TestValues_types(t *testing.T) {
str := "string"
strPtr := &str
timeVal := time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC)
tests := []struct {
in interface{}
want url.Values
}{
{
// basic primitives
struct {
A string
B int
C uint
D float32
E bool
}{},
url.Values{
"A": {""},
"B": {"0"},
"C": {"0"},
"D": {"0"},
"E": {"false"},
},
},
{
// pointers
struct {
A *string
B *int
C **string
D *time.Time
}{
A: strPtr,
C: &strPtr,
D: &timeVal,
},
url.Values{
"A": {str},
"B": {""},
"C": {str},
"D": {"2000-01-01T12:34:56Z"},
},
},
{
// slices and arrays
struct {
A []string
B []string `url:",comma"`
C []string `url:",space"`
D [2]string
E [2]string `url:",comma"`
F [2]string `url:",space"`
G []*string `url:",space"`
H []bool `url:",int,space"`
I []string `url:",brackets"`
J []string `url:",semicolon"`
K []string `url:",numbered"`
}{
A: []string{"a", "b"},
B: []string{"a", "b"},
C: []string{"a", "b"},
D: [2]string{"a", "b"},
E: [2]string{"a", "b"},
F: [2]string{"a", "b"},
G: []*string{&str, &str},
H: []bool{true, false},
I: []string{"a", "b"},
J: []string{"a", "b"},
K: []string{"a", "b"},
},
url.Values{
"A": {"a", "b"},
"B": {"a,b"},
"C": {"a b"},
"D": {"a", "b"},
"E": {"a,b"},
"F": {"a b"},
"G": {"string string"},
"H": {"1 0"},
"I[]": {"a", "b"},
"J": {"a;b"},
"K0": {"a"},
"K1": {"b"},
},
},
{
// other types
struct {
A time.Time
B time.Time `url:",unix"`
C bool `url:",int"`
D bool `url:",int"`
}{
A: time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC),
B: time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC),
C: true,
D: false,
},
url.Values{
"A": {"2000-01-01T12:34:56Z"},
"B": {"946730096"},
"C": {"1"},
"D": {"0"},
},
},
{
struct {
Nest Nested `url:"nest"`
}{
Nested{
A: SubNested{
Value: "that",
},
},
},
url.Values{
"nest[a][value]": {"that"},
"nest[b]": {""},
},
},
{
struct {
Nest Nested `url:"nest"`
}{
Nested{
Ptr: &SubNested{
Value: "that",
},
},
},
url.Values{
"nest[a][value]": {""},
"nest[b]": {""},
"nest[ptr][value]": {"that"},
},
},
{
nil,
url.Values{},
},
}
for i, tt := range tests {
v, err := Values(tt.in)
if err != nil {
t.Errorf("%d. Values(%q) returned error: %v", i, tt.in, err)
}
if !reflect.DeepEqual(tt.want, v) {
t.Errorf("%d. Values(%q) returned %v, want %v", i, tt.in, v, tt.want)
}
}
}
func TestValues_omitEmpty(t *testing.T) {
str := ""
s := struct {
a string
A string
B string `url:",omitempty"`
C string `url:"-"`
D string `url:"omitempty"` // actually named omitempty, not an option
E *string `url:",omitempty"`
}{E: &str}
v, err := Values(s)
if err != nil {
t.Errorf("Values(%q) returned error: %v", s, err)
}
want := url.Values{
"A": {""},
"omitempty": {""},
"E": {""}, // E is included because the pointer is not empty, even though the string being pointed to is
}
if !reflect.DeepEqual(want, v) {
t.Errorf("Values(%q) returned %v, want %v", s, v, want)
}
}
type A struct {
B
}
type B struct {
C string
}
type D struct {
B
C string
}
type e struct {
B
C string
}
type F struct {
e
}
func TestValues_embeddedStructs(t *testing.T) {
tests := []struct {
in interface{}
want url.Values
}{
{
A{B{C: "foo"}},
url.Values{"C": {"foo"}},
},
{
D{B: B{C: "bar"}, C: "foo"},
url.Values{"C": {"foo", "bar"}},
},
{
F{e{B: B{C: "bar"}, C: "foo"}}, // With unexported embed
url.Values{"C": {"foo", "bar"}},
},
}
for i, tt := range tests {
v, err := Values(tt.in)
if err != nil {
t.Errorf("%d. Values(%q) returned error: %v", i, tt.in, err)
}
if !reflect.DeepEqual(tt.want, v) {
t.Errorf("%d. Values(%q) returned %v, want %v", i, tt.in, v, tt.want)
}
}
}
func TestValues_invalidInput(t *testing.T) {
_, err := Values("")
if err == nil {
t.Errorf("expected Values() to return an error on invalid input")
}
}
type EncodedArgs []string
func (m EncodedArgs) EncodeValues(key string, v *url.Values) error {
for i, arg := range m {
v.Set(fmt.Sprintf("%s.%d", key, i), arg)
}
return nil
}
func TestValues_Marshaler(t *testing.T) {
s := struct {
Args EncodedArgs `url:"arg"`
}{[]string{"a", "b", "c"}}
v, err := Values(s)
if err != nil {
t.Errorf("Values(%q) returned error: %v", s, err)
}
want := url.Values{
"arg.0": {"a"},
"arg.1": {"b"},
"arg.2": {"c"},
}
if !reflect.DeepEqual(want, v) {
t.Errorf("Values(%q) returned %v, want %v", s, v, want)
}
}
func TestValues_MarshalerWithNilPointer(t *testing.T) {
s := struct {
Args *EncodedArgs `url:"arg"`
}{}
v, err := Values(s)
if err != nil {
t.Errorf("Values(%q) returned error: %v", s, err)
}
want := url.Values{}
if !reflect.DeepEqual(want, v) {
t.Errorf("Values(%q) returned %v, want %v", s, v, want)
}
}
func TestTagParsing(t *testing.T) {
name, opts := parseTag("field,foobar,foo")
if name != "field" {
t.Fatalf("name = %q, want field", name)
}
for _, tt := range []struct {
opt string
want bool
}{
{"foobar", true},
{"foo", true},
{"bar", false},
{"field", false},
} {
if opts.Contains(tt.opt) != tt.want {
t.Errorf("Contains(%q) = %v", tt.opt, !tt.want)
}
}
}

13
vendor/vendor.json vendored Normal file
View File

@ -0,0 +1,13 @@
{
"comment": "",
"ignore": "",
"package": [
{
"checksumSHA1": "aDjb3mG2PnOgRgQXVXBxzPNc9I0=",
"path": "github.com/google/go-querystring/query",
"revision": "53e6ce116135b80d037921a7fdd5138cf32d7a8a",
"revisionTime": "2017-01-11T10:11:55Z"
}
],
"rootPath": "github.com/irlndts/go-discogs"
}