~pkal/dirconf

36dca8aa444960d4e1463bd2c67547d7d785cb1f — Philip K 2 years ago da2c745
fixed issues with rewrite
2 files changed, 156 insertions(+), 114 deletions(-)

M README.md
M dirconf.go
M README.md => README.md +44 -17
@@ 10,28 10,55 @@ Example

	import dc "git.sr.ht/~zge/dirconf"

	var (
		conf dc.Conf
		name string
		age int
	)

	conf.Add(&dc.String{Ptr: &name, Must: true}, "name")
	conf.Add(&dc.Int{Ptr: &age}, "person", "age")
	err := conf.Parse()
	if err != nil {
		// handle error
	func main() {
		var name string
		var age int

		conf := dc.Conf{
			&dc.String{Ptr: &name, Must: true, Key: "name"},
			&dc.Int{Ptr: &age, Key: "person/age"},
			&dc.Bool{OnSet: someFunc, Key: "test/value"},
		}

		err := conf.Parse()
		if err != nil {
			// handle error
		}
	}

Usage
-----

After specifying a `dirconf.Conf` variable, one has to call `Conf.Add`
for every option one wants to have parsed. Nothing is done, until
`Conf.Parse()` is called, where all the options are attempted to be set,
either to the value in their file, with additional interpretation if
needed, or to some other value, depending on the `dirconf.Option`
object.
The main two steps are:
1. Listing all "Options" in a `dirconf.Conf` variable, which in turn is
   just a type-aliased slice of `dirconf.Option` objects. Such objects
   are `dirconf.String`, `dirconf.Int`, `dirconf.Func`, ...
2. With all options listed, the `Parse` method (or `ParseIn`, if you
   want to manually specify the directory that should be used) will try
   to apply the file structure to the listed options.
   
   `Parse` will by default use `~/.config/[name of current binary]` as
   the directory.
   
Most `Option`s have a few properties in common, although the type system
doesn't recognise this:
- `Ptr`: A pointer to a variable that should be set to the parsed value.
  If `nil` (by default), it's ignored
- `Must`: A Boolean value indicating that this value *must* value must
  have a corresponding file in the directory. If not set, an error will
  be returned from `Parse` (`dirconf.ErrNoValueFound`).
- `OnSet`: Like `Ptr`, but will instead call a function with the value
  that has been found. Will be ignored if `nil` (by default).
- `Default`: Value to use if nothing is found. Doesn't override `Must`.
  Not too useful for `Ptr`, more so for `OnSet`.
- `Key`: A slash (`/`) delimited string that describes the local path
  the file for the value should be found in, within the configuration
  directory.
  
The auxiliary method `WriteSchema`, which is called by `Parse` and
`ParseIn` by default, will write a file in the configuration directory
named `.schema`, that specifies all options. This can be parsed by
configuration editors, to give an overview of all possible options.

Legal
-----

M dirconf.go => dirconf.go +112 -97
@@ 8,8 8,9 @@ import (
	"io/ioutil"
	"os"
	"os/user"
	"path"
	"path/filepath"
	"strconv"
	"strings"
)

const schemaFile = ".schema"


@@ 17,77 18,43 @@ const schemaFile = ".schema"
var (
	// ErrWrongType is returned when the content of a value cannot be
	// cast to the requested type
	ErrWrongType = errors.New("Wrong Type")
	ErrWrongType = errors.New("wrong type")

	// ErrNoValueFound is returned when a value MUST have a value but no
	// such value could be found
	ErrNoValueFound = errors.New("Wrong Type")
	ErrNoValueFound = errors.New("a value was not found")
)

// Option is a option
// Option is any value that
type Option interface {
	Name() string
	Set(c *Conf, r io.Reader) error
}

// Conf represents an open directory configuration
type Conf struct {
	err     error
	path    string
	options map[string]Option
}

func getPath(name string) (string, error) {
	dir := os.Getenv("XDG_CONFIG_HOME")
	if dir == "" {
		me, err := user.Current()
		if err != nil {
			return "", err
		}
		dir = path.Join(me.HomeDir, ".config")
	}
	return path.Join(dir, name), nil
}

// Open a directory configuration object while paying attention to
// XDG variables.
//
// See https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html
func Open(name string) (*Conf, error) {
	dir, err := getPath(name)
	if err != nil {
		return nil, err
	}
	return &Conf{path: dir}, nil
// PathOption is a Option that can offer a relative file that it's value
// would be found in
type PathOption interface {
	Option
	Name() string
	Path() []string
}

// Add adds a option to the current configuration
func (c *Conf) Add(o Option, key ...string) {
	if c.path == "" {
		c.path, c.err = getPath(os.Args[0])
	}
	if c.err != nil {
		return
	}
	if c.options == nil {
		c.options = make(map[string]Option)
	}
	c.options[path.Join(append([]string{c.path}, key...)...)] = o
}
type Conf []Option

func (c *Conf) writeSchema() error {
	file, err := os.OpenFile(path.Join(c.path, schemaFile),
func (c *Conf) WriteSchema(dir string) error {
	file, err := os.OpenFile(filepath.Join(dir, schemaFile),
		os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644)
	if err != nil {
		return err
	}
	defer file.Close()

	for path, opt := range c.options {
		if opt.Name() == "" {
	for _, opt := range c {
		po, ok := opt.(PathOption)
		if !ok {
			continue
		}
		_, err := fmt.Fprintf(file, "%s\t%s\n", opt.Name(), path)
		rfile := filepath.Join(po.Path()...)
		_, err := fmt.Fprintf(file, "%s\t%s\n", po.Name(), rfile)
		if err != nil {
			return err
		}


@@ 96,29 63,43 @@ func (c *Conf) writeSchema() error {
	return nil
}

// Parse is a function
func (c *Conf) Parse() error {
	if c.err != nil {
		return c.err
	name := filepath.Base(os.Args[0])
	dir := os.Getenv("XDG_CONFIG_HOME")
	if dir == "" {
		me, err := user.Current()
		if err != nil {
			return err
		}
		dir = filepath.Join(me.HomeDir, ".config")
	}

	err := os.MkdirAll(c.path, 0755)
	return c.ParseIn(filepath.Joinc(dir, name))
}

func (c *Conf) ParseIn(dir string) error {
	err := os.MkdirAll(dir, 0755)
	if err != nil {
		return err
	}

	err = c.writeSchema()
	err = c.WriteSchema(dir)
	if err != nil {
		return err
	}

	for path, opt := range c.options {
		file, err := os.Open(path)
		if err != nil {
			opt.Set(c, nil)
	for _, opt := range c {
		if po, ok := opt.(PathOption); ok {
			fpath := append([]string{dir}, po.Path()...)
			file, err := os.Open(filepath.Join(fpath...))
			if err != nil {
				opt.Set(c, nil)
			} else {
				opt.Set(c, file)
				file.Close()
			}
		} else {
			opt.Set(c, file)
			file.Close()
			opt.Set(c, nil)
		}
	}



@@ 135,18 116,23 @@ type Int struct {
	Default int
	Must    bool
	OnSet   func(int)
	Key     string
}

// Name returns the name of the option type
func (o *Int) Name() string { return "int" }

// Path returns the relative file of this option
func (o *Int) Path() []string { return strings.Split(o.Key, "/") }

// Set is called when the value of a Int is processed
func (o *Int) Set(c *Conf, r io.Reader) error {
	var val int
	if r == nil {
		if o.Must {
			return ErrNoValueFound
		}
		*o.Ptr = o.Default
		val = o.Default
	} else {
		data, err := ioutil.ReadAll(r)
		if err != nil {


@@ 158,10 144,13 @@ func (o *Int) Set(c *Conf, r io.Reader) error {
			return ErrWrongType
		}

		*o.Ptr = int(i)
		val = int(i)
	}
	if o.Ptr != nil {
		*o.Ptr = val
	}
	if o.OnSet != nil {
		o.OnSet(*o.Ptr)
		o.OnSet(val)
	}
	return nil
}


@@ 176,31 165,39 @@ type Float struct {
	Default float64
	Must    bool
	OnSet   func(float64)
	Key     string
}

// Name returns the name of the option type
func (o *Float) Name() string { return "float" }

// Path returns the relative file of this option
func (o *Float) Path() []string { return strings.Split(o.Key, "/") }

// Set is called when the value of a Int is processed
func (o *Float) Set(c *Conf, r io.Reader) error {
	var val float64
	if r == nil {
		if o.Must {
			return ErrNoValueFound
		}
		*o.Ptr = o.Default
		val = o.Default
	} else {
		data, err := ioutil.ReadAll(r)
		if err != nil {
			return err
		}

		*o.Ptr, err = strconv.ParseFloat(string(data), 64)
		val, err = strconv.ParseFloat(string(data), 64)
		if err != nil {
			return ErrWrongType
		}
	}
	if o.Ptr != nil {
		*o.Ptr = val
	}
	if o.OnSet != nil {
		o.OnSet(*o.Ptr)
		o.OnSet(val)
	}
	return nil
}


@@ 215,28 212,35 @@ type String struct {
	Default string
	Must    bool
	OnSet   func(string)
	Key     string
}

// Name returns the name of the option type
func (o *String) Name() string { return "string" }

// Path returns the relative file of this option
func (o *String) Path() []string { return strings.Split(o.Key, "/") }

// Set is called when the value of a Int is processed
func (o *String) Set(c *Conf, r io.Reader) error {
	var val string
	if r == nil {
		if o.Must {
			return ErrNoValueFound
		}
		*o.Ptr = o.Default
		val = o.Default
	} else {
		data, err := ioutil.ReadAll(r)
		if err != nil {
			return err
		}

		*o.Ptr = string(data)
		val = string(data)
	}
	if o.Ptr != nil {
		*o.Ptr = val
	}
	if o.OnSet != nil {
		o.OnSet(*o.Ptr)
		o.OnSet(val)
	}
	return nil
}


@@ 254,15 258,19 @@ type List struct {
}

// Name returns the name of the option type
func (o *List) Name() string { return "stringarray" }
func (o *List) Name() string { return "list" }

// Path returns the relative file of this option
func (o *List) Path() []string { return strings.Split(o.Key, "/") }

// Set is called when the value of a Int is processed
func (o *List) Set(c *Conf, r io.Reader) error {
	var val []string
	if r == nil {
		if o.Must {
			return ErrNoValueFound
		}
		*o.Ptr = o.Default
		val = o.Default
	} else {
		data, err := ioutil.ReadAll(r)
		if err != nil {


@@ 270,13 278,16 @@ func (o *List) Set(c *Conf, r io.Reader) error {
		}

		list := bytes.Split(data, []byte("\n"))
		*o.Ptr = make([]string, len(list))
		val = make([]string, len(list))
		for i, b := range list {
			(*o.Ptr)[i] = string(b)
			val[i] = string(b)
		}
	}
	if o.Ptr != nil {
		*o.Ptr = val
	}
	if o.OnSet != nil {
		o.OnSet(*o.Ptr)
		o.OnSet(val)
	}
	return nil
}


@@ 289,16 300,22 @@ func (o *List) Set(c *Conf, r io.Reader) error {
type Bool struct {
	Ptr   *bool
	OnSet func(bool)
	Key   string
}

// Name returns the name of the option type
func (o *Bool) Name() string { return "boolean" }

// Path returns the relative file of this option
func (o *Bool) Path() []string { return strings.Split(o.Key, "/") }

// Set is called when the value of a Int is processed
func (o *Bool) Set(c *Conf, r io.Reader) error {
	*o.Ptr = r == nil
	if o.Ptr != nil {
		*o.Ptr = r == nil
	}
	if o.OnSet != nil {
		o.OnSet(*o.Ptr)
		o.OnSet(r == nil)
	}
	return nil
}


@@ 307,15 324,21 @@ func (o *Bool) Set(c *Conf, r io.Reader) error {
// GENERIC OPTION //
////////////////////

// Func is an option for a bool
type Func func(r io.Reader) error
// Func is an option for a generic option
type Func struct {
	Func func(r io.Reader) error
	Key  string
}

// Name returns the name of the option type
func (f *Func) Name() string { return "file" }

// Path returns the relative file of this option
func (f *Func) Path() []string { return strings.Split(f.Key, "/") }

// Set is called when the value of a Int is processed
func (f *Func) Set(c *Conf, r io.Reader) error {
	return (*f)(r)
	return f.Func(r)
}

/////////////////


@@ 325,22 348,14 @@ func (f *Func) Set(c *Conf, r io.Reader) error {
// Path is an option for a to get the path
type Path struct {
	Ptr   *string
	OnSet func(string)
	OnSet func([]string)
	Key   string
}

// Name returns the name of the option type
func (o *Path) Name() string { return "" }

// Set is called when the value of a Int is processed
func (o *Path) Set(c *Conf, r io.Reader) error {
	for p, opt := range c.options {
		if opt == o {
			*o.Ptr = p
			if o.OnSet != nil {
				o.OnSet(p)
			}
			return nil
		}
	}
	panic("this shoudln't happen")
	path := append([]string{c.path}, strings.Split(o.Key, "/")...)
	val := filepath.Join(path...)
	return nil

}