Skip to content

Commit

Permalink
Merge pull request #1 from Henry-Sarabia/update-options
Browse files Browse the repository at this point in the history
Remove Options Object
  • Loading branch information
Henry Sarabia committed Feb 13, 2019
2 parents c441bae + 557518d commit 54b7978
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 319 deletions.
7 changes: 4 additions & 3 deletions apicalypse.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
package apicalypse

import (
"github.com/Henry-Sarabia/whitespace"
"github.com/pkg/errors"
"net/http"
)

// NewRequest returns a request configured for the provided url using the provided method.
// The provided query options are written to the body of the request. The default method is GET.
func NewRequest(method string, url string, options ...FuncOption) (*http.Request, error) {
if isBlank(url) {
if whitespace.IsBlank(url) {
return nil, ErrBlankArgument
}

opt, err := newOptions(options...)
filters, err := newFilters(options...)
if err != nil {
return nil, errors.Wrap(err, "cannot create new options")
}

req, err := http.NewRequest(method, url, opt.reader())
req, err := http.NewRequest(method, url, toReader(filters))
if err != nil {
return nil, errors.Wrapf(err, "cannot create request with method '%s' for url '%s'", method, url)
}
Expand Down
41 changes: 41 additions & 0 deletions filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package apicalypse

import (
"github.com/pkg/errors"
"strings"
)

// newFilters returns a filter map mutated by the provided FuncOption arguments.
// If no FuncOption's are provided, an empty map is returned.
func newFilters(funcOpts ...FuncOption) (map[string]string, error) {
filters := map[string]string{}

for _, f := range funcOpts {
if err := f(filters); err != nil {
return nil, errors.Wrap(err, "cannot create new options")
}
}

return filters, nil
}

// toString returns the filters as a single string.
func toString(f map[string]string) string {
if len(f) <= 0 {
return ""
}

b := strings.Builder{}
for k, v := range f {
b.WriteString(k + " " + v + "; ")
}

return b.String()
}

// reader returns the filters as a *strings.Reader
// to satisfy the io.Reader interface.
func toReader(f map[string]string) *strings.Reader {
s := toString(f)
return strings.NewReader(s)
}
85 changes: 85 additions & 0 deletions filter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package apicalypse

import (
"bytes"
"github.com/pkg/errors"
"reflect"
"strings"
"testing"
)

func TestNewFilters(t *testing.T) {
tests := []struct {
name string
funcOpts []FuncOption
wantFilters map[string]string
wantErr error
}{
{"Empty option", []FuncOption{}, map[string]string{}, nil},
{"Single option", []FuncOption{Limit(15)}, map[string]string{"limit": "15"}, nil},
{"Multiple options", []FuncOption{Limit(15), Offset(10), Fields("name", "rating")}, map[string]string{"limit": "15", "offset": "10", "fields": "name,rating"}, nil},
{"Single error option", []FuncOption{Limit(-99)}, nil, ErrNegativeInput},
{"Multiple error options", []FuncOption{Fields(), Exclude(), Where()}, nil, ErrMissingInput},
{"Mixed options", []FuncOption{Limit(10), Offset(-99)}, nil, ErrNegativeInput},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
filters, err := newFilters(test.funcOpts...)
if !reflect.DeepEqual(errors.Cause(err), test.wantErr) {
t.Errorf("got: <%v>, want: <%v>", err, test.wantErr)
}

if !reflect.DeepEqual(filters, test.wantFilters) {
t.Errorf("got: <%v>, want: <%v>", filters, test.wantFilters)
}
})
}
}

func TestToString(t *testing.T) {
tests := []struct {
name string
filters map[string]string
wants []string
}{
{"Zero filters", map[string]string{}, nil},
{"Single filter", map[string]string{"limit": "15"}, []string{"limit 15; "}},
{"Multiple filters", map[string]string{"limit": "15", "fields": "id,name,rating"}, []string{"limit 15; ", "fields id,name,rating; "}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := toString(test.filters)

for _, want := range test.wants {
if !strings.Contains(got, want) {
t.Errorf("got: <%v>, want: <%v>", got, want)
}
}
})
}
}

func TestToReader(t *testing.T) {
tests := []struct {
name string
filters map[string]string
wants []string
}{
{"Zero filters", map[string]string{}, nil},
{"Single filter", map[string]string{"limit": "15"}, []string{"limit 15; "}},
{"Multiple filters", map[string]string{"limit": "15", "fields": "id,name,rating"}, []string{"limit 15; ", "fields id,name,rating; "}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
buf := bytes.Buffer{}
buf.ReadFrom(toReader(test.filters))
got := buf.String()

for _, want := range test.wants {
if !strings.Contains(got, want) {
t.Errorf("got: <%v>, want: <%v>", got, want)
}
}
})
}
}
89 changes: 43 additions & 46 deletions funcOption.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package apicalypse

import (
"github.com/Henry-Sarabia/whitespace"
"github.com/pkg/errors"
"regexp"
"strconv"
"strings"
)
Expand All @@ -16,49 +16,63 @@ var (
ErrNegativeInput = errors.New("input cannot be a negative number")
)

// FuncOption is a functional option type used to set the options for an API query.
// FuncOption is a functional option type used to set the filters for an API query.
// FuncOption is the first-order function returned by the available functional options
// (e.g. Fields or Limit). For the full list of supported filters and their expected
// syntax, please visit: https://apicalypse.io/syntax/
type FuncOption func(*options) error
type FuncOption func(map[string]string) error

// ComposeOptions composes multiple functional options into a single FuncOption.
// This is primarily used to create a single functional option that can be used
// repeatedly across multiple queries.
func ComposeOptions(funcOpts ...FuncOption) FuncOption {
return func(filters map[string]string) error {
for _, f := range funcOpts {
if err := f(filters); err != nil {
return errors.Wrap(err, "cannot compose functional options")
}
}
return nil
}
}

// Fields is a functional option for setting the included fields in the results from a query.
func Fields(fields ...string) FuncOption {
return func(opt *options) error {
return func(filters map[string]string) error {
if len(fields) <= 0 {
return ErrMissingInput
}

for _, f := range fields {
if isBlank(f) {
if whitespace.IsBlank(f) {
return ErrBlankArgument
}
}

f := strings.Join(fields, ",")
f = removeWhitespace(f)
opt.Filters["fields"] = f
f = whitespace.Remove(f)
filters["fields"] = f

return nil
}
}

// Exclude is a functional option for setting the excluded fields in the results from a query.
func Exclude(fields ...string) FuncOption {
return func(opt *options) error {
return func(filters map[string]string) error {
if len(fields) <= 0 {
return ErrMissingInput
}

for _, f := range fields {
if isBlank(f) {
if whitespace.IsBlank(f) {
return ErrBlankArgument
}
}

f := strings.Join(fields, ",")
f = removeWhitespace(f)
opt.Filters["exclude"] = f
f = whitespace.Remove(f)
filters["exclude"] = f

return nil
}
Expand All @@ -67,20 +81,20 @@ func Exclude(fields ...string) FuncOption {
// Where is a functional option for setting a custom data filter similar to SQL.
// If multiple filters are provided, they are AND'd together.
// For the full list of filters and more information, visit: https://apicalypse.io/syntax/
func Where(filters ...string) FuncOption {
return func(opt *options) error {
if len(filters) <= 0 {
func Where(custom ...string) FuncOption {
return func(filters map[string]string) error {
if len(custom) <= 0 {
return ErrMissingInput
}

for _, f := range filters {
if isBlank(f) {
for _, f := range custom {
if whitespace.IsBlank(f) {
return ErrBlankArgument
}
}

f := strings.Join(filters, " & ")
opt.Filters["where"] = f
c := strings.Join(custom, " & ")
filters["where"] = c

return nil
}
Expand All @@ -89,23 +103,23 @@ func Where(filters ...string) FuncOption {
// Limit is a functional option for setting the number of items to return from a query.
// This usually has a maximum limit.
func Limit(n int) FuncOption {
return func(opt *options) error {
return func(filters map[string]string) error {
if n < 0 {
return ErrNegativeInput
}
opt.Filters["limit"] = strconv.Itoa(n)
filters["limit"] = strconv.Itoa(n)

return nil
}
}

// Offset is a functional option for setting the index to start returning results from a query.
func Offset(n int) FuncOption {
return func(opt *options) error {
return func(filters map[string]string) error {
if n < 0 {
return ErrNegativeInput
}
opt.Filters["offset"] = strconv.Itoa(n)
filters["offset"] = strconv.Itoa(n)

return nil
}
Expand All @@ -114,46 +128,29 @@ func Offset(n int) FuncOption {
// Sort is a functional option for sorting the results of a query by a certain field's
// values and the use of "asc" or "desc" to sort by ascending or descending order.
func Sort(field, order string) FuncOption {
return func(opt *options) error {
if isBlank(field) || isBlank(order) {
return func(filters map[string]string) error {
if whitespace.IsBlank(field) || whitespace.IsBlank(order) {
return ErrBlankArgument
}

opt.Filters["sort"] = field + " " + order
filters["sort"] = field + " " + order
return nil
}
}

// Search is a functional option for searching for a value in a particular column of data.
// If the column is omitted, search will be performed on the default column.
func Search(column, term string) FuncOption {
return func(opt *options) error {
if isBlank(term) {
return func(filters map[string]string) error {
if whitespace.IsBlank(term) {
return ErrBlankArgument
}

if !isBlank(column) {
if !whitespace.IsBlank(column) {
column = column + " "
}

opt.Filters["search"] = column + `"` + term + `"`
filters["search"] = column + `"` + term + `"`
return nil
}
}

// removeWhitespace returns the provided string with all of the whitespace removed.
// This includes spaces, tabs, newlines, returns, and form feeds.
func removeWhitespace(s string) string {
space := regexp.MustCompile(`\s+`)
return space.ReplaceAllString(s, "")
}

// isBlank returns true if the provided string is empty or only consists of whitespace.
// Returns false otherwise.
func isBlank(s string) bool {
if removeWhitespace(s) == "" {
return true
}

return false
}

0 comments on commit 54b7978

Please sign in to comment.