Skip to content

Commit 9c95ce9

Browse files
committed
Merge pull request #157 from Tonkpils/api-client
Clean up API package.
2 parents 0f09005 + c4ba7a2 commit 9c95ce9

15 files changed

Lines changed: 415 additions & 177 deletions

File tree

api/api.go

Lines changed: 84 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,11 @@ import (
77
"fmt"
88
"net/http"
99
"strings"
10-
11-
"github.com/exercism/cli/config"
1210
)
1311

1412
var (
15-
// UserAgent lets the API know where the call is being made from.
16-
// It's set from main() so that we have access to the version.
17-
UserAgent string
18-
19-
UnknownLanguageError = errors.New("the language is unknown")
13+
// ErrUnknownLanguage represents an error returned when the language requested does not exist
14+
ErrUnknownLanguage = errors.New("the language is unknown")
2015
)
2116

2217
// PayloadError represents an error message from the API.
@@ -39,22 +34,52 @@ type PayloadSubmission struct {
3934
// Fetch retrieves problems from the API.
4035
// In most cases these problems consist of a test suite and a README
4136
// from the x-api, but it is also used when restoring earlier iterations.
42-
func Fetch(url string) ([]*Problem, error) {
43-
req, err := http.NewRequest("GET", url, nil)
37+
func (c *Client) Fetch(args []string) ([]*Problem, error) {
38+
var url string
39+
switch len(args) {
40+
case 0:
41+
url = fmt.Sprintf("%s/v2/exercises?key=%s", c.XAPIHost, c.APIKey)
42+
case 1:
43+
language := args[0]
44+
url = fmt.Sprintf("%s/v2/exercises/%s?key=%s", c.XAPIHost, language, c.APIKey)
45+
case 2:
46+
language := args[0]
47+
problem := args[1]
48+
url = fmt.Sprintf("%s/v2/exercises/%s/%s", c.XAPIHost, language, problem)
49+
default:
50+
return nil, fmt.Errorf("Usage: exercism fetch\n or: exercism fetch LANGUAGE\n or: exercism fetch LANGUAGE PROBLEM")
51+
}
52+
53+
req, err := c.NewRequest("GET", url, nil)
4454
if err != nil {
4555
return nil, err
4656
}
4757

48-
res, err := http.DefaultClient.Do(req)
58+
payload := &PayloadProblems{}
59+
res, err := c.Do(req, payload)
60+
if err != nil {
61+
return nil, err
62+
}
63+
64+
if res.StatusCode != http.StatusOK {
65+
return nil, fmt.Errorf(`unable to fetch problems (HTTP: %d) - %s`, res.StatusCode, payload.Error)
66+
}
67+
68+
return payload.Problems, nil
69+
}
70+
71+
// Restore fetches the latest revision of a solution and writes it to disk.
72+
func (c *Client) Restore() ([]*Problem, error) {
73+
url := fmt.Sprintf("%s/api/v1/iterations/%s/restore", c.APIHost, c.APIKey)
74+
req, err := c.NewRequest("GET", url, nil)
4975
if err != nil {
5076
return nil, err
5177
}
52-
defer res.Body.Close()
5378

5479
payload := &PayloadProblems{}
55-
dec := json.NewDecoder(res.Body)
56-
if err := dec.Decode(payload); err != nil {
57-
return nil, fmt.Errorf("error parsing API response - %s", err)
80+
res, err := c.Do(req, payload)
81+
if err != nil {
82+
return nil, err
5883
}
5984

6085
if res.StatusCode != http.StatusOK {
@@ -65,23 +90,18 @@ func Fetch(url string) ([]*Problem, error) {
6590
}
6691

6792
// Download fetches a solution by submission key and writes it to disk.
68-
func Download(url string) (*Submission, error) {
69-
req, err := http.NewRequest("GET", url, nil)
70-
if err != nil {
71-
return nil, err
72-
}
93+
func (c *Client) Download(submissionID string) (*Submission, error) {
94+
url := fmt.Sprintf("%s/api/v1/submissions/%s", c.APIHost, submissionID)
7395

74-
res, err := http.DefaultClient.Do(req)
96+
req, err := c.NewRequest("GET", url, nil)
7597
if err != nil {
7698
return nil, err
7799
}
78-
defer res.Body.Close()
79100

80101
payload := &PayloadSubmission{}
81-
dec := json.NewDecoder(res.Body)
82-
err = dec.Decode(payload)
102+
res, err := c.Do(req, payload)
83103
if err != nil {
84-
return nil, fmt.Errorf("error parsing API response - %s", err)
104+
return nil, err
85105
}
86106

87107
if res.StatusCode != http.StatusOK {
@@ -92,38 +112,44 @@ func Download(url string) (*Submission, error) {
92112
}
93113

94114
// Demo fetches the first problem in each language track.
95-
func Demo(c *config.Config) ([]*Problem, error) {
96-
url := fmt.Sprintf("%s/problems/demo?key=%s", c.XAPI, c.APIKey)
115+
func (c *Client) Demo() ([]*Problem, error) {
116+
url := fmt.Sprintf("%s/problems/demo?key=%s", c.XAPIHost, c.APIKey)
117+
req, err := c.NewRequest("GET", url, nil)
118+
if err != nil {
119+
return nil, err
120+
}
97121

98-
return Fetch(url)
122+
payload := &PayloadProblems{}
123+
res, err := c.Do(req, payload)
124+
if err != nil {
125+
return nil, err
126+
}
127+
128+
if res.StatusCode != http.StatusOK {
129+
return nil, fmt.Errorf(`unable to fetch problems (HTTP: %d) - %s`, res.StatusCode, payload.Error)
130+
}
131+
132+
return payload.Problems, nil
99133
}
100134

101135
// Submit posts code to the API
102-
func Submit(url string, iter *Iteration) (*Submission, error) {
136+
func (c *Client) Submit(iter *Iteration) (*Submission, error) {
137+
url := fmt.Sprintf("%s/api/v1/user/assignments", c.APIHost)
103138
payload, err := json.Marshal(iter)
104139
if err != nil {
105140
return nil, err
106141
}
107142

108-
req, err := http.NewRequest("POST", url, bytes.NewReader(payload))
143+
req, err := c.NewRequest("POST", url, bytes.NewReader(payload))
109144
if err != nil {
110145
return nil, err
111146
}
112147

113-
req.Header.Set("User-Agent", UserAgent)
114-
req.Header.Set("Content-Type", "application/json")
115-
116-
res, err := http.DefaultClient.Do(req)
148+
ps := &PayloadSubmission{}
149+
res, err := c.Do(req, ps)
117150
if err != nil {
118151
return nil, fmt.Errorf("unable to submit solution - %s", err)
119152
}
120-
defer res.Body.Close()
121-
122-
ps := &PayloadSubmission{}
123-
dec := json.NewDecoder(res.Body)
124-
if err := dec.Decode(ps); err != nil {
125-
return nil, fmt.Errorf("error parsing API response - %s", err)
126-
}
127153

128154
if res.StatusCode != http.StatusCreated {
129155
return nil, fmt.Errorf(`unable to submit (HTTP: %d) - %s`, res.StatusCode, ps.Error)
@@ -133,21 +159,22 @@ func Submit(url string, iter *Iteration) (*Submission, error) {
133159
}
134160

135161
// List available problems for a language
136-
func List(language, host string) ([]string, error) {
137-
url := fmt.Sprintf("%s/tracks/%s", host, language)
162+
func (c *Client) List(language string) ([]string, error) {
163+
url := fmt.Sprintf("%s/tracks/%s", c.XAPIHost, language)
138164

139-
req, err := http.NewRequest("GET", url, nil)
165+
req, err := c.NewRequest("GET", url, nil)
140166
if err != nil {
141167
return nil, err
142168
}
143-
req.Header.Set("User-Agent", UserAgent)
144-
res, err := http.DefaultClient.Do(req)
169+
170+
res, err := c.Do(req, nil)
145171
if err != nil {
146172
return nil, err
147173
}
174+
148175
defer res.Body.Close()
149176
if res.StatusCode != http.StatusOK {
150-
return nil, UnknownLanguageError
177+
return nil, ErrUnknownLanguage
151178
}
152179

153180
var payload struct {
@@ -167,51 +194,35 @@ func List(language, host string) ([]string, error) {
167194
}
168195

169196
// Unsubmit deletes a submission.
170-
func Unsubmit(url string) error {
171-
req, err := http.NewRequest("DELETE", url, nil)
197+
func (c *Client) Unsubmit() error {
198+
url := fmt.Sprintf("%s/api/v1/user/assignments?key=%s", c.APIHost, c.APIKey)
199+
req, err := c.NewRequest("DELETE", url, nil)
172200
if err != nil {
173201
return err
174202
}
175-
req.Header.Set("User-Agent", UserAgent)
176-
177-
res, err := http.DefaultClient.Do(req)
178-
if err != nil {
179-
return err
180-
}
181-
defer res.Body.Close()
182-
183-
if res.StatusCode == http.StatusNoContent {
184-
return nil
185-
}
186203

187204
pe := &PayloadError{}
188-
if err := json.NewDecoder(res.Body).Decode(pe); err != nil {
189-
return fmt.Errorf("failed to unsubmit - %s", err)
205+
if _, err := c.Do(req, pe); err != nil {
206+
return fmt.Errorf("failed to unsubmit - %s", pe.Error)
190207
}
191-
return fmt.Errorf("failed to unsubmit - %s", pe.Error)
208+
209+
return nil
192210
}
193211

194212
// Tracks gets the current list of active and inactive language tracks.
195-
func Tracks(url string) ([]*Track, error) {
213+
func (c *Client) Tracks() ([]*Track, error) {
214+
url := fmt.Sprintf("%s/tracks", c.XAPIHost)
196215
req, err := http.NewRequest("GET", url, nil)
197216
if err != nil {
198217
return []*Track{}, err
199218
}
200-
req.Header.Set("User-Agent", UserAgent)
201-
202-
res, err := http.DefaultClient.Do(req)
203-
if err != nil {
204-
return []*Track{}, err
205-
}
206-
defer res.Body.Close()
207219

208220
var payload struct {
209221
Tracks []*Track
210222
}
211-
dec := json.NewDecoder(res.Body)
212-
err = dec.Decode(&payload)
213-
if err != nil {
223+
if _, err := c.Do(req, &payload); err != nil {
214224
return []*Track{}, err
215225
}
226+
216227
return payload.Tracks, nil
217228
}

api/api_test.go

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,119 @@
11
package api
22

33
import (
4+
"fmt"
45
"io"
56
"net/http"
67
"net/http/httptest"
78
"os"
89
"testing"
910

11+
"github.com/exercism/cli/config"
1012
"github.com/stretchr/testify/assert"
1113
)
1214

15+
func respondWithFixture(w http.ResponseWriter, name string) error {
16+
f, err := os.Open("../fixtures/" + name)
17+
if err != nil {
18+
return err
19+
}
20+
21+
io.Copy(w, f)
22+
f.Close()
23+
24+
return nil
25+
}
26+
func TestFetchAllProblem(t *testing.T) {
27+
APIKey := "mykey"
28+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
29+
allProblemsAPI := fmt.Sprintf("/v2/exercises?key=%s", APIKey)
30+
assert.Equal(t, allProblemsAPI, req.RequestURI)
31+
32+
if err := respondWithFixture(w, "problems.json"); err != nil {
33+
t.Fatal(err)
34+
}
35+
}))
36+
defer ts.Close()
37+
38+
client := NewClient(&config.Config{XAPI: ts.URL, APIKey: APIKey})
39+
40+
problems, err := client.Fetch([]string{})
41+
assert.NoError(t, err)
42+
43+
assert.Equal(t, len(problems), 3)
44+
}
45+
46+
func TestFetchATrack(t *testing.T) {
47+
var (
48+
APIKey = "mykey"
49+
language = "go"
50+
)
51+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
52+
languageProblemsAPI := fmt.Sprintf("/v2/exercises/%s?key=%s", language, APIKey)
53+
assert.Equal(t, languageProblemsAPI, req.RequestURI)
54+
55+
if err := respondWithFixture(w, "problems.json"); err != nil {
56+
t.Fatal(err)
57+
}
58+
}))
59+
defer ts.Close()
60+
61+
client := NewClient(&config.Config{XAPI: ts.URL, APIKey: APIKey})
62+
63+
_, err := client.Fetch([]string{language})
64+
assert.NoError(t, err)
65+
}
66+
67+
func TestFetchASpecificProblem(t *testing.T) {
68+
var (
69+
APIKey = "mykey"
70+
language = "go"
71+
problem = "leap"
72+
)
73+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
74+
languageProblemsAPI := fmt.Sprintf("/v2/exercises/%s/%s", language, problem)
75+
assert.Equal(t, languageProblemsAPI, req.RequestURI)
76+
77+
if err := respondWithFixture(w, "problems.json"); err != nil {
78+
t.Fatal(err)
79+
}
80+
}))
81+
defer ts.Close()
82+
83+
client := NewClient(&config.Config{XAPI: ts.URL, APIKey: APIKey})
84+
85+
_, err := client.Fetch([]string{language, problem})
86+
assert.NoError(t, err)
87+
}
88+
1389
func TestListTrack(t *testing.T) {
14-
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
90+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
1591
// check that we correctly built the URI path
16-
assert.Equal(t, "/tracks/clojure", r.RequestURI)
92+
assert.Equal(t, "/tracks/clojure", req.RequestURI)
1793

18-
f, err := os.Open("../fixtures/tracks.json")
19-
if err != nil {
94+
if err := respondWithFixture(w, "tracks.json"); err != nil {
2095
t.Fatal(err)
2196
}
22-
io.Copy(w, f)
23-
f.Close()
2497
}))
2598
defer ts.Close()
2699

27-
problems, err := List("clojure", ts.URL)
100+
client := NewClient(&config.Config{XAPI: ts.URL})
101+
102+
problems, err := client.List("clojure")
28103
assert.NoError(t, err)
29104

30105
assert.Equal(t, len(problems), 34)
31106
assert.Equal(t, problems[0], "bob")
32107
}
33108

34109
func TestUnknownLanguage(t *testing.T) {
35-
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
36-
http.NotFound(w, r)
110+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
111+
http.NotFound(w, req)
37112
}))
38113
defer ts.Close()
39114

40-
_, err := List("rubbbby", ts.URL)
41-
assert.Equal(t, err, UnknownLanguageError)
115+
client := NewClient(&config.Config{XAPI: ts.URL})
116+
117+
_, err := client.List("rubbbby")
118+
assert.Equal(t, err, ErrUnknownLanguage)
42119
}

0 commit comments

Comments
 (0)