Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve open cmd #788

Closed
wants to merge 10 commits into from
181 changes: 178 additions & 3 deletions cmd/open.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
package cmd

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"

"github.com/exercism/cli/api"
"github.com/exercism/cli/browser"
"github.com/exercism/cli/config"
"github.com/exercism/cli/workspace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)

// openCmd opens the designated exercise in the browser.
Expand All @@ -15,17 +25,182 @@ var openCmd = &cobra.Command{

Pass the path to the directory that contains the solution you want to see on the website.
`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
metadata, err := workspace.NewExerciseMetadata(args[0])
cfg := config.NewConfig()

v := viper.New()
v.AddConfigPath(cfg.Dir)
v.SetConfigName("user")
v.SetConfigType("json")
// Ignore error. If the file doesn't exist, that is fine.
_ = v.ReadInConfig()
cfg.UserViperConfig = v

return runOpen(cfg, cmd.Flags(), args)
},
}

func runOpen(cfg config.Config, flags *pflag.FlagSet, args []string) error {
var url string
usrCfg := cfg.UserViperConfig
trackID, err := flags.GetString("track")
if err != nil {
return err
}

exerciseSlug, err := flags.GetString("exercise")
if err != nil {
return err
}

teamID, err := flags.GetString("team")
if err != nil {
return err
}

remote, err := flags.GetBool("remote")
if err != nil {
return err
}

var path string
if len(args) > 0 {
path = args[0]
}

if exerciseSlug == "" {
if path == "" {
return fmt.Errorf("must provide an --exercise slug or an exercise path")
}
// if no --exercise is given, use original functionality
metadata, err := workspace.NewExerciseMetadata(path)
Copy link
Member

Choose a reason for hiding this comment

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

I think that this would be easier to understand if we did something like this:

if exerciseSlug == "" && path == "" {
  return fmt.Errorf...
}

if path != "" {
  // get metadata and open the url
}

// etc

if err != nil {
return err
}
browser.Open(metadata.URL)
return nil
},
}

if remote {
apiURL := fmt.Sprintf("%s/solutions/latest", usrCfg.GetString("apibaseurl"))

client, err := api.NewClient(usrCfg.GetString("token"), usrCfg.GetString("apibaseurl"))
if err != nil {
return err
}

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

q := req.URL.Query()
q.Add("exercise_id", exerciseSlug)
if trackID != "" {
q.Add("track_id", trackID)
}
if teamID != "" {
q.Add("team_id", teamID)
}
req.URL.RawQuery = q.Encode()

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

defer res.Body.Close()
var payload openPayload
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
return fmt.Errorf("unable to parse API response - %s", err)
}
Copy link
Member

Choose a reason for hiding this comment

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

Would it make sense to use the api error decoder helper?

cli/cmd/cmd.go

Line 78 in c3525cf

func decodedAPIError(resp *http.Response) error {

Copy link
Member Author

Choose a reason for hiding this comment

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

I need to decode the json anyways for the solution url. The helper only returns the error. Would it be beneficial to introduce a helper that decodes the entire api response?

Copy link
Member

Choose a reason for hiding this comment

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

Hm. Maybe, but let's hold off on that for now.


if res.StatusCode != http.StatusOK {
switch payload.Error.Type {
case "track_ambiguous":
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if we can make the error decoder helper smarter and add this logic into it.

cli/cmd/cmd.go

Line 78 in c3525cf

func decodedAPIError(resp *http.Response) error {

(If so, let's do that in a separate PR and get that into master quickly so we can use it elsewhere).

return fmt.Errorf("%s: %s", payload.Error.Message, strings.Join(payload.Error.PossibleTrackIDs, ", "))
default:
return errors.New(payload.Error.Message)
}
}

url = payload.Solution.URL
Copy link
Member

Choose a reason for hiding this comment

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

Let's open the URL and return here so that we can out-dent the else.


browser.Open(url)
return nil
}

ws, err := workspace.New(usrCfg.GetString("workspace"))
if err != nil {
return err
}
exercises, err := ws.Exercises()
if err != nil {
return err
}

matchingExerciseMeta := make([]*workspace.ExerciseMetadata, 0, len(exercises))
for _, exercise := range exercises {
metaDir := exercise.MetadataDir()
meta, err := workspace.NewExerciseMetadata(metaDir)
if err != nil {
return err
}

if meta.ExerciseSlug != exerciseSlug {
continue
}

if trackID != "" && meta.Track != trackID {
continue
}

if meta.Team != teamID {
continue
}

matchingExerciseMeta = append(matchingExerciseMeta, meta)
}

switch len(matchingExerciseMeta) {
case 0:
return fmt.Errorf("No matching exercise found")
case 1:
url = matchingExerciseMeta[0].URL
break
default:
tracks := make([]string, 0, len(matchingExerciseMeta))
for _, exercise := range matchingExerciseMeta {
tracks = append(tracks, exercise.Track)
}

return fmt.Errorf("Please specify a track ID: %s", strings.Join(tracks, ", "))
}

browser.Open(url)
return nil
}

type openPayload struct {
Solution struct {
ID string `json:"id"`
URL string `json:"url"`
} `json:"solution"`
Error struct {
Type string `json:"type"`
Message string `json:"message"`
PossibleTrackIDs []string `json:"possible_track_ids"`
} `json:"error,omitempty"`
}

func setupOpenFlags(flags *pflag.FlagSet) {
flags.BoolP("remote", "r", false, "checks for remote solutions")
flags.StringP("track", "t", "", "the track id")
flags.StringP("exercise", "e", "", "the exercise slug")
flags.StringP("team", "T", "", "the team slug")
}

func init() {
RootCmd.AddCommand(openCmd)
setupOpenFlags(openCmd.Flags())
}
1 change: 1 addition & 0 deletions cmd/open_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package cmd
Copy link
Member Author

Choose a reason for hiding this comment

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

I'd like to add tests after this is reviewed, to check that functionality is how we want it

10 changes: 10 additions & 0 deletions workspace/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ func (ws Workspace) PotentialExercises() ([]Exercise, error) {
continue
}

if topInfo.Name() == "teams" {
subInfos, _ := ioutil.ReadDir(filepath.Join(ws.Dir, "teams"))
for _, subInfo := range subInfos {
teamWs, _ := New(filepath.Join(ws.Dir, "teams", subInfo.Name()))
teamExercises, _ := teamWs.PotentialExercises()
exercises = append(exercises, teamExercises...)
}
continue
}

subInfos, err := ioutil.ReadDir(filepath.Join(ws.Dir, topInfo.Name()))
if err != nil {
return nil, err
Expand Down