Skip to content

Commit 53f4edb

Browse files
committed
Clean up API package.
Use an API Client from the cmd package Have functions form up their own urls and give them parameters they need
1 parent 67f2ec9 commit 53f4edb

14 files changed

Lines changed: 296 additions & 169 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: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"testing"
99

10+
"github.com/exercism/cli/config"
1011
"github.com/stretchr/testify/assert"
1112
)
1213

@@ -24,7 +25,10 @@ func TestListTrack(t *testing.T) {
2425
}))
2526
defer ts.Close()
2627

27-
problems, err := List("clojure", ts.URL)
28+
conf := &config.Config{XAPI: ts.URL}
29+
client := NewClient(conf)
30+
31+
problems, err := client.List("clojure")
2832
assert.NoError(t, err)
2933

3034
assert.Equal(t, len(problems), 34)
@@ -37,6 +41,9 @@ func TestUnknownLanguage(t *testing.T) {
3741
}))
3842
defer ts.Close()
3943

40-
_, err := List("rubbbby", ts.URL)
41-
assert.Equal(t, err, UnknownLanguageError)
44+
conf := &config.Config{XAPI: ts.URL}
45+
client := NewClient(conf)
46+
47+
_, err := client.List("rubbbby")
48+
assert.Equal(t, err, ErrUnknownLanguage)
4249
}

api/client.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package api
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
9+
"github.com/exercism/cli/config"
10+
)
11+
12+
var (
13+
// UserAgent lets the API know where the call is being made from.
14+
// It's set from main() so that we have access to the version.
15+
UserAgent string
16+
)
17+
18+
// Client contains the necessary information to contact the Exercism APIs
19+
type Client struct {
20+
client *http.Client
21+
APIHost string
22+
XAPIHost string
23+
APIKey string
24+
}
25+
26+
// NewClient returns an Exercism API Client
27+
func NewClient(c *config.Config) *Client {
28+
return &Client{
29+
client: http.DefaultClient,
30+
APIHost: c.API,
31+
XAPIHost: c.XAPI,
32+
APIKey: c.APIKey,
33+
}
34+
}
35+
36+
// NewRequest returns an http.Request with information for the Exercism API
37+
func (c *Client) NewRequest(method, url string, body io.Reader) (*http.Request, error) {
38+
req, err := http.NewRequest(method, url, body)
39+
if err != nil {
40+
return nil, err
41+
}
42+
43+
req.Header.Set("User-Agent", UserAgent)
44+
req.Header.Set("Content-Type", "application/json")
45+
46+
return req, nil
47+
}
48+
49+
// Do performs an http.Request and optionally parses the response body into the given interface
50+
func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) {
51+
res, err := c.client.Do(req)
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
if res.StatusCode == http.StatusNoContent {
57+
return res, nil
58+
}
59+
60+
if v != nil {
61+
defer res.Body.Close()
62+
if err := json.NewDecoder(res.Body).Decode(v); err != nil {
63+
return nil, fmt.Errorf("error parsing API response - %s", err)
64+
}
65+
}
66+
67+
return res, nil
68+
}

0 commit comments

Comments
 (0)