Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Accounts Service #3

Merged
merged 5 commits into from
Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/oschwald/go-pivotaltracker

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Run go mod tidy. There should be a Go version number.

go 1.20
41 changes: 41 additions & 0 deletions v5/pivotal/account.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package pivotal

import (
"fmt"
"net/http"
"time"
)

type Account struct {
CreatedAt *time.Time `json:"created_at"`
ID int `json:"id"`
Kind string `json:"kind"`
Name string `json:"name"`
Plan string `json:"plan"`
Status string `json:"status"`
UpdatedAt *time.Time `json:"updated_at"`
}

type AccountsService struct {
client *Client
}

func newAccountsService(client *Client) *AccountsService {
return &AccountsService{client}
}

func (service *AccountsService) Get(accountID int) (*Account, *http.Response, error) {
u := fmt.Sprintf("accounts/%d", accountID)
req, err := service.client.NewRequest("GET", u, nil)
if err != nil {
return nil, nil, err
}

var account Account
resp, err := service.client.Do(req, &account)
if err != nil {
return nil, resp, err
}

return &account, resp, nil
}
4 changes: 4 additions & 0 deletions v5/pivotal/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ type Client struct {
// User-Agent header to use when connecting to the Pivotal Tracker API.
userAgent string

// Accounts service
Accounts *AccountsService

// Me service
Me *MeService

Expand Down Expand Up @@ -72,6 +75,7 @@ func NewClient(apiToken string) *Client {
baseURL: baseURL,
userAgent: defaultUserAgent,
}
client.Accounts = newAccountsService(client)
client.Me = newMeService(client)
client.Projects = newProjectService(client)
client.Stories = newStoryService(client)
Expand Down
45 changes: 32 additions & 13 deletions v5/pivotal/me.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,40 @@ import (
"time"
)

// MeProject represents the fields of the projects returned by the MeService.
// These are the only fields returned by the MeService unlike
// the Project service.
type MeProject struct {
Kind string `json:"kind"`
ID int `json:"id"`
ProjectID int `json:"project_id"`
ProjectName string `json:"project_name"`
ProjectColor string `json:"project_color"`
Favorite bool `json:"favorite"`
Role string `json:"owner"`
LastViewedAt *time.Time `json:"last_viewed_at"`
}

// Me is the primary data object for the MeService.
type Me struct {
ID int `json:"id"`
Name string `json:"name"`
Initials string `json:"initials"`
Username string `json:"username"`
TimeZone *TimeZone `json:"time_zone"`
APIToken string `json:"api_token"`
HasGoogleIdentity bool `json:"has_google_identity"`
ProjectIDs *[]int `json:"project_ids"`
WorkspaceIDs *[]int `json:"workspace_ids"`
Email string `json:"email"`
ReceivedInAppNotifications bool `json:"receives_in_app_notifications"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
ID int `json:"id"`
Name string `json:"name"`
Initials string `json:"initials"`
Username string `json:"username"`
TimeZone *TimeZone `json:"time_zone"`
APIToken string `json:"api_token"`
HasGoogleIdentity bool `json:"has_google_identity"`

// TODO: The ProjectIDs field needs to be requested explicitly using
// the fields query parameter. It is never populated unlike Projects,
// which is populated by default.
ProjectIDs *[]int `json:"project_ids"`
Projects *[]MeProject `json:"projects"`
WorkspaceIDs *[]int `json:"workspace_ids"`
Email string `json:"email"`
ReceivedInAppNotifications bool `json:"receives_in_app_notifications"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
}

// MeService wraps the client context for interacting with the Me logic.
Expand Down
56 changes: 53 additions & 3 deletions v5/pivotal/stories.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"io"
"net/http"
"net/url"
"strings"
"time"
)

Expand Down Expand Up @@ -59,6 +60,7 @@ type Story struct {
AcceptedAt *time.Time `json:"accepted_at,omitempty"`
Deadline *time.Time `json:"deadline,omitempty"`
RequestedByID int `json:"requested_by_id,omitempty"`
RequestedBy Person `json:"requested_by,omitempty"`
OwnerIDs []int `json:"owner_ids,omitempty"`
LabelIDs []int `json:"label_ids,omitempty"`
Labels []*Label `json:"labels,omitempty"`
Expand Down Expand Up @@ -198,7 +200,7 @@ func newStoryService(client *Client) *StoryService {
// is not always sorted when using a filter, this approach is required to get
// the right data. Not sure whether this is a bug or a feature.
func (service *StoryService) List(projectID int, filter string) ([]*Story, error) {
reqFunc := newStoriesRequestFunc(service.client, projectID, filter)
reqFunc := newStoriesRequestFunc(service.client, projectID, filter, nil)
cursor, err := newCursor(service.client, reqFunc, 0)
if err != nil {
return nil, err
Expand All @@ -211,17 +213,65 @@ func (service *StoryService) List(projectID int, filter string) ([]*Story, error
return stories, nil
}

func newStoriesRequestFunc(client *Client, projectID int, filter string) func() *http.Request {
func newStoriesRequestFunc(client *Client, projectID int, filter string, fields []string) func() *http.Request {
return func() *http.Request {
u := fmt.Sprintf("projects/%v/stories", projectID)
if filter != "" {
u += "?filter=" + url.QueryEscape(filter)
if len(fields) != 0 {
u += "&fields=" + url.QueryEscape(fieldsToQuery(fields))
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems redundant given that fieldsToQuery also sets the default fields.

I do wonder if not including the param would be less surprising if there are no fields passed, but I am fine with it as is given that we are only using this internally.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree. Thank you, I have removed it. 👍

}
req, _ := client.NewRequest("GET", u, nil)
return req
}
}

var DefaultFields = []string{
"id",
"project_id",
"name",
"description",
"story_type",
"current_state",
"estimate",
"accepted_at",
"deadline",
"requested_by_id",
"owner_ids",
"task_ids",
"follower_ids",
"created_at",
"updated_at",
"url",
"kind",
}

// function returns the fields in a query string format.
func fieldsToQuery(fields []string) string {
if len(fields) == 0 {
fields = DefaultFields
}
return strings.Join(fields, ",")
}

// ListOnlyFields returns all the fields of the stories matching the filter given.
// Example: fields = []string{"id", "name", "description"}
// Having nil or empty fields will return all the fields.
func (service *StoryService) ListWithFields(projectID int, filter string, fields []string) ([]*Story, error) {
reqFunc := newStoriesRequestFunc(service.client, projectID, filter, fields)
cursor, err := newCursor(service.client, reqFunc, 0)
if err != nil {
return nil, err
}

var stories []*Story
if err := cursor.all(&stories); err != nil {
return nil, err
}
return stories, nil
}

// StoryCursor is used to implement the iterator pattern.
type StoryCursor struct {
*cursor
Expand Down Expand Up @@ -250,7 +300,7 @@ func (c *StoryCursor) Next() (s *Story, err error) {
// Iterate returns a cursor that can be used to iterate over the stories specified
// by the filter. More stories are fetched on demand as needed.
func (service *StoryService) Iterate(projectID int, filter string) (c *StoryCursor, err error) {
reqFunc := newStoriesRequestFunc(service.client, projectID, filter)
reqFunc := newStoriesRequestFunc(service.client, projectID, filter, nil)
cursor, err := newCursor(service.client, reqFunc, PageLimit)
if err != nil {
return nil, err
Expand Down