Refactor config getters into Zk

pull/6/head
Mickaël Menu 3 years ago
parent 43a0a3ca7a
commit abe0094dda
No known key found for this signature in database
GPG Key ID: 53D73664CD359895

@ -5,24 +5,20 @@ import (
"os"
"github.com/mickael-menu/zk/adapter/handlebars"
"github.com/mickael-menu/zk/core/zk"
"github.com/mickael-menu/zk/util"
"github.com/mickael-menu/zk/util/date"
)
type Container struct {
Zk *zk.Zk
Date date.Provider
Logger util.Logger
renderer *handlebars.HandlebarsRenderer
}
func NewContainer() *Container {
zk, _ := zk.Open(".")
date := date.NewFrozenNow()
return &Container{
Zk: zk,
Logger: log.New(os.Stderr, "zk: warning: ", 0),
// zk is short-lived, so we freeze the current date to use the same
// date for any rendering during the execution.

@ -3,16 +3,10 @@ package zk
import (
"github.com/hashicorp/hcl/v2/hclsimple"
"github.com/mickael-menu/zk/util/errors"
"github.com/mickael-menu/zk/util/opt"
"github.com/mickael-menu/zk/util/rand"
)
// Config holds the user configuration of a slip box.
type Config struct {
rootConfig rootConfig
}
type rootConfig struct {
// config holds the user configuration of a slip box.
type config struct {
Filename string `hcl:"filename,optional"`
Template string `hcl:"template,optional"`
RandomID *randomIDConfig `hcl:"random_id,block"`
@ -35,127 +29,12 @@ type randomIDConfig struct {
Case string `hcl:"case,optional"`
}
// ParseConfig creates a new Config instance from its HCL representation.
func ParseConfig(content []byte) (*Config, error) {
var root rootConfig
err := hclsimple.Decode(".zk/config.hcl", content, nil, &root)
// 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")
}
return &Config{root}, nil
}
// Filename returns the filename template for the notes in the given directory.
func (c *Config) Filename(dir Dir) string {
dirConfig := c.dirConfig(dir)
switch {
case dirConfig != nil && dirConfig.Filename != "":
return dirConfig.Filename
case c.rootConfig.Filename != "":
return c.rootConfig.Filename
default:
return "{{random-id}}"
}
}
// Template returns the file template to use for the notes in the given directory.
func (c *Config) Template(dir Dir) opt.String {
dirConfig := c.dirConfig(dir)
switch {
case dirConfig != nil && dirConfig.Template != "":
return opt.NewString(dirConfig.Template)
case c.rootConfig.Template != "":
return opt.NewString(c.rootConfig.Template)
default:
return opt.NullString
}
}
// RandIDOpts returns the options to use to generate a random ID for the given directory.
func (c *Config) RandIDOpts(dir Dir) rand.IDOpts {
toCharset := func(charset string) []rune {
switch charset {
case "alphanum":
return rand.AlphanumCharset
case "hex":
return rand.HexCharset
case "letters":
return rand.LettersCharset
case "numbers":
return rand.NumbersCharset
default:
return []rune(charset)
}
}
toCase := func(c string) rand.Case {
switch c {
case "lower":
return rand.LowerCase
case "upper":
return rand.UpperCase
case "mixed":
return rand.MixedCase
default:
return rand.LowerCase
}
}
// Default options
opts := rand.IDOpts{
Charset: rand.AlphanumCharset,
Length: 5,
Case: rand.LowerCase,
}
merge := func(more *randomIDConfig) {
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 := c.rootConfig.RandomID; root != nil {
merge(root)
}
if dir := c.dirConfig(dir); dir != nil && dir.RandomID != nil {
merge(dir.RandomID)
}
return opts
}
// Extra returns the extra variables for the given directory.
func (c *Config) Extra(dir Dir) map[string]string {
extra := make(map[string]string)
for k, v := range c.rootConfig.Extra {
extra[k] = v
}
if dirConfig := c.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 (c *Config) dirConfig(dir Dir) *dirConfig {
for _, dirConfig := range c.rootConfig.Dirs {
if dirConfig.Dir == dir.Name {
return &dirConfig
}
}
return nil
return &config, nil
}

@ -3,21 +3,18 @@ package zk
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/mickael-menu/zk/util/assert"
"github.com/mickael-menu/zk/util/opt"
"github.com/mickael-menu/zk/util/rand"
)
func TestParseMinimal(t *testing.T) {
config, err := ParseConfig([]byte(""))
conf, err := parseConfig([]byte(""))
assert.Nil(t, err)
assert.Equal(t, config, &Config{rootConfig{}})
assert.Equal(t, conf, &config{})
}
func TestParseComplete(t *testing.T) {
config, err := ParseConfig([]byte(`
conf, err := parseConfig([]byte(`
// Comment
editor = "vim"
filename = "{{random-id}}.note"
@ -46,7 +43,7 @@ func TestParseComplete(t *testing.T) {
`))
assert.Nil(t, err)
assert.Equal(t, config, &Config{rootConfig{
assert.Equal(t, conf, &config{
Filename: "{{random-id}}.note",
Template: "default.note",
RandomID: &randomIDConfig{
@ -56,7 +53,7 @@ func TestParseComplete(t *testing.T) {
},
Editor: "vim",
Dirs: []dirConfig{
dirConfig{
{
Dir: "log",
Filename: "{{date}}.md",
Template: "log.md",
@ -72,192 +69,12 @@ func TestParseComplete(t *testing.T) {
"hello": "world",
"salut": "le monde",
},
}})
})
}
func TestParseInvalidConfig(t *testing.T) {
config, err := ParseConfig([]byte("unknown = 'value'"))
conf, err := parseConfig([]byte("unknown = 'value'"))
assert.NotNil(t, err)
assert.Nil(t, config)
}
func TestDefaultFilename(t *testing.T) {
config := &Config{}
assert.Equal(t, config.Filename(dir("")), "{{random-id}}")
assert.Equal(t, config.Filename(dir(".")), "{{random-id}}")
assert.Equal(t, config.Filename(dir("unknown")), "{{random-id}}")
}
func TestCustomFilename(t *testing.T) {
config := &Config{rootConfig{
Filename: "root-filename",
Dirs: []dirConfig{
dirConfig{
Dir: "log",
Filename: "log-filename",
},
},
}}
assert.Equal(t, config.Filename(dir("")), "root-filename")
assert.Equal(t, config.Filename(dir(".")), "root-filename")
assert.Equal(t, config.Filename(dir("unknown")), "root-filename")
assert.Equal(t, config.Filename(dir("log")), "log-filename")
}
func TestDefaultTemplate(t *testing.T) {
config := &Config{}
assert.Equal(t, config.Template(dir("")), opt.NullString)
assert.Equal(t, config.Template(dir(".")), opt.NullString)
assert.Equal(t, config.Template(dir("unknown")), opt.NullString)
}
func TestCustomTemplate(t *testing.T) {
config := &Config{rootConfig{
Template: "root.tpl",
Dirs: []dirConfig{
dirConfig{
Dir: "log",
Template: "log.tpl",
},
},
}}
assert.Equal(t, config.Template(dir("")), opt.NewString("root.tpl"))
assert.Equal(t, config.Template(dir(".")), opt.NewString("root.tpl"))
assert.Equal(t, config.Template(dir("unknown")), opt.NewString("root.tpl"))
assert.Equal(t, config.Template(dir("log")), opt.NewString("log.tpl"))
}
func TestNoExtra(t *testing.T) {
config := &Config{}
assert.Equal(t, config.Extra(dir("")), map[string]string{})
}
func TestMergeExtra(t *testing.T) {
config := &Config{rootConfig{
Extra: map[string]string{
"hello": "world",
"salut": "le monde",
},
Dirs: []dirConfig{
dirConfig{
Dir: "log",
Extra: map[string]string{
"hello": "override",
"additional": "value",
},
},
},
}}
assert.Equal(t, config.Extra(dir("")), map[string]string{
"hello": "world",
"salut": "le monde",
})
assert.Equal(t, config.Extra(dir(".")), map[string]string{
"hello": "world",
"salut": "le monde",
})
assert.Equal(t, config.Extra(dir("unknown")), map[string]string{
"hello": "world",
"salut": "le monde",
})
assert.Equal(t, config.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, config.Extra(dir("")), map[string]string{
"hello": "world",
"salut": "le monde",
})
}
func TestDefaultRandIDOpts(t *testing.T) {
config := &Config{}
defaultOpts := rand.IDOpts{
Charset: rand.AlphanumCharset,
Length: 5,
Case: rand.LowerCase,
}
assert.Equal(t, config.RandIDOpts(dir("")), defaultOpts)
assert.Equal(t, config.RandIDOpts(dir(".")), defaultOpts)
assert.Equal(t, config.RandIDOpts(dir("unknown")), defaultOpts)
}
func TestOverrideRandIDOpts(t *testing.T) {
config := &Config{rootConfig{
RandomID: &randomIDConfig{
Charset: "alphanum",
Length: 42,
},
Dirs: []dirConfig{
dirConfig{
Dir: "log",
RandomID: &randomIDConfig{
Length: 28,
},
},
},
}}
expectedRootOpts := rand.IDOpts{
Charset: rand.AlphanumCharset,
Length: 42,
Case: rand.LowerCase,
}
assert.Equal(t, config.RandIDOpts(dir("")), expectedRootOpts)
assert.Equal(t, config.RandIDOpts(dir(".")), expectedRootOpts)
assert.Equal(t, config.RandIDOpts(dir("unknown")), expectedRootOpts)
assert.Equal(t, config.RandIDOpts(dir("log")), rand.IDOpts{
Charset: rand.AlphanumCharset,
Length: 28,
Case: rand.LowerCase,
})
}
func TestParseRandIDCharset(t *testing.T) {
test := func(charset string, expected []rune) {
config := &Config{rootConfig{
RandomID: &randomIDConfig{
Charset: charset,
},
}}
if !cmp.Equal(config.RandIDOpts(dir("")).Charset, expected) {
t.Errorf("Didn't parse random ID charset `%v` as expected", charset)
}
}
test("alphanum", rand.AlphanumCharset)
test("hex", rand.HexCharset)
test("letters", rand.LettersCharset)
test("numbers", rand.NumbersCharset)
test("HEX", []rune("HEX")) // case sensitive
test("custom", []rune("custom"))
}
func TestParseRandIDCase(t *testing.T) {
test := func(letterCase string, expected rand.Case) {
config := &Config{rootConfig{
RandomID: &randomIDConfig{
Case: letterCase,
},
}}
if !cmp.Equal(config.RandIDOpts(dir("")).Case, expected) {
t.Errorf("Didn't parse random ID case `%v` as expected", letterCase)
}
}
test("lower", rand.LowerCase)
test("upper", rand.UpperCase)
test("mixed", rand.MixedCase)
test("unknown", rand.LowerCase)
}
func dir(name string) Dir {
return Dir{Name: name, Path: name}
assert.Nil(t, conf)
}

@ -7,6 +7,8 @@ import (
"path/filepath"
"github.com/mickael-menu/zk/util/errors"
"github.com/mickael-menu/zk/util/opt"
"github.com/mickael-menu/zk/util/rand"
)
const defaultConfig = `editor = "nvim"
@ -20,7 +22,7 @@ type Zk struct {
// Slip box root path.
Path string
// User configuration parsed from .zk/config.hsl.
Config Config
config config
}
// Dir represents a directory inside a slip box.
@ -49,14 +51,14 @@ func Open(path string) (*Zk, error) {
return nil, wrap(err)
}
config, err := ParseConfig(configContent)
config, err := parseConfig(configContent)
if err != nil {
return nil, wrap(err)
}
return &Zk{
Path: path,
Config: *config,
config: *config,
}, nil
}
@ -145,3 +147,118 @@ func (zk *Zk) DirAt(path string) (*Dir, error) {
Path: path,
}, 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 "{{random-id}}"
}
}
// 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)
switch {
case dirConfig != nil && dirConfig.Template != "":
return opt.NewString(dirConfig.Template)
case zk.config.Template != "":
return opt.NewString(zk.config.Template)
default:
return opt.NullString
}
}
// RandIDOpts returns the options to use to generate a random ID for the given directory.
func (zk *Zk) RandIDOpts(dir Dir) rand.IDOpts {
toCharset := func(charset string) []rune {
switch charset {
case "alphanum":
return rand.AlphanumCharset
case "hex":
return rand.HexCharset
case "letters":
return rand.LettersCharset
case "numbers":
return rand.NumbersCharset
default:
return []rune(charset)
}
}
toCase := func(c string) rand.Case {
switch c {
case "lower":
return rand.LowerCase
case "upper":
return rand.UpperCase
case "mixed":
return rand.MixedCase
default:
return rand.LowerCase
}
}
// Default options
opts := rand.IDOpts{
Charset: rand.AlphanumCharset,
Length: 5,
Case: rand.LowerCase,
}
merge := func(more *randomIDConfig) {
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.RandomID; root != nil {
merge(root)
}
if dir := zk.dirConfig(dir); dir != nil && dir.RandomID != nil {
merge(dir.RandomID)
}
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
}

@ -5,7 +5,10 @@ import (
"path/filepath"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/mickael-menu/zk/util/assert"
"github.com/mickael-menu/zk/util/opt"
"github.com/mickael-menu/zk/util/rand"
)
func TestDirAt(t *testing.T) {
@ -13,10 +16,7 @@ func TestDirAt(t *testing.T) {
wd, err := os.Getwd()
assert.Nil(t, err)
zk := &Zk{
Path: wd,
Config: Config{},
}
zk := &Zk{Path: wd}
for path, name := range map[string]string{
"log": "log",
@ -31,3 +31,183 @@ func TestDirAt(t *testing.T) {
assert.Equal(t, actual, &Dir{Name: name, Path: filepath.Join(wd, name)})
}
}
func TestDefaultFilenameTemplate(t *testing.T) {
zk := &Zk{}
assert.Equal(t, zk.FilenameTemplate(dir("")), "{{random-id}}")
assert.Equal(t, zk.FilenameTemplate(dir(".")), "{{random-id}}")
assert.Equal(t, zk.FilenameTemplate(dir("unknown")), "{{random-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{config: config{
Template: "root.tpl",
Dirs: []dirConfig{
{
Dir: "log",
Template: "log.tpl",
},
},
}}
assert.Equal(t, zk.Template(dir("")), opt.NewString("root.tpl"))
assert.Equal(t, zk.Template(dir(".")), opt.NewString("root.tpl"))
assert.Equal(t, zk.Template(dir("unknown")), opt.NewString("root.tpl"))
assert.Equal(t, zk.Template(dir("log")), opt.NewString("log.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{
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 TestDefaultRandIDOpts(t *testing.T) {
zk := &Zk{}
defaultOpts := rand.IDOpts{
Charset: rand.AlphanumCharset,
Length: 5,
Case: rand.LowerCase,
}
assert.Equal(t, zk.RandIDOpts(dir("")), defaultOpts)
assert.Equal(t, zk.RandIDOpts(dir(".")), defaultOpts)
assert.Equal(t, zk.RandIDOpts(dir("unknown")), defaultOpts)
}
func TestOverrideRandIDOpts(t *testing.T) {
zk := &Zk{config: config{
RandomID: &randomIDConfig{
Charset: "alphanum",
Length: 42,
},
Dirs: []dirConfig{
{
Dir: "log",
RandomID: &randomIDConfig{
Length: 28,
},
},
},
}}
expectedRootOpts := rand.IDOpts{
Charset: rand.AlphanumCharset,
Length: 42,
Case: rand.LowerCase,
}
assert.Equal(t, zk.RandIDOpts(dir("")), expectedRootOpts)
assert.Equal(t, zk.RandIDOpts(dir(".")), expectedRootOpts)
assert.Equal(t, zk.RandIDOpts(dir("unknown")), expectedRootOpts)
assert.Equal(t, zk.RandIDOpts(dir("log")), rand.IDOpts{
Charset: rand.AlphanumCharset,
Length: 28,
Case: rand.LowerCase,
})
}
func TestParseRandIDCharset(t *testing.T) {
test := func(charset string, expected []rune) {
zk := &Zk{config: config{
RandomID: &randomIDConfig{
Charset: charset,
},
}}
if !cmp.Equal(zk.RandIDOpts(dir("")).Charset, expected) {
t.Errorf("Didn't parse random ID charset `%v` as expected", charset)
}
}
test("alphanum", rand.AlphanumCharset)
test("hex", rand.HexCharset)
test("letters", rand.LettersCharset)
test("numbers", rand.NumbersCharset)
test("HEX", []rune("HEX")) // case sensitive
test("custom", []rune("custom"))
}
func TestParseRandIDCase(t *testing.T) {
test := func(letterCase string, expected rand.Case) {
zk := &Zk{config: config{
RandomID: &randomIDConfig{
Case: letterCase,
},
}}
if !cmp.Equal(zk.RandIDOpts(dir("")).Case, expected) {
t.Errorf("Didn't parse random ID case `%v` as expected", letterCase)
}
}
test("lower", rand.LowerCase)
test("upper", rand.UpperCase)
test("mixed", rand.MixedCase)
test("unknown", rand.LowerCase)
}
func dir(name string) Dir {
return Dir{Name: name, Path: name}
}

@ -5,7 +5,7 @@ type String struct {
value *string
}
// NullString repreents an empty optional String.
// NullString represents an empty optional String.
var NullString = String{nil}
// NewString creates a new optional String with the given value.
@ -13,6 +13,16 @@ func NewString(value string) String {
return String{&value}
}
// NewNotEmptyString creates a new optional String with the given value or
// returns NullString if the value is an empty string.
func NewNotEmptyString(value string) String {
if value == "" {
return NullString
} else {
return NewString(value)
}
}
// IsNull returns whether the optional String has no value.
func (s String) IsNull() bool {
return s.value == nil
@ -26,3 +36,8 @@ func (s String) OrDefault(def string) string {
return *s.value
}
}
// Unwrap returns the optional String value or an empty String if none is set.
func (s String) Unwrap() string {
return s.OrDefault("")
}

Loading…
Cancel
Save