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 15, 2024
1 parent 508b03c commit 9f485a9
Show file tree
Hide file tree
Showing 30 changed files with 387 additions and 626 deletions.
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
70 changes: 54 additions & 16 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"html/template"
"io/fs"
"log/slog"

"github.com/nats-io/nats-server/v2/server"
)

func New() (c *Config) {
Expand All @@ -26,19 +28,26 @@ 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:"-"`
KVs []DotKVProvider `json:"kvs" arg:"-"`
Nats []DotNatsProvider `json:"nats" arg:"-"`
CustomProviders []DotProvider `json:"-" arg:"-"`

// Whether to start a bult-in nats server
StartInternalNatsServer bool `json:"start_internal_nats_server" default:"false"`
NatsServerOpts *server.Options `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 All @@ -50,6 +59,40 @@ type Config struct {
Logger *slog.Logger `json:"-" arg:"-"`
}

func (config *Config) CheckName(name string) error {
for _, d := range config.Databases {
if name == d.FieldName() {
return fmt.Errorf("field name '%s' conflicts with database field name '%s'", name, d.FieldName())
}
}
for _, f := range config.Flags {
if name == f.FieldName() {
return fmt.Errorf("field name '%s' conflicts with flags field name '%s'", name, f.FieldName())
}
}
for _, d := range config.Directories {
if name == d.FieldName() {
return fmt.Errorf("field name '%s' conflicts with directory field name '%s'", name, d.FieldName())
}
}
for _, kv := range config.KVs {
if name == kv.FieldName() {
return fmt.Errorf("field name '%s' conflicts with flags field name '%s'", name, kv.FieldName())
}
}
for _, d := range config.Nats {
if name == d.FieldName() {
return fmt.Errorf("field name '%s' conflicts with directory field name '%s'", name, d.FieldName())
}
}
for _, d := range config.CustomProviders {
if name == d.FieldName() {
return fmt.Errorf("field name '%s' conflicts with directory field name '%s'", name, d.FieldName())
}
}
return nil
}

// FillDefaults sets default values for unset fields
func (config *Config) Defaults() *Config {
if config.TemplatesDir == "" {
Expand Down Expand Up @@ -117,17 +160,12 @@ func WithFuncMaps(fm ...template.FuncMap) Option {
}
}

func WithProvider(name string, p DotProvider) Option {
func WithProvider(p DotProvider) 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
}
if err := c.CheckName(p.FieldName()); err != nil {
return err
}
c.Dot = append(c.Dot, DotConfig{Name: name, DotProvider: p})
c.CustomProviders = append(c.CustomProviders, p)
return nil
}
}
131 changes: 14 additions & 117 deletions dot.go
Original file line number Diff line number Diff line change
@@ -1,36 +1,16 @@
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
DotProvider
ServerCtx context.Context
W http.ResponseWriter
R *http.Request
Expand All @@ -41,125 +21,42 @@ type DotProvider interface {
// also returns an error. Value will be called with mock values at least
// once and still must not panic.
Value(Request) (any, error)
}

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

type CleanupDotProvider interface {
DotProvider
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 []DotProvider) 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 []DotProvider
cleanups []cleanup
pool *sync.Pool
}
Expand All @@ -172,11 +69,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
55 changes: 55 additions & 0 deletions dot_db_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package xtemplate

import (
"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)
}
if err := c.CheckName(name); err != nil {
return err
}
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) Value(r Request) (any, error) {
if d.DB == nil {
db, err := sql.Open(d.Driver, d.Connstr)
if err != nil {
return &DotDB{}, 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 &DotDB{}, fmt.Errorf("failed to ping database on open: %w", err)
}
d.DB = db
}
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())
}
}
Loading

0 comments on commit 9f485a9

Please sign in to comment.