Initial project
This commit is contained in:
commit
e715024cbd
23 files changed
+1547
No files matched your search
+43
@@ -0,0 +1,43 @@
|
||||
# ---> Go
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
|
||||
# config file(s)
|
||||
config.yaml
|
||||
config.json
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
|
||||
# Build directory
|
||||
build/
|
||||
|
||||
# Release directory
|
||||
release/
|
||||
|
||||
# Generated code directories
|
||||
internal/config/util/
|
||||
!internal/config/util/util.go
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"version": "2",
|
||||
"run": {
|
||||
"timeout": "5m",
|
||||
"tests": true
|
||||
},
|
||||
"linters": {
|
||||
"enable": [
|
||||
"cyclop",
|
||||
"dogsled",
|
||||
"errcheck",
|
||||
"godox",
|
||||
"govet",
|
||||
"gosec",
|
||||
"ineffassign",
|
||||
"nilerr",
|
||||
"nilnil",
|
||||
"nolintlint",
|
||||
"unused",
|
||||
"whitespace"
|
||||
]
|
||||
},
|
||||
"formatters": {
|
||||
"enable": [
|
||||
"gci",
|
||||
"gofmt",
|
||||
"goimports",
|
||||
"golines"
|
||||
],
|
||||
"settings": {
|
||||
"golines": {
|
||||
"max-len": 100,
|
||||
"tab-len": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"max-same-issues": 0,
|
||||
"max-issues-per-linter": 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
@@ -0,0 +1,4 @@
|
||||
# JobSearch
|
||||
|
||||
JobSearch is a service for scraping for contract opportunities. It is primarily
|
||||
a re-write of the existing nodeJS JobSearch codebase.
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.avsdev.uk/Go/job-search/internal/cli"
|
||||
cmdConfig "git.avsdev.uk/Go/job-search/internal/cli/config"
|
||||
cmdVersion "git.avsdev.uk/Go/job-search/internal/cli/version"
|
||||
"git.avsdev.uk/Go/job-search/internal/config"
|
||||
"git.avsdev.uk/Go/job-search/internal/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "job-search",
|
||||
Short: "JobSearch - AVSDev Future Opportunities!",
|
||||
Long: `JobSearch is a scraping tool for searching for possible contract
|
||||
opportunities.
|
||||
`,
|
||||
CompletionOptions: cobra.CompletionOptions{
|
||||
HiddenDefaultCmd: true,
|
||||
},
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Check if the command is expecting to use terminal for messages
|
||||
if val, ok := cmd.Annotations["terminal_log"]; ok && val == "1" {
|
||||
log.LogToTerminal()
|
||||
}
|
||||
|
||||
// Check for debug level via args
|
||||
dbgCount, _ := cmd.Flags().GetCount("debug")
|
||||
conf := config.JobSearchConfig{
|
||||
Logging: config.JobSearchConfigLogging{},
|
||||
}
|
||||
if dbgCount > 1 {
|
||||
conf.Logging.Level = "debug"
|
||||
} else if dbgCount > 0 {
|
||||
conf.Logging.Level = "info"
|
||||
}
|
||||
if conf.Logging.LogLevel() <= slog.LevelDebug {
|
||||
log.EnableDebug()
|
||||
} else if conf.Logging.LogLevel() <= slog.LevelInfo {
|
||||
log.EnableInfo()
|
||||
}
|
||||
|
||||
// With logging set up, these will be visible if trace is active
|
||||
slog.Debug("root.PersistentPreRun")
|
||||
slog.Debug(
|
||||
"command: \"" +
|
||||
strings.Join(strings.Split(cmd.CommandPath(), " ")[1:], " ") +
|
||||
"\"",
|
||||
)
|
||||
|
||||
// Always load config unless command indicates not to
|
||||
if val, ok := cmd.Annotations["no_config"]; !ok || val == "0" {
|
||||
// Load config - from specified file if provided
|
||||
cfgFile, _ := cmd.Flags().GetString("config")
|
||||
if err := config.Init(cfgFile, &conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Attach config to command context
|
||||
ctx := cli.WithConfig(cmd.Context(), conf)
|
||||
cmd.SetContext(ctx)
|
||||
|
||||
// Change logging level based off newly loaded config
|
||||
if conf.Logging.LogLevel() <= slog.LevelDebug {
|
||||
log.EnableDebug()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
PersistentPostRun: func(cmd *cobra.Command, args []string) {
|
||||
slog.Debug("root.PersistentPostRun")
|
||||
},
|
||||
}
|
||||
|
||||
// Execute, called by main.main() handles all child commands and appropriate
|
||||
// flags.
|
||||
func Execute() {
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.EnableTraverseRunHooks = true
|
||||
|
||||
log.Init()
|
||||
|
||||
pFlags := rootCmd.PersistentFlags()
|
||||
pFlags.StringP(
|
||||
"config", "c", "",
|
||||
"config file (default: "+config.EtcConfigPath+"/config.yaml)")
|
||||
pFlags.CountP("debug", "d", "enable debug output (repeat for trace)")
|
||||
|
||||
rootCmd.AddCommand(cmdVersion.NewCommand())
|
||||
rootCmd.AddCommand(cmdConfig.NewCommand())
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
module git.avsdev.uk/Go/job-search
|
||||
|
||||
go 1.26.8
|
||||
|
||||
require (
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/lmittmann/tint v1.2.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/viper v1.21.0
|
||||
go.yaml.in/yaml/v3 v3.0.5
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/lmittmann/tint v1.2.0 h1:AogHRHy8HUJUnNJBHJlYa+fR4YY8mko2cnCp67xn9JY=
|
||||
github.com/lmittmann/tint v1.2.0/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -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"
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
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 main
|
||||
|
||||
import (
|
||||
"git.avsdev.uk/Go/job-search/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Build script for JobSearch release
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BUILD_DIR="build"
|
||||
RELEASE_DIR="release"
|
||||
|
||||
DO_RELEASE=1
|
||||
|
||||
BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S')
|
||||
GIT_COMMIT=
|
||||
VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo 'unknown')
|
||||
VERSION_TAG=$(git describe --tags 2>/dev/null || echo 'unknown')
|
||||
|
||||
if [ "$VERSION" != "$VERSION_TAG" ]; then
|
||||
echo "Development build detected! (not installing to release)"
|
||||
GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')
|
||||
VERSION="${VERSION}.${GIT_COMMIT}"
|
||||
DO_RELEASE=0
|
||||
fi
|
||||
|
||||
LD_FLAGS=${LD_FLAGS:-""}
|
||||
LD_FLAGS="$LD_FLAGS -X git.avsdev.uk/Go/JobSearch/internal/version.Version=${VERSION}"
|
||||
LD_FLAGS="$LD_FLAGS -X git.avsdev.uk/Go/JobSearch/internal/version.BuildTime=${BUILD_TIME}"
|
||||
if [ "$GIT_COMMIT" != "" ]; then
|
||||
LD_FLAGS="$LD_FLAGS -X git.avsdev.uk/Go/JobSearch/internal/version.GitCommit=${GIT_COMMIT}"
|
||||
fi
|
||||
|
||||
# Build binary with version info
|
||||
go build \
|
||||
-ldflags="$LD_FLAGS" \
|
||||
-trimpath \
|
||||
-o $BUILD_DIR/job-search-$VERSION \
|
||||
./
|
||||
|
||||
# If proper release, copy to output directory for packaging
|
||||
if [[ $DO_RELEASE -eq 1 ]]; then
|
||||
install -DT $BUILD_DIR/job-search-$VERSION $RELEASE_DIR/job-search
|
||||
fi
|
||||
Reference in new issue
Block a user