Repository: sivchari/gotwtr
Pull request #97 (2021-12-02 15:11:47)

add api of ListSpecifiedUserMemberOf
it relates #81

Note

- At https://github.com/senk8/gotwtr/blob/ret-all-lists-specified-user-member/list_members_test.go#L309 ,
`'id'` was `` `id` ``in the original response.  Do you know of a way to include`` ` ` `` in`` ` ` ``? 

sivchari: Please rebase to up-to-date.

> 'id' was `id`in the original response. Do you know of a way to include` ` in` `?

FYI
https://github.com/sivchari/gotwtr/blob/9c2c77aa4f3cdc37f27d06bfb8d748566b51674e/tweet_lookup_test.go#L320-L335
senk8: > Please rebase to up-to-date.
> 
> > 'id' was `id`in the original response. Do you know of a way to include` ` in` `?
> 
> FYI
> 
> https://github.com/sivchari/gotwtr/blob/9c2c77aa4f3cdc37f27d06bfb8d748566b51674e/tweet_lookup_test.go#L320-L335

Done
sivchari: I suggest to method name and error message is unified.
And this method returns `list` array.
So, I think it's better to modify `ListSpecifiedUser` to `ListsSpecifiedUser`(related endpoint name and option, response, too).
How do you think ?
senk8: @sivchari 
LGTM. I have no special intention in using listSpecifedUsers.
I work on this.

Patch:
diff --git a/client.go b/client.go
index d390d82..9d72121 100644
--- a/client.go
+++ b/client.go
@@ -57,6 +57,7 @@ type Lists interface {
 	LookUpAllListsOwned(ctx context.Context, userID string, opt ...*AllListsOwnedOption) (*AllListsOwnedResponse, error)
 	LookUpListTweets(ctx context.Context, listID string, opt ...*ListTweetsOption) (*ListTweetsResponse, error)
 	ListMembers(ctx context.Context, listID string, opt ...*ListMembersOption) (*ListMembersResponse, error)
+	ListsSpecifiedUser(ctx context.Context, userID string, opt ...*ListsSpecifiedUserOption) (*ListsSpecifiedUserResponse, error)
 	LookUpListFollowers(ctx context.Context, listID string, opt ...*ListFollowersOption) (*ListFollowersResponse, error)
 	LookUpAllListsUserFollows(ctx context.Context, userID string, opt ...*ListFollowsOption) (*AllListsUserFollowsResponse, error)
 }
@@ -198,6 +199,10 @@ func (c *client) LookUpListTweets(ctx context.Context, listID string, opt ...*Li
 	return lookUpListTweets(ctx, c, listID, opt...)
 }
 
+func (c *client) ListsSpecifiedUser(ctx context.Context, userID string, opt ...*ListsSpecifiedUserOption) (*ListsSpecifiedUserResponse, error) {
+	return listsSpecifiedUser(ctx, c, userID, opt...)
+}
+
 func (c *client) ListMembers(ctx context.Context, listid string, opt ...*ListMembersOption) (*ListMembersResponse, error) {
 	return listMembers(ctx, c, listid, opt...)
 }
diff --git a/endpoint.go b/endpoint.go
index b88586b..56395b0 100644
--- a/endpoint.go
+++ b/endpoint.go
@@ -38,6 +38,7 @@ const (
 	lookUpListURL                = "https://api.twitter.com/2/lists/%v"
 	lookUpAllListsOwnedURL       = "https://api.twitter.com/2/users/%v/owned_lists"
 	lookUpListTweetsURL          = "https://api.twitter.com/2/lists/%v/tweets"
+	listsSpecifiedUserURL        = "https://api.twitter.com/2/users/%v/list_memberships"
 	listMembersURL               = "https://api.twitter.com/2/lists/%v/members"
 	lookUpListFollowersURL       = "https://api.twitter.com/2/lists/%v/followers"
 	lookUpAllListsUserFollowsURL = "https://api.twitter.com/2/users/%v/followed_lists"
diff --git a/example/list/listsusermember/main.go b/example/list/listsusermember/main.go
new file mode 100644
index 0000000..998a0ad
--- /dev/null
+++ b/example/list/listsusermember/main.go
@@ -0,0 +1,19 @@
+package main
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/sivchari/gotwtr"
+)
+
+func main() {
+	client := gotwtr.New("key")
+	lmr, err := client.ListsSpecifiedUser(context.Background(), "84839422")
+	if err != nil {
+		panic(err)
+	}
+	for _, lm := range lmr.Lists {
+		fmt.Println(lm)
+	}
+}
diff --git a/list.go b/list.go
index e08ce02..866c5aa 100644
--- a/list.go
+++ b/list.go
@@ -91,3 +91,13 @@ type ListMembersResponse struct {
 	Detail   string              `json:"detail,omitempty"`
 	Type     string              `json:"type,omitempty"`
 }
+
+type ListsSpecifiedUserResponse struct {
+	Lists    []*List             `json:"data"`
+	Includes *ListIncludes       `json:"includes,omitempty"`
+	Errors   []*APIResponseError `json:"errors,omitempty"`
+	Meta     *ListMeta           `json:"meta"`
+	Title    string              `json:"title,omitempty"`
+	Detail   string              `json:"detail,omitempty"`
+	Type     string              `json:"type,omitempty"`
+}
diff --git a/list_members.go b/list_members.go
index cb39f82..688d5a7 100644
--- a/list_members.go
+++ b/list_members.go
@@ -64,3 +64,61 @@ func listMembers(ctx context.Context, c *client, listid string, opt ...*ListMemb
 
 	return &lmr, nil
 }
+
+func listsSpecifiedUser(ctx context.Context, c *client, userID string, opt ...*ListsSpecifiedUserOption) (*ListsSpecifiedUserResponse, error) {
+	if userID == "" {
+		return nil, errors.New("lists specified user: userID parameter is required")
+	}
+
+	lm := fmt.Sprintf(listsSpecifiedUserURL, userID)
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, lm, nil)
+	if err != nil {
+		return nil, fmt.Errorf("lists specified user: %w", err)
+	}
+
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.bearerToken))
+
+	var lopt ListsSpecifiedUserOption
+	switch len(opt) {
+	case 0:
+		// do nothing
+	case 1:
+		lopt = *opt[0]
+	default:
+		return nil, errors.New("lists specified user: only one option is allowed")
+	}
+	const (
+		minimumMaxResults = 1
+		maximumMaxResults = 100
+		defaultMaxResults = 100
+	)
+	if lopt.MaxResults == 0 {
+		lopt.MaxResults = defaultMaxResults
+	}
+	if lopt.MaxResults < minimumMaxResults || lopt.MaxResults > maximumMaxResults {
+		return nil, fmt.Errorf("lists specified user: max results must be between %d and %d", minimumMaxResults, maximumMaxResults)
+	}
+	lopt.addQuery(req)
+
+	resp, err := c.client.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("lists specified user: %w", err)
+	}
+
+	defer resp.Body.Close()
+
+	var lmr ListsSpecifiedUserResponse
+	if err := json.NewDecoder(resp.Body).Decode(&lmr); err != nil {
+		return nil, fmt.Errorf("lists specified user: %w", err)
+	}
+	if resp.StatusCode != http.StatusOK {
+		return &lmr, &HTTPError{
+			APIName: "lists specified user",
+			Status:  resp.Status,
+			URL:     req.URL.String(),
+		}
+	}
+
+	return &lmr, nil
+}
diff --git a/list_option.go b/list_option.go
index 180d2f0..a703e7a 100644
--- a/list_option.go
+++ b/list_option.go
@@ -178,6 +178,36 @@ func (l ListFollowersOption) addQuery(req *http.Request) {
 	}
 }
 
+type ListsSpecifiedUserOption struct {
+	Expansions      []Expansion
+	ListFields      []ListField
+	MaxResults      int
+	PaginationToken string
+	UserFields      []UserField
+}
+
+func (l *ListsSpecifiedUserOption) addQuery(req *http.Request) {
+	q := req.URL.Query()
+	if len(l.Expansions) > 0 {
+		q.Add("expansions", strings.Join(expansionsToString(l.Expansions), ","))
+	}
+	if len(l.ListFields) > 0 {
+		q.Add("tweet.fields", strings.Join(listFieldsToString(l.ListFields), "."))
+	}
+	if l.MaxResults > 0 {
+		q.Add("max_results", strconv.Itoa(l.MaxResults))
+	}
+	if l.PaginationToken != "" {
+		q.Add("pagination_token", l.PaginationToken)
+	}
+	if len(l.UserFields) > 0 && len(l.Expansions) > 0 {
+		q.Add("user.fields", strings.Join(userFieldsToString(l.UserFields), ","))
+	}
+	if len(q) > 0 {
+		req.URL.RawQuery = q.Encode()
+	}
+}
+
 func listFieldsToString(lfs []ListField) []string {
 	slice := make([]string, len(lfs))
 	for i, lf := range lfs {


Test patch:
diff --git a/list_members_test.go b/list_members_test.go
index c5fb0c8..012c92d 100644
--- a/list_members_test.go
+++ b/list_members_test.go
@@ -2,6 +2,7 @@ package gotwtr_test
 
 import (
 	"context"
+	"fmt"
 	"io"
 	"net/http"
 	"strings"
@@ -125,3 +126,232 @@ func Test_listMembers(t *testing.T) {
 		})
 	}
 }
+
+func Test_ListSpecifiedUser(t *testing.T) {
+	t.Parallel()
+	type args struct {
+		ctx    context.Context
+		client *http.Client
+		id     string
+		opt    []*gotwtr.ListsSpecifiedUserOption
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *gotwtr.ListsSpecifiedUserResponse
+		wantErr bool
+	}{
+		{
+			name: "200 ok default payload",
+			args: args{
+				ctx: context.Background(),
+				client: mockHTTPClient(func(request *http.Request) *http.Response {
+					data := `{
+						"data": [
+							   {
+							     "id": "1451951974291689472",
+							     "name": "Twitter"
+							   },
+							   {
+							     "id": "1451812298184540161",
+							     "name": "Updates"
+							   },
+							   {
+							     "id": "1450519480132509697",
+							     "name": "Twitter"
+							   }
+                        ],
+					    "meta": {
+							"result_count": 3
+						}
+					}`
+					return &http.Response{
+						StatusCode: http.StatusOK,
+						Body:       io.NopCloser(strings.NewReader(data)),
+					}
+				}),
+				id:  "84839422",
+				opt: []*gotwtr.ListsSpecifiedUserOption{},
+			},
+			want: &gotwtr.ListsSpecifiedUserResponse{
+				Lists: []*gotwtr.List{
+					{
+						ID:   "1451951974291689472",
+						Name: "Twitter",
+					},
+					{
+						ID:   "1451812298184540161",
+						Name: "Updates",
+					},
+					{
+						ID:   "1450519480132509697",
+						Name: "Twitter",
+					},
+				},
+				Meta: &gotwtr.ListMeta{
+					ResultCount: 3,
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "200 ok option",
+			args: args{
+				ctx: context.Background(),
+				client: mockHTTPClient(func(request *http.Request) *http.Response {
+					data := `{
+						"data": [
+							{
+								"follower_count": 5,
+								"id": "1451951974291689472",
+								"name": "Twitter",
+								"owner_id": "1227213680120479745"
+							}
+						],
+						"includes": {
+							"users": [
+								{
+									"name": "구돆",
+									"created_at": "2020-02-11T12:52:11.000Z",
+									"id": "1227213680120479745",
+									"username": "Follow__Y0U"
+								}
+							]
+						},
+						"meta": {
+							"result_count": 1
+						}
+					}`
+					return &http.Response{
+						StatusCode: http.StatusOK,
+						Body:       io.NopCloser(strings.NewReader(data)),
+					}
+				}),
+				id:  "84839422",
+				opt: []*gotwtr.ListsSpecifiedUserOption{},
+			},
+			want: &gotwtr.ListsSpecifiedUserResponse{
+				Lists: []*gotwtr.List{
+					{
+						ID:            "1451951974291689472",
+						Name:          "Twitter",
+						FollowerCount: 5,
+						OwnerID:       "1227213680120479745",
+					},
+				},
+				Includes: &gotwtr.ListIncludes{
+					Users: []*gotwtr.User{
+						{
+							Name:      "구돆",
+							CreatedAt: "2020-02-11T12:52:11.000Z",
+							ID:        "1227213680120479745",
+							UserName:  "Follow__Y0U",
+						},
+					},
+				},
+				Meta: &gotwtr.ListMeta{
+					ResultCount: 1,
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "User Not Found",
+			args: args{
+				ctx: context.Background(),
+				client: mockHTTPClient(func(request *http.Request) *http.Response {
+					data := `{
+						"errors":[
+							{
+								"value":"849422",
+								"detail":"Could not find user with id: [849422].",
+								"title":"Not Found Error",
+								"resource_type":"user",
+								"parameter":"id",
+								"resource_id":"849422",
+								"type":"https://api.twitter.com/2/problems/resource-not-found"
+							}
+						]
+					}`
+					return &http.Response{
+						StatusCode: http.StatusNotFound,
+						Body:       io.NopCloser(strings.NewReader(data)),
+					}
+				}),
+				id:  "849422",
+				opt: []*gotwtr.ListsSpecifiedUserOption{},
+			},
+			want: &gotwtr.ListsSpecifiedUserResponse{
+				Errors: []*gotwtr.APIResponseError{
+					{
+						Value:        "849422",
+						Detail:       "Could not find user with id: [849422].",
+						Title:        "Not Found Error",
+						ResourceType: "user",
+						Parameter:    "id",
+						ResourceID:   "849422",
+						Type:         "https://api.twitter.com/2/problems/resource-not-found",
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "Invalid Request",
+			args: args{
+				ctx: context.Background(),
+				client: mockHTTPClient(func(request *http.Request) *http.Response {
+					data := fmt.Sprintf(`{
+						"errors":[
+							{
+								"parameters":{
+									"id": ["8488877666666666666666666666666622839422"]
+								},
+								"message":"The %v query parameter value [8488877666666666666666666666666622839422] is not valid"
+							}
+						],
+						"title":"Invalid Request",
+						"detail":"One or more parameters to your request was invalid.",
+						"type":"https://api.twitter.com/2/problems/invalid-request"
+					}`, "`id`")
+					return &http.Response{
+						StatusCode: http.StatusBadRequest,
+						Body:       io.NopCloser(strings.NewReader(data)),
+					}
+				}),
+				id:  "8488877666666666666666666666666622839422",
+				opt: []*gotwtr.ListsSpecifiedUserOption{},
+			},
+			want: &gotwtr.ListsSpecifiedUserResponse{
+				Errors: []*gotwtr.APIResponseError{
+					{
+						Parameters: gotwtr.Parameter{
+							ID: []string{"8488877666666666666666666666666622839422"},
+						},
+						Message: "The `id` query parameter value [8488877666666666666666666666666622839422] is not valid",
+					},
+				},
+				Title:  "Invalid Request",
+				Detail: "One or more parameters to your request was invalid.",
+				Type:   "https://api.twitter.com/2/problems/invalid-request",
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			c := gotwtr.New("test-key", gotwtr.WithHTTPClient(tt.args.client))
+			got, err := c.ListsSpecifiedUser(tt.args.ctx, tt.args.id, tt.args.opt...)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("client.ListSpecifiedUser() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if diff := cmp.Diff(tt.want, got); diff != "" {
+				t.Errorf("client.ListSpecifiedUser() mismatch (-want +got):\n%s", diff)
+				return
+			}
+		})
+	}
+}