diff --git a/client/client.go b/client/client.go index 25cc336..819c6d7 100644 --- a/client/client.go +++ b/client/client.go @@ -14,16 +14,17 @@ type Client struct { client *http.Client } -type workspace struct { - GID string `json:"gid"` - Name string `json:"name"` +type errorDetails struct { + Message string `json:"message"` } -type workspacesResponse struct { - Data []*workspace `json:"data"` +type errorResponse struct { + Errors []*errorDetails `json:"errors"` } -const perPage = 100 +type emptyResponse struct { + Data interface{} `json:"data"` +} func NewClient(token string) *Client { c := &Client{ @@ -41,43 +42,8 @@ func NewClientFromEnv() *Client { return NewClient(os.Getenv("ASANA_TOKEN")) } -func (c *Client) InWorkspace(name string) (*WorkspaceClient, error) { - wrk, err := c.getWorkspaceByName(name) - if err != nil { - return nil, err - } - - return &WorkspaceClient{ - client: c, - workspace: wrk, - }, nil -} - -func (c *Client) getWorkspaces() ([]*workspace, error) { - resp := &workspacesResponse{} - err := c.get("workspaces", nil, resp) - if err != nil { - return nil, err - } - return resp.Data, nil -} - -func (c *Client) getWorkspaceByName(name string) (*workspace, error) { - wrks, err := c.getWorkspaces() - if err != nil { - return nil, err - } - - for _, wrk := range wrks { - if wrk.Name == name { - return wrk, nil - } - } - - return nil, fmt.Errorf("Workspace `%s` not found", name) -} - const baseURL = "https://app.asana.com/api/1.0/" +const perPage = 100 func (c *Client) get(path string, values *url.Values, out interface{}) error { if values == nil { @@ -164,7 +130,3 @@ func (c *Client) doWithBody(method string, path string, body interface{}, out in return nil } - -func (wrk *workspace) String() string { - return fmt.Sprintf("%s (%s)", wrk.GID, wrk.Name) -} diff --git a/client/project.go b/client/project.go new file mode 100644 index 0000000..78213f9 --- /dev/null +++ b/client/project.go @@ -0,0 +1,30 @@ +package client + +import "fmt" + +type Project struct { + GID string `json:"gid"` + Name string `json:"name"` +} + +type projectResponse struct { + Data *Project `json:"data"` +} + +type projectsResponse struct { + Data []*Project `json:"data"` +} + +func (wc *WorkspaceClient) GetProjects() ([]*Project, error) { + path := fmt.Sprintf("workspaces/%s/projects", wc.workspace.GID) + resp := &projectsResponse{} + err := wc.client.get(path, nil, resp) + if err != nil { + return nil, err + } + return resp.Data, nil +} + +func (p *Project) String() string { + return fmt.Sprintf("%s (%s)", p.GID, p.Name) +} diff --git a/client/search.go b/client/search.go new file mode 100644 index 0000000..0fb069e --- /dev/null +++ b/client/search.go @@ -0,0 +1,119 @@ +package client + +import "fmt" +import "net/url" +import "strings" + +import "cloud.google.com/go/civil" + +type SearchQuery struct { + SectionsAny []*Section + Completed *bool + Due *bool + DueOn *civil.Date + DueBefore *civil.Date + DueAfter *civil.Date + TagsAny []*Tag + TagsNot []*Tag +} + +var _TRUE = true +var TRUE = &_TRUE +var _FALSE = false +var FALSE = &_FALSE + +func (wc *WorkspaceClient) Search(q *SearchQuery) ([]*Task, error) { + path := fmt.Sprintf("workspaces/%s/tasks/search", wc.workspace.GID) + + values := &url.Values{ + "sort_by": []string{"created_at"}, + "sort_ascending": []string{"true"}, + } + + values.Add("opt_fields", "created_at,due_on,html_notes,name") + + if len(q.SectionsAny) > 0 { + gids := []string{} + for _, sec := range q.SectionsAny { + gids = append(gids, sec.GID) + } + values.Add("sections.any", strings.Join(gids, ",")) + } + + if q.Completed != nil { + values.Add("completed", fmt.Sprintf("%t", *q.Completed)) + } + + if q.Due != nil { + if *q.Due { + values.Add("due_on.after", "1970-01-01") + } else { + values.Add("due_on", "null") + } + } + + if q.DueOn != nil { + values.Add("due_on", q.DueOn.String()) + } + + if q.DueBefore != nil { + values.Add("due_on.before", q.DueBefore.String()) + } + + if q.DueAfter != nil { + values.Add("due_on.after", q.DueAfter.String()) + } + + if len(q.TagsAny) > 0 { + gids := []string{} + for _, sec := range q.TagsAny { + gids = append(gids, sec.GID) + } + values.Add("tags.any", strings.Join(gids, ",")) + } + + if len(q.TagsNot) > 0 { + gids := []string{} + for _, sec := range q.TagsNot { + gids = append(gids, sec.GID) + } + values.Add("tags.not", strings.Join(gids, ",")) + } + + tasksByGID := map[string]*Task{} + + for { + resp := &tasksResponse{} + err := wc.client.get(path, values, resp) + if err != nil { + return nil, err + } + + maxCreatedAt := "" + + for _, task := range resp.Data { + err := task.parse() + if err != nil { + return nil, err + } + tasksByGID[task.GID] = task + + if task.CreatedAt > maxCreatedAt { + maxCreatedAt = task.CreatedAt + } + } + + if len(resp.Data) < perPage { + break + } + + values.Set("created_at.after", maxCreatedAt) + } + + tasks := []*Task{} + for _, task := range tasksByGID { + tasks = append(tasks, task) + } + + return tasks, nil +} diff --git a/client/section.go b/client/section.go new file mode 100644 index 0000000..a3c0e5a --- /dev/null +++ b/client/section.go @@ -0,0 +1,90 @@ +package client + +import "fmt" + +type Section struct { + GID string `json:"gid"` + Name string `json:"name"` +} + +type sectionsResponse struct { + Data []*Section `json:"data"` +} + +type sectionAddTaskData struct { + Task string `json:"task"` +} + +type sectionAddTaskRequest struct { + Data *sectionAddTaskData `json:"data"` +} + +func (wc *WorkspaceClient) GetSections(project *Project) ([]*Section, error) { + path := fmt.Sprintf("projects/%s/sections", project.GID) + resp := §ionsResponse{} + err := wc.client.get(path, nil, resp) + if err != nil { + return nil, err + } + return resp.Data, nil +} + +func (wc *WorkspaceClient) GetSectionsByName(project *Project) (map[string]*Section, error) { + secs, err := wc.GetSections(project) + if err != nil { + return nil, err + } + + secsByName := map[string]*Section{} + for _, sec := range secs { + secsByName[sec.Name] = sec + } + + return secsByName, err +} + +func (wc *WorkspaceClient) GetSectionByName(project *Project, name string) (*Section, error) { + secsByName, err := wc.GetSectionsByName(project) + if err != nil { + return nil, err + } + + sec, found := secsByName[name] + if !found { + return nil, fmt.Errorf("Section '%s' not found", name) + } + + return sec, nil +} + +func (wc *WorkspaceClient) AddTaskToSection(task *Task, section *Section) error { + req := §ionAddTaskRequest{ + Data: §ionAddTaskData{ + Task: task.GID, + }, + } + + resp := &emptyResponse{} + + path := fmt.Sprintf("sections/%s/addTask", section.GID) + err := wc.client.post(path, req, resp) + if err != nil { + return err + } + + return nil +} + +func (wc *WorkspaceClient) GetTasksFromSection(section *Section) ([]*Task, error) { + path := fmt.Sprintf("sections/%s/tasks", section.GID) + resp := &tasksResponse{} + err := wc.client.get(path, nil, resp) + if err != nil { + return nil, err + } + return resp.Data, nil +} + +func (s *Section) String() string { + return fmt.Sprintf("%s (%s)", s.GID, s.Name) +} diff --git a/client/tag.go b/client/tag.go new file mode 100644 index 0000000..b7f1e05 --- /dev/null +++ b/client/tag.go @@ -0,0 +1,36 @@ +package client + +import "fmt" + +type Tag struct { + GID string `json:"gid"` + Name string `json:"name"` +} + +type tagsResponse struct { + Data []*Tag `json:"data"` +} + +func (wc *WorkspaceClient) GetTags() ([]*Tag, error) { + path := fmt.Sprintf("workspaces/%s/tags", wc.workspace.GID) + resp := &tagsResponse{} + err := wc.client.get(path, nil, resp) + if err != nil { + return nil, err + } + return resp.Data, nil +} + +func (wc *WorkspaceClient) GetTagsByName() (map[string]*Tag, error) { + tags, err := wc.GetTags() + if err != nil { + return nil, err + } + + tagsByName := map[string]*Tag{} + for _, tag := range tags { + tagsByName[tag.Name] = tag + } + + return tagsByName, err +} diff --git a/client/task.go b/client/task.go new file mode 100644 index 0000000..f4dd5ca --- /dev/null +++ b/client/task.go @@ -0,0 +1,70 @@ +package client + +import "fmt" +import "strings" + +import "cloud.google.com/go/civil" +import "golang.org/x/net/html" + +type Task struct { + GID string `json:"gid,omitempty"` + Name string `json:"name,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + DueOn string `json:"due_on,omitempty"` + ParsedDueOn *civil.Date `json:"-"` + HTMLNotes string `json:"html_notes,omitempty"` + ParsedHTMLNotes *html.Node `json:"-"` +} + +type taskResponse struct { + Data *Task `json:"data"` +} + +type tasksResponse struct { + Data []*Task `json:"data"` +} + +type taskUpdate struct { + Data *Task `json:"data"` +} + +func (wc *WorkspaceClient) UpdateTask(task *Task) error { + path := fmt.Sprintf("tasks/%s", task.GID) + + task.GID = "" + + update := &taskUpdate{ + Data: task, + } + + resp := &taskResponse{} + err := wc.client.put(path, update, resp) + if err != nil { + return err + } + + return nil +} + +func (t *Task) String() string { + return fmt.Sprintf("%s (%s)", t.GID, t.Name) +} + +func (t *Task) parse() error { + r := strings.NewReader(t.HTMLNotes) + root, err := html.Parse(r) + if err != nil { + return err + } + t.ParsedHTMLNotes = root + + if t.DueOn != "" { + d, err := civil.ParseDate(t.DueOn) + if err != nil { + return err + } + t.ParsedDueOn = &d + } + + return nil +} diff --git a/client/user.go b/client/user.go new file mode 100644 index 0000000..6457b56 --- /dev/null +++ b/client/user.go @@ -0,0 +1,26 @@ +package client + +import "fmt" + +type User struct { + GID string `json:"gid"` + Name string `json:"name"` + Email string `json:"email"` +} + +type userResponse struct { + Data *User `json:"data"` +} + +func (wc *WorkspaceClient) GetMe() (*User, error) { + resp := &userResponse{} + err := wc.client.get("users/me", nil, resp) + if err != nil { + return nil, err + } + return resp.Data, nil +} + +func (u *User) String() string { + return fmt.Sprintf("%s (%s <%s>)", u.GID, u.Name, u.Email) +} diff --git a/client/usertasklist.go b/client/usertasklist.go new file mode 100644 index 0000000..736469b --- /dev/null +++ b/client/usertasklist.go @@ -0,0 +1,27 @@ +package client + +import "fmt" +import "net/url" + +// UserTaskLists are actually Projects + +func (wc *WorkspaceClient) GetUserTaskList(user *User) (*Project, error) { + path := fmt.Sprintf("users/%s/user_task_list", user.GID) + values := &url.Values{} + values.Add("workspace", wc.workspace.GID) + resp := &projectResponse{} + err := wc.client.get(path, values, resp) + if err != nil { + return nil, err + } + return resp.Data, nil +} + +func (wc *WorkspaceClient) GetMyUserTaskList() (*Project, error) { + me, err := wc.GetMe() + if err != nil { + return nil, err + } + + return wc.GetUserTaskList(me) +} diff --git a/client/workspace.go b/client/workspace.go new file mode 100644 index 0000000..0989d58 --- /dev/null +++ b/client/workspace.go @@ -0,0 +1,52 @@ +package client + +import "fmt" + +type Workspace struct { + GID string `json:"gid"` + Name string `json:"name"` +} + +type workspacesResponse struct { + Data []*Workspace `json:"data"` +} + +func (c *Client) InWorkspace(name string) (*WorkspaceClient, error) { + wrk, err := c.GetWorkspaceByName(name) + if err != nil { + return nil, err + } + + return &WorkspaceClient{ + client: c, + workspace: wrk, + }, nil +} + +func (c *Client) GetWorkspaces() ([]*Workspace, error) { + resp := &workspacesResponse{} + err := c.get("workspaces", nil, resp) + if err != nil { + return nil, err + } + return resp.Data, nil +} + +func (c *Client) GetWorkspaceByName(name string) (*Workspace, error) { + wrks, err := c.GetWorkspaces() + if err != nil { + return nil, err + } + + for _, wrk := range wrks { + if wrk.Name == name { + return wrk, nil + } + } + + return nil, fmt.Errorf("Workspace `%s` not found", name) +} + +func (wrk *Workspace) String() string { + return fmt.Sprintf("%s (%s)", wrk.GID, wrk.Name) +} diff --git a/client/workspaceclient.go b/client/workspaceclient.go index 5c4ce56..29138ed 100644 --- a/client/workspaceclient.go +++ b/client/workspaceclient.go @@ -1,391 +1,6 @@ package client -import "fmt" -import "net/url" -import "strings" - -import "cloud.google.com/go/civil" -import "golang.org/x/net/html" - -var _TRUE = true -var TRUE = &_TRUE -var _FALSE = false -var FALSE = &_FALSE - type WorkspaceClient struct { client *Client - workspace *workspace -} - -type SearchQuery struct { - SectionsAny []*Section - Completed *bool - Due *bool - DueOn *civil.Date - DueBefore *civil.Date - DueAfter *civil.Date - TagsAny []*Tag - TagsNot []*Tag -} - -type Project struct { - GID string `json:"gid"` - Name string `json:"name"` -} - -type Section struct { - GID string `json:"gid"` - Name string `json:"name"` -} - -type Tag struct { - GID string `json:"gid"` - Name string `json:"name"` -} - -type Task struct { - GID string `json:"gid,omitempty"` - Name string `json:"name,omitempty"` - CreatedAt string `json:"created_at,omitempty"` - DueOn string `json:"due_on,omitempty"` - ParsedDueOn *civil.Date `json:"-"` - HTMLNotes string `json:"html_notes,omitempty"` - ParsedHTMLNotes *html.Node `json:"-"` -} - -type User struct { - GID string `json:"gid"` - Name string `json:"name"` - Email string `json:"email"` -} - -type addTaskDetails struct { - Task string `json:"task"` -} - -type addTaskRequest struct { - Data *addTaskDetails `json:"data"` -} - -type emptyResponse struct { - Data interface{} `json:"data"` -} - -type errorDetails struct { - Message string `json:"message"` -} - -type errorResponse struct { - Errors []*errorDetails `json:"errors"` -} - -type projectResponse struct { - Data *Project `json:"data"` -} - -type projectsResponse struct { - Data []*Project `json:"data"` -} - -type sectionsResponse struct { - Data []*Section `json:"data"` -} - -type tagsResponse struct { - Data []*Tag `json:"data"` -} - -type taskResponse struct { - Data *Task `json:"data"` -} - -type tasksResponse struct { - Data []*Task `json:"data"` -} - -type userResponse struct { - Data *User `json:"data"` -} - -type taskUpdate struct { - Data *Task `json:"data"` -} - -func (wc *WorkspaceClient) GetMe() (*User, error) { - resp := &userResponse{} - err := wc.client.get("users/me", nil, resp) - if err != nil { - return nil, err - } - return resp.Data, nil -} - -func (wc *WorkspaceClient) AddTaskToSection(task *Task, section *Section) error { - req := &addTaskRequest{ - Data: &addTaskDetails{ - Task: task.GID, - }, - } - - resp := &emptyResponse{} - - path := fmt.Sprintf("sections/%s/addTask", section.GID) - err := wc.client.post(path, req, resp) - if err != nil { - return err - } - - return nil -} - -func (wc *WorkspaceClient) GetProjects() ([]*Project, error) { - path := fmt.Sprintf("workspaces/%s/projects", wc.workspace.GID) - resp := &projectsResponse{} - err := wc.client.get(path, nil, resp) - if err != nil { - return nil, err - } - return resp.Data, nil -} - -func (wc *WorkspaceClient) GetSections(project *Project) ([]*Section, error) { - path := fmt.Sprintf("projects/%s/sections", project.GID) - resp := §ionsResponse{} - err := wc.client.get(path, nil, resp) - if err != nil { - return nil, err - } - return resp.Data, nil -} - -func (wc *WorkspaceClient) GetSectionsByName(project *Project) (map[string]*Section, error) { - secs, err := wc.GetSections(project) - if err != nil { - return nil, err - } - - secsByName := map[string]*Section{} - for _, sec := range secs { - secsByName[sec.Name] = sec - } - - return secsByName, err -} - -func (wc *WorkspaceClient) GetSectionByName(project *Project, name string) (*Section, error) { - secsByName, err := wc.GetSectionsByName(project) - if err != nil { - return nil, err - } - - sec, found := secsByName[name] - if !found { - return nil, fmt.Errorf("Section '%s' not found", name) - } - - return sec, nil -} - -func (wc *WorkspaceClient) GetTags() ([]*Tag, error) { - path := fmt.Sprintf("workspaces/%s/tags", wc.workspace.GID) - resp := &tagsResponse{} - err := wc.client.get(path, nil, resp) - if err != nil { - return nil, err - } - return resp.Data, nil -} - -func (wc *WorkspaceClient) GetTagsByName() (map[string]*Tag, error) { - tags, err := wc.GetTags() - if err != nil { - return nil, err - } - - tagsByName := map[string]*Tag{} - for _, tag := range tags { - tagsByName[tag.Name] = tag - } - - return tagsByName, err -} - -func (wc *WorkspaceClient) GetTasksFromSection(section *Section) ([]*Task, error) { - path := fmt.Sprintf("sections/%s/tasks", section.GID) - resp := &tasksResponse{} - err := wc.client.get(path, nil, resp) - if err != nil { - return nil, err - } - return resp.Data, nil -} - -func (wc *WorkspaceClient) GetUserTaskList(user *User) (*Project, error) { - path := fmt.Sprintf("users/%s/user_task_list", user.GID) - values := &url.Values{} - values.Add("workspace", wc.workspace.GID) - resp := &projectResponse{} - err := wc.client.get(path, values, resp) - if err != nil { - return nil, err - } - return resp.Data, nil -} - -func (wc *WorkspaceClient) GetMyUserTaskList() (*Project, error) { - me, err := wc.GetMe() - if err != nil { - return nil, err - } - - return wc.GetUserTaskList(me) -} - -func (wc *WorkspaceClient) Search(q *SearchQuery) ([]*Task, error) { - path := fmt.Sprintf("workspaces/%s/tasks/search", wc.workspace.GID) - - values := &url.Values{ - "sort_by": []string{"created_at"}, - "sort_ascending": []string{"true"}, - } - - values.Add("opt_fields", "created_at,due_on,html_notes,name") - - if len(q.SectionsAny) > 0 { - gids := []string{} - for _, sec := range q.SectionsAny { - gids = append(gids, sec.GID) - } - values.Add("sections.any", strings.Join(gids, ",")) - } - - if q.Completed != nil { - values.Add("completed", fmt.Sprintf("%t", *q.Completed)) - } - - if q.Due != nil { - if *q.Due { - values.Add("due_on.after", "1970-01-01") - } else { - values.Add("due_on", "null") - } - } - - if q.DueOn != nil { - values.Add("due_on", q.DueOn.String()) - } - - if q.DueBefore != nil { - values.Add("due_on.before", q.DueBefore.String()) - } - - if q.DueAfter != nil { - values.Add("due_on.after", q.DueAfter.String()) - } - - if len(q.TagsAny) > 0 { - gids := []string{} - for _, sec := range q.TagsAny { - gids = append(gids, sec.GID) - } - values.Add("tags.any", strings.Join(gids, ",")) - } - - if len(q.TagsNot) > 0 { - gids := []string{} - for _, sec := range q.TagsNot { - gids = append(gids, sec.GID) - } - values.Add("tags.not", strings.Join(gids, ",")) - } - - tasksByGID := map[string]*Task{} - - for { - resp := &tasksResponse{} - err := wc.client.get(path, values, resp) - if err != nil { - return nil, err - } - - maxCreatedAt := "" - - for _, task := range resp.Data { - err := task.parse() - if err != nil { - return nil, err - } - tasksByGID[task.GID] = task - - if task.CreatedAt > maxCreatedAt { - maxCreatedAt = task.CreatedAt - } - } - - if len(resp.Data) < perPage { - break - } - - values.Set("created_at.after", maxCreatedAt) - } - - tasks := []*Task{} - for _, task := range tasksByGID { - tasks = append(tasks, task) - } - - return tasks, nil -} - -func (wc *WorkspaceClient) UpdateTask(task *Task) error { - path := fmt.Sprintf("tasks/%s", task.GID) - - task.GID = "" - - update := &taskUpdate{ - Data: task, - } - - resp := &taskResponse{} - err := wc.client.put(path, update, resp) - if err != nil { - return err - } - - return nil -} - -func (p *Project) String() string { - return fmt.Sprintf("%s (%s)", p.GID, p.Name) -} - -func (s *Section) String() string { - return fmt.Sprintf("%s (%s)", s.GID, s.Name) -} - -func (t *Task) String() string { - return fmt.Sprintf("%s (%s)", t.GID, t.Name) -} - -func (u *User) String() string { - return fmt.Sprintf("%s (%s <%s>)", u.GID, u.Name, u.Email) -} - -func (t *Task) parse() error { - r := strings.NewReader(t.HTMLNotes) - root, err := html.Parse(r) - if err != nil { - return err - } - t.ParsedHTMLNotes = root - - if t.DueOn != "" { - d, err := civil.ParseDate(t.DueOn) - if err != nil { - return err - } - t.ParsedDueOn = &d - } - - return nil + workspace *Workspace }