Refactor user config

This commit is contained in:
Mickaël Menu 2020-12-31 13:44:17 +01:00
parent 6a4a4c77e6
commit 4d0c0ccdda
No known key found for this signature in database
GPG Key ID: 53D73664CD359895
7 changed files with 479 additions and 379 deletions

View File

@ -1,40 +1,185 @@
package zk
import (
"path/filepath"
"github.com/hashicorp/hcl/v2/hclsimple"
"github.com/mickael-menu/zk/util/errors"
"github.com/mickael-menu/zk/util/opt"
)
// config holds the user configuration of a slip box.
type config struct {
Filename string `hcl:"filename,optional"`
Template string `hcl:"template,optional"`
ID *idConfig `hcl:"id,block"`
Editor string `hcl:"editor,optional"`
Dirs []dirConfig `hcl:"dir,block"`
Extra map[string]string `hcl:"extra,optional"`
// Config holds the global user configuration.
type Config struct {
DirConfig
Dirs map[string]DirConfig
Editor opt.String
}
type dirConfig struct {
// DirConfig holds the user configuration for a given directory.
type DirConfig struct {
FilenameTemplate string
BodyTemplatePath opt.String
IDOptions IDOptions
Extra map[string]string
}
// ConfigOverrides holds user configuration overriden values, for example fed
// from CLI flags.
type ConfigOverrides struct {
BodyTemplatePath opt.String
Extra map[string]string
}
// ParseConfig creates a new Config instance from its HCL representation.
// templatesDir is the base path for the relative templates.
func ParseConfig(content []byte, templatesDir string) (*Config, error) {
var hcl hclConfig
err := hclsimple.Decode(".zk/config.hcl", content, nil, &hcl)
if err != nil {
return nil, errors.Wrap(err, "failed to read config")
}
root := DirConfig{
FilenameTemplate: "{{id}}",
BodyTemplatePath: opt.NullString,
IDOptions: IDOptions{
Charset: CharsetAlphanum,
Length: 5,
Case: CaseLower,
},
Extra: make(map[string]string),
}
if hcl.Filename != "" {
root.FilenameTemplate = hcl.Filename
}
if hcl.Template != "" {
root.BodyTemplatePath = templatePathFromString(hcl.Template, templatesDir)
}
if hcl.ID != nil {
if hcl.ID.Length != 0 {
root.IDOptions.Length = hcl.ID.Length
}
if hcl.ID.Charset != "" {
root.IDOptions.Charset = charsetFromString(hcl.ID.Charset)
}
if hcl.ID.Case != "" {
root.IDOptions.Case = caseFromString(hcl.ID.Case)
}
}
if hcl.Extra != nil {
for k, v := range hcl.Extra {
root.Extra[k] = v
}
}
config := Config{
DirConfig: root,
Dirs: make(map[string]DirConfig),
Editor: opt.NewNotEmptyString(hcl.Editor),
}
for _, dirHCL := range hcl.Dirs {
config.Dirs[dirHCL.Dir] = root.merge(dirHCL, templatesDir)
}
return &config, nil
}
func (c DirConfig) merge(hcl hclDirConfig, templatesDir string) DirConfig {
res := DirConfig{
FilenameTemplate: c.FilenameTemplate,
BodyTemplatePath: c.BodyTemplatePath,
IDOptions: c.IDOptions,
Extra: make(map[string]string),
}
for k, v := range c.Extra {
res.Extra[k] = v
}
if hcl.Filename != "" {
res.FilenameTemplate = hcl.Filename
}
if hcl.Template != "" {
res.BodyTemplatePath = templatePathFromString(hcl.Template, templatesDir)
}
if hcl.ID != nil {
if hcl.ID.Length != 0 {
res.IDOptions.Length = hcl.ID.Length
}
if hcl.ID.Charset != "" {
res.IDOptions.Charset = charsetFromString(hcl.ID.Charset)
}
if hcl.ID.Case != "" {
res.IDOptions.Case = caseFromString(hcl.ID.Case)
}
}
if hcl.Extra != nil {
for k, v := range hcl.Extra {
res.Extra[k] = v
}
}
return res
}
// hclConfig holds the HCL representation of Config
type hclConfig struct {
Filename string `hcl:"filename,optional"`
Template string `hcl:"template,optional"`
ID *hclIDConfig `hcl:"id,block"`
Extra map[string]string `hcl:"extra,optional"`
Dirs []hclDirConfig `hcl:"dir,block"`
Editor string `hcl:"editor,optional"`
}
type hclDirConfig struct {
Dir string `hcl:"dir,label"`
Filename string `hcl:"filename,optional"`
Template string `hcl:"template,optional"`
ID *idConfig `hcl:"id,block"`
ID *hclIDConfig `hcl:"id,block"`
Extra map[string]string `hcl:"extra,optional"`
}
type idConfig struct {
type hclIDConfig struct {
Charset string `hcl:"charset,optional"`
Length int `hcl:"length,optional"`
Case string `hcl:"case,optional"`
}
// parseConfig creates a new Config instance from its HCL representation.
func parseConfig(content []byte) (*config, error) {
var config config
err := hclsimple.Decode(".zk/config.hcl", content, nil, &config)
if err != nil {
return nil, errors.Wrap(err, "failed to read config")
func charsetFromString(charset string) Charset {
switch charset {
case "alphanum":
return CharsetAlphanum
case "hex":
return CharsetHex
case "letters":
return CharsetLetters
case "numbers":
return CharsetNumbers
default:
return Charset(charset)
}
return &config, nil
}
func caseFromString(c string) Case {
switch c {
case "lower":
return CaseLower
case "upper":
return CaseUpper
case "mixed":
return CaseMixed
default:
return CaseLower
}
}
func templatePathFromString(template string, templatesDir string) opt.String {
if template == "" {
return opt.NullString
}
if !filepath.IsAbs(template) {
template = filepath.Join(templatesDir, template)
}
return opt.NewString(template)
}

View File

@ -1,20 +1,43 @@
package zk
import (
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/mickael-menu/zk/util/assert"
"github.com/mickael-menu/zk/util/opt"
)
func TestParseMinimal(t *testing.T) {
conf, err := parseConfig([]byte(""))
func TestParseDefaultConfig(t *testing.T) {
conf, err := ParseConfig([]byte(""), "")
assert.Nil(t, err)
assert.Equal(t, conf, &config{})
assert.Equal(t, conf, &Config{
Editor: opt.NullString,
DirConfig: DirConfig{
FilenameTemplate: "{{id}}",
BodyTemplatePath: opt.NullString,
IDOptions: IDOptions{
Length: 5,
Charset: CharsetAlphanum,
Case: CaseLower,
},
Extra: make(map[string]string),
},
Dirs: make(map[string]DirConfig),
})
}
func TestParseInvalidConfig(t *testing.T) {
conf, err := ParseConfig([]byte("unknown = 'value'"), "")
assert.NotNil(t, err)
assert.Nil(t, conf)
}
func TestParseComplete(t *testing.T) {
conf, err := parseConfig([]byte(`
conf, err := ParseConfig([]byte(`
// Comment
editor = "vim"
filename = "{{id}}.note"
@ -40,41 +63,179 @@ func TestParseComplete(t *testing.T) {
log-ext = "value"
}
}
`))
dir "ref" {
filename = "{{slug title}}.md"
}
`), "")
assert.Nil(t, err)
assert.Equal(t, conf, &config{
Filename: "{{id}}.note",
Template: "default.note",
ID: &idConfig{
Charset: "alphanum",
Length: 4,
Case: "lower",
},
Editor: "vim",
Dirs: []dirConfig{
{
Dir: "log",
Filename: "{{date}}.md",
Template: "log.md",
ID: &idConfig{
Charset: "letters",
Length: 8,
Case: "mixed",
},
Extra: map[string]string{"log-ext": "value"},
assert.Equal(t, conf, &Config{
DirConfig: DirConfig{
FilenameTemplate: "{{id}}.note",
BodyTemplatePath: opt.NewString("default.note"),
IDOptions: IDOptions{
Length: 4,
Charset: CharsetAlphanum,
Case: CaseLower,
},
Extra: map[string]string{
"hello": "world",
"salut": "le monde",
},
},
Extra: map[string]string{
"hello": "world",
"salut": "le monde",
Dirs: map[string]DirConfig{
"log": {
FilenameTemplate: "{{date}}.md",
BodyTemplatePath: opt.NewString("log.md"),
IDOptions: IDOptions{
Length: 8,
Charset: CharsetLetters,
Case: CaseMixed,
},
Extra: map[string]string{
"hello": "world",
"salut": "le monde",
"log-ext": "value",
},
},
"ref": {
FilenameTemplate: "{{slug title}}.md",
BodyTemplatePath: opt.NewString("default.note"),
IDOptions: IDOptions{
Length: 4,
Charset: CharsetAlphanum,
Case: CaseLower,
},
Extra: map[string]string{
"hello": "world",
"salut": "le monde",
},
},
},
Editor: opt.NewString("vim"),
})
}
func TestParseMergesDirConfig(t *testing.T) {
conf, err := ParseConfig([]byte(`
filename = "root-filename"
template = "root-template"
id {
charset = "letters"
length = 42
case = "upper"
}
extra = {
hello = "world"
salut = "le monde"
}
dir "log" {
filename = "log-filename"
template = "log-template"
id {
charset = "numbers"
length = 8
case = "mixed"
}
extra = {
hello = "override"
log-ext = "value"
}
}
dir "inherited" {}
`), "")
assert.Nil(t, err)
assert.Equal(t, conf, &Config{
DirConfig: DirConfig{
FilenameTemplate: "root-filename",
BodyTemplatePath: opt.NewString("root-template"),
IDOptions: IDOptions{
Length: 42,
Charset: CharsetLetters,
Case: CaseUpper,
},
Extra: map[string]string{
"hello": "world",
"salut": "le monde",
},
},
Dirs: map[string]DirConfig{
"log": {
FilenameTemplate: "log-filename",
BodyTemplatePath: opt.NewString("log-template"),
IDOptions: IDOptions{
Length: 8,
Charset: CharsetNumbers,
Case: CaseMixed,
},
Extra: map[string]string{
"hello": "override",
"salut": "le monde",
"log-ext": "value",
},
},
"inherited": {
FilenameTemplate: "root-filename",
BodyTemplatePath: opt.NewString("root-template"),
IDOptions: IDOptions{
Length: 42,
Charset: CharsetLetters,
Case: CaseUpper,
},
Extra: map[string]string{
"hello": "world",
"salut": "le monde",
},
},
},
})
}
func TestParseInvalidConfig(t *testing.T) {
conf, err := parseConfig([]byte("unknown = 'value'"))
func TestParseIDCharset(t *testing.T) {
test := func(charset string, expected Charset) {
hcl := fmt.Sprintf(`id { charset = "%v" }`, charset)
conf, err := ParseConfig([]byte(hcl), "")
assert.Nil(t, err)
if !cmp.Equal(conf.IDOptions.Charset, expected) {
t.Errorf("Didn't parse ID charset `%v` as expected", charset)
}
}
assert.NotNil(t, err)
assert.Nil(t, conf)
test("alphanum", CharsetAlphanum)
test("hex", CharsetHex)
test("letters", CharsetLetters)
test("numbers", CharsetNumbers)
test("HEX", []rune("HEX")) // case sensitive
test("custom", []rune("custom"))
}
func TestParseIDCase(t *testing.T) {
test := func(letterCase string, expected Case) {
hcl := fmt.Sprintf(`id { case = "%v" }`, letterCase)
conf, err := ParseConfig([]byte(hcl), "")
assert.Nil(t, err)
if !cmp.Equal(conf.IDOptions.Case, expected) {
t.Errorf("Didn't parse ID case `%v` as expected", letterCase)
}
}
test("lower", CaseLower)
test("upper", CaseUpper)
test("mixed", CaseMixed)
test("unknown", CaseLower)
}
func TestParseResolvesTemplatePaths(t *testing.T) {
test := func(template string, expected string) {
hcl := fmt.Sprintf(`template = "%v"`, template)
conf, err := ParseConfig([]byte(hcl), "/test/.zk/templates")
assert.Nil(t, err)
if !cmp.Equal(conf.BodyTemplatePath, opt.NewString(expected)) {
t.Errorf("Didn't resolve template `%v` as expected: %v", template, conf.BodyTemplatePath)
}
}
test("template.tpl", "/test/.zk/templates/template.tpl")
test("/abs/template.tpl", "/abs/template.tpl")
}

View File

@ -7,7 +7,7 @@ import (
"path/filepath"
"github.com/mickael-menu/zk/util/errors"
"github.com/mickael-menu/zk/util/opt"
"github.com/mickael-menu/zk/util/paths"
)
const defaultConfig = `editor = "nvim"
@ -20,8 +20,8 @@ dir "log" {
type Zk struct {
// Slip box root path.
Path string
// User configuration parsed from .zk/config.hsl.
config config
// Global user configuration.
Config Config
}
// Dir represents a directory inside a slip box.
@ -30,6 +30,8 @@ type Dir struct {
Name string
// Absolute path to the directory.
Path string
// User configuration for this directory.
Config DirConfig
}
// Open locates a slip box at the given path and parses its configuration.
@ -50,14 +52,15 @@ func Open(path string) (*Zk, error) {
return nil, wrap(err)
}
config, err := parseConfig(configContent)
templatesDir := filepath.Join(path, ".zk/templates")
config, err := ParseConfig(configContent, templatesDir)
if err != nil {
return nil, wrap(err)
}
return &Zk{
Path: path,
config: *config,
Config: *config,
}, nil
}
@ -104,29 +107,20 @@ func locateRoot(path string) (string, error) {
if currentPath == "/" || currentPath == "." {
return "", fmt.Errorf("no slip box found in %v or a parent directory", path)
}
if dotPath := filepath.Join(currentPath, ".zk"); dirExists(dotPath) {
exists, err := paths.DirExists(filepath.Join(currentPath, ".zk"))
switch {
case err != nil:
return "", err
case exists:
return currentPath, nil
default:
return locate(filepath.Dir(currentPath))
}
return locate(filepath.Dir(currentPath))
}
return locate(path)
}
func dirExists(path string) bool {
fi, err := os.Stat(path)
if err != nil {
return false
}
switch mode := fi.Mode(); {
case mode.IsDir():
return true
default:
return false
}
}
// DirAt creates a Dir representation of the slip box directory at the given path.
func (zk *Zk) DirAt(path string) (*Dir, error) {
wrap := errors.Wrapperf("%v: not a valid slip box directory", path)
@ -141,132 +135,32 @@ func (zk *Zk) DirAt(path string) (*Dir, error) {
return nil, wrap(err)
}
config, ok := zk.Config.Dirs[name]
if !ok {
// Fallback on root config.
config = zk.Config.DirConfig
}
return &Dir{
Name: name,
Path: path,
Name: name,
Path: path,
Config: config,
}, nil
}
// FilenameTemplate returns the filename template for the notes in the given directory.
func (zk *Zk) FilenameTemplate(dir Dir) string {
dirConfig := zk.dirConfig(dir)
switch {
case dirConfig != nil && dirConfig.Filename != "":
return dirConfig.Filename
case zk.config.Filename != "":
return zk.config.Filename
default:
return "{{id}}"
// RequiredDirAt is the same as DirAt, but checks that the directory exists
// before returning the Dir.
func (zk *Zk) RequireDirAt(path string) (*Dir, error) {
dir, err := zk.DirAt(path)
if err != nil {
return nil, err
}
}
// Template returns the file template to use for the notes in the given directory.
func (zk *Zk) Template(dir Dir) opt.String {
dirConfig := zk.dirConfig(dir)
var template string
switch {
case dirConfig != nil && dirConfig.Template != "":
template = dirConfig.Template
case zk.config.Template != "":
template = zk.config.Template
}
if template == "" {
return opt.NullString
}
if !filepath.IsAbs(template) {
template = filepath.Join(zk.Path, ".zk/templates", template)
}
return opt.NewString(template)
}
// IDOptions returns the options to use to generate an ID for the given directory.
func (zk *Zk) IDOptions(dir Dir) IDOptions {
toCharset := func(charset string) Charset {
switch charset {
case "alphanum":
return CharsetAlphanum
case "hex":
return CharsetHex
case "letters":
return CharsetLetters
case "numbers":
return CharsetNumbers
default:
return Charset(charset)
}
}
toCase := func(c string) Case {
switch c {
case "lower":
return CaseLower
case "upper":
return CaseUpper
case "mixed":
return CaseMixed
default:
return CaseLower
}
}
// Default options
opts := IDOptions{
Charset: CharsetAlphanum,
Length: 5,
Case: CaseLower,
}
merge := func(more *idConfig) {
if more.Charset != "" {
opts.Charset = toCharset(more.Charset)
}
if more.Length > 0 {
opts.Length = more.Length
}
if more.Case != "" {
opts.Case = toCase(more.Case)
}
}
if root := zk.config.ID; root != nil {
merge(root)
}
if dir := zk.dirConfig(dir); dir != nil && dir.ID != nil {
merge(dir.ID)
}
return opts
}
// Extra returns the extra variables for the given directory.
func (zk *Zk) Extra(dir Dir) map[string]string {
extra := make(map[string]string)
for k, v := range zk.config.Extra {
extra[k] = v
}
if dirConfig := zk.dirConfig(dir); dirConfig != nil {
for k, v := range dirConfig.Extra {
extra[k] = v
}
}
return extra
}
// dirConfig returns the dirConfig instance for the given directory.
func (zk *Zk) dirConfig(dir Dir) *dirConfig {
for _, dirConfig := range zk.config.Dirs {
if dirConfig.Dir == dir.Name {
return &dirConfig
}
}
return nil
exists, err := paths.Exists(dir.Path)
if err != nil {
return nil, err
}
if !exists {
return nil, fmt.Errorf("%v: directory not found", path)
}
return dir, nil
}

View File

@ -5,12 +5,11 @@ import (
"path/filepath"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/mickael-menu/zk/util/assert"
"github.com/mickael-menu/zk/util/opt"
)
func TestDirAt(t *testing.T) {
func TestDirAtGivenPath(t *testing.T) {
// The tests are relative to the working directory, for convenience.
wd, err := os.Getwd()
assert.Nil(t, err)
@ -31,190 +30,43 @@ func TestDirAt(t *testing.T) {
}
}
func TestDefaultFilenameTemplate(t *testing.T) {
zk := &Zk{}
assert.Equal(t, zk.FilenameTemplate(dir("")), "{{id}}")
assert.Equal(t, zk.FilenameTemplate(dir(".")), "{{id}}")
assert.Equal(t, zk.FilenameTemplate(dir("unknown")), "{{id}}")
}
func TestCustomFilenameTemplate(t *testing.T) {
zk := &Zk{config: config{
Filename: "root-filename",
Dirs: []dirConfig{
{
Dir: "log",
Filename: "log-filename",
},
},
}}
assert.Equal(t, zk.FilenameTemplate(dir("")), "root-filename")
assert.Equal(t, zk.FilenameTemplate(dir(".")), "root-filename")
assert.Equal(t, zk.FilenameTemplate(dir("unknown")), "root-filename")
assert.Equal(t, zk.FilenameTemplate(dir("log")), "log-filename")
}
func TestDefaultTemplate(t *testing.T) {
zk := &Zk{}
assert.Equal(t, zk.Template(dir("")), opt.NullString)
assert.Equal(t, zk.Template(dir(".")), opt.NullString)
assert.Equal(t, zk.Template(dir("unknown")), opt.NullString)
}
func TestCustomTemplate(t *testing.T) {
zk := &Zk{
// When requesting the root directory `.`, the config is the default one.
func TestDirAtRoot(t *testing.T) {
zk := Zk{
Path: "/test",
config: config{
Template: "root.tpl",
Dirs: []dirConfig{
{
Dir: "log",
Template: "log.tpl",
Config: Config{
DirConfig: DirConfig{
FilenameTemplate: "{{id}}.note",
BodyTemplatePath: opt.NewString("default.note"),
IDOptions: IDOptions{
Length: 4,
Charset: CharsetAlphanum,
Case: CaseLower,
},
{
Dir: "abs",
Template: "/abs/template.tpl",
Extra: map[string]string{
"hello": "world",
},
},
Dirs: map[string]DirConfig{
"log": {
FilenameTemplate: "{{date}}.md",
},
},
},
}
assert.Equal(t, zk.Template(dir("")), opt.NewString("/test/.zk/templates/root.tpl"))
assert.Equal(t, zk.Template(dir(".")), opt.NewString("/test/.zk/templates/root.tpl"))
assert.Equal(t, zk.Template(dir("unknown")), opt.NewString("/test/.zk/templates/root.tpl"))
assert.Equal(t, zk.Template(dir("log")), opt.NewString("/test/.zk/templates/log.tpl"))
assert.Equal(t, zk.Template(dir("abs")), opt.NewString("/abs/template.tpl"))
}
func TestNoExtra(t *testing.T) {
zk := &Zk{}
assert.Equal(t, zk.Extra(dir("")), map[string]string{})
}
func TestMergeExtra(t *testing.T) {
zk := &Zk{config: config{
dir, err := zk.DirAt(".")
assert.Nil(t, err)
assert.Equal(t, dir.Config, DirConfig{
FilenameTemplate: "{{id}}.note",
BodyTemplatePath: opt.NewString("default.note"),
IDOptions: IDOptions{
Length: 4,
Charset: CharsetAlphanum,
Case: CaseLower,
},
Extra: map[string]string{
"hello": "world",
"salut": "le monde",
},
Dirs: []dirConfig{
{
Dir: "log",
Extra: map[string]string{
"hello": "override",
"additional": "value",
},
},
},
}}
assert.Equal(t, zk.Extra(dir("")), map[string]string{
"hello": "world",
"salut": "le monde",
})
assert.Equal(t, zk.Extra(dir(".")), map[string]string{
"hello": "world",
"salut": "le monde",
})
assert.Equal(t, zk.Extra(dir("unknown")), map[string]string{
"hello": "world",
"salut": "le monde",
})
assert.Equal(t, zk.Extra(dir("log")), map[string]string{
"hello": "override",
"salut": "le monde",
"additional": "value",
})
// Makes sure we didn't modify the extra in place by getting the `log` ones.
assert.Equal(t, zk.Extra(dir("")), map[string]string{
"hello": "world",
"salut": "le monde",
})
}
func TestDefaultIDOptions(t *testing.T) {
zk := &Zk{}
defaultOpts := IDOptions{
Charset: CharsetAlphanum,
Length: 5,
Case: CaseLower,
}
assert.Equal(t, zk.IDOptions(dir("")), defaultOpts)
assert.Equal(t, zk.IDOptions(dir(".")), defaultOpts)
assert.Equal(t, zk.IDOptions(dir("unknown")), defaultOpts)
}
func TestOverrideIDOptions(t *testing.T) {
zk := &Zk{config: config{
ID: &idConfig{
Charset: "alphanum",
Length: 42,
},
Dirs: []dirConfig{
{
Dir: "log",
ID: &idConfig{
Length: 28,
},
},
},
}}
expectedRootOpts := IDOptions{
Charset: CharsetAlphanum,
Length: 42,
Case: CaseLower,
}
assert.Equal(t, zk.IDOptions(dir("")), expectedRootOpts)
assert.Equal(t, zk.IDOptions(dir(".")), expectedRootOpts)
assert.Equal(t, zk.IDOptions(dir("unknown")), expectedRootOpts)
assert.Equal(t, zk.IDOptions(dir("log")), IDOptions{
Charset: CharsetAlphanum,
Length: 28,
Case: CaseLower,
})
}
func TestParseIDCharset(t *testing.T) {
test := func(charset string, expected Charset) {
zk := &Zk{config: config{
ID: &idConfig{
Charset: charset,
},
}}
if !cmp.Equal(zk.IDOptions(dir("")).Charset, expected) {
t.Errorf("Didn't parse ID charset `%v` as expected", charset)
}
}
test("alphanum", CharsetAlphanum)
test("hex", CharsetHex)
test("letters", CharsetLetters)
test("numbers", CharsetNumbers)
test("HEX", []rune("HEX")) // case sensitive
test("custom", []rune("custom"))
}
func TestParseIDCase(t *testing.T) {
test := func(letterCase string, expected Case) {
zk := &Zk{config: config{
ID: &idConfig{
Case: letterCase,
},
}}
if !cmp.Equal(zk.IDOptions(dir("")).Case, expected) {
t.Errorf("Didn't parse ID case `%v` as expected", letterCase)
}
}
test("lower", CaseLower)
test("upper", CaseUpper)
test("mixed", CaseMixed)
test("unknown", CaseLower)
}
func dir(name string) Dir {
return Dir{Name: name, Path: name}
}

View File

@ -1,6 +1,7 @@
package assert
import (
"encoding/json"
"reflect"
"testing"
)
@ -24,6 +25,13 @@ func isNil(value interface{}) bool {
func Equal(t *testing.T, actual, expected interface{}) {
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Received `%+v` (type %v), expected `%+v` (type %v)", actual, reflect.TypeOf(actual), expected, reflect.TypeOf(expected))
t.Errorf("Received (type %v):\n%+v\n---\nBut expected (type %v):\n%+v", reflect.TypeOf(actual), toJSON(t, actual), reflect.TypeOf(expected), toJSON(t, expected))
}
}
func toJSON(t *testing.T, obj interface{}) string {
json, err := json.Marshal(obj)
// json, err := json.MarshalIndent(obj, "", " ")
Nil(t, err)
return string(json)
}

View File

@ -1,5 +1,7 @@
package opt
import "fmt"
// String holds an optional string value.
type String struct {
value *string
@ -28,9 +30,20 @@ func (s String) IsNull() bool {
return s.value == nil
}
// OrDefault returns the optional String value or the given default string if it is null.
// Or returns the receiver if it is not null, otherwise the given optional
// String.
func (s String) Or(other String) String {
if s.IsNull() {
return other
} else {
return s
}
}
// OrDefault returns the optional String value or the given default string if
// it is null.
func (s String) OrDefault(def string) string {
if s.value == nil {
if s.IsNull() {
return def
} else {
return *s.value
@ -42,6 +55,14 @@ func (s String) Unwrap() string {
return s.OrDefault("")
}
func (s String) Equal(other String) bool {
return s.value == other.value || *s.value == *other.value
}
func (s String) String() string {
return s.OrDefault("")
}
func (s String) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%v"`, s)), nil
}

View File

@ -8,12 +8,31 @@ import (
// Exists returns whether the given path exists on the file system.
func Exists(path string) (bool, error) {
if _, err := os.Stat(path); err == nil {
return true, nil
} else if os.IsNotExist(err) {
return false, nil
} else {
fi, err := fileInfo(path)
if err != nil {
return false, err
} else {
return fi != nil, nil
}
}
// DirExists returns whether the given path exists and is a directory.
func DirExists(path string) (bool, error) {
fi, err := fileInfo(path)
if err != nil {
return false, err
} else {
return fi != nil && (*fi).Mode().IsDir(), nil
}
}
func fileInfo(path string) (*os.FileInfo, error) {
if fi, err := os.Stat(path); err == nil {
return &fi, nil
} else if os.IsNotExist(err) {
return nil, nil
} else {
return nil, err
}
}