Skip to content

Commit

Permalink
Merge pull request sequelize#441 from exercism/nextercism-cli-cfg
Browse files Browse the repository at this point in the history
[nextercism] Add CLI-specific config
  • Loading branch information
Katrina Owen authored Aug 17, 2017
2 parents b78ccc2 + 0658a2d commit 9723324
Show file tree
Hide file tree
Showing 7 changed files with 263 additions and 6 deletions.
13 changes: 9 additions & 4 deletions cmd/prepare.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ var prepareCmd = &cobra.Command{
Long: `Prepare downloads settings and dependencies for Exercism and the language tracks.
When called without any arguments, this downloads all the copy for the CLI so we
know what to say in all the various situations, as well as an up-to-date list
know what to say in all the various situations. It also provides an up-to-date list
of the API endpoints to use.
When called with a track ID, it will download the files that the track maintainers
have said are necessary for the track in general. Any files that are only
necessary for a specific exercise will only be downloaded with the exercise.
When called with a track ID, it will do specific setup for that track. This
might include downloading the files that the track maintainers have said are
necessary for the track in general. Any files that are only necessary for a specific
exercise will only be downloaded with the exercise.
To customize the CLI to suit your own preferences, use the configure command.
`,
Expand All @@ -28,6 +29,10 @@ To customize the CLI to suit your own preferences, use the configure command.
},
}

func initPrepareCmd() {
prepareCmd.Flags().StringP("track", "t", "", "the track you want to prepare")
}

func init() {
RootCmd.AddCommand(prepareCmd)
}
5 changes: 3 additions & 2 deletions config/api_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import (
var (
defaultBaseURL = "https://api.exercism.io/v1"
defaultEndpoints = map[string]string{
"download": "/solutions/%s",
"submit": "/solutions/%s",
"download": "/solutions/%s",
"submit": "/solutions/%s",
"prepare-track": "/tracks/%s/settings",
}
)

Expand Down
62 changes: 62 additions & 0 deletions config/cli_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package config

import "github.com/spf13/viper"

// CLIConfig contains settings specific to the behavior of the CLI.
type CLIConfig struct {
*Config
Tracks Tracks
}

// NewCLIConfig loads the config file in the config directory.
func NewCLIConfig() (*CLIConfig, error) {
cfg := NewEmptyCLIConfig()

if err := cfg.Load(viper.New()); err != nil {
return nil, err
}
cfg.SetDefaults()

return cfg, nil
}

// NewEmptyCLIConfig doesn't load the config from file or set default values.
func NewEmptyCLIConfig() *CLIConfig {
return &CLIConfig{
Config: New(Dir(), "cli"),
Tracks: Tracks{},
}
}

// Write stores the config to disk.
func (cfg *CLIConfig) Write() error {
cfg.SetDefaults()
if err := cfg.Validate(); err != nil {
return err
}
return Write(cfg)
}

// Validate ensures that the config is valid.
// This is called before writing it.
func (cfg *CLIConfig) Validate() error {
for _, track := range cfg.Tracks {
if err := track.CompileRegexes(); err != nil {
return err
}
}
return nil
}

// SetDefaults ensures that we have all the necessary settings for the CLI.
func (cfg *CLIConfig) SetDefaults() {
for _, track := range cfg.Tracks {
track.SetDefaults()
}
}

// Load reads a viper configuration into the config.
func (cfg *CLIConfig) Load(v *viper.Viper) error {
cfg.readIn(v)
return v.Unmarshal(&cfg)
}
89 changes: 89 additions & 0 deletions config/cli_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package config

import (
"io/ioutil"
"os"
"sort"
"testing"

"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
)

func TestCLIConfig(t *testing.T) {
dir, err := ioutil.TempDir("", "cli-config")
assert.NoError(t, err)
defer os.RemoveAll(dir)

cfg := &CLIConfig{
Config: New(dir, "cli"),
Tracks: Tracks{
"bogus": &Track{ID: "bogus"},
"fake": &Track{ID: "fake", IgnorePatterns: []string{"c", "b", "a"}},
},
}

// write it
err = cfg.Write()
assert.NoError(t, err)

// reload it
cfg = &CLIConfig{
Config: New(dir, "cli"),
}
err = cfg.Load(viper.New())
assert.NoError(t, err)
assert.Equal(t, "bogus", cfg.Tracks["bogus"].ID)
assert.Equal(t, "fake", cfg.Tracks["fake"].ID)

// The ignore patterns got sorted.
expected := append(defaultIgnorePatterns, "a", "b", "c")
sort.Strings(expected)
assert.Equal(t, expected, cfg.Tracks["fake"].IgnorePatterns)
}

func TestCLIConfigValidate(t *testing.T) {
cfg := &CLIConfig{
Tracks: Tracks{
"fake": &Track{
ID: "fake",
IgnorePatterns: []string{"(?=re)"}, // not a valid regex
},
},
}

err := cfg.Validate()
assert.Error(t, err)
}

func TestCLIConfigSetDefaults(t *testing.T) {
// No tracks, no defaults.
cfg := &CLIConfig{}
cfg.SetDefaults()
assert.Equal(t, &CLIConfig{}, cfg)

// With a track, gets defaults.
cfg = &CLIConfig{
Tracks: map[string]*Track{
"bogus": &Track{
ID: "bogus",
},
},
}
cfg.SetDefaults()
assert.Equal(t, defaultIgnorePatterns, cfg.Tracks["bogus"].IgnorePatterns)

// With partial defaults and extras, gets everything.
cfg = &CLIConfig{
Tracks: map[string]*Track{
"bogus": &Track{
ID: "bogus",
IgnorePatterns: []string{".solution.json", "_spec[.]ext$"},
},
},
}
cfg.SetDefaults()
expected := append(defaultIgnorePatterns, "_spec[.]ext$")
sort.Strings(expected)
assert.Equal(t, expected, cfg.Tracks["bogus"].IgnorePatterns)
}
67 changes: 67 additions & 0 deletions config/track.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package config

import (
"regexp"
"sort"
)

var defaultIgnorePatterns = []string{
".solution.json",
"README.md",
}

// Track holds the CLI-related settings for a track.
type Track struct {
ID string
IgnorePatterns []string
ignoreRegexes []*regexp.Regexp
}

func NewTrack(id string) *Track {
t := &Track{
ID: id,
}
t.SetDefaults()
return t
}

func (t *Track) SetDefaults() {
m := map[string]bool{}
for _, pattern := range t.IgnorePatterns {
m[pattern] = true
}
for _, pattern := range defaultIgnorePatterns {
if !m[pattern] {
t.IgnorePatterns = append(t.IgnorePatterns, pattern)
}
}
sort.Strings(t.IgnorePatterns)
}

func (t *Track) AcceptFilename(f string) (bool, error) {
if err := t.CompileRegexes(); err != nil {
return false, err
}

for _, re := range t.ignoreRegexes {
if re.MatchString(f) {
return false, nil
}
}
return true, nil
}

func (t *Track) CompileRegexes() error {
if len(t.ignoreRegexes) == len(t.IgnorePatterns) {
return nil
}

for _, pattern := range t.IgnorePatterns {
re, err := regexp.Compile(pattern)
if err != nil {
return err
}
t.ignoreRegexes = append(t.ignoreRegexes, re)
}
return nil
}
29 changes: 29 additions & 0 deletions config/track_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package config

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestTrackIgnoreString(t *testing.T) {
track := &Track{
IgnorePatterns: []string{
"con[.]txt",
"pro.f",
},
}

tests := map[string]bool{
"falcon.txt": false,
"beacon|txt": true,
"beacon.ext": true,
"proof": false,
}

for name, acceptable := range tests {
ok, err := track.AcceptFilename(name)
assert.NoError(t, err, name)
assert.Equal(t, acceptable, ok, name)
}
}
4 changes: 4 additions & 0 deletions config/tracks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package config

// Tracks is a collection of track-specific settings.
type Tracks map[string]*Track

0 comments on commit 9723324

Please sign in to comment.