Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 84 additions & 73 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,11 @@ import (
"fmt"
"net/http"
"strings"

"github.com/exercism/cli/config"
)

var (
// UserAgent lets the API know where the call is being made from.
// It's set from main() so that we have access to the version.
UserAgent string

UnknownLanguageError = errors.New("the language is unknown")
// ErrUnknownLanguage represents an error returned when the language requested does not exist
ErrUnknownLanguage = errors.New("the language is unknown")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much more idiomatic!

)

// PayloadError represents an error message from the API.
Expand All @@ -39,22 +34,52 @@ type PayloadSubmission struct {
// Fetch retrieves problems from the API.
// In most cases these problems consist of a test suite and a README
// from the x-api, but it is also used when restoring earlier iterations.
func Fetch(url string) ([]*Problem, error) {
req, err := http.NewRequest("GET", url, nil)
func (c *Client) Fetch(args []string) ([]*Problem, error) {
var url string
switch len(args) {
case 0:
url = fmt.Sprintf("%s/v2/exercises?key=%s", c.XAPIHost, c.APIKey)
case 1:
language := args[0]
url = fmt.Sprintf("%s/v2/exercises/%s?key=%s", c.XAPIHost, language, c.APIKey)
case 2:
language := args[0]
problem := args[1]
url = fmt.Sprintf("%s/v2/exercises/%s/%s", c.XAPIHost, language, problem)
default:
return nil, fmt.Errorf("Usage: exercism fetch\n or: exercism fetch LANGUAGE\n or: exercism fetch LANGUAGE PROBLEM")
}

req, err := c.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}

res, err := http.DefaultClient.Do(req)
payload := &PayloadProblems{}
res, err := c.Do(req, payload)
if err != nil {
return nil, err
}

if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf(`unable to fetch problems (HTTP: %d) - %s`, res.StatusCode, payload.Error)
}

return payload.Problems, nil
}

// Restore fetches the latest revision of a solution and writes it to disk.
func (c *Client) Restore() ([]*Problem, error) {
url := fmt.Sprintf("%s/api/v1/iterations/%s/restore", c.APIHost, c.APIKey)
req, err := c.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
defer res.Body.Close()

payload := &PayloadProblems{}
dec := json.NewDecoder(res.Body)
if err := dec.Decode(payload); err != nil {
return nil, fmt.Errorf("error parsing API response - %s", err)
res, err := c.Do(req, payload)
if err != nil {
return nil, err
}

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

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

res, err := http.DefaultClient.Do(req)
req, err := c.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
defer res.Body.Close()

payload := &PayloadSubmission{}
dec := json.NewDecoder(res.Body)
err = dec.Decode(payload)
res, err := c.Do(req, payload)
if err != nil {
return nil, fmt.Errorf("error parsing API response - %s", err)
return nil, err
}

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

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

return Fetch(url)
payload := &PayloadProblems{}
res, err := c.Do(req, payload)
if err != nil {
return nil, err
}

if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf(`unable to fetch problems (HTTP: %d) - %s`, res.StatusCode, payload.Error)
}

return payload.Problems, nil
}

// Submit posts code to the API
func Submit(url string, iter *Iteration) (*Submission, error) {
func (c *Client) Submit(iter *Iteration) (*Submission, error) {
url := fmt.Sprintf("%s/api/v1/user/assignments", c.APIHost)
payload, err := json.Marshal(iter)
if err != nil {
return nil, err
}

req, err := http.NewRequest("POST", url, bytes.NewReader(payload))
req, err := c.NewRequest("POST", url, bytes.NewReader(payload))
if err != nil {
return nil, err
}

req.Header.Set("User-Agent", UserAgent)
req.Header.Set("Content-Type", "application/json")

res, err := http.DefaultClient.Do(req)
ps := &PayloadSubmission{}
res, err := c.Do(req, ps)
if err != nil {
return nil, fmt.Errorf("unable to submit solution - %s", err)
}
defer res.Body.Close()

ps := &PayloadSubmission{}
dec := json.NewDecoder(res.Body)
if err := dec.Decode(ps); err != nil {
return nil, fmt.Errorf("error parsing API response - %s", err)
}

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

// List available problems for a language
func List(language, host string) ([]string, error) {
url := fmt.Sprintf("%s/tracks/%s", host, language)
func (c *Client) List(language string) ([]string, error) {
url := fmt.Sprintf("%s/tracks/%s", c.XAPIHost, language)

req, err := http.NewRequest("GET", url, nil)
req, err := c.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", UserAgent)
res, err := http.DefaultClient.Do(req)

res, err := c.Do(req, nil)
if err != nil {
return nil, err
}

defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, UnknownLanguageError
return nil, ErrUnknownLanguage
}

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

// Unsubmit deletes a submission.
func Unsubmit(url string) error {
req, err := http.NewRequest("DELETE", url, nil)
func (c *Client) Unsubmit() error {
url := fmt.Sprintf("%s/api/v1/user/assignments?key=%s", c.APIHost, c.APIKey)
req, err := c.NewRequest("DELETE", url, nil)
if err != nil {
return err
}
req.Header.Set("User-Agent", UserAgent)

res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()

if res.StatusCode == http.StatusNoContent {
return nil
}

pe := &PayloadError{}
if err := json.NewDecoder(res.Body).Decode(pe); err != nil {
return fmt.Errorf("failed to unsubmit - %s", err)
if _, err := c.Do(req, pe); err != nil {
return fmt.Errorf("failed to unsubmit - %s", pe.Error)
}
return fmt.Errorf("failed to unsubmit - %s", pe.Error)

return nil
}

// Tracks gets the current list of active and inactive language tracks.
func Tracks(url string) ([]*Track, error) {
func (c *Client) Tracks() ([]*Track, error) {
url := fmt.Sprintf("%s/tracks", c.XAPIHost)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return []*Track{}, err
}
req.Header.Set("User-Agent", UserAgent)

res, err := http.DefaultClient.Do(req)
if err != nil {
return []*Track{}, err
}
defer res.Body.Close()

var payload struct {
Tracks []*Track
}
dec := json.NewDecoder(res.Body)
err = dec.Decode(&payload)
if err != nil {
if _, err := c.Do(req, &payload); err != nil {
return []*Track{}, err
}

return payload.Tracks, nil
}
13 changes: 10 additions & 3 deletions api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"testing"

"github.com/exercism/cli/config"
"github.com/stretchr/testify/assert"
)

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

problems, err := List("clojure", ts.URL)
conf := &config.Config{XAPI: ts.URL}
client := NewClient(conf)

problems, err := client.List("clojure")
assert.NoError(t, err)

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

_, err := List("rubbbby", ts.URL)
assert.Equal(t, err, UnknownLanguageError)
conf := &config.Config{XAPI: ts.URL}
client := NewClient(conf)

_, err := client.List("rubbbby")
assert.Equal(t, err, ErrUnknownLanguage)
}
68 changes: 68 additions & 0 deletions api/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package api

import (
"encoding/json"
"fmt"
"io"
"net/http"

"github.com/exercism/cli/config"
)

var (
// UserAgent lets the API know where the call is being made from.
// It's set from main() so that we have access to the version.
UserAgent string
)

// Client contains the necessary information to contact the Exercism APIs
type Client struct {
client *http.Client
APIHost string
XAPIHost string
APIKey string
}

// NewClient returns an Exercism API Client
func NewClient(c *config.Config) *Client {
return &Client{
client: http.DefaultClient,
APIHost: c.API,
XAPIHost: c.XAPI,
APIKey: c.APIKey,
}
}

// NewRequest returns an http.Request with information for the Exercism API
func (c *Client) NewRequest(method, url string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}

req.Header.Set("User-Agent", UserAgent)
req.Header.Set("Content-Type", "application/json")

return req, nil
}

// Do performs an http.Request and optionally parses the response body into the given interface
func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) {
res, err := c.client.Do(req)
if err != nil {
return nil, err
}

if res.StatusCode == http.StatusNoContent {
return res, nil
}

if v != nil {
defer res.Body.Close()
if err := json.NewDecoder(res.Body).Decode(v); err != nil {
return nil, fmt.Errorf("error parsing API response - %s", err)
}
}

return res, nil
}
Loading