Use functional opts for searches

Signed-off-by: Vartan Benohanian <vartanbeno@gmail.com>
This commit is contained in:
Vartan Benohanian 2020-05-18 19:25:24 -04:00
parent 2696d8e32c
commit 7b83e8366a
3 changed files with 378 additions and 161 deletions

404
search.go
View file

@ -3,7 +3,6 @@ package geddit
import ( import (
"context" "context"
"fmt" "fmt"
"net/http"
"strings" "strings"
) )
@ -12,10 +11,12 @@ import (
// IMPORTANT: for searches to include NSFW results, the // IMPORTANT: for searches to include NSFW results, the
// user must check the following in their preferences: // user must check the following in their preferences:
// "include not safe for work (NSFW) search results in searches" // "include not safe for work (NSFW) search results in searches"
// Note: The "limit" parameter in searches is prone to inconsistent
// behaviour.
type SearchService interface { type SearchService interface {
Posts(query string) *PostSearchBuilder Posts(query string, opts ...SearchOpt) *PostSearcher
Subreddits(query string) *SubredditSearchBuilder Subreddits(query string, opts ...SearchOpt) *SubredditSearcher
Users(query string) *UserSearchBuilder Users(query string, opts ...SearchOpt) *UserSearcher
} }
// SearchServiceOp implements the VoteService interface // SearchServiceOp implements the VoteService interface
@ -27,115 +28,58 @@ var _ SearchService = &SearchServiceOp{}
// Posts searches for posts. // Posts searches for posts.
// By default, it searches for the most relevant posts of all time. // By default, it searches for the most relevant posts of all time.
// To change the sorting, use PostSearchBuilder.Sort(). func (s *SearchServiceOp) Posts(query string, opts ...SearchOpt) *PostSearcher {
// Possible sort options: relevance, hot, top, new, comments. sr := new(PostSearcher)
// To change the timespan, use PostSearchBuilder.Timespan(). sr.client = s.client
// Possible timespan options: hour, day, week, month, year, all. sr.opts.Query = query
func (s *SearchServiceOp) Posts(query string) *PostSearchBuilder { sr.opts.Type = "link"
b := new(PostSearchBuilder) sr.opts.Sort = SortRelevance.String()
b.client = s.client sr.opts.Timespan = TimespanAll.String()
b.opts.Query = query for _, opt := range opts {
b.opts.Type = "link" opt(sr)
return b.Sort(SortRelevance).Timespan(TimespanAll) }
return sr
} }
// Subreddits searches for subreddits. // Subreddits searches for subreddits.
func (s *SearchServiceOp) Subreddits(query string) *SubredditSearchBuilder { func (s *SearchServiceOp) Subreddits(query string, opts ...SearchOpt) *SubredditSearcher {
b := new(SubredditSearchBuilder) sr := new(SubredditSearcher)
b.client = s.client sr.client = s.client
b.opts.Query = query sr.opts.Query = query
b.opts.Type = "sr" sr.opts.Type = "sr"
return b for _, opt := range opts {
opt(sr)
}
return sr
} }
// Users searches for users. // Users searches for users.
func (s *SearchServiceOp) Users(query string) *UserSearchBuilder { func (s *SearchServiceOp) Users(query string, opts ...SearchOpt) *UserSearcher {
b := new(UserSearchBuilder) sr := new(UserSearcher)
b.client = s.client sr.client = s.client
b.opts.Query = query sr.opts.Query = query
b.opts.Type = "user" sr.opts.Type = "user"
return b for _, opt := range opts {
opt(sr)
}
return sr
} }
type searchOpts struct { // PostSearcher helps conducts searches that return posts.
Query string `url:"q"` type PostSearcher struct {
Type string `url:"type,omitempty"` clientSearcher
After string `url:"after,omitempty"`
Before string `url:"before,omitempty"`
Limit int `url:"limit,omitempty"`
RestrictSubreddits bool `url:"restrict_sr,omitempty"`
Sort string `url:"sort,omitempty"`
Timespan string `url:"t,omitempty"`
}
// PostSearchBuilder helps conducts searches that return posts.
type PostSearchBuilder struct {
client *Client
subreddits []string subreddits []string
opts searchOpts after string
Results []Link
} }
// After sets the after option. func (s *PostSearcher) search(ctx context.Context) (*Links, *Response, error) {
func (b *PostSearchBuilder) After(after string) *PostSearchBuilder {
b.opts.After = after
return b
}
// Before sets the before option.
func (b *PostSearchBuilder) Before(before string) *PostSearchBuilder {
b.opts.Before = before
return b
}
// Limit sets the limit option.
func (b *PostSearchBuilder) Limit(limit int) *PostSearchBuilder {
b.opts.Limit = limit
return b
}
// FromSubreddits restricts the search to happen in the specified subreddits only.
func (b *PostSearchBuilder) FromSubreddits(subreddits ...string) *PostSearchBuilder {
b.subreddits = subreddits
b.opts.RestrictSubreddits = len(subreddits) > 0
return b
}
// FromAll runs the search against r/all.
func (b *PostSearchBuilder) FromAll() *PostSearchBuilder {
return b.FromSubreddits()
}
// Sort sets the sort option.
func (b *PostSearchBuilder) Sort(sort Sort) *PostSearchBuilder {
b.opts.Sort = sort.String()
return b
}
// Timespan sets the timespan option.
func (b *PostSearchBuilder) Timespan(timespan Timespan) *PostSearchBuilder {
b.opts.Timespan = timespan.String()
return b
}
// Do conducts the search.
func (b *PostSearchBuilder) Do(ctx context.Context) (*Links, *Response, error) {
path := "search" path := "search"
if len(b.subreddits) > 0 { if len(s.subreddits) > 0 {
path = fmt.Sprintf("r/%s/search", strings.Join(b.subreddits, "+")) path = fmt.Sprintf("r/%s/search", strings.Join(s.subreddits, "+"))
} }
path, err := addOptions(path, b.opts) root, resp, err := s.clientSearcher.Do(ctx, path)
if err != nil {
return nil, nil, err
}
req, err := b.client.NewRequest(http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(rootListing)
resp, err := b.client.Do(ctx, req, root)
if err != nil { if err != nil {
return nil, resp, err return nil, resp, err
} }
@ -143,94 +87,232 @@ func (b *PostSearchBuilder) Do(ctx context.Context) (*Links, *Response, error) {
return root.getLinks(), resp, nil return root.getLinks(), resp, nil
} }
// SubredditSearchBuilder helps conducts searches that return subreddits. // Search runs the searcher.
type SubredditSearchBuilder struct { // The first return value tells the user if there are
client *Client // more results that were cut off (due to the limit).
opts searchOpts func (s *PostSearcher) Search(ctx context.Context) (bool, *Response, error) {
root, resp, err := s.search(ctx)
if err != nil {
return false, resp, err
}
s.Results = root.Links
s.after = root.After
// if the "after" value is non-empty, it
// means there are more results to come.
moreResultsExist := s.after != ""
return moreResultsExist, resp, nil
} }
// After sets the after option. // More runs the searcher again and adds to the results.
func (b *SubredditSearchBuilder) After(after string) *SubredditSearchBuilder { // The first return value tells the user if there are
b.opts.After = after // more results that were cut off (due to the limit).
return b func (s *PostSearcher) More(ctx context.Context) (bool, *Response, error) {
if s.after == "" {
return s.Search(ctx)
}
s.setAfter(s.after)
root, resp, err := s.search(ctx)
if err != nil {
return false, resp, err
}
s.Results = append(s.Results, root.Links...)
s.after = root.After
// if the "after" value is non-empty, it
// means there are more results to come.
moreResultsExist := s.after != ""
return moreResultsExist, resp, nil
} }
// Before sets the before option. // All runs the searcher until it yields no more results.
func (b *SubredditSearchBuilder) Before(before string) *SubredditSearchBuilder { // The limit is set to 100, just to make the least amount
b.opts.Before = before // of requests possible. It is reset to its original value after.
return b func (s *PostSearcher) All(ctx context.Context) error {
limit := s.opts.Limit
s.setLimit(100)
defer s.setLimit(limit)
var ok = true
var err error
for ok {
ok, _, err = s.More(ctx)
if err != nil {
return err
}
}
return nil
} }
// Limit sets the limit option. // SubredditSearcher helps conducts searches that return subreddits.
func (b *SubredditSearchBuilder) Limit(limit int) *SubredditSearchBuilder { type SubredditSearcher struct {
b.opts.Limit = limit clientSearcher
return b after string
Results []Subreddit
} }
// Do conducts the search. func (s *SubredditSearcher) search(ctx context.Context) (*Subreddits, *Response, error) {
func (b *SubredditSearchBuilder) Do(ctx context.Context) (*Subreddits, *Response, error) {
path := "search" path := "search"
path, err := addOptions(path, b.opts) root, resp, err := s.clientSearcher.Do(ctx, path)
if err != nil {
return nil, nil, err
}
req, err := b.client.NewRequest(http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(rootListing)
resp, err := b.client.Do(ctx, req, root)
if err != nil { if err != nil {
return nil, resp, err return nil, resp, err
} }
return root.getSubreddits(), resp, nil return root.getSubreddits(), resp, nil
} }
// UserSearchBuilder helps conducts searches that return posts. // Search runs the searcher.
type UserSearchBuilder struct { // The first return value tells the user if there are
client *Client // more results that were cut off (due to the limit).
opts searchOpts func (s *SubredditSearcher) Search(ctx context.Context) (bool, *Response, error) {
root, resp, err := s.search(ctx)
if err != nil {
return false, resp, err
}
s.Results = root.Subreddits
s.after = root.After
// if the "after" value is non-empty, it
// means there are more results to come.
moreResultsExist := s.after != ""
return moreResultsExist, resp, nil
} }
// After sets the after option. // More runs the searcher again and adds to the results.
func (b *UserSearchBuilder) After(after string) *UserSearchBuilder { // The first return value tells the user if there are
b.opts.After = after // more results that were cut off (due to the limit).
return b func (s *SubredditSearcher) More(ctx context.Context) (bool, *Response, error) {
if s.after == "" {
return s.Search(ctx)
}
s.setAfter(s.after)
root, resp, err := s.search(ctx)
if err != nil {
return false, resp, err
}
s.Results = append(s.Results, root.Subreddits...)
s.after = root.After
// if the "after" value is non-empty, it
// means there are more results to come.
moreResultsExist := s.after != ""
return moreResultsExist, resp, nil
} }
// Before sets the before option. // All runs the searcher until it yields no more results.
func (b *UserSearchBuilder) Before(before string) *UserSearchBuilder { // The limit is set to 100, just to make the least amount
b.opts.Before = before // of requests possible. It is reset to its original value after.
return b func (s *SubredditSearcher) All(ctx context.Context) error {
limit := s.opts.Limit
s.setLimit(100)
defer s.setLimit(limit)
var ok = true
var err error
for ok {
ok, _, err = s.More(ctx)
if err != nil {
return err
}
}
return nil
} }
// Limit sets the limit option. // UserSearcher helps conducts searches that return users.
func (b *UserSearchBuilder) Limit(limit int) *UserSearchBuilder { type UserSearcher struct {
b.opts.Limit = limit clientSearcher
return b after string
Results []User
} }
// Do conducts the search. func (s *UserSearcher) search(ctx context.Context) (*Users, *Response, error) {
func (b *UserSearchBuilder) Do(ctx context.Context) (*Users, *Response, error) {
path := "search" path := "search"
path, err := addOptions(path, b.opts) root, resp, err := s.clientSearcher.Do(ctx, path)
if err != nil {
return nil, nil, err
}
req, err := b.client.NewRequest(http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(rootListing)
resp, err := b.client.Do(ctx, req, root)
if err != nil { if err != nil {
return nil, resp, err return nil, resp, err
} }
return root.getUsers(), resp, nil return root.getUsers(), resp, nil
} }
// Search runs the searcher.
// The first return value tells the user if there are
// more results that were cut off (due to the limit).
func (s *UserSearcher) Search(ctx context.Context) (bool, *Response, error) {
root, resp, err := s.search(ctx)
if err != nil {
return false, resp, err
}
s.Results = root.Users
s.after = root.After
// if the "after" value is non-empty, it
// means there are more results to come.
moreResultsExist := s.after != ""
return moreResultsExist, resp, nil
}
// More runs the searcher again and adds to the results.
// The first return value tells the user if there are
// more results that were cut off (due to the limit).
func (s *UserSearcher) More(ctx context.Context) (bool, *Response, error) {
if s.after == "" {
return s.Search(ctx)
}
s.setAfter(s.after)
root, resp, err := s.search(ctx)
if err != nil {
return false, resp, err
}
s.Results = append(s.Results, root.Users...)
s.after = root.After
// if the "after" value is non-empty, it
// means there are more results to come.
moreResultsExist := s.after != ""
return moreResultsExist, resp, nil
}
// All runs the searcher until it yields no more results.
// The limit is set to 100, just to make the least amount
// of requests possible. It is reset to its original value after.
func (s *UserSearcher) All(ctx context.Context) error {
limit := s.opts.Limit
s.setLimit(100)
defer s.setLimit(limit)
var ok = true
var err error
for ok {
ok, _, err = s.More(ctx)
if err != nil {
return err
}
}
return nil
}

134
searcher.go Normal file
View file

@ -0,0 +1,134 @@
package geddit
import (
"context"
"net/http"
)
// todo: query parameter "show" = "all"
// Searcher defines some parameters common to all requests
// used to conduct searches against the Reddit API.
type Searcher interface {
setAfter(string)
setBefore(string)
setLimit(int)
setSort(Sort)
setTimespan(Timespan)
}
// Contains all options used for searching.
// Not all are used for every search endpoint.
// For example, for getting a user's posts, "q" is not used.
// After/Before are used as the anchor points for subsequent searches.
// Limit is the maximum number of items to be returned (default: 25, max: 100).
// Sort: hot, new, top, controversial, etc.
// Timespan: hour, day, week, month, year, all.
type searchOpts struct {
Query string `url:"q,omitempty"`
Type string `url:"type,omitempty"`
After string `url:"after,omitempty"`
Before string `url:"before,omitempty"`
Limit int `url:"limit,omitempty"`
RestrictSubreddits bool `url:"restrict_sr,omitempty"`
Sort string `url:"sort,omitempty"`
Timespan string `url:"t,omitempty"`
}
type clientSearcher struct {
client *Client
opts searchOpts
}
var _ Searcher = &clientSearcher{}
func (s *clientSearcher) setAfter(v string) {
s.opts.After = v
}
func (s *clientSearcher) setBefore(v string) {
s.opts.Before = v
}
func (s *clientSearcher) setLimit(v int) {
s.opts.Limit = v
}
func (s *clientSearcher) setSort(v Sort) {
s.opts.Sort = v.String()
}
func (s *clientSearcher) setTimespan(v Timespan) {
s.opts.Timespan = v.String()
}
func (s *clientSearcher) Do(ctx context.Context, path string) (*rootListing, *Response, error) {
path, err := addOptions(path, s.opts)
if err != nil {
return nil, nil, err
}
req, err := s.client.NewRequest(http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(rootListing)
resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
return root, resp, nil
}
// SearchOpt sets search options.
type SearchOpt func(s Searcher)
// SetAfter sets the after option.
func SetAfter(v string) SearchOpt {
return func(s Searcher) {
s.setAfter(v)
}
}
// SetBefore sets the before option.
func SetBefore(v string) SearchOpt {
return func(s Searcher) {
s.setBefore(v)
}
}
// SetLimit sets the limit option.
func SetLimit(v int) SearchOpt {
return func(s Searcher) {
s.setLimit(v)
}
}
// SetSort sets the sort option.
func SetSort(v Sort) SearchOpt {
return func(s Searcher) {
s.setSort(v)
}
}
// SetTimespan sets the timespan option.
func SetTimespan(v Timespan) SearchOpt {
return func(s Searcher) {
s.setTimespan(v)
}
}
// FromSubreddits is an option that restricts the
// search to happen in the specified subreddits.
// If none are specified, it's like searching r/all.
// This option is only applicable to the PostSearcher.
func FromSubreddits(subreddits ...string) SearchOpt {
return func(s Searcher) {
if ps, ok := s.(*PostSearcher); ok {
ps.subreddits = subreddits
ps.opts.RestrictSubreddits = len(subreddits) > 0
}
}
}

View file

@ -55,6 +55,7 @@ type User struct {
IsEmployee bool `json:"is_employee"` IsEmployee bool `json:"is_employee"`
HasVerifiedEmail bool `json:"has_verified_email"` HasVerifiedEmail bool `json:"has_verified_email"`
NSFW bool `json:"over_18"` NSFW bool `json:"over_18"`
IsSuspended bool `json:"is_suspended"`
} }
// UserShort represents a Reddit user, but contains fewer pieces of information // UserShort represents a Reddit user, but contains fewer pieces of information