mirror of
https://github.com/mickael-menu/zk
synced 2024-11-07 15:20:21 +00:00
Refactor user config
This commit is contained in:
parent
6a4a4c77e6
commit
4d0c0ccdda
@ -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)
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
182
core/zk/zk.go
182
core/zk/zk.go
@ -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
|
||||
}
|
||||
|
@ -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}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user