github.com/lmorg/murex@v0.0.0-20240217211045-e081c89cd4ef/config/config.go (about)

     1  package config
     2  
     3  import (
     4  	"fmt"
     5  	"sync"
     6  
     7  	mxapp "github.com/lmorg/murex/app"
     8  	"github.com/lmorg/murex/lang/ref"
     9  	"github.com/lmorg/murex/lang/types"
    10  )
    11  
    12  // InitConf is a table of global config options
    13  var InitConf = newGlobal()
    14  
    15  // Properties is the Config defaults and descriptions
    16  type Properties struct {
    17  	Description string
    18  	Default     interface{}
    19  	DataType    string
    20  	Options     []string
    21  	Global      bool
    22  	Dynamic     DynamicProperties
    23  	GoFunc      GoFuncProperties
    24  	FileRefDef  *ref.File
    25  }
    26  
    27  // DynamicProperties is used for dynamic values written in murex
    28  type DynamicProperties struct {
    29  	Read       string
    30  	Write      string
    31  	GetDynamic func() (interface{}, error) `json:"-"`
    32  	SetDynamic func(interface{}) error     `json:"-"`
    33  }
    34  
    35  // GoFuncProperties are used for dynamic values written in Go
    36  type GoFuncProperties struct {
    37  	Read  func() (interface{}, error) `json:"-"`
    38  	Write func(interface{}) error     `json:"-"`
    39  }
    40  
    41  // Config is used to store all the configuration settings, `config`, in a thread-safe API
    42  type Config struct {
    43  	mutex      sync.RWMutex
    44  	properties map[string]map[string]Properties  // This will be the main configuration metadata for each configuration option
    45  	fileRefSet map[string]map[string]*ref.File   // This is separate from Properties because it gets updated more frequently (eg custom setters)
    46  	values     map[string]map[string]interface{} // This stores the values when no custom getter and setter have been defined
    47  	global     *Config
    48  }
    49  
    50  func newGlobal() *Config {
    51  	conf := new(Config)
    52  	conf.properties = make(map[string]map[string]Properties)
    53  	conf.fileRefSet = make(map[string]map[string]*ref.File)
    54  	conf.values = make(map[string]map[string]interface{})
    55  	return conf
    56  }
    57  
    58  func newConfiguration(global *Config) *Config {
    59  	conf := new(Config)
    60  	conf.properties = make(map[string]map[string]Properties)
    61  	conf.fileRefSet = make(map[string]map[string]*ref.File)
    62  	conf.values = make(map[string]map[string]interface{})
    63  	conf.global = global
    64  	return conf
    65  }
    66  
    67  // Copy creates a new *Config instance referenced to the parent
    68  func (conf *Config) Copy() *Config {
    69  	if conf.global == nil {
    70  		return newConfiguration(conf)
    71  	}
    72  
    73  	return newConfiguration(conf.global)
    74  }
    75  
    76  // ExistsAndGlobal checks if a config option exists and/or is global
    77  func (conf *Config) ExistsAndGlobal(app, key string) (exists, global bool) {
    78  	conf.mutex.RLock()
    79  	exists = conf.properties[app] != nil && conf.properties[app][key].DataType != "" && conf.properties[app][key].Description != ""
    80  	global = exists && conf.properties[app][key].Global
    81  	conf.mutex.RUnlock()
    82  	return
    83  }
    84  
    85  // Get retrieves a setting from the Config. Returns an interface{} for the value and err for any failures
    86  //
    87  //	app == tooling name
    88  //	key == name of setting
    89  //	dataType == what `types.dataType` to cast the return value into
    90  func (conf *Config) Get(app, key, dataType string) (interface{}, error) {
    91  	v, _, err := conf.GetFileRef(app, key, dataType)
    92  	return v, err
    93  }
    94  
    95  // GetFileRef retrieves a setting from the Config. Returns an interface{} for the value and err for any failures
    96  func (conf *Config) GetFileRef(app, key, dataType string) (interface{}, *ref.File, error) {
    97  	conf.mutex.RLock()
    98  
    99  	if conf.global != nil && conf.values[app] != nil && conf.values[app][key] != nil {
   100  		v := conf.values[app][key]
   101  		fileRef := conf.fileRefSet[app][key]
   102  		conf.mutex.RUnlock()
   103  		value, err := types.ConvertGoType(v, dataType)
   104  		return value, fileRef, err
   105  	}
   106  
   107  	if conf.properties[app] == nil || conf.properties[app][key].DataType == "" || conf.properties[app][key].Description == "" {
   108  		conf.mutex.RUnlock()
   109  
   110  		if conf.global != nil {
   111  			return conf.global.GetFileRef(app, key, dataType)
   112  		}
   113  		return nil, nil, fmt.Errorf("cannot get config. No config has been defined for app `%s`, key `%s`", app, key)
   114  	}
   115  
   116  	var (
   117  		v       interface{}
   118  		err     error
   119  		fileRef = conf.fileRefSet[app][key]
   120  	)
   121  
   122  	switch {
   123  	case conf.properties[app][key].Dynamic.GetDynamic != nil:
   124  		v, err = conf.properties[app][key].Dynamic.GetDynamic()
   125  		conf.mutex.RUnlock()
   126  		if err != nil {
   127  			return nil, nil, err
   128  		}
   129  
   130  	case conf.properties[app][key].GoFunc.Read != nil:
   131  		v, err = conf.properties[app][key].GoFunc.Read()
   132  		conf.mutex.RUnlock()
   133  		if err != nil {
   134  			return nil, nil, err
   135  		}
   136  
   137  	default:
   138  		v = conf.values[app][key]
   139  		if v == nil {
   140  			v = conf.properties[app][key].Default
   141  		}
   142  		conf.mutex.RUnlock()
   143  	}
   144  
   145  	value, err := types.ConvertGoType(v, dataType)
   146  	return value, fileRef, err
   147  }
   148  
   149  // Set changes a setting in the Config object
   150  //
   151  //	app == tooling name
   152  //	key == name of setting
   153  //	value == the setting itself
   154  func (conf *Config) Set(app string, key string, value interface{}, fileRef *ref.File) error {
   155  	// first check if we're in a global, and whether we should be
   156  	if conf.global != nil {
   157  		exists, global := conf.global.ExistsAndGlobal(app, key)
   158  		if !exists || global {
   159  			return conf.global.Set(app, key, value, fileRef)
   160  		}
   161  	}
   162  
   163  	conf.mutex.Lock()
   164  
   165  	if conf.global == nil {
   166  		if conf.properties[app] == nil || conf.properties[app][key].DataType == "" || conf.properties[app][key].Description == "" {
   167  			conf.mutex.Unlock()
   168  			return fmt.Errorf("cannot set config. No config has been defined for app `%s`, key `%s`", app, key)
   169  		}
   170  	}
   171  
   172  	switch {
   173  	case conf.properties[app][key].Dynamic.SetDynamic != nil:
   174  		conf.mutex.Unlock()
   175  		err := conf.properties[app][key].Dynamic.SetDynamic(value)
   176  		if err == nil {
   177  			conf.mutex.Lock()
   178  			conf.fileRefSet[app][key] = fileRef
   179  			conf.mutex.Unlock()
   180  		}
   181  		return err
   182  
   183  	case conf.properties[app][key].GoFunc.Write != nil:
   184  		conf.mutex.Unlock()
   185  		err := conf.properties[app][key].GoFunc.Write(value)
   186  		if err == nil {
   187  			conf.mutex.Lock()
   188  			conf.fileRefSet[app][key] = fileRef
   189  			conf.mutex.Unlock()
   190  		}
   191  		return err
   192  
   193  	default:
   194  		if len(conf.values) == 0 {
   195  			conf.values = make(map[string]map[string]interface{})
   196  			conf.fileRefSet = make(map[string]map[string]*ref.File)
   197  		}
   198  		if len(conf.values[app]) == 0 {
   199  			conf.values[app] = make(map[string]interface{})
   200  			conf.fileRefSet[app] = make(map[string]*ref.File)
   201  		}
   202  
   203  		conf.fileRefSet[app][key] = fileRef
   204  		conf.values[app][key] = value
   205  
   206  		conf.mutex.Unlock()
   207  		return nil
   208  	}
   209  }
   210  
   211  // Default resets a config option back to its default
   212  func (conf *Config) Default(app string, key string, fileRef *ref.File) error {
   213  	c := conf.global
   214  	if c == nil {
   215  		c = conf
   216  	}
   217  
   218  	exists, _ := c.ExistsAndGlobal(app, key)
   219  
   220  	if !exists {
   221  		return fmt.Errorf("cannot default config. No config has been defined for app `%s`, key `%s`", app, key)
   222  	}
   223  
   224  	c.mutex.RLock()
   225  	v := c.properties[app][key].Default
   226  	c.mutex.RUnlock()
   227  
   228  	return conf.Set(app, key, v, fileRef)
   229  }
   230  
   231  // DataType retrieves the murex data type for a given Config property
   232  func (conf *Config) DataType(app, key string) string {
   233  	if conf.global != nil {
   234  		return conf.global.DataType(app, key)
   235  	}
   236  
   237  	conf.mutex.Lock()
   238  	dt := conf.properties[app][key].DataType
   239  	conf.mutex.Unlock()
   240  	return dt
   241  }
   242  
   243  // Define allows new properties to be created in the Config object
   244  func (conf *Config) Define(app string, key string, properties Properties) {
   245  	if properties.FileRefDef == nil {
   246  		properties.FileRefDef = ref.NewModule(mxapp.ShellModule)
   247  	}
   248  	if conf.global != nil {
   249  		conf.global.Define(app, key, properties)
   250  		return
   251  	}
   252  
   253  	conf.mutex.Lock()
   254  	if conf.properties[app] == nil {
   255  		conf.properties[app] = make(map[string]Properties)
   256  		conf.fileRefSet[app] = make(map[string]*ref.File)
   257  		conf.values[app] = make(map[string]interface{})
   258  	}
   259  
   260  	// don't set the value to the default if it's a dynamic property
   261  	if properties.Dynamic.Read == "" && properties.GoFunc.Read == nil {
   262  		conf.values[app][key] = properties.Default
   263  	} else {
   264  		properties.Global = true
   265  	}
   266  	conf.properties[app][key] = properties
   267  	conf.fileRefSet[app][key] = properties.FileRefDef
   268  	conf.mutex.Unlock()
   269  }
   270  
   271  // DumpRuntime returns an object based on Config which is optimised for JSON
   272  // serialisation for the `runtime --config` CLI command
   273  func (conf *Config) DumpRuntime() (obj map[string]map[string]map[string]interface{}) {
   274  	if conf.global != nil {
   275  		return conf.global.DumpRuntime()
   276  	}
   277  
   278  	conf.mutex.RLock()
   279  	obj = make(map[string]map[string]map[string]interface{})
   280  	for app := range conf.properties {
   281  		obj[app] = make(map[string]map[string]interface{})
   282  		for key := range conf.properties[app] {
   283  			obj[app][key] = make(map[string]interface{})
   284  			obj[app][key]["Description"] = conf.properties[app][key].Description
   285  			obj[app][key]["Data-Type"] = conf.properties[app][key].DataType
   286  			obj[app][key]["Default"] = conf.properties[app][key].Default
   287  			obj[app][key]["Value"] = conf.values[app][key]
   288  			obj[app][key]["FileRefDefined"] = conf.properties[app][key].FileRefDef
   289  			obj[app][key]["FileRefSet"] = conf.fileRefSet[app][key]
   290  
   291  			//if conf.properties[app][key].Global {
   292  			obj[app][key]["Global"] = conf.properties[app][key].Global
   293  			//}
   294  
   295  			//if len(conf.properties[app][key].Options) != 0 {
   296  			obj[app][key]["Options"] = conf.properties[app][key].Options
   297  			//}
   298  
   299  			//if len(conf.properties[app][key].Dynamic.Read) != 0 {
   300  			obj[app][key]["Dynamic"] = conf.properties[app][key].Dynamic
   301  			//}
   302  
   303  			//if conf.properties[app][key].GoFunc.Read != nil {
   304  			obj[app][key]["GoFunc"] = map[string]bool{
   305  				"Read":  conf.properties[app][key].GoFunc.Read != nil,
   306  				"Write": conf.properties[app][key].GoFunc.Write != nil,
   307  			}
   308  			//}
   309  
   310  		}
   311  	}
   312  	conf.mutex.RUnlock()
   313  	return
   314  }
   315  
   316  // DumpConfig returns an object based on Config which is optimised for JSON
   317  // serialisation for the `config` CLI command
   318  func (conf *Config) DumpConfig() (obj map[string]map[string]map[string]interface{}) {
   319  	if conf.global != nil {
   320  		return conf.global.DumpConfig()
   321  	}
   322  
   323  	conf.mutex.RLock()
   324  	obj = make(map[string]map[string]map[string]interface{})
   325  	for app := range conf.properties {
   326  		obj[app] = make(map[string]map[string]interface{})
   327  		for key := range conf.properties[app] {
   328  			obj[app][key] = make(map[string]interface{})
   329  			obj[app][key]["Description"] = conf.properties[app][key].Description
   330  			obj[app][key]["Data-Type"] = conf.properties[app][key].DataType
   331  			obj[app][key]["Default"] = conf.properties[app][key].Default
   332  
   333  			switch {
   334  			case conf.properties[app][key].GoFunc.Read != nil:
   335  				v, err := conf.properties[app][key].GoFunc.Read()
   336  				if err == nil {
   337  					obj[app][key]["Value"] = v
   338  				}
   339  			case len(conf.properties[app][key].Dynamic.Read) == 0:
   340  				obj[app][key]["Value"] = conf.values[app][key]
   341  			}
   342  
   343  			obj[app][key]["Global"] = conf.properties[app][key].Global
   344  
   345  			if len(conf.properties[app][key].Options) != 0 {
   346  				obj[app][key]["Options"] = conf.properties[app][key].Options
   347  			}
   348  
   349  			obj[app][key]["Dynamic"] = len(conf.properties[app][key].Dynamic.Read) != 0
   350  
   351  		}
   352  	}
   353  	conf.mutex.RUnlock()
   354  	return
   355  }