-
Notifications
You must be signed in to change notification settings - Fork 2
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
Add aggregator #2
Changes from 8 commits
675cf68
43e4a20
e7c84a5
94049bf
77d2226
d320018
5e1de1c
353dbd8
1eca196
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
package pivotal | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"math" | ||
) | ||
|
||
const aggregatorURL = "https://www.pivotaltracker.com/services/v5/aggregator" | ||
|
||
const requestsPerAggregation = 15 // Number of requests per aggregation. | ||
|
||
// AggregatorService is used to wrap the client. | ||
type AggregatorService struct { | ||
client *Client | ||
} | ||
|
||
// Aggregation is the data object for an aggregation. | ||
// It is used for storing the urls of the requests and the response. | ||
type Aggregation struct { | ||
// Requests are the GET requests that are used by the aggregator. | ||
requests []string | ||
|
||
// Storing the response json along with the urls. | ||
aggregatedResponse map[string]interface{} | ||
|
||
// The service of the aggregation | ||
service *AggregatorService | ||
} | ||
|
||
// GetBuilder returns a builder for building an aggregation. | ||
func (a *AggregatorService) GetBuilder() *Aggregation { | ||
aggregation := Aggregation{ | ||
aggregatedResponse: make(map[string]interface{}), | ||
service: a, | ||
} | ||
return &aggregation | ||
} | ||
|
||
func newAggregatorService(client *Client) *AggregatorService { | ||
return &AggregatorService{client} | ||
} | ||
|
||
// Story adds a story request to the aggregation. | ||
func (a *Aggregation) Story(projectID, storyID int) *Aggregation { | ||
a.requests = append(a.requests, buildStoryURL(projectID, storyID)) | ||
return a | ||
} | ||
|
||
// StoryUsingStoryID adds a story request using only using the story ID. | ||
func (a *Aggregation) StoryUsingStoryID(storyID int) *Aggregation { | ||
a.requests = append(a.requests, buildStoryURLOnlyUsingStoryID(storyID)) | ||
return a | ||
} | ||
|
||
// Stories adds a list of story requests to an aggregation. | ||
func (a *Aggregation) Stories(projectID int, storyIDs []int) *Aggregation { | ||
for _, storyID := range storyIDs { | ||
a.Story(projectID, storyID) | ||
} | ||
return a | ||
} | ||
|
||
// CommentsOfStory adds a request for getting the comments of a story. | ||
func (a *Aggregation) CommentsOfStory(projectID, storyID int) { | ||
a.requests = append(a.requests, buildCommentsURL(projectID, storyID)) | ||
} | ||
|
||
// CommentsOfStories adds multiple requests for getting the comments of a list of stories. | ||
func (a *Aggregation) CommentsOfStories(projectID int, storyIDs []int) *Aggregation { | ||
for _, storyID := range storyIDs { | ||
a.CommentsOfStory(projectID, storyID) | ||
} | ||
return a | ||
} | ||
|
||
// ReviewsOfStory adds multiple requests for getting the reviews of a story. | ||
func (a *Aggregation) ReviewsOfStory(projectID, storyID int) *Aggregation { | ||
a.requests = append(a.requests, buildReviewsURL(projectID, storyID)) | ||
return a | ||
} | ||
|
||
// ReviewsOfStories adds multiple requests for getting the reviews of multiple stories. | ||
func (a *Aggregation) ReviewsOfStories(projectID int, storyIDs []int) *Aggregation { | ||
for _, storyID := range storyIDs { | ||
a.ReviewsOfStory(projectID, storyID) | ||
} | ||
return a | ||
} | ||
|
||
func buildStoryURLOnlyUsingStoryID(storyID int) string { | ||
return fmt.Sprintf("/services/v5/stories/%d", storyID) | ||
} | ||
|
||
func buildStoryURL(projectID, storyID int) string { | ||
return fmt.Sprintf("/services/v5/projects/%d/stories/%d", projectID, storyID) | ||
} | ||
|
||
func buildCommentsURL(projectID, storyID int) string { | ||
return fmt.Sprintf("/services/v5/projects/%d/stories/%d/comments", projectID, storyID) | ||
} | ||
|
||
func buildReviewsURL(projectID, storyID int) string { | ||
return fmt.Sprintf("/services/v5/projects/%d/stories/%d/reviews?fields=id,story_id,review_type,review_type_id,reviewer_id,status,created_at,updated_at,kind", projectID, storyID) | ||
} | ||
|
||
func maxPagesPagination(total int, perPage int) int { | ||
pagesNumber := math.Ceil(float64(total) / float64(perPage)) | ||
return int(pagesNumber) | ||
} | ||
|
||
func paginate(reqs []string, currentPage, total int, perPage int) []string { | ||
firstEntry := (currentPage - 1) * perPage | ||
lastEntry := firstEntry + perPage | ||
|
||
if lastEntry > total { | ||
lastEntry = total | ||
} | ||
|
||
return reqs[firstEntry:lastEntry] | ||
} | ||
|
||
// Send completes the aggregation and sends it to Pivotal Tracker. | ||
func (a *Aggregation) Send() (*Aggregation, error) { | ||
N := len(a.requests) | ||
max := maxPagesPagination(N, requestsPerAggregation) | ||
for currentPage := 1; currentPage <= max; currentPage++ { | ||
currentReqs := paginate(a.requests, currentPage, N, requestsPerAggregation) | ||
|
||
aggregatedResponse := make(map[string]interface{}) | ||
|
||
req, err := a.service.client.NewRequest("POST", aggregatorURL, currentReqs) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
_, err = a.service.client.Do(req, &aggregatedResponse) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Appending the response body into the current aggregation for getters. | ||
for url, response := range aggregatedResponse { | ||
a.aggregatedResponse[url] = response | ||
} | ||
} | ||
|
||
return a, nil | ||
} | ||
|
||
// GetStoryOnlyUsingStoryID returns the story using only the story ID. | ||
func (a *Aggregation) GetStoryOnlyUsingStoryID(storyID int) (*Story, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
u := buildStoryURLOnlyUsingStoryID(storyID) | ||
response, ok := a.aggregatedResponse[u] | ||
if !ok { | ||
return nil, fmt.Errorf("Story %d doesn't exist.", storyID) | ||
} | ||
byteData, _ := json.Marshal(response) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should return an error if this fails. Same with the other I wonder if it would make sense to wrap this in a helper function, e.g.:
This should reduce the repeated code and in the future might make it easier to replace this with something more efficient, e.g., https://github.com/mitchellh/mapstructure. |
||
// Handling get story requests if it isn't comments/reviews. | ||
var story Story | ||
err := json.Unmarshal(byteData, &story) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &story, nil | ||
} | ||
|
||
// GetStory returns the story using both the project ID and the story ID. | ||
func (a *Aggregation) GetStory(projectID, storyID int) (*Story, error) { | ||
u := buildStoryURL(projectID, storyID) | ||
response, ok := a.aggregatedResponse[u] | ||
if !ok { | ||
return nil, fmt.Errorf("Story %d doesn't exist for project %d.", storyID, projectID) | ||
} | ||
byteData, _ := json.Marshal(response) | ||
|
||
// Handling get story requests if it isn't comments/reviews. | ||
var story Story | ||
err := json.Unmarshal(byteData, &story) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &story, nil | ||
} | ||
|
||
// GetComments returns the comments of a story. | ||
func (a *Aggregation) GetComments(projectID, storyID int) ([]Comment, error) { | ||
u := buildCommentsURL(projectID, storyID) | ||
response, ok := a.aggregatedResponse[u] | ||
if !ok { | ||
return nil, fmt.Errorf("Story %d comments don't exist for project %d.", storyID, projectID) | ||
} | ||
byteData, _ := json.Marshal(response) | ||
|
||
var comments []Comment | ||
err := json.Unmarshal(byteData, &comments) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return comments, nil | ||
} | ||
|
||
// GetReviews returns the reviews of a story. | ||
func (a *Aggregation) GetReviews(projectID, storyID int) ([]Review, error) { | ||
u := buildReviewsURL(projectID, storyID) | ||
response, ok := a.aggregatedResponse[u] | ||
if !ok { | ||
return nil, fmt.Errorf("Story %d reviews don't exist for project %d.", storyID, projectID) | ||
} | ||
byteData, _ := json.Marshal(response) | ||
var reviews []Review | ||
err := json.Unmarshal(byteData, &reviews) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return reviews, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think my previous comments were lost on a rebase or something, but this should return
*http.Response
as the second return value to match the rest of the API in this module.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, ok. Maybe just ignore this comment then. We don't need it ourselves. If upstream ever shows a willingness to merge this, we can deal with the concern then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I send bulks of 15 requests in
Send
. So, there are multiplehttp.Responses
.In my next commit, I've changed
Send
to only send the next bulk in the aggregation and return the correspondinghttp.Response
. And then I've added anotherSendAll
method, which sends all the bulks in one go.SendAll
returns thehttp.Response
s in a slice. Do you think that is fine?