Skip to content

Commit

Permalink
Load config from json
Browse files Browse the repository at this point in the history
  • Loading branch information
infogulch committed Mar 28, 2024
1 parent 9631d3b commit 0480f6e
Show file tree
Hide file tree
Showing 11 changed files with 262 additions and 98 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ xtemplate
xtemplate.*
caddy
dist
__debug_bin*
19 changes: 13 additions & 6 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
# TODO

- [ ] Accept JSON
- [x] Add config flag to load config from JSON file
- [x] Allow raw config with --config and config file with --config-file
- [x] Parse args -> decode config files in args to args -> decode config
values in args to args -> parse args again
- [ ] Test that everything can be configured, load config -> dump back
- [ ] Validate that your type is correct on call to Value
- [ ] Dot Provider system
- [ ] Add config flag to load config from JSON file
- [ ] Parse flags -> config file flag -> parse json -> parse flags again
- [ ] Accept configuration from JSON.
- [ ] Figure out how to parse json dynamically dispatching to the unmarshaller
- [ ] Try
- [ ] Accept configuration from JSON
- [ ] Update `xtemplate-caddy`. Note only caddy 2.8.0 uses Go 1.22
- [ ] Must test on caddy head?
- [ ] Accept dot provider configuration from Caddyfile
- [ ]
- [ ] Add/update documentation
- [ ] Creating a provider
- [ ] Using the new go-arg cli flags
Expand Down Expand Up @@ -66,8 +71,10 @@

# DONE

## v0.5 beta - Mar 2024
## Next

- Accept JSON configuration
- [x] Implement Json Unmarshaller https://pkg.go.dev/encoding/json
- [-] Downgrade to go 1.21 - Cannot due to using 1.22 ServeMux
- Add/update documentation
- [x] Readme
Expand Down
89 changes: 69 additions & 20 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
package main

import (
"bytes"
"encoding/json"
"log/slog"
"os"
"time"
Expand All @@ -20,46 +22,93 @@ func main() {
Main()
}

type configt struct {
xtemplate.Config
Watch []string `json:"watch_dirs" arg:",separate"`
WatchTemplates bool `json:"watch_templates" default:"true"`
Listen string `json:"listen" arg:"-l" default:"0.0.0.0:8080"`
LogLevel int `json:"log_level" default:"-2"`
Configs []string `json:"-" arg:"-c,--config,separate"`
ConfigFiles []string `json:"-" arg:"-f,--config-file,separate"`
}

// Main can be called from your func main() if you want your program to act like
// the default xtemplate cli, or use it as a reference for making your own.
// Provide configs to override the defaults like: `xtemplate.Main(xtemplate.WithFooConfig())`
func Main(overrides ...xtemplate.ConfigOverride) {
var args struct {
xtemplate.Config `arg:"-w"`
Watch []string
WatchTemplates bool `default:"true"`
Listen string `arg:"-l" default:"0.0.0.0:8080"`
LogLevel int `default:"-2"`
}
arg.MustParse(&args)
args.Defaults()
var config configt
var log *slog.Logger

{
arg.MustParse(&config)
config.Defaults()

level := config.LogLevel
log = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.Level(level)}))

var jsonConfig configt
var decoded bool
for _, name := range config.ConfigFiles {
func() {
file, err := os.OpenFile(name, os.O_RDONLY, 0)
if err != nil {
log.Error("failed to open config file '%s': %w", name, err)
os.Exit(1)
}
defer file.Close()
err = json.NewDecoder(file).Decode(&jsonConfig)
if err != nil {
log.Error("failed to decode args from json file", slog.String("filename", name), slog.Any("error", err))
os.Exit(1)
}
decoded = true
log.Debug("incorporated json file", slog.String("filename", name), slog.Any("config", &jsonConfig))
}() // use func to close file on every iteration
}

for _, conf := range config.Configs {
err := json.NewDecoder(bytes.NewBuffer([]byte(conf))).Decode(&jsonConfig)
if err != nil {
log.Error("failed to decode arg from json flag", slog.Any("error", err))
os.Exit(1)
}
decoded = true
log.Debug("incorporated json value", slog.String("json_string", conf), slog.Any("config", &jsonConfig))
}

if decoded {
arg.MustParse(&jsonConfig)
config = jsonConfig
}

if config.LogLevel != level {
log = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.Level(config.LogLevel)}))
}

log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.Level(args.LogLevel)}))
args.Config.Logger = log
config.Logger = log

for _, o := range overrides {
o(&args.Config)
log.Debug("loaded configuration", slog.Any("config", &config))
}

server, err := args.Config.Server()
server, err := config.Server(overrides...)
if err != nil {
log.Error("failed to load xtemplate", slog.Any("error", err))
os.Exit(2)
}

if args.WatchTemplates {
args.Watch = append(args.Watch, args.Config.TemplatesDir)
if config.WatchTemplates {
config.Watch = append(config.Watch, config.TemplatesDir)
}
if len(args.Watch) != 0 {
_, err := watch.Watch(args.Watch, 200*time.Millisecond, log.WithGroup("fswatch"), func() bool {
if len(config.Watch) != 0 {
_, err := watch.Watch(config.Watch, 200*time.Millisecond, log.WithGroup("fswatch"), func() bool {
server.Reload()
return true
})
if err != nil {
log.Info("failed to watch directories", slog.Any("error", err), slog.Any("directories", args.Watch))
log.Info("failed to watch directories", slog.Any("error", err), slog.Any("directories", config.Watch))
os.Exit(4)
}
}

log.Info("server stopped", slog.Any("exit", server.Serve(args.Listen)))
log.Info("server stopped", slog.Any("exit", server.Serve(config.Listen)))
}
10 changes: 9 additions & 1 deletion config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type Config struct {

// A list of additional custom fields to add to the template dot value
// `{{.}}`.
Dot []DotConfig `json:"dot_config" arg:"-d,--dot,separate"`
Dot []DotConfig `json:"dot" arg:"-d,--dot,separate"`

// Additional functions to add to the template execution context.
FuncMaps []template.FuncMap `json:"-" arg:"-"`
Expand Down Expand Up @@ -100,6 +100,14 @@ func WithFuncMaps(fm ...template.FuncMap) ConfigOverride {

func WithProvider(name string, p DotProvider) ConfigOverride {
return func(c *Config) {
for _, d := range c.Dot {
if d.Name == name {
if d.DotProvider != p {
c.Logger.Warn("tried to assign different providers the same name", slog.String("name", d.Name), slog.Any("old", d.DotProvider), slog.Any("new", p))
}
return
}
}
c.Dot = append(c.Dot, DotConfig{Name: name, DotProvider: p})
}
}
87 changes: 62 additions & 25 deletions dot.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
Expand All @@ -15,17 +16,17 @@ import (
var registrations map[string]RegisteredDotProvider = make(map[string]RegisteredDotProvider)

func RegisterDot(r RegisteredDotProvider) {
name := r.Name()
name := r.Type()
if old, ok := registrations[name]; ok {
panic(fmt.Sprintf("DotProvider name already registered: %s (%v)", name, old))
}
registrations[name] = r
}

type DotConfig struct {
Name string
Type string
DotProvider
Name string `json:"name"`
Type string `json:"type"`
DotProvider `json:"-"`
}

type Request struct {
Expand All @@ -44,7 +45,7 @@ type DotProvider interface {

type RegisteredDotProvider interface {
DotProvider
Name() string
Type() string
New() DotProvider
}

Expand All @@ -53,17 +54,39 @@ type CleanupDotProvider interface {
Cleanup(any, error) error
}

var _ encoding.TextMarshaler = &DotConfig{}

func (d *DotConfig) MarshalText() ([]byte, error) {
var parts [][]byte
if r, ok := d.DotProvider.(RegisteredDotProvider); ok {
parts = [][]byte{[]byte(d.Name), {':'}, []byte(r.Type())}
} else {
return nil, fmt.Errorf("dot provider cannot be marshalled: %v (%T)", d.DotProvider, d.DotProvider)
}
if m, ok := d.DotProvider.(encoding.TextMarshaler); ok {
b, err := m.MarshalText()
if err != nil {
return nil, err
}
parts = append(parts, []byte{':'}, b)
}
return slices.Concat(parts...), nil
}

var _ encoding.TextUnmarshaler = &DotConfig{}

func (d *DotConfig) UnmarshalText(b []byte) error {
parts := bytes.SplitN(b, []byte{':'}, 3)
if len(parts) < 2 {
return fmt.Errorf("failed to parse DotConfig not enough sections. required format: NAME:PROVIDER_NAME[:PROVIDER_CONFIG]")
}
name, providerName := string(parts[0]), string(parts[1])
reg, ok := registrations[providerName]
name, providerType := string(parts[0]), string(parts[1])
reg, ok := registrations[providerType]
if !ok {
return fmt.Errorf("dot provider with name '%s' is not registered", providerName)
return fmt.Errorf("dot provider with name '%s' is not registered", providerType)
}
d.Name = name
d.Type = providerType
d.DotProvider = reg.New()
if unm, ok := d.DotProvider.(encoding.TextUnmarshaler); ok {
var rest []byte
Expand All @@ -72,31 +95,45 @@ func (d *DotConfig) UnmarshalText(b []byte) error {
}
err := unm.UnmarshalText(rest)
if err != nil {
return fmt.Errorf("failed to configure provider %s: %w", providerName, err)
return fmt.Errorf("failed to configure provider %s: %w", providerType, err)
}
}
return nil
}

func (d *DotConfig) MarshalText() ([]byte, error) {
var parts [][]byte
if r, ok := d.DotProvider.(RegisteredDotProvider); ok {
parts = [][]byte{[]byte(d.Name), {':'}, []byte(r.Name())}
} else {
return nil, fmt.Errorf("dot provider cannot be marshalled: %v (%T)", d.DotProvider, d.DotProvider)
}
if m, ok := d.DotProvider.(encoding.TextMarshaler); ok {
b, err := m.MarshalText()
if err != nil {
return nil, err
}
parts = append(parts, []byte{':'}, b)
var _ json.Marshaler = &DotConfig{}

func (d *DotConfig) MarshalJSON() ([]byte, error) {
b := new(bytes.Buffer)
if err := json.NewEncoder(b).Encode(d); err != nil {
return nil, err
}
return slices.Concat(parts...), nil
return b.Bytes(), nil
}

var _ encoding.TextUnmarshaler = &DotConfig{}
var _ encoding.TextMarshaler = &DotConfig{}
var _ json.Unmarshaler = &DotConfig{}

func (d *DotConfig) UnmarshalJSON(b []byte) error {
var dc = struct {
Name string `json:"name"`
Type string `json:"type"`
}{}
if err := json.NewDecoder(bytes.NewBuffer(b)).Decode(&dc); err != nil {
return err
}
r, ok := registrations[dc.Type]
if !ok {
return fmt.Errorf("no provider registered with the type '%s': %+v", dc.Type, dc)
}
p := r.New()
if err := json.NewDecoder(bytes.NewBuffer(b)).Decode(p); err != nil {
return fmt.Errorf("failed to decode provider %s (%v): %w", dc.Type, p, err)
}
d.Name = dc.Name
d.Type = dc.Type
d.DotProvider = p
return nil
}

func makeDot(dcs []DotConfig) dot {
fields := make([]reflect.StructField, 0, len(dcs))
Expand Down
6 changes: 5 additions & 1 deletion instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,14 @@ type Instance struct {
}

// Instance creates a new *Instance from the given config
func (config Config) Instance() (*Instance, *InstanceStats, []InstanceRoute, error) {
func (config Config) Instance(cfgs ...ConfigOverride) (*Instance, *InstanceStats, []InstanceRoute, error) {
start := time.Now()

config.Defaults()
for _, c := range cfgs {
c(&config)
}

inst := &Instance{
config: config,
id: nextInstanceIdentity.Add(1),
Expand Down
Loading

0 comments on commit 0480f6e

Please sign in to comment.