Initial project
This commit is contained in:
commit
e715024cbd
23 files changed
+1547
No files matched your search
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
Copyright © 2026 Craig Williams <craig@avsdev.uk>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type JobSearchConfigLogging struct {
|
||||
Level string
|
||||
}
|
||||
|
||||
// Default configuration
|
||||
func defaultConfigLogging() JobSearchConfigLogging {
|
||||
return JobSearchConfigLogging{
|
||||
Level: "warn",
|
||||
}
|
||||
}
|
||||
|
||||
// Converts configuration to a map
|
||||
func (conf JobSearchConfigLogging) Map() map[string]any {
|
||||
return map[string]any{
|
||||
"level": conf.Level,
|
||||
}
|
||||
}
|
||||
|
||||
// Converts configuration to a flat dotted format
|
||||
func (conf JobSearchConfigLogging) Flatten() map[string]any {
|
||||
confMap := conf.Map()
|
||||
retMap := map[string]any{}
|
||||
for k, v := range confMap {
|
||||
retMap["logging."+k] = v
|
||||
}
|
||||
return retMap
|
||||
}
|
||||
|
||||
// Returns the expected type of a configuration key value
|
||||
func (c JobSearchConfigLogging) TypeOf(key string) (reflect.Kind, error) {
|
||||
switch key {
|
||||
case "level":
|
||||
return reflect.String, nil
|
||||
default:
|
||||
return reflect.Invalid, errors.New("unknown configuration key")
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the value of a configuration key
|
||||
func (c JobSearchConfigLogging) ValueOf(key string) (any, error) {
|
||||
switch key {
|
||||
case "level":
|
||||
return c.Level, nil
|
||||
default:
|
||||
return "", errors.New("unknown configuration key")
|
||||
}
|
||||
}
|
||||
|
||||
// Sets the value of a configuration key
|
||||
func (c *JobSearchConfigLogging) SetValue(key string, value any) error {
|
||||
switch key {
|
||||
case "level":
|
||||
newLevel := value.(string)
|
||||
switch newLevel {
|
||||
case "debug", "info", "warn", "error":
|
||||
c.Level = value.(string)
|
||||
default:
|
||||
return errors.New(
|
||||
"invalid log level. Must be one of: debug, info, warn, error")
|
||||
}
|
||||
default:
|
||||
return errors.New("unknown configuration key")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ConfigLevelToLogLevel(level string) slog.Level {
|
||||
switch level {
|
||||
case "debug":
|
||||
return slog.LevelDebug
|
||||
case "info":
|
||||
return slog.LevelInfo
|
||||
case "warn":
|
||||
return slog.LevelWarn
|
||||
case "error":
|
||||
return slog.LevelError
|
||||
default:
|
||||
return 99
|
||||
}
|
||||
}
|
||||
|
||||
func (c *JobSearchConfigLogging) LogLevel() slog.Level {
|
||||
return ConfigLevelToLogLevel(c.Level)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
Copyright © 2026 Craig Williams <craig@avsdev.uk>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"maps"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const EnvPrefix string = "JOBSEARCH"
|
||||
const EtcConfigPath string = "/etc/job-search"
|
||||
|
||||
type JobSearchConfig struct {
|
||||
Logging JobSearchConfigLogging
|
||||
}
|
||||
|
||||
// Default configuration
|
||||
func LoadDefaultConfig() {
|
||||
conf := JobSearchConfig{
|
||||
Logging: defaultConfigLogging(),
|
||||
}
|
||||
LoadToViper(conf)
|
||||
}
|
||||
|
||||
// Converts configuration to a map
|
||||
func (conf JobSearchConfig) Map() map[string]any {
|
||||
confMap := map[string]any{}
|
||||
confMap["logging"] = conf.Logging.Map()
|
||||
return confMap
|
||||
}
|
||||
|
||||
// Converts configuration to a flat dotted format
|
||||
func (conf JobSearchConfig) Flatten() map[string]any {
|
||||
confFlat := map[string]any{}
|
||||
maps.Copy(confFlat, conf.Logging.Flatten())
|
||||
return confFlat
|
||||
}
|
||||
|
||||
// Returns the expected type of a configuration key value
|
||||
func (c JobSearchConfig) TypeOf(key string) (reflect.Kind, error) {
|
||||
keyParts := strings.Split(key, ".")
|
||||
switch keyParts[0] {
|
||||
case "logging":
|
||||
return c.Logging.TypeOf(strings.Join(keyParts[1:], "."))
|
||||
default:
|
||||
return reflect.Invalid, errors.New("unknown configuration key")
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the expected type of a configuration value
|
||||
func (c JobSearchConfig) ValueOf(key string) (any, error) {
|
||||
keyParts := strings.Split(key, ".")
|
||||
switch keyParts[0] {
|
||||
case "logging":
|
||||
return c.Logging.ValueOf(strings.Join(keyParts[1:], "."))
|
||||
default:
|
||||
return nil, errors.New("unknown configuration key")
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the expected type of a configuration value
|
||||
func (c *JobSearchConfig) SetValue(key string, value any) error {
|
||||
keyParts := strings.Split(key, ".")
|
||||
switch keyParts[0] {
|
||||
case "logging":
|
||||
return c.Logging.SetValue(strings.Join(keyParts[1:], "."), value)
|
||||
default:
|
||||
return errors.New("unknown configuration key")
|
||||
}
|
||||
}
|
||||
|
||||
func LoadFromViper(conf *JobSearchConfig) {
|
||||
keys := viper.AllKeys()
|
||||
for idx := range keys {
|
||||
key := keys[idx]
|
||||
keyType, _ := conf.TypeOf(key)
|
||||
if keyType == reflect.Invalid {
|
||||
continue
|
||||
}
|
||||
switch keyType {
|
||||
case reflect.String:
|
||||
conf.SetValue(key, viper.GetString(key))
|
||||
case reflect.Int:
|
||||
conf.SetValue(key, viper.GetInt(key))
|
||||
case reflect.Bool:
|
||||
conf.SetValue(key, viper.GetBool(key))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func LoadToViper(conf JobSearchConfig) {
|
||||
flatMap := conf.Flatten()
|
||||
for key := range flatMap {
|
||||
viper.SetDefault(key, flatMap[key])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
Copyright © 2026 Craig Williams <craig@avsdev.uk>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Debug: Prints detected environment variables
|
||||
func printEnv() {
|
||||
slog.Debug("Environment:")
|
||||
env := os.Environ()
|
||||
slices.Sort(env)
|
||||
for _, e := range env {
|
||||
pair := strings.SplitN(e, "=", 2)
|
||||
if strings.HasPrefix(pair[0], EnvPrefix) {
|
||||
slog.Debug("\t" + pair[0] + "=" + pair[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: Prints loaded configuration
|
||||
func printLoadedConfig() {
|
||||
slog.Debug("Loaded config:")
|
||||
keys := viper.AllKeys()
|
||||
for i := range keys {
|
||||
k := keys[i]
|
||||
v := viper.Get(k)
|
||||
t := reflect.TypeOf(v)
|
||||
slog.Debug(
|
||||
"\t" + k + " = " + fmt.Sprintf("%#v", v) + " <" + t.Kind().String() + ">")
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: Prints parsed configuration
|
||||
func printConfig(conf JobSearchConfig) {
|
||||
slog.Debug("Config:")
|
||||
conf.PrintLog(slog.LevelDebug)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
Copyright © 2026 Craig Williams <craig@avsdev.uk>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Reads in dotenv file for configuration
|
||||
func loadEnv() error {
|
||||
if err := godotenv.Load(); err != nil {
|
||||
return fmt.Errorf("invalid .env file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reads in config file and ENV variables if set to form a configuration.
|
||||
func loadConfig(cfgFile string) error {
|
||||
if cfgFile != "" {
|
||||
// Use config file from the flag.
|
||||
viper.SetConfigFile(cfgFile)
|
||||
} else {
|
||||
// Search config in /etc or local directory.
|
||||
viper.AddConfigPath(EtcConfigPath)
|
||||
viper.AddConfigPath(".")
|
||||
|
||||
viper.SetConfigName("config")
|
||||
}
|
||||
|
||||
// Load default configuration
|
||||
LoadDefaultConfig()
|
||||
|
||||
viper.SetEnvPrefix(EnvPrefix)
|
||||
viper.AutomaticEnv() // read in environment variables that match
|
||||
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
||||
// Indicates an explicitly set config file is not found or
|
||||
// that no config file was found in any search path
|
||||
slog.Warn("No config file found!")
|
||||
} else {
|
||||
// Config file was found but another error was produced
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Config file found and successfully parsed
|
||||
slog.Info("Using config file: " + viper.ConfigFileUsed())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Initilises environment file configuration and general app configuration
|
||||
func Init(cfgFile string, conf *JobSearchConfig) error {
|
||||
if err := loadEnv(); err != nil {
|
||||
return err
|
||||
}
|
||||
if conf.Logging.LogLevel() <= slog.LevelDebug {
|
||||
printEnv()
|
||||
}
|
||||
|
||||
if err := loadConfig(cfgFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oldLogLevel := conf.Logging.Level
|
||||
LoadFromViper(conf)
|
||||
if ConfigLevelToLogLevel(oldLogLevel) < conf.Logging.LogLevel() {
|
||||
conf.Logging.Level = oldLogLevel
|
||||
}
|
||||
|
||||
if conf.Logging.LogLevel() <= slog.LevelDebug {
|
||||
printLoadedConfig()
|
||||
printConfig(*conf)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
Copyright © 2026 Craig Williams <craig@avsdev.uk>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
||||
// Prints configuration to stdout in flat dotted format
|
||||
func (conf JobSearchConfig) PrintTerminal() {
|
||||
for k, v := range conf.Flatten() {
|
||||
typ, _ := conf.TypeOf(k)
|
||||
fmt.Print(k, " = ", v, " <", typ, ">\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Prints configuration to stdout as JSON
|
||||
func (conf JobSearchConfig) PrintJson() {
|
||||
confMap := conf.Map()
|
||||
jbytes, _ := json.MarshalIndent(confMap, "", " ")
|
||||
fmt.Print(string(jbytes) + "\n")
|
||||
}
|
||||
|
||||
// Prints configuration to stdout as YAML
|
||||
func (conf JobSearchConfig) PrintYaml() {
|
||||
confMap := conf.Map()
|
||||
jbytes, _ := yaml.Marshal(confMap)
|
||||
fmt.Print(string(jbytes) + "\n")
|
||||
}
|
||||
|
||||
// Prints configuration to logrus log at debug, info (default) or error level
|
||||
func (conf JobSearchConfig) PrintLog(atLevel slog.Level) {
|
||||
var logFn func(args ...interface{})
|
||||
|
||||
switch atLevel {
|
||||
case slog.LevelDebug:
|
||||
logFn = func(args ...interface{}) {
|
||||
slog.Debug(fmt.Sprint(args...))
|
||||
}
|
||||
case slog.LevelError:
|
||||
logFn = func(args ...interface{}) {
|
||||
slog.Error(fmt.Sprint(args...))
|
||||
}
|
||||
case slog.LevelWarn:
|
||||
logFn = func(args ...interface{}) {
|
||||
slog.Warn(fmt.Sprint(args...))
|
||||
}
|
||||
default:
|
||||
logFn = func(args ...interface{}) {
|
||||
slog.Info(fmt.Sprint(args...))
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range conf.Flatten() {
|
||||
logFn("\t", k, " = ", v)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user