Parse config values

pull/6/head
Mickaël Menu 4 years ago
parent c4a5648238
commit 8b8a66f2f2
No known key found for this signature in database
GPG Key ID: 53D73664CD359895

@ -1,45 +1,42 @@
package zk
import (
"github.com/google/go-cmp/cmp"
"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 {
root rootConfig
}
func (c1 Config) Equal(c2 Config) bool {
return cmp.Equal(c1.root, c2.root)
rootConfig rootConfig
}
type rootConfig struct {
Editor string `hcl:"editor,optional"`
Extension string `hcl:"extension,optional"`
Filename string `hcl:"filename,optional"`
Template string `hcl:"template,optional"`
RandomID *randomIDConfig `hcl:"random_id,block"`
Dirs []dirConfig `hcl:"dir,block"`
Ext map[string]string `hcl:"ext,optional"`
}
type randomIDConfig struct {
Charset string `hcl:"charset,optional"`
Length int `hcl:"length,optional"`
Case string `hcl:"case,optional"`
Filename string `hcl:"filename,optional"`
Template string `hcl:"template,optional"`
RandomID *randomIDConfig `hcl:"random_id,block"`
Editor string `hcl:"editor,optional"`
Dirs []dirConfig `hcl:"dir,block"`
Ext map[string]string `hcl:"ext,optional"`
}
type dirConfig struct {
Dir string `hcl:"dir,label"`
Filename string `hcl:"filename,optional"`
Template string `hcl:"template,optional"`
RandomID *randomIDConfig `hcl:"random_id,block"`
Ext map[string]string `hcl:"ext,optional"`
}
// parseConfig creates a new Config instance from its HCL representation.
func parseConfig(content []byte) (*Config, error) {
type randomIDConfig 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 root rootConfig
err := hclsimple.Decode(".zk/config.hcl", content, nil, &root)
if err != nil {
@ -47,3 +44,118 @@ func parseConfig(content []byte) (*Config, error) {
}
return &Config{root}, nil
}
// Filename returns the filename template for the notes in the given directory.
func (c *Config) Filename(dir string) 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 string) 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 string) 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
}
// Ext returns the extra variables for the given directory.
func (c *Config) Ext(dir string) map[string]string {
ext := make(map[string]string)
for k, v := range c.rootConfig.Ext {
ext[k] = v
}
if dirConfig := c.dirConfig(dir); dirConfig != nil {
for k, v := range dirConfig.Ext {
ext[k] = v
}
}
return ext
}
// dirConfig returns the dirConfig instance for the given directory.
func (c *Config) dirConfig(dir string) *dirConfig {
for _, dirConfig := range c.rootConfig.Dirs {
if dirConfig.Dir == dir {
return &dirConfig
}
}
return nil
}

@ -3,23 +3,23 @@ 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"
)
// Parse a minimal configuration file.
func TestParseMinimal(t *testing.T) {
config, err := parseConfig([]byte(""))
config, err := ParseConfig([]byte(""))
assert.Nil(t, err)
assert.Equal(t, config, &Config{rootConfig{}})
}
// Parse a complete configuration file.
func TestParseComplete(t *testing.T) {
config, err := parseConfig([]byte(`
config, err := ParseConfig([]byte(`
// Comment
editor = "vim"
extension = "note"
filename = "{{random-id}}.note"
template = "default.note"
random_id {
@ -34,6 +34,11 @@ func TestParseComplete(t *testing.T) {
dir "log" {
filename = "{{date}}.md"
template = "log.md"
random_id {
charset = "letters"
length = 8
case = "mixed"
}
ext = {
log-ext = "value"
}
@ -42,21 +47,25 @@ func TestParseComplete(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, config, &Config{rootConfig{
Editor: "vim",
Extension: "note",
Filename: "{{random-id}}.note",
Template: "default.note",
Filename: "{{random-id}}.note",
Template: "default.note",
RandomID: &randomIDConfig{
Charset: "alphanum",
Length: 4,
Case: "lower",
},
Editor: "vim",
Dirs: []dirConfig{
dirConfig{
Dir: "log",
Filename: "{{date}}.md",
Template: "log.md",
Ext: map[string]string{"log-ext": "value"},
RandomID: &randomIDConfig{
Charset: "letters",
Length: 8,
Case: "mixed",
},
Ext: map[string]string{"log-ext": "value"},
},
},
Ext: map[string]string{
@ -66,10 +75,185 @@ func TestParseComplete(t *testing.T) {
}})
}
// Parsing failure
func TestParseInvalidConfig(t *testing.T) {
config, err := parseConfig([]byte("unknown = 'value'"))
config, 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(""), "{{random-id}}")
assert.Equal(t, config.Filename("."), "{{random-id}}")
assert.Equal(t, config.Filename("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(""), "root-filename")
assert.Equal(t, config.Filename("."), "root-filename")
assert.Equal(t, config.Filename("unknown"), "root-filename")
assert.Equal(t, config.Filename("log"), "log-filename")
}
func TestDefaultTemplate(t *testing.T) {
config := &Config{}
assert.Equal(t, config.Template(""), opt.NullString)
assert.Equal(t, config.Template("."), opt.NullString)
assert.Equal(t, config.Template("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(""), opt.NewString("root.tpl"))
assert.Equal(t, config.Template("."), opt.NewString("root.tpl"))
assert.Equal(t, config.Template("unknown"), opt.NewString("root.tpl"))
assert.Equal(t, config.Template("log"), opt.NewString("log.tpl"))
}
func TestNoExtra(t *testing.T) {
config := &Config{}
assert.Equal(t, config.Ext(""), map[string]string{})
}
func TestMergeExtra(t *testing.T) {
config := &Config{rootConfig{
Ext: map[string]string{
"hello": "world",
"salut": "le monde",
},
Dirs: []dirConfig{
dirConfig{
Dir: "log",
Ext: map[string]string{
"hello": "override",
"additional": "value",
},
},
},
}}
assert.Equal(t, config.Ext(""), map[string]string{
"hello": "world",
"salut": "le monde",
})
assert.Equal(t, config.Ext("."), map[string]string{
"hello": "world",
"salut": "le monde",
})
assert.Equal(t, config.Ext("unknown"), map[string]string{
"hello": "world",
"salut": "le monde",
})
assert.Equal(t, config.Ext("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.Ext(""), 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(""), defaultOpts)
assert.Equal(t, config.RandIDOpts("."), defaultOpts)
assert.Equal(t, config.RandIDOpts("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(""), expectedRootOpts)
assert.Equal(t, config.RandIDOpts("."), expectedRootOpts)
assert.Equal(t, config.RandIDOpts("unknown"), expectedRootOpts)
assert.Equal(t, config.RandIDOpts("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("").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("").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)
}

@ -37,7 +37,7 @@ 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)
}

@ -3,8 +3,6 @@ package assert
import (
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
)
func Nil(t *testing.T, value interface{}) {
@ -24,8 +22,8 @@ func isNil(value interface{}) bool {
(reflect.ValueOf(value).Kind() == reflect.Ptr && reflect.ValueOf(value).IsNil())
}
func Equal(t *testing.T, value interface{}, expected interface{}) {
if !cmp.Equal(value, expected) {
t.Errorf("Received %+v (type %v), expected %+v (type %v)", value, reflect.TypeOf(value), expected, reflect.TypeOf(expected))
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))
}
}

@ -0,0 +1,28 @@
package opt
// String holds an optional string value.
type String struct {
value *string
}
// NewString creates a new optional String with the given value.
func NewString(value string) String {
return String{&value}
}
// NullString repreents an empty optional String.
var NullString = String{nil}
// IsNull returns whether the optional String has no value.
func (s String) IsNull() bool {
return s.value == nil
}
// 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 {
return def
} else {
return *s.value
}
}

@ -0,0 +1,37 @@
package rand
var (
// AlphanumCharset is a charset containing letters and numbers.
AlphanumCharset = []rune("0123456789abcdefghijklmnopqrstuvwxyz")
// AlphanumCharset is a charset containing hexadecimal characters.
HexCharset = []rune("0123456789abcdef")
// LettersCharset is a charset containing only letters.
LettersCharset = []rune("0123456789abcdefghijklmnopqrstuvwxyz")
// NumbersCharset is a charset containing only numbers.
NumbersCharset = []rune("0123456789abcdefghijklmnopqrstuvwxyz")
)
// Case represents the letter case to use when generating a string.
type Case int
const (
LowerCase Case = iota
UpperCase
MixedCase
)
// IDOpts holds the options used to generate a random ID.
type IDOpts struct {
Charset []rune
Length int
Case Case
}
// GenID creates a new random string ID using the given options.
func GenID(options IDOpts) string {
if options.Length < 1 {
panic("IDOpts.Length must be at least 1")
}
return ""
}
Loading…
Cancel
Save