Skip to content

Commit

Permalink
Adds CreateProject and UpdateProject and basic PUT/POST handling.
Browse files Browse the repository at this point in the history
  • Loading branch information
adlio committed Aug 24, 2017
1 parent bfab051 commit 36e328c
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 15 deletions.
93 changes: 82 additions & 11 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import (
"fmt"
"io/ioutil"
"net/http"
"regexp"

"github.com/pkg/errors"
)

const HARVEST_DOMAIN = "harvestapp.com"

var idRegexp = regexp.MustCompile("\\d+$")

type API struct {
client *http.Client
BaseURL string
Expand Down Expand Up @@ -64,19 +67,62 @@ func (a *API) Get(path string, args Arguments, target interface{}) error {
return nil
}

func (a *API) Put(path string, args Arguments, postData interface{}, target interface{}) error {
url := fmt.Sprintf("%s%s", a.BaseURL, path)
urlWithParams := fmt.Sprintf("%s?%s", url, args.ToURLValues().Encode())

buffer := new(bytes.Buffer)
if postData != nil {
json.NewEncoder(buffer).Encode(postData)
}

req, err := http.NewRequest("PUT", urlWithParams, buffer)
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.Header.Set("User-Agent", "github.com/adlio/harvest")
if a.User != "" && a.Password != "" {
req.SetBasicAuth(a.User, a.Password)
}
if err != nil {
return errors.Wrapf(err, "Invalid PUT request %s", url)
}

resp, err := a.client.Do(req)
if err != nil {
return errors.Wrapf(err, "HTTP request failure on %s", url)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode > 299 {
var body []byte
body, err = ioutil.ReadAll(resp.Body)
return errors.Wrapf(err, "HTTP request failure on %s: %s %s", url, string(body), err)
}

// Harvest V1 API returns an empty response, with a Location header including the
// URI of the created object (e.g. /projects/254454)
redirectDestination := resp.Header.Get("Location")
if idStr := idRegexp.FindString(redirectDestination); idStr != "" {
return a.Get(redirectDestination, args, target)
} else {
return errors.Errorf("PUT to %s failed to return a Location header. This means we couldn't fetch the new state of the record.", url)
}

return nil
}

func (a *API) Post(path string, args Arguments, postData interface{}, target interface{}) error {
url := fmt.Sprintf("%s%s", a.BaseURL, path)
urlWithParams := fmt.Sprintf("%s?%s", url, args.ToURLValues().Encode())

buffer := new(bytes.Buffer)
if postData != nil {
json.NewEncoder(buffer).Encode(postData)
fmt.Printf("%v", buffer)
}

req, err := http.NewRequest("POST", urlWithParams, buffer)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.Header.Set("User-Agent", "github.com/adlio/harvest")
if a.User != "" && a.Password != "" {
req.SetBasicAuth(a.User, a.Password)
}
Expand All @@ -89,21 +135,46 @@ func (a *API) Post(path string, args Arguments, postData interface{}, target int
return errors.Wrapf(err, "HTTP request failure on %s", url)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
if resp.StatusCode < 200 || resp.StatusCode > 299 {
var body []byte
body, err = ioutil.ReadAll(resp.Body)
return errors.Wrapf(err, "HTTP request failure on %s: %s %s", url, string(body), err)
}

decoder := json.NewDecoder(resp.Body)
err = decoder.Decode(target)
// Harvest V1 API returns an empty response, with a Location header including the
// URI of the created object (e.g. /projects/254454)
redirectDestination := resp.Header.Get("Location")
if idStr := idRegexp.FindString(redirectDestination); idStr != "" {
return a.Get(redirectDestination, args, target)
} else {
return errors.Errorf("POST to %s failed to return a Location header. This means we couldn't fetch the new state of the record.", url)
}

return nil
}

func (a *API) Delete(path string, args Arguments) error {
url := fmt.Sprintf("%s%s", a.BaseURL, path)
urlWithParams := fmt.Sprintf("%s?%s", url, args.ToURLValues().Encode())

req, err := http.NewRequest("DELETE", urlWithParams, nil)
req.Header.Set("Accept", "application/json")
if a.User != "" && a.Password != "" {
req.SetBasicAuth(a.User, a.Password)
}
if err != nil {
body, _ := ioutil.ReadAll(resp.Body)
if len(body) == 0 {
return nil
} else {
return errors.Wrapf(err, "JSON decode failed on %s: %s %s", url, string(body), err)
}
return errors.Wrapf(err, "Invalid DELETE request %s", url)
}

resp, err := a.client.Do(req)
if err != nil {
return errors.Wrapf(err, "HTTP request failure on %s", url)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
var body []byte
body, err = ioutil.ReadAll(resp.Body)
return errors.Wrapf(err, "HTTP request failure on %s: %s %s", url, string(body), err)
}

return nil
Expand Down
22 changes: 20 additions & 2 deletions project.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,26 @@ func (a *API) GetProjects(args Arguments) (projects []*Project, err error) {
return projects, err
}

func (a *API) SaveProject(p *Project, args Arguments) error {
if p.ID != 0 {
return a.UpdateProject(p, args)
} else {
return a.CreateProject(p, args)
}
}

func (a *API) UpdateProject(p *Project, args Arguments) error {
projectRequest := ProjectRequest{Project: p}
path := fmt.Sprintf("/projects/%d", p.ID)
return a.Put(path, args, &projectRequest, &projectRequest)
}

func (a *API) CreateProject(p *Project, args Arguments) error {
projectRequest := ProjectRequest{Project: p}
response := ""
return a.Post("/projects", args, &projectRequest, &response)
return a.Post("/projects", args, &projectRequest, &projectRequest)
}

func (a *API) DeleteProject(p *Project, args Arguments) error {
path := fmt.Sprintf("/projects/%d", p.ID)
return a.Delete(path, args)
}
29 changes: 27 additions & 2 deletions project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ func TestGetProjectWithStartEndDates(t *testing.T) {

func TestCreateProject(t *testing.T) {
a := testAPI()
emptyResponse := mockResponse("common/empty")
a.BaseURL = emptyResponse.URL
projectResponse := mockRedirectResponse("projects", "12670372.json")
a.BaseURL = projectResponse.URL

p := Project{
Name: "New Name",
Expand All @@ -128,4 +128,29 @@ func TestCreateProject(t *testing.T) {
if err != nil {
t.Fatal(err)
}

if *p.HourlyRate != 120.0 {
t.Errorf("Hourly rate should have been picked up in the CreateProject response")
}
}

func TestUpdateProject(t *testing.T) {
a := testAPI()
projectResponse := mockRedirectResponse("projects", "12670372.json")
a.BaseURL = projectResponse.URL

p := Project{
Name: "New Name",
Active: true,
ClientID: 12345,
}

err := a.UpdateProject(&p, Defaults())
if err != nil {
t.Fatal(err)
}

if *p.HourlyRate != 120.0 {
t.Errorf("Hourly rate should have been picked up in the CreateProject response")
}
}

0 comments on commit 36e328c

Please sign in to comment.