Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,23 @@ FROM golang:1.25.4-trixie
# Install basic dependencies
RUN apt update && \
apt install -y \
git \
curl \
git \
tar \
&& \
rm -rf /var/lib/apt/lists/*

# Install task
RUN sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d v3.36.0
RUN sh -c "$(curl -L https://taskfile.dev/install.sh)" -- -d v3.36.0

# Install golangci-lint
RUN sh -c "$(curl --location https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh)" -- v2.6.2
RUN sh -c "$(curl -L https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh)" -- v2.6.2

# Install gotestsum
RUN curl -L https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_linux_amd64.tar.gz -o gotestsum.tar.gz && \
tar -xzf gotestsum.tar.gz -C /usr/local/bin gotestsum && \
rm gotestsum.tar.gz

# Install jd
RUN curl -L https://github.com/josephburnett/jd/releases/download/v2.3.0/jd-amd64-linux -o /usr/local/bin/jd && \
chmod +x /usr/local/bin/jd
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"image": "ghcr.io/codetent/confless/dev:sha-382640a",
"image": "ghcr.io/codetent/confless/dev:sha-6fd6dc8",
"runArgs": [
"--platform=linux/amd64"
],
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
check:
runs-on: ubuntu-latest
container:
image: ghcr.io/codetent/confless/dev:sha-382640a
image: ghcr.io/codetent/confless/dev:sha-6fd6dc8
env:
GOFLAGS: -buildvcs=false
steps:
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Sources are applied in the following order (later sources override earlier ones)
1. **Files** (in registration order)
2. **Command-line flags**
3. **Environment variables** (highest precedence)
4. **Dynamically registered files**

### Files

Expand Down Expand Up @@ -112,7 +113,9 @@ database:

You can mark a field in your configuration with the `confless:"file"` tag to automatically load it as a configuration file. This is useful for environment-specific configurations.

The format can be specified explicitly in the tag (`confless:"file,format=json"`) otherwise it defaults to JSON.
The format can be specified explicitly in the tag (`confless:"file,format=yaml"`) otherwise it defaults to JSON.

Note that dynamically registered files are loaded at the end, while statically registered files are loaded first.

```go
type Config struct {
Expand Down
8 changes: 7 additions & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
version: '3'

includes:
examples:
dir: examples
taskfile: examples/Taskfile.yml

tasks:
test:
cmds:
- go test -v ./...
- gotestsum --format testdox
- task: examples:test

lint:
cmds:
Expand Down
6 changes: 6 additions & 0 deletions confless.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ var (
defaultLoader = NewLoader()
)

// Configure the default loader with the given options.
// Note that reconfiguring the default loader will reset the registered sources.
func Configure(opts ...loaderOption) {
defaultLoader = NewLoader(opts...)
}

// Register an environment variable prefix to load.
// Names are converted to dot-separated paths (e.g. "MY_FLAG" -> "my.flag").
func RegisterEnv(pre string) {
Expand Down
4 changes: 0 additions & 4 deletions example/config.json

This file was deleted.

45 changes: 0 additions & 45 deletions example/main.go

This file was deleted.

5 changes: 0 additions & 5 deletions example/other.json

This file was deleted.

17 changes: 17 additions & 0 deletions examples/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: '3'

tasks:
test:
cmds:
- cmd: |
cd {{ .ITEM.dir }} &&
{{ .ITEM.cmd }} | jd {{ .ITEM.expected }}
for:
- dir: simple
cmd: >-
APP_DATABASE_USERNAME=user
APP_DATABASE_PASSWORD=pass
go run main.go
--debug
--config=files/override.yaml
expected: expected.json
13 changes: 13 additions & 0 deletions examples/simple/expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "app",
"host": "0.0.0.0",
"port": 8080,
"debug": true,
"database": {
"host": "db.example.com",
"port": 5432,
"username": "user",
"password": "pass"
},
"Config": "files/override.yaml"
}
5 changes: 5 additions & 0 deletions examples/simple/files/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "app",
"host": "0.0.0.0",
"port": 8080
}
4 changes: 4 additions & 0 deletions examples/simple/files/override.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
database:
host: db.example.com
port: 5432
ssl: true
67 changes: 67 additions & 0 deletions examples/simple/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package main

import (
"encoding/json"
"flag"
"log"
"os"

"github.com/codetent/confless"
)

type Config struct {
Name string `json:"name"`
Host string `json:"host"`
Port int `json:"port"`
Debug bool `json:"debug"`

Database struct {
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
} `json:"database"`

Config string `confless:"file,format=yaml"`
}

func main() {
// Define command-line flags
flag.String("host", "", "Server host")
flag.Int("port", 0, "Server port")
flag.Bool("debug", false, "Enable debug mode")
flag.String("database-host", "", "Database host")
flag.Int("database-port", 0, "Database port")
flag.Bool("database-ssl", false, "Enable SSL for database")
flag.String("config", "", "Path to configuration file")

// Set default values
config := &Config{
Name: "default",
Host: "localhost",
Port: 8080,
Debug: false,
}

// Read configuration from command-line flags
confless.RegisterFlags(flag.CommandLine)
// Read configuration from environment variables starting with "APP_"
confless.RegisterEnv("APP")
// Read configuration from config.json
confless.RegisterFile("files/config.json", confless.FileFormatJSON)

// Parse flags before loading
flag.Parse()

// Load configuration from all registered sources
err := confless.Load(config)
if err != nil {
log.Fatalf("Failed to load configuration: %v", err)
}

// Print the final configuration to stdout
enc := json.NewEncoder(os.Stdout)
if err := enc.Encode(config); err != nil {
log.Fatalf("Failed to encode config as JSON: %v", err)
}
}
54 changes: 32 additions & 22 deletions loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,29 +69,8 @@ func (l *loader) RegisterFlags(f *flag.FlagSet) {

// Populate the object by applying the registered sources.
func (l *loader) Load(obj any) error {
files := make([]*configFile, 0, len(l.files))
files = append(files, l.files...)

// Add file paths from tagged fields.
for field, format := range findFileFields(obj) {
path := field.String()
if path == "" {
continue
}

format := fileFormat(format)
if format == "" {
format = FileFormatJSON
}

files = append(files, &configFile{
path: path,
format: format,
})
}

// Load the files.
for _, file := range files {
for _, file := range l.files {
// Open the file.
f, err := l.fs.Open(file.path)
if err != nil {
Expand Down Expand Up @@ -127,5 +106,36 @@ func (l *loader) Load(obj any) error {
}
}

// Load dynamically files.
for field, format := range findFileFields(obj) {
path := field.String()
if path == "" {
continue
}

format := fileFormat(format)
if format == "" {
format = FileFormatJSON
}

// Open the file.
f, err := l.fs.Open(path)
if err != nil {
if os.IsNotExist(err) {
// Skip if file does not exist.
continue
}

return fmt.Errorf("failed to open file: %w", err)
}

// Populate the object by the file.
err = populateByFile(f, string(format), obj)
_ = f.Close()
if err != nil {
return fmt.Errorf("failed to load file: %w", err)
}
}

return nil
}
Loading