diff --git a/cmd/new.go b/cmd/new.go index 313176c..af6f147 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -1,11 +1,17 @@ package cmd -import "github.com/mickael-menu/zk/core/zk" +import ( + "fmt" + + "github.com/mickael-menu/zk/core/zk" +) type New struct { Directory string `arg optional name:"directory" default:"."` } func (cmd *New) Run() error { - return zk.Open(cmd.Directory) + zk, err := zk.Open(cmd.Directory) + fmt.Printf("%+v\n", zk) + return err } diff --git a/core/zk/config.go b/core/zk/config.go new file mode 100644 index 0000000..f680c61 --- /dev/null +++ b/core/zk/config.go @@ -0,0 +1,49 @@ +package zk + +import ( + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2/hclsimple" + "github.com/mickael-menu/zk/util/errors" +) + +// 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) +} + +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"` +} + +type dirConfig struct { + Dir string `hcl:"dir,label"` + Filename string `hcl:"filename,optional"` + Template string `hcl:"template,optional"` + Ext map[string]string `hcl:"ext,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 { + return nil, errors.Wrap(err, "failed to read config") + } + return &Config{root}, nil +} diff --git a/core/zk/config_test.go b/core/zk/config_test.go new file mode 100644 index 0000000..81806a4 --- /dev/null +++ b/core/zk/config_test.go @@ -0,0 +1,75 @@ +package zk + +import ( + "testing" + + "github.com/mickael-menu/zk/util/assert" +) + +// Parse a minimal configuration file. +func TestParseMinimal(t *testing.T) { + 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(` + // Comment + editor = "vim" + extension = "note" + filename = "{{random-id}}.note" + template = "default.note" + random_id { + charset = "alphanum" + length = 4 + case = "lower" + } + ext = { + hello = "world" + salut = "le monde" + } + dir "log" { + filename = "{{date}}.md" + template = "log.md" + ext = { + log-ext = "value" + } + } + `)) + + assert.Nil(t, err) + assert.Equal(t, config, &Config{rootConfig{ + Editor: "vim", + Extension: "note", + Filename: "{{random-id}}.note", + Template: "default.note", + RandomID: &randomIDConfig{ + Charset: "alphanum", + Length: 4, + Case: "lower", + }, + Dirs: []dirConfig{ + dirConfig{ + Dir: "log", + Filename: "{{date}}.md", + Template: "log.md", + Ext: map[string]string{"log-ext": "value"}, + }, + }, + Ext: map[string]string{ + "hello": "world", + "salut": "le monde", + }, + }}) +} + +// Parsing failure +func TestParseInvalidConfig(t *testing.T) { + config, err := parseConfig([]byte("unknown = 'value'")) + + assert.NotNil(t, err) + assert.Nil(t, config) +} diff --git a/core/zk/zk.go b/core/zk/zk.go index 7cd687e..8b212a0 100644 --- a/core/zk/zk.go +++ b/core/zk/zk.go @@ -2,27 +2,47 @@ package zk import ( "fmt" + "io/ioutil" "os" "path/filepath" "github.com/mickael-menu/zk/util/errors" ) -const defaultConfig = ` -# Test -test = thing +const defaultConfig = `editor = "nvim" +dir "log" { + template = "log.md" +} ` +type Zk struct { + Config Config +} + // Open locates a slip box at the given path and parses its configuration. -func Open(path string) error { +func Open(path string) (*Zk, error) { wrap := errors.Wrapper("open failed") path, err := filepath.Abs(path) if err != nil { - return wrap(err) + return nil, wrap(err) } - _, err = locateRoot(path) - return wrap(err) + path, err = locateRoot(path) + if err != nil { + return nil, wrap(err) + } + + configContent, err := ioutil.ReadFile(filepath.Join(path, ".zk/config.hcl")) + if err != nil { + return nil, wrap(err) + } + + config, err := parseConfig(configContent) + if err != nil { + return nil, wrap(err) + } + + return &Zk{*config}, nil } // Create initializes a new slip box at the given path. @@ -45,7 +65,7 @@ func Create(path string) error { } // Write default config.toml. - f, err := os.Create(filepath.Join(path, ".zk/config.toml")) + f, err := os.Create(filepath.Join(path, ".zk/config.hcl")) if err != nil { return wrap(err) } diff --git a/go.mod b/go.mod index 710254c..dcf1a2e 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,8 @@ module github.com/mickael-menu/zk go 1.15 -require github.com/alecthomas/kong v0.2.12 +require ( + github.com/alecthomas/kong v0.2.12 + github.com/google/go-cmp v0.3.1 + github.com/hashicorp/hcl/v2 v2.8.1 +) diff --git a/go.sum b/go.sum index 8102a3a..30a9b7a 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,46 @@ +github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= +github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/alecthomas/kong v0.2.12 h1:X3kkCOXGUNzLmiu+nQtoxWqj4U2a39MpSJR3QdQXOwI= github.com/alecthomas/kong v0.2.12/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= +github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= +github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0= +github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= +github.com/apparentlymart/go-textseg/v12 v12.0.0 h1:bNEQyAGak9tojivJNkoqWErVCQbjdL7GzRt3F8NvfJ0= +github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl/v2 v2.8.1 h1:FJ60CIYaMyJOKzPndhMyjiz353Fd+2jr6PodF5Xzb08= +github.com/hashicorp/hcl/v2 v2.8.1/go.mod h1:bQTN5mpo+jewjJgh8jr0JUguIi7qPHUF6yIfAEN3jqY= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/zclconf/go-cty v1.2.0 h1:sPHsy7ADcIZQP3vILvTjrh74ZA175TFP5vqiNK1UmlI= +github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/util/assert/assert.go b/util/assert/assert.go new file mode 100644 index 0000000..c19ad83 --- /dev/null +++ b/util/assert/assert.go @@ -0,0 +1,31 @@ +package assert + +import ( + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func Nil(t *testing.T, value interface{}) { + if !isNil(value) { + t.Errorf("Expected %v (type %v) to be nil", value, reflect.TypeOf(value)) + } +} + +func NotNil(t *testing.T, value interface{}) { + if isNil(value) { + t.Errorf("Expected %v (type %v) to not be nil", value, reflect.TypeOf(value)) + } +} + +func isNil(value interface{}) bool { + return value == nil || + (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)) + } +}