Move ID model to core

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

@ -3,7 +3,6 @@ package note
import (
"fmt"
"path/filepath"
"strings"
"github.com/mickael-menu/zk/core"
"github.com/mickael-menu/zk/core/zk"
@ -27,7 +26,7 @@ type CreateOpts struct {
Extra map[string]string
}
// Create generates a new note in the given slip box from the given options.
// Create generates a new note from the given options.
func Create(zk *zk.Zk, opts CreateOpts, templateLoader core.TemplateLoader) (string, error) {
wrap := errors.Wrapper("note creation failed")
@ -74,7 +73,7 @@ type renderContext struct {
Path string
Filename string
FilenameStem string `handlebars:"filename-stem"`
RandomID string `handlebars:"random-id"`
ID string
Extra map[string]string
}
@ -93,10 +92,11 @@ func newRenderContext(zk *zk.Zk, opts CreateOpts, templateLoader core.TemplateLo
return renderContext{}, err
}
idGenerator := rand.NewIDGenerator(zk.RandIDOpts(opts.Dir))
contextGenerator := newRenderContextGenerator(template, opts)
genContext := newRenderContextGenerator(template, opts)
genId := rand.NewIDGenerator(zk.IDOptions(opts.Dir))
// Attempts to generate a new render context until the generated filepath doesn't exist.
for {
context, err := contextGenerator(idGenerator())
context, err := genContext(genId())
if err != nil {
return context, err
}
@ -110,7 +110,7 @@ func newRenderContext(zk *zk.Zk, opts CreateOpts, templateLoader core.TemplateLo
}
}
type renderContextGenerator func(randomID string) (renderContext, error)
type renderContextGenerator func(id string) (renderContext, error)
func newRenderContextGenerator(
filenameTemplate core.Template,
@ -124,26 +124,28 @@ func newRenderContextGenerator(
}
i := 0
isRandom := false
return func(randomID string) (renderContext, error) {
return func(id string) (renderContext, error) {
i++
// Attempts 50ish tries if the filename template contains a random ID before failing.
if i > 1 && !isRandom || i >= 50 {
// Attempts 50ish tries before failing.
if i >= 50 {
return context, fmt.Errorf("%v: file already exists", context.Path)
}
context.RandomID = randomID
context.ID = id
filename, err := filenameTemplate.Render(context)
if err != nil {
return context, err
}
isRandom = strings.Contains(filename, randomID)
// FIXME Customize extension in config
path := filepath.Join(opts.Dir.Path, filename+".md")
// Same path as before? We can fail because there's no random component
// in the filename template.
if context.Path == path {
return context, fmt.Errorf("%v: file already exists", path)
}
context.Path = path
context.Filename = filepath.Base(path)
context.FilenameStem = paths.FilenameStem(path)

@ -9,7 +9,7 @@ import (
type config struct {
Filename string `hcl:"filename,optional"`
Template string `hcl:"template,optional"`
RandomID *randomIDConfig `hcl:"random_id,block"`
ID *idConfig `hcl:"id,block"`
Editor string `hcl:"editor,optional"`
Dirs []dirConfig `hcl:"dir,block"`
Extra map[string]string `hcl:"extra,optional"`
@ -19,11 +19,11 @@ type dirConfig struct {
Dir string `hcl:"dir,label"`
Filename string `hcl:"filename,optional"`
Template string `hcl:"template,optional"`
RandomID *randomIDConfig `hcl:"random_id,block"`
ID *idConfig `hcl:"id,block"`
Extra map[string]string `hcl:"extra,optional"`
}
type randomIDConfig struct {
type idConfig struct {
Charset string `hcl:"charset,optional"`
Length int `hcl:"length,optional"`
Case string `hcl:"case,optional"`

@ -17,9 +17,9 @@ func TestParseComplete(t *testing.T) {
conf, err := parseConfig([]byte(`
// Comment
editor = "vim"
filename = "{{random-id}}.note"
filename = "{{id}}.note"
template = "default.note"
random_id {
id {
charset = "alphanum"
length = 4
case = "lower"
@ -31,7 +31,7 @@ func TestParseComplete(t *testing.T) {
dir "log" {
filename = "{{date}}.md"
template = "log.md"
random_id {
id {
charset = "letters"
length = 8
case = "mixed"
@ -44,9 +44,9 @@ func TestParseComplete(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, conf, &config{
Filename: "{{random-id}}.note",
Filename: "{{id}}.note",
Template: "default.note",
RandomID: &randomIDConfig{
ID: &idConfig{
Charset: "alphanum",
Length: 4,
Case: "lower",
@ -57,7 +57,7 @@ func TestParseComplete(t *testing.T) {
Dir: "log",
Filename: "{{date}}.md",
Template: "log.md",
RandomID: &randomIDConfig{
ID: &idConfig{
Charset: "letters",
Length: 8,
Case: "mixed",

@ -0,0 +1,30 @@
package zk
// IDOptions holds the options used to generate an ID.
type IDOptions struct {
Length int
Charset Charset
Case Case
}
type Charset []rune
var (
// CharsetAlphanum is a charset containing letters and numbers.
CharsetAlphanum = Charset("0123456789abcdefghijklmnopqrstuvwxyz")
// CharsetAlphanum is a charset containing hexadecimal characters.
CharsetHex = Charset("0123456789abcdef")
// CharsetLetters is a charset containing only letters.
CharsetLetters = Charset("abcdefghijklmnopqrstuvwxyz")
// CharsetNumbers is a charset containing only numbers.
CharsetNumbers = Charset("0123456789")
)
// Case represents the letter case to use when generating an ID.
type Case int
const (
CaseLower Case = iota + 1
CaseUpper
CaseMixed
)

@ -8,7 +8,6 @@ import (
"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"
@ -158,7 +157,7 @@ func (zk *Zk) FilenameTemplate(dir Dir) string {
case zk.config.Filename != "":
return zk.config.Filename
default:
return "{{random-id}}"
return "{{id}}"
}
}
@ -185,44 +184,44 @@ func (zk *Zk) Template(dir Dir) opt.String {
return opt.NewString(template)
}
// 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 {
// 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 rand.AlphanumCharset
return CharsetAlphanum
case "hex":
return rand.HexCharset
return CharsetHex
case "letters":
return rand.LettersCharset
return CharsetLetters
case "numbers":
return rand.NumbersCharset
return CharsetNumbers
default:
return []rune(charset)
return Charset(charset)
}
}
toCase := func(c string) rand.Case {
toCase := func(c string) Case {
switch c {
case "lower":
return rand.LowerCase
return CaseLower
case "upper":
return rand.UpperCase
return CaseUpper
case "mixed":
return rand.MixedCase
return CaseMixed
default:
return rand.LowerCase
return CaseLower
}
}
// Default options
opts := rand.IDOpts{
Charset: rand.AlphanumCharset,
opts := IDOptions{
Charset: CharsetAlphanum,
Length: 5,
Case: rand.LowerCase,
Case: CaseLower,
}
merge := func(more *randomIDConfig) {
merge := func(more *idConfig) {
if more.Charset != "" {
opts.Charset = toCharset(more.Charset)
}
@ -234,12 +233,12 @@ func (zk *Zk) RandIDOpts(dir Dir) rand.IDOpts {
}
}
if root := zk.config.RandomID; root != nil {
if root := zk.config.ID; root != nil {
merge(root)
}
if dir := zk.dirConfig(dir); dir != nil && dir.RandomID != nil {
merge(dir.RandomID)
if dir := zk.dirConfig(dir); dir != nil && dir.ID != nil {
merge(dir.ID)
}
return opts

@ -8,7 +8,6 @@ import (
"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) {
@ -34,9 +33,9 @@ func TestDirAt(t *testing.T) {
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}}")
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) {
@ -131,89 +130,89 @@ func TestMergeExtra(t *testing.T) {
})
}
func TestDefaultRandIDOpts(t *testing.T) {
func TestDefaultIDOptions(t *testing.T) {
zk := &Zk{}
defaultOpts := rand.IDOpts{
Charset: rand.AlphanumCharset,
defaultOpts := IDOptions{
Charset: CharsetAlphanum,
Length: 5,
Case: rand.LowerCase,
Case: CaseLower,
}
assert.Equal(t, zk.RandIDOpts(dir("")), defaultOpts)
assert.Equal(t, zk.RandIDOpts(dir(".")), defaultOpts)
assert.Equal(t, zk.RandIDOpts(dir("unknown")), defaultOpts)
assert.Equal(t, zk.IDOptions(dir("")), defaultOpts)
assert.Equal(t, zk.IDOptions(dir(".")), defaultOpts)
assert.Equal(t, zk.IDOptions(dir("unknown")), defaultOpts)
}
func TestOverrideRandIDOpts(t *testing.T) {
func TestOverrideIDOptions(t *testing.T) {
zk := &Zk{config: config{
RandomID: &randomIDConfig{
ID: &idConfig{
Charset: "alphanum",
Length: 42,
},
Dirs: []dirConfig{
{
Dir: "log",
RandomID: &randomIDConfig{
ID: &idConfig{
Length: 28,
},
},
},
}}
expectedRootOpts := rand.IDOpts{
Charset: rand.AlphanumCharset,
expectedRootOpts := IDOptions{
Charset: CharsetAlphanum,
Length: 42,
Case: rand.LowerCase,
Case: CaseLower,
}
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.IDOptions(dir("")), expectedRootOpts)
assert.Equal(t, zk.IDOptions(dir(".")), expectedRootOpts)
assert.Equal(t, zk.IDOptions(dir("unknown")), expectedRootOpts)
assert.Equal(t, zk.RandIDOpts(dir("log")), rand.IDOpts{
Charset: rand.AlphanumCharset,
assert.Equal(t, zk.IDOptions(dir("log")), IDOptions{
Charset: CharsetAlphanum,
Length: 28,
Case: rand.LowerCase,
Case: CaseLower,
})
}
func TestParseRandIDCharset(t *testing.T) {
test := func(charset string, expected []rune) {
func TestParseIDCharset(t *testing.T) {
test := func(charset string, expected Charset) {
zk := &Zk{config: config{
RandomID: &randomIDConfig{
ID: &idConfig{
Charset: charset,
},
}}
if !cmp.Equal(zk.RandIDOpts(dir("")).Charset, expected) {
t.Errorf("Didn't parse random ID charset `%v` as expected", charset)
if !cmp.Equal(zk.IDOptions(dir("")).Charset, expected) {
t.Errorf("Didn't parse ID charset `%v` as expected", charset)
}
}
test("alphanum", rand.AlphanumCharset)
test("hex", rand.HexCharset)
test("letters", rand.LettersCharset)
test("numbers", rand.NumbersCharset)
test("alphanum", CharsetAlphanum)
test("hex", CharsetHex)
test("letters", CharsetLetters)
test("numbers", CharsetNumbers)
test("HEX", []rune("HEX")) // case sensitive
test("custom", []rune("custom"))
}
func TestParseRandIDCase(t *testing.T) {
test := func(letterCase string, expected rand.Case) {
func TestParseIDCase(t *testing.T) {
test := func(letterCase string, expected Case) {
zk := &Zk{config: config{
RandomID: &randomIDConfig{
ID: &idConfig{
Case: letterCase,
},
}}
if !cmp.Equal(zk.RandIDOpts(dir("")).Case, expected) {
t.Errorf("Didn't parse random ID case `%v` as expected", letterCase)
if !cmp.Equal(zk.IDOptions(dir("")).Case, expected) {
t.Errorf("Didn't parse ID case `%v` as expected", letterCase)
}
}
test("lower", rand.LowerCase)
test("upper", rand.UpperCase)
test("mixed", rand.MixedCase)
test("unknown", rand.LowerCase)
test("lower", CaseLower)
test("upper", CaseUpper)
test("mixed", CaseMixed)
test("unknown", CaseLower)
}
func dir(name string) Dir {

@ -4,54 +4,29 @@ import (
"math/rand"
"time"
"unicode"
)
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("abcdefghijklmnopqrstuvwxyz")
// NumbersCharset is a charset containing only numbers.
NumbersCharset = []rune("0123456789")
)
// Case represents the letter case to use when generating a string.
type Case int
const (
LowerCase Case = iota + 1
UpperCase
MixedCase
"github.com/mickael-menu/zk/core/zk"
)
// IDOpts holds the options used to generate a random ID.
type IDOpts struct {
Length int
Charset []rune
Case Case
}
// NewIDGenerator returns a function generating string IDs using the given options.
// Inspired by https://www.calhoun.io/creating-random-strings-in-go/
func NewIDGenerator(options IDOpts) func() string {
func NewIDGenerator(options zk.IDOptions) func() string {
if options.Length < 1 {
panic("IDOpts.Length must be at least 1")
panic("IDOptions.Length must be at least 1")
}
var charset []rune
for _, char := range options.Charset {
switch options.Case {
case LowerCase:
case zk.CaseLower:
charset = append(charset, unicode.ToLower(char))
case UpperCase:
case zk.CaseUpper:
charset = append(charset, unicode.ToUpper(char))
case MixedCase:
case zk.CaseMixed:
charset = append(charset, unicode.ToLower(char))
charset = append(charset, unicode.ToUpper(char))
default:
panic("unknown rand.Case value")
panic("unknown zk.Case value")
}
}

Loading…
Cancel
Save