diff --git a/api/api.go b/api/api.go index b4e54cf6..a19002d8 100644 --- a/api/api.go +++ b/api/api.go @@ -3,8 +3,10 @@ package api import ( "bytes" "encoding/json" + "errors" "fmt" "net/http" + "strings" "github.com/exercism/cli/config" ) @@ -13,6 +15,8 @@ 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") ) // PayloadError represents an error message from the API. @@ -128,6 +132,40 @@ func Submit(url string, iter *Iteration) (*Submission, error) { return ps.Submission, nil } +// List available problems for a language +func List(language, host string) ([]string, error) { + url := fmt.Sprintf("%s/tracks/%s", host, language) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", UserAgent) + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, UnknownLanguageError + } + + var payload struct { + Track Track + } + if err := json.NewDecoder(res.Body).Decode(&payload); err != nil { + return nil, err + } + problems := make([]string, len(payload.Track.Problems)) + prefix := language + "/" + + for n, p := range payload.Track.Problems { + problems[n] = strings.TrimPrefix(p, prefix) + } + + return problems, nil +} + // Unsubmit deletes a submission. func Unsubmit(url string) error { req, err := http.NewRequest("DELETE", url, nil) diff --git a/api/api_test.go b/api/api_test.go new file mode 100644 index 00000000..e5f5ed18 --- /dev/null +++ b/api/api_test.go @@ -0,0 +1,42 @@ +package api + +import ( + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestListTrack(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // check that we correctly built the URI path + assert.Equal(t, "/tracks/clojure", r.RequestURI) + + f, err := os.Open("../fixtures/tracks.json") + if err != nil { + t.Fatal(err) + } + io.Copy(w, f) + f.Close() + })) + defer ts.Close() + + problems, err := List("clojure", ts.URL) + assert.NoError(t, err) + + assert.Equal(t, len(problems), 34) + assert.Equal(t, problems[0], "bob") +} + +func TestUnknownLanguage(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.NotFound(w, r) + })) + defer ts.Close() + + _, err := List("rubbbby", ts.URL) + assert.Equal(t, err, UnknownLanguageError) +} diff --git a/cmd/list.go b/cmd/list.go new file mode 100644 index 00000000..d05177d7 --- /dev/null +++ b/cmd/list.go @@ -0,0 +1,41 @@ +package cmd + +import ( + "fmt" + "log" + + "github.com/codegangsta/cli" + "github.com/exercism/cli/api" + "github.com/exercism/cli/config" +) + +const msgExplainFetch = "In order to fetch a specific assignment, call the fetch command with a specific assignment.\n\nexercism fetch ruby matrix" + +// List returns the full list of assignments for a given language +func List(ctx *cli.Context) { + c, err := config.New(ctx.GlobalString("config")) + if err != nil { + log.Fatal(err) + } + args := ctx.Args() + + if len(args) != 1 { + msg := "Usage: execism list LANGUAGE" + log.Fatal(msg) + } + + language := args[0] + + problems, err := api.List(language, c.XAPI) + if err != nil { + if err == api.UnknownLanguageError { + log.Fatalf("The requested language '%s' is unknown", language) + } + log.Fatal(err) + } + + for _, p := range problems { + fmt.Printf("%s\n", p) + } + fmt.Printf("\n%s\n\n", msgExplainFetch) +} diff --git a/exercism/main.go b/exercism/main.go index c4a758ea..9cf96d99 100644 --- a/exercism/main.go +++ b/exercism/main.go @@ -29,6 +29,7 @@ const ( descLongRestore = "Restore will pull the latest revisions of exercises that have already been submitted. It will *not* overwrite existing files. If you have made changes to a file and have not submitted it, and you're trying to restore the last submitted version, first move that file out of the way, then call restore." descDownload = "Downloads and saves a specified submission into the local system" + descList = "Lists all available assignments for a given language" ) func main() { @@ -120,6 +121,12 @@ func main() { Usage: descDownload, Action: cmd.Download, }, + { + Name: "list", + ShortName: "li", + Usage: descList, + Action: cmd.List, + }, } if err := app.Run(os.Args); err != nil { diff --git a/fixtures/tracks.json b/fixtures/tracks.json new file mode 100644 index 00000000..aae48707 --- /dev/null +++ b/fixtures/tracks.json @@ -0,0 +1,45 @@ +{ + "track":{ + "language":"Clojure", + "active":true, + "id":"clojure", + "slug":"clojure", + "problems":[ + "clojure/bob", + "clojure/rna-transcription", + "clojure/word-count", + "clojure/anagram", + "clojure/beer-song", + "clojure/nucleotide-count", + "clojure/point-mutations", + "clojure/phone-number", + "clojure/grade-school", + "clojure/robot-name", + "clojure/leap", + "clojure/etl", + "clojure/meetup", + "clojure/space-age", + "clojure/grains", + "clojure/gigasecond", + "clojure/triangle", + "clojure/scrabble-score", + "clojure/roman-numerals", + "clojure/binary", + "clojure/prime-factors", + "clojure/raindrops", + "clojure/allergies", + "clojure/atbash-cipher", + "clojure/bank-account", + "clojure/crypto-square", + "clojure/kindergarten-garden", + "clojure/robot-simulator", + "clojure/queen-attack", + "clojure/accumulate", + "clojure/binary-search-tree", + "clojure/difference-of-squares", + "clojure/hexadecimal", + "clojure/largest-series-product" + ], + "repository":"https://github.com/exercism/xclojure" + } +}