Initial project
This commit is contained in:
commit
e715024cbd
23 files changed
+1547
No files matched your search
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
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 (
|
||||
"log/slog"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Configuration management",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
slog.Debug("config.PersistentPreRun")
|
||||
},
|
||||
PersistentPostRun: func(cmd *cobra.Command, args []string) {
|
||||
slog.Debug("config.PersistentPostRun")
|
||||
},
|
||||
}
|
||||
cmd.AddCommand(commandShow())
|
||||
cmd.AddCommand(commandGet())
|
||||
cmd.AddCommand(commandSet())
|
||||
cmd.AddCommand(commandInit())
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
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"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"git.avsdev.uk/Go/job-search/internal/cli"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func commandGet() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "get [config_key]",
|
||||
Args: cobra.ExactArgs(1),
|
||||
// Args: func(cmd *cobra.Command, args []string) error {
|
||||
// // Optionally run one of the validators provided by cobra
|
||||
// if err := cobra.ExactArgs(1)(cmd, args); err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// conf, ok := cli.ConfigFromContext(cmd.Context())
|
||||
// if !ok {
|
||||
// return errors.New("no configuration loaded")
|
||||
// }
|
||||
|
||||
// flatConf := config.FlattenConfig(conf)
|
||||
// if !util.HasKey(flatConf, args[0]) {
|
||||
// return fmt.Errorf("invalid config key specified: %s", args[0])
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// },
|
||||
Short: "Get a value from configuration",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
slog.Debug("config.get.PersistentPreRun")
|
||||
},
|
||||
PersistentPostRun: func(cmd *cobra.Command, args []string) {
|
||||
slog.Debug("config.get.PersistentPostRun")
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
slog.Debug("config.get.Run")
|
||||
|
||||
if err := cobra.ExactArgs(1)(cmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conf, ok := cli.ConfigFromContext(cmd.Context())
|
||||
if !ok {
|
||||
return errors.New("no configuration loaded")
|
||||
}
|
||||
|
||||
v, err := conf.ValueOf(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t, _ := conf.TypeOf(args[0])
|
||||
fmt.Println(v, "<"+t.String()+">")
|
||||
|
||||
return nil
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"terminal_log": "1",
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
@@ -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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"git.avsdev.uk/Go/job-search/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func commandInit() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "init [config_path]",
|
||||
Args: cobra.MatchAll(cobra.MinimumNArgs(0), cobra.MaximumNArgs(1)),
|
||||
Short: "Create an empty configuration file",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
slog.Debug("config.init.PersistentPreRun")
|
||||
},
|
||||
PersistentPostRun: func(cmd *cobra.Command, args []string) {
|
||||
slog.Debug("config.init.PersistentPostRun")
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
slog.Debug("config.init.Run")
|
||||
|
||||
if err := cobra.MinimumNArgs(0)(cmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cobra.MaximumNArgs(1)(cmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfgFile, _ := cmd.Flags().GetString("config")
|
||||
if cfgFile != "" {
|
||||
// Use config file from the flag.
|
||||
viper.SetConfigFile(cfgFile)
|
||||
} else {
|
||||
// Search config in /etc or local directory.
|
||||
viper.AddConfigPath(config.EtcConfigPath)
|
||||
viper.AddConfigPath(".")
|
||||
viper.SetConfigName("config")
|
||||
}
|
||||
|
||||
err := viper.ReadInConfig()
|
||||
if err == nil {
|
||||
return errors.New("configuration file already exists")
|
||||
}
|
||||
|
||||
_, ok := err.(viper.ConfigFileNotFoundError)
|
||||
if !errors.Is(err, os.ErrNotExist) && !ok {
|
||||
// Config file was found but another error was produced
|
||||
return err
|
||||
}
|
||||
|
||||
// Load default configuration
|
||||
config.LoadDefaultConfig()
|
||||
|
||||
if viper.ConfigFileUsed() == "" {
|
||||
viper.SetConfigFile(config.EtcConfigPath + "/config.yaml")
|
||||
}
|
||||
|
||||
fmt.Print(
|
||||
"writing default configuration to file: " +
|
||||
viper.ConfigFileUsed() +
|
||||
"\n")
|
||||
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"no_config": "1",
|
||||
"terminal_log": "1",
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
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"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"git.avsdev.uk/Go/job-search/internal/cli"
|
||||
"git.avsdev.uk/Go/job-search/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func setConfig(conf config.JobSearchConfig, key string, value string) error {
|
||||
var confValue any = nil
|
||||
var confType reflect.Kind
|
||||
var err error
|
||||
|
||||
confType, err = conf.TypeOf(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch confType {
|
||||
case reflect.Bool:
|
||||
confValue, err = strconv.ParseBool(value)
|
||||
case reflect.String:
|
||||
confValue = value
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
confValue, err = strconv.ParseInt(value, 10, 64)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
confValue, err = strconv.ParseUint(value, 10, 64)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
confValue, err = strconv.ParseFloat(value, 64)
|
||||
default:
|
||||
err = errors.ErrUnsupported
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("config value type not accepted, expected <%s>", confType)
|
||||
}
|
||||
|
||||
viper.Set(key, confValue)
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Print(key, " = ", confValue, " <", confType, ">\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
func unsetConfig(conf config.JobSearchConfig, key string) error {
|
||||
if _, err := conf.TypeOf(key); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v := viper.New()
|
||||
v.SetConfigFile(viper.ConfigFileUsed())
|
||||
|
||||
keys := viper.AllKeys()
|
||||
for i := range keys {
|
||||
k := keys[i]
|
||||
if k != key {
|
||||
v.Set(k, viper.Get(k))
|
||||
}
|
||||
}
|
||||
|
||||
if err := v.WriteConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func commandSet() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "set [config_key] [config_value]",
|
||||
Args: cobra.MatchAll(cobra.MinimumNArgs(1), cobra.MaximumNArgs(2)),
|
||||
Short: "Set a value to configuration",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
slog.Debug("config.set.PersistentPreRun")
|
||||
},
|
||||
PersistentPostRun: func(cmd *cobra.Command, args []string) {
|
||||
slog.Debug("config.set.PersistentPostRun")
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
slog.Debug("config.set.Run")
|
||||
|
||||
if err := cobra.MinimumNArgs(1)(cmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cobra.MaximumNArgs(2)(cmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
unset, unsetFlagErr := cmd.Flags().GetBool("unset")
|
||||
if unsetFlagErr != nil {
|
||||
return unsetFlagErr
|
||||
}
|
||||
if err := cobra.ExactArgs(2)(cmd, args); err != nil && !unset {
|
||||
return err
|
||||
}
|
||||
|
||||
conf, ok := cli.ConfigFromContext(cmd.Context())
|
||||
if !ok {
|
||||
return errors.New("no configuration loaded")
|
||||
}
|
||||
|
||||
if viper.ConfigFileUsed() == "" {
|
||||
return errors.New("no configuration file exists")
|
||||
}
|
||||
|
||||
if unset {
|
||||
return unsetConfig(conf, args[0])
|
||||
} else {
|
||||
return setConfig(conf, args[0], args[1])
|
||||
}
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"terminal_log": "1",
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolP("unset", "x", false, "clear configuration setting")
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
"git.avsdev.uk/Go/job-search/internal/cli"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func commandShow() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "show",
|
||||
Short: "Print config information",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
slog.Debug("config.show.PersistentPreRun")
|
||||
},
|
||||
PersistentPostRun: func(cmd *cobra.Command, args []string) {
|
||||
slog.Debug("config.show.PersistentPostRun")
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
slog.Debug("config.show.Run")
|
||||
|
||||
conf, ok := cli.ConfigFromContext(cmd.Context())
|
||||
if !ok {
|
||||
return errors.New("no configuration loaded")
|
||||
}
|
||||
|
||||
if yaml, _ := cmd.Flags().GetBool("yaml"); yaml {
|
||||
conf.PrintYaml()
|
||||
} else if json, _ := cmd.Flags().GetBool("json"); json {
|
||||
conf.PrintJson()
|
||||
} else {
|
||||
conf.PrintTerminal()
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"terminal_log": "1",
|
||||
},
|
||||
}
|
||||
cmd.Flags().Bool("json", false, "format configuration as json")
|
||||
cmd.Flags().Bool("yaml", false, "format configuration as yaml")
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
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 version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"git.avsdev.uk/Go/job-search/internal/version"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print version information",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
slog.Debug("version.PersistentPreRun")
|
||||
},
|
||||
PersistentPostRun: func(cmd *cobra.Command, args []string) {
|
||||
slog.Debug("version.PersistentPostRun")
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
slog.Debug("version.Run")
|
||||
|
||||
fmt.Printf("PBCNinja - %s\n", version.Version)
|
||||
if version.BuildTime != "unknown" {
|
||||
fmt.Printf("Built: %s\n", version.BuildTime)
|
||||
}
|
||||
if version.GitCommit != "unknown" {
|
||||
fmt.Printf("Commit: %s\n", version.GitCommit)
|
||||
}
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"no_config": "1",
|
||||
"terminal_log": "1",
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
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 cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.avsdev.uk/Go/job-search/internal/config"
|
||||
)
|
||||
|
||||
// Define a private key type to avoid collisions
|
||||
type configContextKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
// Define the key
|
||||
var configKey = &configContextKey{"config"}
|
||||
|
||||
// Public accessor to store a value to ctx
|
||||
func WithConfig(ctx context.Context, conf config.JobSearchConfig) context.Context {
|
||||
// No allocation here since userIDKey is a pointer to a struct
|
||||
return context.WithValue(ctx, configKey, conf)
|
||||
}
|
||||
|
||||
// Public accessor to fetch a value from ctx
|
||||
func ConfigFromContext(ctx context.Context) (config.JobSearchConfig, bool) {
|
||||
v, ok := ctx.Value(configKey).(config.JobSearchConfig)
|
||||
return v, ok
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
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 log
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/lmittmann/tint"
|
||||
)
|
||||
|
||||
var logLevel slog.LevelVar
|
||||
|
||||
// Set up the logger using defaults
|
||||
func Init() {
|
||||
logLevel.Set(slog.LevelWarn)
|
||||
opts := &slog.HandlerOptions{
|
||||
Level: &logLevel,
|
||||
}
|
||||
handler := slog.NewTextHandler(os.Stdout, opts)
|
||||
logger := slog.New(handler)
|
||||
slog.SetDefault(logger)
|
||||
}
|
||||
|
||||
// Enable info level logging
|
||||
func EnableInfo() {
|
||||
if logLevel.Level() > slog.LevelInfo {
|
||||
logLevel.Set(slog.LevelInfo)
|
||||
slog.Info("Info logging enabled")
|
||||
}
|
||||
}
|
||||
|
||||
// Enable debug level logging
|
||||
func EnableDebug() {
|
||||
if logLevel.Level() > slog.LevelDebug {
|
||||
logLevel.Set(slog.LevelDebug)
|
||||
slog.Debug("Debug logging enabled")
|
||||
}
|
||||
}
|
||||
|
||||
func LogToTerminal() {
|
||||
opts := &tint.Options{
|
||||
Level: &logLevel,
|
||||
TimeFormat: "*",
|
||||
}
|
||||
handler := tint.NewTextHandler(os.Stdout, opts)
|
||||
logger := slog.New(handler)
|
||||
slog.SetDefault(logger)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package version
|
||||
|
||||
// Version information (set at build time with -ldflags)
|
||||
var (
|
||||
Version = "dev"
|
||||
BuildTime = "unknown"
|
||||
GitCommit = "unknown"
|
||||
)
|
||||
Reference in new issue
Block a user