143 lines
4.8 KiB
Go
143 lines
4.8 KiB
Go
|
// Package config provides a configuration system for applications.
|
||
|
package config
|
||
|
|
||
|
import "fmt"
|
||
|
import "math"
|
||
|
import "strconv"
|
||
|
import "git.tebibyte.media/tomo/tomo/event"
|
||
|
|
||
|
type configError string;
|
||
|
func (err configError) Error () string { return string(err) }
|
||
|
|
||
|
const (
|
||
|
// ErrClosed is returned when Get/Set/Reset is called after the config
|
||
|
// has been closed.
|
||
|
ErrClosed = configError("attempt to access a closed config")
|
||
|
// ErrNonexistentEntry is returned when an entry was not found.
|
||
|
ErrNonexistentEntry = configError("nonexistent entry")
|
||
|
// ErrMalformedEntry is returned when a config entry could not be
|
||
|
// parsed.
|
||
|
ErrMalformedEntry = configError("malformed entry")
|
||
|
// ErrMalformedKey is returned when a key has invalid runes.
|
||
|
ErrMalformedKey = configError("malformed key")
|
||
|
// ErrMalformedValue is returned when a value could not be parsed.
|
||
|
ErrMalformedValue = configError("malformed value")
|
||
|
// ErrMalformedString is returned when a string value could not be
|
||
|
// parsed.
|
||
|
ErrMalformedStringValue = configError("malformed string value")
|
||
|
// ErrMalformedNumber is returned when a number value could not be
|
||
|
// parsed.
|
||
|
ErrMalformedNumberValue = configError("malformed number value")
|
||
|
// ErrMalformedBool is returned when a boolean value could not be
|
||
|
// parsed.
|
||
|
ErrMalformedBoolValue = configError("malformed bool value")
|
||
|
// ErrMalformedEscapeSequence us returned when an escape sequence could
|
||
|
// not be parsed.
|
||
|
ErrMalformedEscapeSequence = configError("malformed escape sequence")
|
||
|
)
|
||
|
|
||
|
// Config provides access to an application's configuration, and can notify an
|
||
|
// application of changes to it.
|
||
|
type Config interface {
|
||
|
// Get gets a value, first considering the user-level config file, and
|
||
|
// then falling back to system level config files. If the value could
|
||
|
// not be found anywhere, the specified fallback value is returned. If
|
||
|
// the key is invalid, it returns nil, ErrMalformedKey.
|
||
|
Get (key string, fallback Value) (Value, error)
|
||
|
// GetString is like Get, but will only return strings. If the value is
|
||
|
// not a string, it will return fallback.
|
||
|
GetString (key string, fallback string) (string, error)
|
||
|
// GetNumber is like Get, but will only return numbers. If the value is
|
||
|
// not a number, it will return fallback.
|
||
|
GetNumber (key string, fallback float64) (float64, error)
|
||
|
// GetBool is like Get, but will only return booleans. If the value is
|
||
|
// not a boolean, it will return fallback.
|
||
|
GetBool (key string, fallback bool) (bool, error)
|
||
|
// Set sets a value in the user-level config file. If the key is
|
||
|
// invalid, it returns ErrMalformedKey.
|
||
|
Set (key string, value Value) error
|
||
|
// Reset removes the value from the user-level config file, resetting it
|
||
|
// to what is described by the system-level config files. If the key is
|
||
|
// invalid, it returns ErrMalformedKey.
|
||
|
Reset (key string) error
|
||
|
// OnChange specifies a function to be called whenever a value is
|
||
|
// changed. The callback is always run within the backend's event loop
|
||
|
// using tomo.Do. This could have been a channel but I didn't want to do
|
||
|
// that to people.
|
||
|
OnChange (func (key string)) event.Cookie
|
||
|
}
|
||
|
|
||
|
// ConfigCloser is a config with a Close behavior, which stops watching the
|
||
|
// config file and causes any subsequent sets/gets to return errors. Anything
|
||
|
// that receives a ConfigCloser must close it when done.
|
||
|
type ConfigCloser interface {
|
||
|
Config
|
||
|
// Close closes the config, causing it to stop watching for changes.
|
||
|
// Reads or writes to the config after this will return an error.
|
||
|
Close () error
|
||
|
}
|
||
|
|
||
|
var negativeZero = math.Copysign(0, -1)
|
||
|
|
||
|
// Value is a config value. Its String behavior produces a lossless and
|
||
|
// syntactically valid representation of the value.
|
||
|
type Value interface {
|
||
|
value ()
|
||
|
fmt.Stringer
|
||
|
}
|
||
|
|
||
|
// ValueString is a string value.
|
||
|
type ValueString string
|
||
|
var _ Value = ValueString("")
|
||
|
func (ValueString) value () { }
|
||
|
func (value ValueString) String () string {
|
||
|
return fmt.Sprintf("\"%s\"", escape(string(value)))
|
||
|
}
|
||
|
|
||
|
// ValueNumber is a number value.
|
||
|
type ValueNumber float64
|
||
|
var _ Value = ValueNumber(0)
|
||
|
func (ValueNumber) value () { }
|
||
|
func (value ValueNumber) String () string {
|
||
|
number := float64(value)
|
||
|
// the requirements I wrote said lossless in all cases. here's lossless
|
||
|
// in all cases!
|
||
|
switch {
|
||
|
case math.IsInf(number, 0):
|
||
|
if math.Signbit(number) {
|
||
|
return "-Inf"
|
||
|
} else {
|
||
|
return "Inf"
|
||
|
}
|
||
|
|
||
|
case math.IsNaN(number):
|
||
|
return "NaN"
|
||
|
|
||
|
case number == 0, number == negativeZero:
|
||
|
if math.Signbit(number) {
|
||
|
return "-0"
|
||
|
} else {
|
||
|
return "0"
|
||
|
}
|
||
|
|
||
|
case math.Round(number) == number:
|
||
|
return strconv.FormatInt(int64(number), 10)
|
||
|
|
||
|
default:
|
||
|
return strconv.FormatFloat(number, 'f', -1, 64)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
// ValueBool is a boolean value.
|
||
|
var _ Value = ValueBool(false)
|
||
|
type ValueBool bool
|
||
|
func (ValueBool) value () { }
|
||
|
func (value ValueBool) String () string {
|
||
|
if value {
|
||
|
return "true"
|
||
|
} else {
|
||
|
return "false"
|
||
|
}
|
||
|
}
|