Skip to content

Commit

Permalink
Refactor to simplify dot config
Browse files Browse the repository at this point in the history
  • Loading branch information
infogulch committed Oct 26, 2024
1 parent 508b03c commit 542a02e
Show file tree
Hide file tree
Showing 32 changed files with 424 additions and 745 deletions.
1 change: 0 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ ARG LDFLAGS

COPY app ./app/
COPY cmd ./cmd/
COPY providers ./providers/
COPY *.go ./
RUN CGO_ENABLED=1 \
GOFLAGS='-tags="sqlite_json"' \
Expand Down
2 changes: 0 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@
- [ ] See if its possible to implement sql queryrows with https://go.dev/wiki/RangefuncExperiment
- Not until caddy releases 2.8.0 and upgrades to 1.22.
- [ ] Add command that pre-compresses static files
- [ ] Schema migration? https://david.rothlis.net/declarative-schema-migration-for-sqlite/
- [ ] Schema generator: https://gitlab.com/Screwtapello/sqlite-schema-diagram/-/blob/main/sqlite-schema-diagram.sql?ref_type=heads
- [ ] Add a way to register additional routes dynamically during init
- [ ] Organize docs according to https://diataxis.fr/
- [ ] Research alternative template loading strategies:
Expand Down
3 changes: 0 additions & 3 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ package main
import (
"github.com/infogulch/xtemplate/app"

_ "github.com/infogulch/xtemplate/providers"
_ "github.com/infogulch/xtemplate/providers/nats"

_ "github.com/mattn/go-sqlite3"
)

Expand Down
28 changes: 11 additions & 17 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,21 @@ type Config struct {
// File extension to search for to find template files. Default `.html`.
TemplateExtension string `json:"template_extension,omitempty" arg:"--template-ext" default:".html"`

// Whether html templates are minified at load time. Default `true`.
Minify bool `json:"minify,omitempty" arg:"-m,--minify" default:"true"`

Databases []DotDBConfig `json:"databases" arg:"-"`
Flags []DotFlagsConfig `json:"flags" arg:"-"`
Directories []DotDirConfig `json:"directories" arg:"-"`
Nats []DotNatsConfig `json:"nats" arg:"-"`
CustomProviders []DotConfig `json:"-" arg:"-"`

// Left template action delimiter. Default `{{`.
LDelim string `json:"left,omitempty" arg:"--ldelim" default:"{{"`

// Right template action delimiter. Default `}}`.
RDelim string `json:"right,omitempty" arg:"--rdelim" default:"}}"`

// Whether html templates are minified at load time. Default `true`.
Minify bool `json:"minify,omitempty" arg:"-m,--minify" default:"true"`

// A list of additional custom fields to add to the template dot value
// `{{.}}`.
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 @@ -117,17 +119,9 @@ func WithFuncMaps(fm ...template.FuncMap) Option {
}
}

func WithProvider(name string, p DotProvider) Option {
func WithProvider(p DotConfig) Option {
return func(c *Config) error {
for _, d := range c.Dot {
if d.Name == name {
if d.DotProvider != p {
return fmt.Errorf("tried to assign different providers the same name. name: %s; old: %v; new: %v", d.Name, d.DotProvider, p)
}
return nil
}
}
c.Dot = append(c.Dot, DotConfig{Name: name, DotProvider: p})
c.CustomProviders = append(c.CustomProviders, p)
return nil
}
}
138 changes: 16 additions & 122 deletions dot.go
Original file line number Diff line number Diff line change
@@ -1,165 +1,59 @@
package xtemplate

import (
"bytes"
"context"
"encoding"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"reflect"
"slices"
"sync"
)

var registrations map[string]RegisteredDotProvider = make(map[string]RegisteredDotProvider)

func RegisterDot(r RegisteredDotProvider) {
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 `json:"name"`
Type string `json:"type"`
DotProvider `json:"-"`
}

type Request struct {
DotConfig
ServerCtx context.Context
W http.ResponseWriter
R *http.Request
}

type DotProvider interface {
// Value must always return a valid instance of the same type, even if it
// also returns an error. Value will be called with mock values at least
// once and still must not panic.
type DotConfig interface {
FieldName() string
Init(context.Context) error
Value(Request) (any, error)
}

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

type CleanupDotProvider interface {
DotProvider
DotConfig
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, providerType := string(parts[0]), string(parts[1])
reg, ok := registrations[providerType]
if !ok {
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
if len(parts) == 3 {
rest = parts[2]
}
err := unm.UnmarshalText(rest)
if err != nil {
return fmt.Errorf("failed to configure provider %s: %w", providerType, err)
}
}
return nil
}

var _ json.Marshaler = &DotConfig{}

func (d *DotConfig) MarshalJSON() ([]byte, error) {
type T DotConfig
return json.Marshal((*T)(d))
}

var _ json.Unmarshaler = &DotConfig{}

func (d *DotConfig) UnmarshalJSON(b []byte) error {
type T DotConfig
dc := T{}
if err := json.Unmarshal(b, &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.Unmarshal(b, 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))
func makeDot(dps []DotConfig) dot {
fields := make([]reflect.StructField, 0, len(dps))
cleanups := []cleanup{}
mockHttpRequest := httptest.NewRequest("GET", "/", nil)
for i, dc := range dcs {
mockRequest := Request{dc, context.Background(), mockResponseWriter{}, mockHttpRequest}
a, _ := dc.DotProvider.Value(mockRequest)
for i, dp := range dps {
mockRequest := Request{dp, context.Background(), mockResponseWriter{}, mockHttpRequest}
a, _ := dp.Value(mockRequest)
t := reflect.TypeOf(a)
if t.Kind() == reflect.Interface && t.NumMethod() == 0 {
t = t.Elem()
}
f := reflect.StructField{
Name: dc.Name,
Name: dp.FieldName(),
Type: t,
Anonymous: false, // alas
}
if f.Name == "" {
f.Name = f.Type.Name()
}
fields = append(fields, f)
if cdp, ok := dc.DotProvider.(CleanupDotProvider); ok {
if cdp, ok := dp.(CleanupDotProvider); ok {
cleanups = append(cleanups, cleanup{i, cdp})
}
}
typ := reflect.StructOf(fields)
return dot{dcs, cleanups, &sync.Pool{New: func() any { v := reflect.New(typ).Elem(); return &v }}}
return dot{dps, cleanups, &sync.Pool{New: func() any { v := reflect.New(typ).Elem(); return &v }}}
}

type dot struct {
dcs []DotConfig
dps []DotConfig
cleanups []cleanup
pool *sync.Pool
}
Expand All @@ -172,11 +66,11 @@ type cleanup struct {
func (d *dot) value(sctx context.Context, w http.ResponseWriter, r *http.Request) (val *reflect.Value, err error) {
val = d.pool.Get().(*reflect.Value)
val.SetZero()
for i, dc := range d.dcs {
for i, dp := range d.dps {
var a any
a, err = dc.Value(Request{dc, sctx, w, r})
a, err = dp.Value(Request{dp, sctx, w, r})
if err != nil {
err = fmt.Errorf("failed to construct dot value for %s (%v): %w", dc.Name, dc.DotProvider, err)
err = fmt.Errorf("failed to construct dot value for %s (%v): %w", dp.FieldName(), dp, err)
val.SetZero()
d.pool.Put(val)
val = nil
Expand Down
2 changes: 1 addition & 1 deletion providers/db.go → dot_db.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package providers
package xtemplate

import (
"context"
Expand Down
57 changes: 57 additions & 0 deletions dot_db_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package xtemplate

import (
"context"
"database/sql"
"errors"
"fmt"
)

func WithDB(name string, db *sql.DB, opt *sql.TxOptions) Option {
return func(c *Config) error {
if db == nil {
return fmt.Errorf("cannot create database provider with nil sql.DB. name: %s", name)
}
c.Databases = append(c.Databases, DotDBConfig{Name: name, DB: db, TxOptions: opt})
return nil
}
}

type DotDBConfig struct {
*sql.DB `json:"-"`
*sql.TxOptions `json:"-"`
Name string `json:"name"`
Driver string `json:"driver"`
Connstr string `json:"connstr"`
MaxOpenConns int `json:"max_open_conns"`
}

var _ CleanupDotProvider = &DotDBConfig{}

func (d *DotDBConfig) FieldName() string { return d.Name }
func (d *DotDBConfig) Init(ctx context.Context) error {
if d.DB != nil {
return nil
}
db, err := sql.Open(d.Driver, d.Connstr)
if err != nil {
return fmt.Errorf("failed to open database with driver name '%s': %w", d.Driver, err)
}
db.SetMaxOpenConns(d.MaxOpenConns)
if err := db.Ping(); err != nil {
return fmt.Errorf("failed to ping database on open: %w", err)
}
d.DB = db
return nil
}
func (d *DotDBConfig) Value(r Request) (any, error) {
return &DotDB{d.DB, GetLogger(r.R.Context()), r.R.Context(), d.TxOptions, nil}, nil
}
func (dp *DotDBConfig) Cleanup(v any, err error) error {
d := v.(*DotDB)
if err != nil {
return errors.Join(err, d.rollback())
} else {
return errors.Join(err, d.commit())
}
}
35 changes: 35 additions & 0 deletions dot_flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package xtemplate

import (
"context"
"fmt"
)

type DotFlags struct {
m map[string]string
}

func (d DotFlags) Value(key string) string {
return d.m[key]
}

func WithFlags(name string, flags map[string]string) Option {
return func(c *Config) error {
if flags == nil {
return fmt.Errorf("cannot create DotKVProvider with null map with name %s", name)
}
c.Flags = append(c.Flags, DotFlagsConfig{name, flags})
return nil
}
}

type DotFlagsConfig struct {
Name string `json:"name"`
Values map[string]string `json:"values"`
}

var _ DotConfig = &DotFlagsConfig{}

func (d *DotFlagsConfig) FieldName() string { return d.Name }
func (d *DotFlagsConfig) Init(_ context.Context) error { return nil }
func (d *DotFlagsConfig) Value(_ Request) (any, error) { return DotFlags{d.Values}, nil }
2 changes: 2 additions & 0 deletions dot_flush.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (

type dotFlushProvider struct{}

func (dotFlushProvider) FieldName() string { return "Flush" }
func (dotFlushProvider) Init(_ context.Context) error { return nil }
func (dotFlushProvider) Value(r Request) (any, error) {
f, ok := r.W.(flusher)
if !ok {
Expand Down
Loading

0 comments on commit 542a02e

Please sign in to comment.