Make config list less fallible
This commit is contained in:
parent
8a82e3a856
commit
8a4f32b4e2
6 changed files with 280 additions and 106 deletions
|
|
@ -42,7 +42,7 @@ type cfg struct {
|
|||
cfg *ghConfig.Config
|
||||
}
|
||||
|
||||
func (c *cfg) Get(hostname, key string) o.Option[string] {
|
||||
func (c *cfg) get(hostname, key string) o.Option[string] {
|
||||
if hostname != "" {
|
||||
val, err := c.cfg.Get([]string{hostsKey, hostname, key})
|
||||
if err == nil {
|
||||
|
|
@ -59,7 +59,7 @@ func (c *cfg) Get(hostname, key string) o.Option[string] {
|
|||
}
|
||||
|
||||
func (c *cfg) GetOrDefault(hostname, key string) o.Option[string] {
|
||||
if val := c.Get(hostname, key); val.IsSome() {
|
||||
if val := c.get(hostname, key); val.IsSome() {
|
||||
return val
|
||||
}
|
||||
|
||||
|
|
@ -126,7 +126,7 @@ func (c *cfg) Prompt(hostname string) string {
|
|||
}
|
||||
|
||||
func (c *cfg) Version() o.Option[string] {
|
||||
return c.Get("", versionKey)
|
||||
return c.get("", versionKey)
|
||||
}
|
||||
|
||||
func (c *cfg) Migrate(m gh.Migration) error {
|
||||
|
|
@ -513,6 +513,7 @@ type ConfigOption struct {
|
|||
Description string
|
||||
DefaultValue string
|
||||
AllowedValues []string
|
||||
CurrentValue func(c gh.Config, hostname string) string
|
||||
}
|
||||
|
||||
func ConfigOptions() []ConfigOption {
|
||||
|
|
@ -522,32 +523,50 @@ func ConfigOptions() []ConfigOption {
|
|||
Description: "the protocol to use for git clone and push operations",
|
||||
DefaultValue: "https",
|
||||
AllowedValues: []string{"https", "ssh"},
|
||||
CurrentValue: func(c gh.Config, hostname string) string {
|
||||
return c.GitProtocol(hostname)
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: editorKey,
|
||||
Description: "the text editor program to use for authoring text",
|
||||
DefaultValue: "",
|
||||
CurrentValue: func(c gh.Config, hostname string) string {
|
||||
return c.Editor(hostname)
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: promptKey,
|
||||
Description: "toggle interactive prompting in the terminal",
|
||||
DefaultValue: "enabled",
|
||||
AllowedValues: []string{"enabled", "disabled"},
|
||||
CurrentValue: func(c gh.Config, hostname string) string {
|
||||
return c.Prompt(hostname)
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: pagerKey,
|
||||
Description: "the terminal pager program to send standard output to",
|
||||
DefaultValue: "",
|
||||
CurrentValue: func(c gh.Config, hostname string) string {
|
||||
return c.Pager(hostname)
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: httpUnixSocketKey,
|
||||
Description: "the path to a Unix socket through which to make an HTTP connection",
|
||||
DefaultValue: "",
|
||||
CurrentValue: func(c gh.Config, hostname string) string {
|
||||
return c.HTTPUnixSocket(hostname)
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: browserKey,
|
||||
Description: "the web browser to use for opening URLs",
|
||||
DefaultValue: "",
|
||||
CurrentValue: func(c gh.Config, hostname string) string {
|
||||
return c.Browser(hostname)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,67 +32,6 @@ func TestNewConfigProvidesFallback(t *testing.T) {
|
|||
requireKeyWithValue(t, spiedCfg, []string{browserKey}, "")
|
||||
}
|
||||
|
||||
func TestGetNonExistentKey(t *testing.T) {
|
||||
// Given we have no top level configuration
|
||||
cfg := newTestConfig()
|
||||
|
||||
// When we get a key that has no value
|
||||
optionalVal := cfg.Get("", "non-existent-key")
|
||||
|
||||
// Then it returns a None variant
|
||||
require.True(t, optionalVal.IsNone(), "expected there to be no value")
|
||||
}
|
||||
|
||||
func TestGetNonExistentHostSpecificKey(t *testing.T) {
|
||||
// Given have no top level configuration
|
||||
cfg := newTestConfig()
|
||||
|
||||
// When we get a key for a host that has no value
|
||||
optionalVal := cfg.Get("non-existent-host", "non-existent-key")
|
||||
|
||||
// Then it returns a None variant
|
||||
require.True(t, optionalVal.IsNone(), "expected there to be no value")
|
||||
}
|
||||
|
||||
func TestGetExistingTopLevelKey(t *testing.T) {
|
||||
// Given have a top level config entry
|
||||
cfg := newTestConfig()
|
||||
cfg.Set("", "top-level-key", "top-level-value")
|
||||
|
||||
// When we get that key
|
||||
optionalVal := cfg.Get("non-existent-host", "top-level-key")
|
||||
|
||||
// Then it returns a Some variant containing the correct value
|
||||
require.True(t, optionalVal.IsSome(), "expected there to be a value")
|
||||
require.Equal(t, "top-level-value", optionalVal.Unwrap())
|
||||
}
|
||||
|
||||
func TestGetExistingHostSpecificKey(t *testing.T) {
|
||||
// Given have a host specific config entry
|
||||
cfg := newTestConfig()
|
||||
cfg.Set("github.com", "host-specific-key", "host-specific-value")
|
||||
|
||||
// When we get that key
|
||||
optionalVal := cfg.Get("github.com", "host-specific-key")
|
||||
|
||||
// Then it returns a Some variant containing the correct value
|
||||
require.True(t, optionalVal.IsSome(), "expected there to be a value")
|
||||
require.Equal(t, "host-specific-value", optionalVal.Unwrap())
|
||||
}
|
||||
|
||||
func TestGetHostnameSpecificKeyFallsBackToTopLevel(t *testing.T) {
|
||||
// Given have a top level config entry
|
||||
cfg := newTestConfig()
|
||||
cfg.Set("", "key", "value")
|
||||
|
||||
// When we get that key on a specific host
|
||||
optionalVal := cfg.Get("github.com", "key")
|
||||
|
||||
// Then it returns a Some variant containing the correct value by falling back to the top level config
|
||||
require.True(t, optionalVal.IsSome(), "expected there to be a value")
|
||||
require.Equal(t, "value", optionalVal.Unwrap())
|
||||
}
|
||||
|
||||
func TestGetOrDefaultApplicationDefaults(t *testing.T) {
|
||||
tests := []struct {
|
||||
key string
|
||||
|
|
@ -121,29 +60,65 @@ func TestGetOrDefaultApplicationDefaults(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGetOrDefaultExistingKey(t *testing.T) {
|
||||
// Given have a top level config entry
|
||||
func TestGetOrDefaultNonExistentKey(t *testing.T) {
|
||||
// Given we have no top level configuration
|
||||
cfg := newTestConfig()
|
||||
cfg.Set("", gitProtocolKey, "ssh")
|
||||
|
||||
// When we get that key
|
||||
optionalVal := cfg.GetOrDefault("", gitProtocolKey)
|
||||
// When we get a key that has no value
|
||||
optionalVal := cfg.GetOrDefault("", "non-existent-key")
|
||||
|
||||
// Then it returns successfully with the correct value, and doesn't fall back
|
||||
// to the default
|
||||
require.True(t, optionalVal.IsSome(), "expected there to be a value")
|
||||
require.Equal(t, "ssh", optionalVal.Unwrap())
|
||||
// Then it returns a None variant
|
||||
require.True(t, optionalVal.IsNone(), "expected there to be no value")
|
||||
}
|
||||
|
||||
func TestGetOrDefaultNotFoundAndNoDefault(t *testing.T) {
|
||||
// Given have no configuration
|
||||
func TestGetOrDefaultNonExistentHostSpecificKey(t *testing.T) {
|
||||
// Given have no top level configuration
|
||||
cfg := newTestConfig()
|
||||
|
||||
// When we get a non-existent-key that has no default
|
||||
optionalEntry := cfg.GetOrDefault("", "non-existent-key")
|
||||
// When we get a key for a host that has no value
|
||||
optionalVal := cfg.GetOrDefault("non-existent-host", "non-existent-key")
|
||||
|
||||
// Then it returns with no entry
|
||||
require.False(t, optionalEntry.IsSome(), "expected the config to not contain a value")
|
||||
// Then it returns a None variant
|
||||
require.True(t, optionalVal.IsNone(), "expected there to be no value")
|
||||
}
|
||||
|
||||
func TestGetOrDefaultExistingTopLevelKey(t *testing.T) {
|
||||
// Given have a top level config entry
|
||||
cfg := newTestConfig()
|
||||
cfg.Set("", "top-level-key", "top-level-value")
|
||||
|
||||
// When we get that key
|
||||
optionalVal := cfg.GetOrDefault("non-existent-host", "top-level-key")
|
||||
|
||||
// Then it returns a Some variant containing the correct value
|
||||
require.True(t, optionalVal.IsSome(), "expected there to be a value")
|
||||
require.Equal(t, "top-level-value", optionalVal.Unwrap())
|
||||
}
|
||||
|
||||
func TestGetOrDefaultExistingHostSpecificKey(t *testing.T) {
|
||||
// Given have a host specific config entry
|
||||
cfg := newTestConfig()
|
||||
cfg.Set("github.com", "host-specific-key", "host-specific-value")
|
||||
|
||||
// When we get that key
|
||||
optionalVal := cfg.GetOrDefault("github.com", "host-specific-key")
|
||||
|
||||
// Then it returns a Some variant containing the correct value
|
||||
require.True(t, optionalVal.IsSome(), "expected there to be a value")
|
||||
require.Equal(t, "host-specific-value", optionalVal.Unwrap())
|
||||
}
|
||||
|
||||
func TestGetOrDefaultHostnameSpecificKeyFallsBackToTopLevel(t *testing.T) {
|
||||
// Given have a top level config entry
|
||||
cfg := newTestConfig()
|
||||
cfg.Set("", "key", "value")
|
||||
|
||||
// When we get that key on a specific host
|
||||
optionalVal := cfg.GetOrDefault("github.com", "key")
|
||||
|
||||
// Then it returns a Some variant containing the correct value by falling back to the top level config
|
||||
require.True(t, optionalVal.IsSome(), "expected there to be a value")
|
||||
require.Equal(t, "value", optionalVal.Unwrap())
|
||||
}
|
||||
|
||||
func TestFallbackConfig(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -57,13 +57,8 @@ func listRun(opts *ListOptions) error {
|
|||
|
||||
configOptions := config.ConfigOptions()
|
||||
|
||||
for _, key := range configOptions {
|
||||
optionalValue := cfg.GetOrDefault(host, key.Key)
|
||||
if optionalValue.IsNone() {
|
||||
return fmt.Errorf("invalid key: %s", key.Key)
|
||||
}
|
||||
|
||||
fmt.Fprintf(opts.IO.Out, "%s=%s\n", key.Key, optionalValue.Unwrap())
|
||||
for _, option := range configOptions {
|
||||
fmt.Fprintf(opts.IO.Out, "%s=%s\n", option.Key, option.CurrentValue(cfg, host))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewCmdConfigList(t *testing.T) {
|
||||
|
|
@ -108,9 +109,8 @@ browser=brave
|
|||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := listRun(tt.input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.stdout, stdout.String())
|
||||
//assert.Equal(t, tt.stderr, stderr.String())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.stdout, stdout.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,24 @@
|
|||
// MIT License
|
||||
|
||||
// Copyright (c) 2022 Tom Godkin
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
package o
|
||||
|
||||
import "fmt"
|
||||
|
|
@ -76,15 +97,6 @@ func (o Option[T]) IsSome() bool {
|
|||
return o.present
|
||||
}
|
||||
|
||||
// IsSome returns true if the [Option] is a [Some] variant and the value inside of it equals the provided value.
|
||||
// func (o Option[T]) Is(t T) bool {
|
||||
// return o.present && o.value == t
|
||||
// }
|
||||
|
||||
func (o Option[T]) IsSomeAnd(f func(T) bool) bool {
|
||||
return o.present && f(o.value)
|
||||
}
|
||||
|
||||
// IsNone returns true if the [Option] is a [None] variant.
|
||||
func (o Option[T]) IsNone() bool {
|
||||
return !o.present
|
||||
|
|
@ -105,11 +117,3 @@ func (o Option[T]) Expect(message string) T {
|
|||
|
||||
panic(message)
|
||||
}
|
||||
|
||||
func Map[T any, U any](f func(T) U, o Option[T]) Option[U] {
|
||||
if o.present {
|
||||
return Some(f(o.value))
|
||||
}
|
||||
|
||||
return None[U]()
|
||||
}
|
||||
|
|
|
|||
181
pkg/option/option_test.go
Normal file
181
pkg/option/option_test.go
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
// MIT License
|
||||
|
||||
// Copyright (c) 2022 Tom Godkin
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
package o_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
o "github.com/cli/cli/v2/pkg/option"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func ExampleOption_Unwrap() {
|
||||
fmt.Println(o.Some(4).Unwrap())
|
||||
// Output: 4
|
||||
}
|
||||
|
||||
func ExampleOption_UnwrapOr() {
|
||||
fmt.Println(o.Some(4).UnwrapOr(3))
|
||||
fmt.Println(o.None[int]().UnwrapOr(3))
|
||||
// Output:
|
||||
// 4
|
||||
// 3
|
||||
}
|
||||
|
||||
func ExampleOption_UnwrapOrElse() {
|
||||
fmt.Println(o.Some(4).UnwrapOrElse(func() int {
|
||||
return 3
|
||||
}))
|
||||
|
||||
fmt.Println(o.None[int]().UnwrapOrElse(func() int {
|
||||
return 3
|
||||
}))
|
||||
|
||||
// Output:
|
||||
// 4
|
||||
// 3
|
||||
}
|
||||
|
||||
func ExampleOption_UnwrapOrZero() {
|
||||
fmt.Println(o.Some(4).UnwrapOrZero())
|
||||
fmt.Println(o.None[int]().UnwrapOrZero())
|
||||
|
||||
// Output
|
||||
// 4
|
||||
// 0
|
||||
}
|
||||
|
||||
func ExampleOption_IsSome() {
|
||||
fmt.Println(o.Some(4).IsSome())
|
||||
fmt.Println(o.None[int]().IsSome())
|
||||
|
||||
// Output:
|
||||
// true
|
||||
// false
|
||||
}
|
||||
|
||||
func ExampleOption_IsNone() {
|
||||
fmt.Println(o.Some(4).IsNone())
|
||||
fmt.Println(o.None[int]().IsNone())
|
||||
|
||||
// Output:
|
||||
// false
|
||||
// true
|
||||
}
|
||||
|
||||
func ExampleOption_Value() {
|
||||
value, ok := o.Some(4).Value()
|
||||
fmt.Println(value)
|
||||
fmt.Println(ok)
|
||||
|
||||
// Output:
|
||||
// 4
|
||||
// true
|
||||
}
|
||||
|
||||
func ExampleOption_Expect() {
|
||||
fmt.Println(o.Some(4).Expect("oops"))
|
||||
|
||||
// Output: 4
|
||||
}
|
||||
|
||||
func TestSomeStringer(t *testing.T) {
|
||||
require.Equal(t, fmt.Sprintf("%s", o.Some("foo")), "Some(foo)") //nolint:gosimple
|
||||
require.Equal(t, fmt.Sprintf("%s", o.Some(42)), "Some(42)") //nolint:gosimple
|
||||
}
|
||||
|
||||
func TestNoneStringer(t *testing.T) {
|
||||
require.Equal(t, fmt.Sprintf("%s", o.None[string]()), "None") //nolint:gosimple
|
||||
}
|
||||
|
||||
func TestSomeUnwrap(t *testing.T) {
|
||||
require.Equal(t, o.Some(42).Unwrap(), 42)
|
||||
}
|
||||
|
||||
func TestNoneUnwrap(t *testing.T) {
|
||||
defer func() {
|
||||
require.Equal(t, fmt.Sprint(recover()), "called `Option.Unwrap()` on a `None` value")
|
||||
}()
|
||||
|
||||
o.None[string]().Unwrap()
|
||||
t.Error("did not panic")
|
||||
}
|
||||
|
||||
func TestSomeUnwrapOr(t *testing.T) {
|
||||
require.Equal(t, o.Some(42).UnwrapOr(3), 42)
|
||||
}
|
||||
|
||||
func TestNoneUnwrapOr(t *testing.T) {
|
||||
require.Equal(t, o.None[int]().UnwrapOr(3), 3)
|
||||
}
|
||||
|
||||
func TestSomeUnwrapOrElse(t *testing.T) {
|
||||
require.Equal(t, o.Some(42).UnwrapOrElse(func() int { return 41 }), 42)
|
||||
}
|
||||
|
||||
func TestNoneUnwrapOrElse(t *testing.T) {
|
||||
require.Equal(t, o.None[int]().UnwrapOrElse(func() int { return 41 }), 41)
|
||||
}
|
||||
|
||||
func TestSomeUnwrapOrZero(t *testing.T) {
|
||||
require.Equal(t, o.Some(42).UnwrapOrZero(), 42)
|
||||
}
|
||||
|
||||
func TestNoneUnwrapOrZero(t *testing.T) {
|
||||
require.Equal(t, o.None[int]().UnwrapOrZero(), 0)
|
||||
}
|
||||
|
||||
func TestIsSome(t *testing.T) {
|
||||
require.True(t, o.Some(42).IsSome())
|
||||
require.False(t, o.None[int]().IsSome())
|
||||
}
|
||||
|
||||
func TestIsNone(t *testing.T) {
|
||||
require.False(t, o.Some(42).IsNone())
|
||||
require.True(t, o.None[int]().IsNone())
|
||||
}
|
||||
|
||||
func TestSomeValue(t *testing.T) {
|
||||
value, ok := o.Some(42).Value()
|
||||
require.Equal(t, value, 42)
|
||||
require.True(t, ok)
|
||||
}
|
||||
|
||||
func TestNoneValue(t *testing.T) {
|
||||
value, ok := o.None[int]().Value()
|
||||
require.Equal(t, value, 0)
|
||||
require.False(t, ok)
|
||||
}
|
||||
|
||||
func TestSomeExpect(t *testing.T) {
|
||||
require.Equal(t, o.Some(42).Expect("oops"), 42)
|
||||
}
|
||||
|
||||
func TestNoneExpect(t *testing.T) {
|
||||
defer func() {
|
||||
require.Equal(t, fmt.Sprint(recover()), "oops")
|
||||
}()
|
||||
|
||||
o.None[int]().Expect("oops")
|
||||
t.Error("did not panic")
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue