newt/ycfg/ycfg.go (464 lines of code) (raw):
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package ycfg
import (
"fmt"
"mynewt.apache.org/newt/newt/cfgv"
"strings"
"github.com/spf13/cast"
"mynewt.apache.org/newt/newt/parse"
"mynewt.apache.org/newt/util"
"mynewt.apache.org/newt/yaml"
)
// YAML configuration object. This is a substitute for a viper configuration
// object, with the following newt-specific advantages:
// 1. Case sensitive.
// 2. Efficient conditionals based on syscfg values.
//
// A single YCfg setting is implemented as a tree of nodes. Each word in the
// setting name represents a node; each "." in the name is a link in the tree.
// For example, the following syscfg lines:
//
// OS_MAIN_STACK_SIZE: 100
// OS_MAIN_STACK_SIZE.BLE_DEVICE: 200
// OS_MAIN_STACK_SIZE.SHELL_TASK: 300
//
// Is represented as the following tree:
//
// [OS_MAIN_STACK_SIZE (100)]
// / \
// [BLE_DEVICE (200)] [SHELL_TASK (300)]
//
// This allows us to quickly determine the value of OS_MAIN_STACK_SIZE. After
// finding the OS_MAIN_STACK_SIZE node, the logic is something like this:
//
// Is BLE_DEVICE true? --> 200
// Is SHELL_TASK true? --> 300
// Else: --> 100
//
// The tree structure also allows for arbitrary expressions as conditionals, as
// opposed to simple setting names. For example:
//
// OS_MAIN_STACK_SIZE: 100
// OS_MAIN_STACK_SIZE.'(BLE_DEVICE && !SHELL_TASK): 200
// OS_MAIN_STACK_SIZE.'(SHELL_TASK && !BLE_DEVICE): 300
// OS_MAIN_STACK_SIZE.'(SHELL_TASK && BLE_DEVICE): 400
//
// Since each expression is a child node of the setting in question, they are
// all known at the time of the lookup. To determine the value of the setting,
// each expression is parsed, and only the one evaluating to true is selected.
type YCfg struct {
// Name of config; typically a YAML filename.
name string
// The settings.
tree YCfgTree
}
type YCfgEntry struct {
Value interface{}
Expr *parse.Node
}
type YCfgNode struct {
Overwrite bool
Name string
Value interface{}
Children YCfgTree
Parent *YCfgNode
FileInfo *util.FileInfo
}
type YCfgTree map[string]*YCfgNode
func (yc *YCfg) Tree() YCfgTree {
return yc.tree
}
func NewYCfgNode() *YCfgNode {
return &YCfgNode{Children: YCfgTree{}}
}
func (node *YCfgNode) addChild(name string) (*YCfgNode, error) {
if node.Children == nil {
node.Children = YCfgTree{}
}
if node.Children[name] != nil {
return nil, fmt.Errorf("Duplicate YCfgNode: %s", name)
}
child := NewYCfgNode()
child.Name = name
child.Parent = node
node.Children[name] = child
return child, nil
}
func (yc *YCfg) ReplaceFromFile(key string, val interface{},
fileInfo *util.FileInfo) error {
elems := strings.Split(key, ".")
if len(elems) == 0 {
return fmt.Errorf("Invalid ycfg key: \"\"")
}
var overwrite bool
if elems[len(elems)-1] == "OVERWRITE" {
overwrite = true
elems = elems[:len(elems)-1]
}
var parent *YCfgNode
for i, e := range elems {
var parentChildren YCfgTree
if parent == nil {
parentChildren = yc.tree
} else {
if parent.Children == nil {
parent.Children = YCfgTree{}
}
parentChildren = parent.Children
}
child := parentChildren[e]
if child == nil {
var err error
if parent != nil {
child, err = parent.addChild(e)
if err != nil {
return err
}
} else {
child = NewYCfgNode()
child.Name = e
parentChildren[e] = child
}
}
if i == len(elems)-1 {
child.Overwrite = overwrite
child.Value = val
}
child.FileInfo = fileInfo
parent = child
}
return nil
}
// MergeFromFile merges the given value into a tree node. Only two value types
// can be merged:
//
// map[interface{}]interface{}
// []interface{}
//
// The node's current value must have the same type as the value being merged.
// In the map case, each key-value pair in the given value is inserted into the
// current value, overwriting as necessary. In the slice case, the given value
// is appended to the current value.
//
// If no node with the specified key exists, a new node is created containing
// the given value.
func (yc *YCfg) MergeFromFile(key string, val interface{},
fileInfo *util.FileInfo) error {
// Find the node being merged into.
node := yc.find(key)
// If the node doesn't exist, create one with the new value.
if node == nil || node.Value == nil {
return yc.ReplaceFromFile(key, val, fileInfo)
}
// The null value gets interpreted as an empty string during YAML
// parsing. A null merge is a no-op.
if s, ok := val.(string); ok && s == "" {
val = nil
}
if val == nil {
return nil
}
mergeErr := func() error {
return util.FmtNewtError(
"can't merge type %T into cfg node \"%s\"; node type: %T",
val, key, node.Value)
}
switch nodeVal := node.Value.(type) {
case map[interface{}]interface{}:
newVal, ok := val.(map[interface{}]interface{})
if !ok {
return mergeErr()
}
for k, v := range newVal {
nodeVal[k] = v
}
case []interface{}:
newVal, ok := val.([]interface{})
if !ok {
return mergeErr()
}
node.Value = append(nodeVal, newVal...)
default:
return mergeErr()
}
return nil
}
func (yc *YCfg) Replace(key string, val interface{}) error {
return yc.ReplaceFromFile(key, val, nil)
}
func NewYCfg(name string) YCfg {
return YCfg{
name: name,
tree: YCfgTree{},
}
}
func (yc *YCfg) find(key string) *YCfgNode {
elems := strings.Split(key, ".")
if len(elems) == 0 {
return nil
}
cur := &YCfgNode{
Children: yc.tree,
}
for _, e := range elems {
if cur.Children == nil {
return nil
}
cur = cur.Children[e]
if cur == nil {
return nil
}
}
return cur
}
func (yc *YCfg) HasKey(key string) bool {
return yc.find(key) != nil
}
// Get retrieves all nodes with the specified key. If it encounters a parse
// error in the tree, it ignores the bad node and continues the search. All
// bad nodes are indicated in the returned error. In this sense, the returned
// object is valid even if there is an error, and the error can be thought of
// as a set of warnings.
func (yc *YCfg) Get(key string,
settings *cfgv.Settings) ([]YCfgEntry, error) {
node := yc.find(key)
if node == nil {
return nil, nil
}
entries := []YCfgEntry{}
if node.Value != nil {
entry := YCfgEntry{Value: node.Value}
entries = append(entries, entry)
}
var errLines []string
for _, child := range node.Children {
expr, err := parse.LexAndParse(child.Name)
if err != nil {
errLines = append(errLines,
fmt.Sprintf("%s: %s", yc.name, err.Error()))
continue
}
val, err := parse.Eval(expr, settings)
if err != nil {
errLines = append(errLines,
fmt.Sprintf("%s: %s", yc.name, err.Error()))
continue
}
if val {
entry := YCfgEntry{
Value: child.Value,
Expr: expr,
}
if child.Overwrite {
entries = []YCfgEntry{entry}
break
}
entries = append(entries, entry)
}
}
if len(errLines) > 0 {
return entries, util.NewNewtError(strings.Join(errLines, "\n"))
} else {
return entries, nil
}
}
// GetSlice retrieves all entries with the specified key and coerces their
// values to type []interface{}. The returned []YCfgEntry is formed from the
// union of all these slices. The returned error is a set of warnings just as
// in `Get`.
func (yc *YCfg) GetSlice(key string, settings *cfgv.Settings) ([]YCfgEntry, error) {
sliceEntries, getErr := yc.Get(key, settings)
if len(sliceEntries) == 0 {
return nil, getErr
}
result := []YCfgEntry{}
for _, sliceEntry := range sliceEntries {
if sliceEntry.Value != nil {
slice, err := cast.ToSliceE(sliceEntry.Value)
if err != nil {
// Not a slice. Put the single value in a new slice.
slice = []interface{}{sliceEntry.Value}
}
for _, v := range slice {
entry := YCfgEntry{
Value: v,
Expr: sliceEntry.Expr,
}
result = append(result, entry)
}
}
}
return result, getErr
}
// GetValSlice retrieves all entries with the specified key and coerces their
// values to type []interface{}. The returned slice is the union of all these
// slices. The returned error is a set of warnings just as in `Get`.
func (yc *YCfg) GetValSlice(
key string, settings *cfgv.Settings) ([]interface{}, error) {
entries, getErr := yc.GetSlice(key, settings)
if len(entries) == 0 {
return nil, getErr
}
vals := make([]interface{}, len(entries))
for i, e := range entries {
vals[i] = e.Value
}
return vals, getErr
}
// GetStringSlice retrieves all entries with the specified key and coerces
// their values to type []string. The returned []YCfgEntry is formed from the
// union of all these slices. The returned error is a set of warnings just as
// in `Get`.
func (yc *YCfg) GetStringSlice(key string,
settings *cfgv.Settings) ([]YCfgEntry, error) {
sliceEntries, getErr := yc.Get(key, settings)
if len(sliceEntries) == 0 {
return nil, getErr
}
result := []YCfgEntry{}
for _, sliceEntry := range sliceEntries {
if sliceEntry.Value != nil {
slice, err := cast.ToStringSliceE(sliceEntry.Value)
if err != nil {
// Not a slice. Put the single value in a new slice.
slice = []string{cast.ToString(sliceEntry.Value)}
}
for _, v := range slice {
entry := YCfgEntry{
Value: v,
Expr: sliceEntry.Expr,
}
result = append(result, entry)
}
}
}
return result, getErr
}
// GetValStringSlice retrieves all entries with the specified key and coerces
// their values to type []string. The returned []string is the union of all
// these slices. The returned error is a set of warnings just as in `Get`.
func (yc *YCfg) GetValStringSlice(
key string, settings *cfgv.Settings) ([]string, error) {
entries, getErr := yc.GetStringSlice(key, settings)
if len(entries) == 0 {
return nil, getErr
}
vals := make([]string, len(entries))
for i, e := range entries {
if e.Value != nil {
vals[i] = cast.ToString(e.Value)
}
}
return vals, getErr
}
// GetValStringSliceNonempty retrieves all entries with the specified key and
// coerces their values to type []string. The returned []string is the union
// of all these slices. Empty strings are excluded from this union. The
// returned error is a set of warnings just as in `Get`.
func (yc *YCfg) GetValStringSliceNonempty(
key string, settings *cfgv.Settings) ([]string, error) {
strs, getErr := yc.GetValStringSlice(key, settings)
filtered := make([]string, 0, len(strs))
for _, s := range strs {
if s != "" {
filtered = append(filtered, s)
}
}
return filtered, getErr
}
// GetStringMap retrieves all entries with the specified key and coerces their
// values to type map[string]interface{}. The returned map[string]YCfgEntry is
// formed from the union of all these maps. The returned error is a set of
// warnings just as in `Get`.
func (yc *YCfg) GetStringMap(
key string, settings *cfgv.Settings) (map[string]YCfgEntry, error) {
mapEntries, getErr := yc.Get(key, settings)
if len(mapEntries) == 0 {
return nil, getErr
}
result := map[string]YCfgEntry{}
for _, mapEntry := range mapEntries {
for k, v := range cast.ToStringMap(mapEntry.Value) {
entry := YCfgEntry{
Value: v,
Expr: mapEntry.Expr,
}
// XXX: Report collisions?
result[k] = entry
}
}
return result, getErr
}
// GetValStringMap retrieves all entries with the specified key and coerces
// their values to type map[string]interface{}. The returned
// map[string]YCfgEntry is the union of all these maps. The returned error is
// a set of warnings just as in `Get`.
func (yc *YCfg) GetValStringMap(
key string, settings *cfgv.Settings) (map[string]interface{}, error) {
entryMap, getErr := yc.GetStringMap(key, settings)
smap := make(map[string]interface{}, len(entryMap))
for k, v := range entryMap {
if v.Value != nil {
smap[k] = v.Value
}
}
return smap, getErr
}
// GetFirst retrieves the first entry with the specified key. The bool return
// value is true if a matching entry was found. The returned error is a set of
// warnings just as in `Get`.
func (yc *YCfg) GetFirst(key string,
settings *cfgv.Settings) (YCfgEntry, bool, error) {
entries, getErr := yc.Get(key, settings)
if len(entries) == 0 {
return YCfgEntry{}, false, getErr
}
return entries[0], true, getErr
}
// GetFirstVal retrieves the first entry with the specified key and returns its
// value. It returns nil if no matching entry is found. The returned error is
// a set of warnings just as in `Get`.
func (yc *YCfg) GetFirstVal(key string,
settings *cfgv.Settings) (interface{}, error) {
entry, ok, getErr := yc.GetFirst(key, settings)
if !ok {
return nil, getErr
}
return entry.Value, getErr
}
// GetValString retrieves the first entry with the specified key and returns
// its value coerced to a string. It returns "" if no matching entry is found.
// The returned error is a set of warnings just as in `Get`.
func (yc *YCfg) GetValString(key string,
settings *cfgv.Settings) (string, error) {
entry, ok, getErr := yc.GetFirst(key, settings)
if !ok {
return "", getErr
} else {
return cast.ToString(entry.Value), getErr
}
}
// GetValInt retrieves the first entry with the specified key and returns its
// value coerced to an int. It returns 0 if no matching entry is found. The
// returned error is a set of warnings just as in `Get`.
func (yc *YCfg) GetValInt(key string, settings *cfgv.Settings) (int, error) {
entry, ok, getErr := yc.GetFirst(key, settings)
if !ok {
return 0, getErr
} else {
return cast.ToInt(entry.Value), getErr
}
}
// GetValIntDflt retrieves the first entry with the specified key and returns its
// value coerced to an int. It returns the specified default if no matching entry
// is found. The returned error is a set of warnings just as in `Get`.
func (yc *YCfg) GetValIntDflt(key string, settings *cfgv.Settings, dflt int) (int, error) {
entry, ok, getErr := yc.GetFirst(key, settings)
if !ok {
return dflt, getErr
} else {
return cast.ToInt(entry.Value), getErr
}
}
// GetValBoolDflt retrieves the first entry with the specified key and returns
// its value coerced to a bool. It returns the specified default if no
// matching entry is found. The returned error is a set of warnings just as in
// `Get`.
func (yc *YCfg) GetValBoolDflt(key string, settings *cfgv.Settings,
dflt bool) (bool, error) {
entry, ok, getErr := yc.GetFirst(key, settings)
if !ok {
return dflt, getErr
} else {
return cast.ToBool(entry.Value), getErr
}
}
// GetValBoolDflt retrieves the first entry with the specified key and returns
// its value coerced to a bool. It returns false if no matching entry is
// found. The returned error is a set of warnings just as in `Get`.
func (yc *YCfg) GetValBool(key string,
settings *cfgv.Settings) (bool, error) {
return yc.GetValBoolDflt(key, settings, false)
}
// GetStringMapString retrieves all entries with the specified key and coerces
// their values to type map[string]string. The returned map[string]YCfgEntry
// is formed from the union of all these maps. The returned error is a set of
// warnings just as in `Get`.
func (yc *YCfg) GetStringMapString(key string,
settings *cfgv.Settings) (map[string]YCfgEntry, error) {
mapEntries, getErr := yc.Get(key, settings)
if len(mapEntries) == 0 {
return nil, getErr
}
result := map[string]YCfgEntry{}
for _, mapEntry := range mapEntries {
for k, v := range cast.ToStringMapString(mapEntry.Value) {
entry := YCfgEntry{
Value: v,
Expr: mapEntry.Expr,
}
// XXX: Report collisions?
result[k] = entry
}
}
return result, getErr
}
// GetStringMapString retrieves all entries with the specified key and coerces
// their values to type map[string]string. The returned map[string]YCfgEntry
// is the union of all these maps. The returned error is a set of warnings
// just as in `Get`.
func (yc *YCfg) GetValStringMapString(key string,
settings *cfgv.Settings) (map[string]string, error) {
entryMap, getErr := yc.GetStringMapString(key, settings)
valMap := make(map[string]string, len(entryMap))
for k, v := range entryMap {
if v.Value != nil {
valMap[k] = cast.ToString(v.Value)
}
}
return valMap, getErr
}
// FullName calculates a node's name with the following form:
// [...].<grandparent>.<parent>.<node>
func (node *YCfgNode) FullName() string {
tokens := []string{}
for n := node; n != nil; n = n.Parent {
tokens = append(tokens, n.Name)
}
last := len(tokens) - 1
for i := 0; i < len(tokens)/2; i++ {
tokens[i], tokens[last-i] = tokens[last-i], tokens[i]
}
return strings.Join(tokens, ".")
}
// Delete deletes all entries with the specified key.
func (yc *YCfg) Delete(key string) {
delete(yc.tree, key)
}
// Clear removes all entries from the YCfg.
func (yc *YCfg) Clear() {
yc.tree = YCfgTree{}
}
// Traverse performs an in-order traversal of the YCfg tree. The specified
// function is applied to each node.
func (yc *YCfg) Traverse(cb func(node *YCfgNode, depth int)) {
var traverseLevel func(
node *YCfgNode,
cb func(node *YCfgNode, depth int),
depth int)
traverseLevel = func(
node *YCfgNode,
cb func(node *YCfgNode, depth int),
depth int) {
cb(node, depth)
for _, child := range node.Children {
traverseLevel(child, cb, depth+1)
}
}
for _, n := range yc.tree {
traverseLevel(n, cb, 0)
}
}
// AllSettings converts the YCfg into a map with the following form:
// <node-full-name>: <node-value>
func (yc *YCfg) AllSettings() map[string]interface{} {
settings := map[string]interface{}{}
yc.Traverse(func(node *YCfgNode, depth int) {
if node.Value != nil {
settings[node.FullName()] = node.Value
}
})
return settings
}
// AllSettingsAsStrings converts the YCfg into a map with the following form:
// <node-full-name>: <node-value>
//
// All values in the map have been coerced to strings.
func (yc *YCfg) AllSettingsAsStrings() map[string]string {
settings := yc.AllSettings()
smap := make(map[string]string, len(settings))
for k, v := range settings {
smap[k] = fmt.Sprintf("%v", v)
}
return smap
}
// String produces a user-friendly string representation of the YCfg.
func (yc *YCfg) String() string {
lines := make([]string, 0, len(yc.tree))
yc.Traverse(func(node *YCfgNode, depth int) {
line := strings.Repeat(" ", depth*4) + node.Name
if node.Value != nil {
line += fmt.Sprintf(": %+v", node.Value)
}
lines = append(lines, line)
})
return strings.Join(lines, "\n")
}
// YAML converts the YCfg to a map and encodes the map as YAML.
func (yc *YCfg) YAML() string {
return yaml.MapToYaml(yc.AllSettings())
}