Skip to content

Commit a883dad

Browse files
author
Tambet Masik
committed
Initial commit
0 parents  commit a883dad

File tree

4 files changed

+297
-0
lines changed

4 files changed

+297
-0
lines changed

Makefile

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
default: fmt build
2+
3+
fmt:
4+
go fmt
5+
6+
build:
7+
go build

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
go-freshbooks
2+
=====
3+
This project implements a [Go](http://golang.org) client library for the [freshbooks API](http://developers.freshbooks.com/)

freshbooks.go

+257
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
package gofreshbooks
2+
3+
import (
4+
"bytes"
5+
"encoding/xml"
6+
"errors"
7+
"fmt"
8+
"io/ioutil"
9+
"net/http"
10+
)
11+
12+
type (
13+
Api struct {
14+
apiUrl string
15+
apiToken string
16+
perPage int
17+
users []User
18+
tasks []Task
19+
clients []Client
20+
projects []Project
21+
}
22+
Request struct {
23+
XMLName xml.Name `xml:"request"`
24+
Method string `xml:"method,attr"`
25+
PerPage int `xml:"per_page"`
26+
Page int `xml:"page"`
27+
}
28+
TimeEntryRequest struct {
29+
XMLName xml.Name `xml:"request"`
30+
Method string `xml:"method,attr"`
31+
TimeEntry TimeEntry `xml:"time_entry"`
32+
}
33+
Response struct {
34+
Error string `xml:"error"`
35+
Clients ClientList `xml:"clients"`
36+
Projects ProjectList `xml:"projects"`
37+
Tasks TaskList `xml:"tasks"`
38+
Users UserList `xml:"staff_members"`
39+
}
40+
TimeEntryResponse struct {
41+
Status string `xml:"status,attr"`
42+
Error string `xml:"error"`
43+
Code string `xml:"code"`
44+
Field string `xml:"field"`
45+
TimeEntryId int `xml:"time_entry_id"`
46+
}
47+
Pagination struct {
48+
Page int `xml:"page,attr"`
49+
Total int `xml:"total,attr"`
50+
PerPage int `xml:"per_page,attr"`
51+
}
52+
ClientList struct {
53+
Pagination
54+
Clients []Client `xml:"client"`
55+
}
56+
ProjectList struct {
57+
Pagination
58+
Projects []Project `xml:"project"`
59+
}
60+
TaskList struct {
61+
Pagination
62+
Tasks []Task `xml:"task"`
63+
}
64+
UserList struct {
65+
Pagination
66+
Users []User `xml:"member"`
67+
}
68+
Client struct {
69+
ClientId int `xml:"client_id"`
70+
Name string `xml:"organization"`
71+
}
72+
Project struct {
73+
ProjectId int `xml:"project_id"`
74+
ClientId string `xml:"client_id"`
75+
Name string `xml:"name"`
76+
TaskIds []int `xml:"tasks>task>task_id"`
77+
UserIds []int `xml:"staff>staff>staff_id"`
78+
}
79+
Task struct {
80+
TaskId int `xml:"task_id"`
81+
Name string `xml:"name"`
82+
}
83+
User struct {
84+
UserId int `xml:"staff_id"`
85+
Email string `xml:"email"`
86+
FirstName string `xml:"first_name"`
87+
LastName string `xml:"last_name"`
88+
}
89+
TimeEntry struct {
90+
TimeEntryId int `xml:"time_entry_id"`
91+
ProjectId int `xml:"project_id"` // Required
92+
TaskId int `xml:"task_id"` // Required
93+
UserId int `xml:"staff_id"` // Required
94+
Date string `xml:"date"` // Required
95+
Notes string `xml:"notes"`
96+
Hours float64 `xml:"hours"`
97+
}
98+
)
99+
100+
func NewApi(account string, token string) *Api {
101+
url := fmt.Sprintf("https://%s.freshbooks.com/api/2.1/xml-in", account)
102+
fb := Api{apiUrl: url, apiToken: token, perPage: 25}
103+
fb.users = make([]User, 0)
104+
fb.tasks = make([]Task, 0)
105+
fb.clients = make([]Client, 0)
106+
fb.projects = make([]Project, 0)
107+
return &fb
108+
}
109+
110+
func (this *Api) Clients() ([]Client, error) {
111+
err := this.fetchClients(1)
112+
return this.clients, err
113+
}
114+
115+
func (this *Api) Projects() ([]Project, error) {
116+
err := this.fetchProjects(1)
117+
return this.projects, err
118+
}
119+
120+
func (this *Api) Tasks() ([]Task, error) {
121+
err := this.fetchTasks(1)
122+
return this.tasks, err
123+
}
124+
125+
func (this *Api) Users() ([]User, error) {
126+
err := this.fetchUsers(1)
127+
return this.users, err
128+
}
129+
130+
func (this *Api) fetchClients(page int) error {
131+
request := &Request{Method: "client.list", Page: page, PerPage: this.perPage}
132+
result, err := this.makeRequest(request)
133+
if err != nil {
134+
return err
135+
}
136+
parsedInto := Response{}
137+
if err := xml.Unmarshal(*result, &parsedInto); err != nil {
138+
return err
139+
}
140+
if len(parsedInto.Error) > 0 {
141+
return errors.New(parsedInto.Error)
142+
}
143+
this.clients = append(this.clients, parsedInto.Clients.Clients...)
144+
if parsedInto.Clients.Total > parsedInto.Clients.PerPage*page {
145+
return this.fetchClients(page + 1)
146+
}
147+
return nil
148+
}
149+
150+
func (this *Api) fetchProjects(page int) error {
151+
request := &Request{Method: "project.list", Page: page, PerPage: this.perPage}
152+
result, err := this.makeRequest(request)
153+
if err != nil {
154+
return err
155+
}
156+
parsedInto := Response{}
157+
if err := xml.Unmarshal(*result, &parsedInto); err != nil {
158+
return (err)
159+
}
160+
if len(parsedInto.Error) > 0 {
161+
return errors.New(parsedInto.Error)
162+
}
163+
this.projects = append(this.projects, parsedInto.Projects.Projects...)
164+
if parsedInto.Projects.Total > parsedInto.Projects.PerPage*page {
165+
return this.fetchProjects(page + 1)
166+
}
167+
return nil
168+
}
169+
170+
func (this *Api) fetchTasks(page int) error {
171+
request := &Request{Method: "task.list", Page: page, PerPage: this.perPage}
172+
result, err := this.makeRequest(request)
173+
if err != nil {
174+
return err
175+
}
176+
parsedInto := Response{}
177+
if err := xml.Unmarshal(*result, &parsedInto); err != nil {
178+
return err
179+
}
180+
if len(parsedInto.Error) > 0 {
181+
return errors.New(parsedInto.Error)
182+
}
183+
this.tasks = append(this.tasks, parsedInto.Tasks.Tasks...)
184+
if parsedInto.Tasks.Total > parsedInto.Tasks.PerPage*page {
185+
return this.fetchTasks(page + 1)
186+
}
187+
return nil
188+
}
189+
190+
func (this *Api) fetchUsers(page int) error {
191+
request := &Request{Method: "staff.list", Page: page, PerPage: this.perPage}
192+
result, err := this.makeRequest(request)
193+
if err != nil {
194+
return err
195+
}
196+
parsedInto := Response{}
197+
if err := xml.Unmarshal(*result, &parsedInto); err != nil {
198+
return err
199+
}
200+
if len(parsedInto.Error) > 0 {
201+
return errors.New(parsedInto.Error)
202+
}
203+
this.users = append(this.users, parsedInto.Users.Users...)
204+
if parsedInto.Users.Total > parsedInto.Users.PerPage*page {
205+
return this.fetchUsers(page + 1)
206+
}
207+
return nil
208+
}
209+
210+
func (this *Api) SaveTimeEntry(timeEntry *TimeEntry) (int, error) {
211+
var method string
212+
if timeEntry.TimeEntryId != 0 {
213+
method = "time_entry.update"
214+
} else {
215+
method = "time_entry.create"
216+
}
217+
request := &TimeEntryRequest{Method: method, TimeEntry: *timeEntry}
218+
result, err := this.makeRequest(request)
219+
if err != nil {
220+
return 0, err
221+
}
222+
parsedInto := TimeEntryResponse{}
223+
if err := xml.Unmarshal(*result, &parsedInto); err != nil {
224+
return 0, err
225+
}
226+
if parsedInto.Status == "ok" {
227+
return parsedInto.TimeEntryId, nil
228+
}
229+
return 0, errors.New(parsedInto.Error)
230+
}
231+
232+
func (this *Api) makeRequest(request interface{}) (*[]byte, error) {
233+
xmlRequest, err := xml.MarshalIndent(request, "", " ")
234+
if err != nil {
235+
return nil, err
236+
}
237+
req, err := http.NewRequest("POST", this.apiUrl, bytes.NewBuffer(xmlRequest))
238+
if err != nil {
239+
return nil, err
240+
}
241+
req.SetBasicAuth(this.apiToken, "X")
242+
243+
response, err := http.DefaultClient.Do(req)
244+
if err != nil {
245+
return nil, err
246+
}
247+
defer response.Body.Close()
248+
if response.StatusCode != http.StatusOK {
249+
return nil, errors.New(response.Status)
250+
}
251+
252+
result, err := ioutil.ReadAll(response.Body)
253+
if err != nil {
254+
return nil, err
255+
}
256+
return &result, nil
257+
}

freshbooks_test.go

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package gofreshbooks
2+
3+
import (
4+
"os"
5+
"testing"
6+
)
7+
8+
func getCredentials(t *testing.T) (string, string) {
9+
var account, token string
10+
if account = os.Getenv("FRESHBOOKS_ACCOUNT"); account == "" {
11+
t.Fatal("Unable to get FRESHBOOKS_ACCOUNT environment variable")
12+
}
13+
14+
if token = os.Getenv("FRESHBOOKS_TOKEN"); token == "" {
15+
t.Fatal("Unable to get FRESHBOOKS_TOKEN environment variable")
16+
}
17+
return account, token
18+
}
19+
20+
func TestGetUsers(t *testing.T) {
21+
api := NewApi(getCredentials(t))
22+
users, err := api.Users()
23+
24+
if err != nil {
25+
t.Fatal("Freshbooks retured an error:", err.Error())
26+
}
27+
if len(users) < 1 {
28+
t.Fatal("There should be at least one user")
29+
}
30+
}

0 commit comments

Comments
 (0)