Repository: exercism/cli
Pull request #424 (2017-08-06 16:07:41)

[nextercism] Implement configure command

For now this takes a token, a workspace, and the base URL of the API.

We will eventually want to add more flags, but that should be trivial compared to figuring out how to combine all the pieces (and write tests) in the first place.

Closes https://github.com/exercism/cli/issues/417


Patch:
diff --git a/cmd/cmd.go b/cmd/cmd.go
new file mode 100644
index 0000000..7e8bca6
--- /dev/null
+++ b/cmd/cmd.go
@@ -0,0 +1,13 @@
+package cmd
+
+import (
+	"log"
+	"os"
+)
+
+// Bail handles exitable errors in commands.
+// TODO: figure out what goes here.
+func Bail(err error) {
+	log.Println(err)
+	os.Exit(1)
+}
diff --git a/cmd/configure.go b/cmd/configure.go
index 087c4b8..c7058eb 100644
--- a/cmd/configure.go
+++ b/cmd/configure.go
@@ -1,9 +1,14 @@
 package cmd
 
 import (
-	"fmt"
-
+	"github.com/exercism/cli/config"
 	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+)
+
+var (
+	viperUserConfig *viper.Viper
+	viperAPIConfig  *viper.Viper
 )
 
 // configureCmd configures the command-line client with user-specific settings.
@@ -20,10 +25,41 @@ places.
 You can also override certain default settings to suit your preferences.
 `,
 	Run: func(cmd *cobra.Command, args []string) {
-		fmt.Println("configure called")
+		usrCfg := config.NewEmptyUserConfig()
+		if err := usrCfg.Load(viperUserConfig); err != nil {
+			Bail(err)
+		}
+		if err := usrCfg.Write(); err != nil {
+			Bail(err)
+		}
+
+		apiCfg := config.NewEmptyAPIConfig()
+		if err := apiCfg.Load(viperAPIConfig); err != nil {
+			Bail(err)
+		}
+		if err := apiCfg.Write(); err != nil {
+			Bail(err)
+		}
+
+		return
 	},
 }
 
+func initConfigureCfg() {
+	configureCmd.Flags().StringP("token", "t", "", "authentication token used to connect to exercism.io")
+	configureCmd.Flags().StringP("workspace", "w", "", "directory for exercism exercises")
+	configureCmd.Flags().StringP("api", "a", "", "API base url")
+
+	viperUserConfig = viper.New()
+	viperUserConfig.BindPFlag("token", configureCmd.Flags().Lookup("token"))
+	viperUserConfig.BindPFlag("workspace", configureCmd.Flags().Lookup("workspace"))
+
+	viperAPIConfig = viper.New()
+	viperAPIConfig.BindPFlag("baseurl", configureCmd.Flags().Lookup("api"))
+}
+
 func init() {
 	RootCmd.AddCommand(configureCmd)
+
+	initConfigureCfg()
 }
diff --git a/config/api_config.go b/config/api_config.go
index 71f3dbc..7a6328c 100644
--- a/config/api_config.go
+++ b/config/api_config.go
@@ -2,10 +2,19 @@ package config
 
 import (
 	"fmt"
+	"strings"
 
 	"github.com/spf13/viper"
 )
 
+var (
+	defaultBaseURL   = "https://api.exercism.com/v1"
+	defaultEndpoints = map[string]string{
+		"download": "/solutions/%s",
+		"submit":   "/solutions/%s",
+	}
+)
+
 // APIConfig provides API-specific configuration values.
 type APIConfig struct {
 	*Config
@@ -17,20 +26,33 @@ type APIConfig struct {
 func NewAPIConfig() (*APIConfig, error) {
 	cfg := NewEmptyAPIConfig()
 
-	// Set defaults.
-	cfg.BaseURL = "https://api.exercism.com/v1"
-	cfg.Endpoints = map[string]string{
-		"download": "/solutions/%s",
-		"submit":   "/solutions/%s",
-	}
-
 	if err := cfg.Load(viper.New()); err != nil {
 		return nil, err
 	}
 
+	cfg.SetDefaults()
+
 	return cfg, nil
 }
 
+// SetDefaults ensures that we have all the necessary settings for the API.
+func (cfg *APIConfig) SetDefaults() {
+	if cfg.BaseURL == "" {
+		cfg.BaseURL = "https://api.exercism.com/v1"
+	}
+
+	if cfg.Endpoints == nil {
+		cfg.Endpoints = defaultEndpoints
+		return
+	}
+
+	for key, endpoint := range defaultEndpoints {
+		if cfg.Endpoints[key] == "" {
+			cfg.Endpoints[key] = endpoint
+		}
+	}
+}
+
 // URL provides the API URL for a given endpoint key.
 func (cfg *APIConfig) URL(key string) string {
 	return fmt.Sprintf("%s%s", cfg.BaseURL, cfg.Endpoints[key])
@@ -45,6 +67,9 @@ func NewEmptyAPIConfig() *APIConfig {
 
 // Write stores the config to disk.
 func (cfg *APIConfig) Write() error {
+	cfg.BaseURL = strings.Trim(cfg.BaseURL, "/")
+	cfg.SetDefaults()
+
 	return Write(cfg)
 }
 


Test patch:
diff --git a/cmd/configure_test.go b/cmd/configure_test.go
new file mode 100644
index 0000000..0a7adcd
--- /dev/null
+++ b/cmd/configure_test.go
@@ -0,0 +1,106 @@
+package cmd
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"testing"
+
+	"github.com/exercism/cli/config"
+	"github.com/spf13/cobra"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestConfigure(t *testing.T) {
+	// Make sure we put back the config env var.
+	cfgHomeKey := "EXERCISM_CONFIG_HOME"
+	cfgHome := os.Getenv(cfgHomeKey)
+	defer os.Setenv(cfgHomeKey, cfgHome)
+
+	// Make sure we put back the real command-line arguments.
+	osArgs := os.Args
+	defer func() {
+		os.Args = osArgs
+	}()
+
+	// Set up a bogus root command.
+	fakeCmd := &cobra.Command{}
+	// Add the real configureCmd to it.
+	fakeCmd.AddCommand(configureCmd)
+
+	tests := []struct {
+		args           []string
+		existingUsrCfg *config.UserConfig
+		expectedUsrCfg *config.UserConfig
+		existingAPICfg *config.APIConfig
+		expectedAPICfg *config.APIConfig
+	}{
+		{
+			// It writes the flags when there is no config file.
+			args:           []string{"fakeapp", "configure", "--token", "a", "--workspace", "/a", "--api", "http://example.com"},
+			existingUsrCfg: nil,
+			expectedUsrCfg: &config.UserConfig{Token: "a", Workspace: "/a"},
+			existingAPICfg: nil,
+			expectedAPICfg: &config.APIConfig{BaseURL: "http://example.com"},
+		},
+		{
+			// It overwrites the flags in the config file.
+			args:           []string{"fakeapp", "configure", "--token", "b", "--workspace", "/b", "--api", "http://example.com/v2"},
+			existingUsrCfg: &config.UserConfig{Token: "token-b", Workspace: "/workspace-b"},
+			expectedUsrCfg: &config.UserConfig{Token: "b", Workspace: "/b"},
+			existingAPICfg: &config.APIConfig{BaseURL: "http://example.com/v1"},
+			expectedAPICfg: &config.APIConfig{BaseURL: "http://example.com/v2"},
+		},
+		{
+			args: []string{"fakeapp", "configure", "--token", "c"},
+			// It overwrites the flags that are passed without losing the ones that are not.
+			existingUsrCfg: &config.UserConfig{Token: "token-c", Workspace: "/workspace-c"},
+			expectedUsrCfg: &config.UserConfig{Token: "c", Workspace: "/workspace-c"},
+			// It gets the default API base URL.
+			existingAPICfg: &config.APIConfig{},
+			expectedAPICfg: &config.APIConfig{BaseURL: "https://api.exercism.com/v1"},
+		},
+	}
+
+	for i, test := range tests {
+		// Create a fake config dir.
+		dir, err := ioutil.TempDir("", fmt.Sprintf("user-config-%d", i))
+		assert.NoError(t, err)
+		defer os.RemoveAll(dir)
+
+		// Override the environment to use the fake config dir.
+		os.Setenv(cfgHomeKey, dir)
+
+		if test.existingUsrCfg != nil {
+			// Write a fake config.
+			cfg := config.NewEmptyUserConfig()
+			cfg.Token = test.existingUsrCfg.Token
+			cfg.Workspace = test.existingUsrCfg.Workspace
+			err = cfg.Write()
+			assert.NoError(t, err)
+		}
+
+		// Fake out the command-line arguments with the correct subcommand.
+		os.Args = test.args
+
+		// Re-initialize the command so it picks up the fake environment.
+		configureCmd.ResetFlags()
+		// Rerun the config initialization so that the flags get bound properly.
+		initConfigureCfg()
+
+		// Finally. Execute the configure command.
+		fakeCmd.Execute()
+
+		// Now let's get a new config and see that it got written properly.
+		usrCfg, err := config.NewUserConfig()
+		assert.NoError(t, err)
+
+		assert.Equal(t, test.expectedUsrCfg.Token, usrCfg.Token)
+		assert.Equal(t, test.expectedUsrCfg.Workspace, usrCfg.Workspace)
+
+		apiCfg, err := config.NewAPIConfig()
+		assert.NoError(t, err)
+
+		assert.Equal(t, test.expectedAPICfg.BaseURL, apiCfg.BaseURL)
+	}
+}
diff --git a/config/api_config_test.go b/config/api_config_test.go
index d3f648a..92b3c09 100644
--- a/config/api_config_test.go
+++ b/config/api_config_test.go
@@ -37,3 +37,32 @@ func TestAPIConfig(t *testing.T) {
 	assert.Equal(t, "/a", cfg.Endpoints["a"])
 	assert.Equal(t, "/b", cfg.Endpoints["b"])
 }
+
+func TestAPIConfigSetDefaults(t *testing.T) {
+	// All defaults.
+	cfg := &APIConfig{}
+	cfg.SetDefaults()
+	assert.Equal(t, "https://api.exercism.com/v1", cfg.BaseURL)
+	assert.Equal(t, "/solutions/%s", cfg.Endpoints["download"])
+	assert.Equal(t, "/solutions/%s", cfg.Endpoints["submit"])
+
+	// Override just the base url.
+	cfg = &APIConfig{
+		BaseURL: "http://example.com/v1",
+	}
+	cfg.SetDefaults()
+	assert.Equal(t, "http://example.com/v1", cfg.BaseURL)
+	assert.Equal(t, "/solutions/%s", cfg.Endpoints["download"])
+	assert.Equal(t, "/solutions/%s", cfg.Endpoints["submit"])
+
+	// Override just one of the endpoints.
+	cfg = &APIConfig{
+		Endpoints: map[string]string{
+			"download": "/download/%d",
+		},
+	}
+	cfg.SetDefaults()
+	assert.Equal(t, "https://api.exercism.com/v1", cfg.BaseURL)
+	assert.Equal(t, "/download/%d", cfg.Endpoints["download"])
+	assert.Equal(t, "/solutions/%s", cfg.Endpoints["submit"])
+}