settings

package module
v0.0.0-...-ba454ba Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 24, 2025 License: MIT, Unlicense Imports: 2 Imported by: 2

README

Settings

Settings allow for an consistent, transparent and uniform way to setup configurable types which are composed of other configurable types especially in the context of test-fixtures.

The basic pattern of such a setting is

type Setting func(*T) error

type T struct { field any }

func (t *T) Apply(oo ...Setting) {
    for _, o := range oo {
        o(t)
    }
}

func SetField(value any) Setting {
    return func(t *T) error {
        t.field = value
    }
}

To just set a field-value this seems over-engineered but often enough we have to do more. On the up-side this closure-settings approach gives us

  • strong encapsulation
  • all the flexibility which is ever needed
  • they avoid to crowd T's namespace with setters (instead they crowd the package namespace ;)

In the above example we have an package specific settings-type. This package provides an cross-package Setting type to setup uniformly a configurable type which is composed of other configurable types. In order for an cross-package setting to know where it should be applied to, we need a small indirection which is implemented in this package as settings.Contexts.

Let T of package pkg be a composed type of the types T1, ..., TN from their respective packages pkg1, ..., pkgN then the implementation of T and a TI looks like this

package pkt

import (
    "settings"
    "pkg1"
    // ...
    "pkgI"
    // ...
    "pkgN"
    "github.com/google/uuid"
)

type Setting = settings.Setting

var tSettingsContextID = uuid.New().String()

type T struct {
    field any
    t1 *pkg1.T1
    // ...
    ti *pkgI.TI
    // ...
    tn *pkgN.TN
    scc *settings.Contexts
}

// Apply settings for given T-instance t and for its composing instances
// T1, ..., TN.
func (t *T) Apply(ss ...Setting) error {
    if t.scc != nil {
        return t.scc.Apply(ss...)
    }
    t.scc = new(settings.Contexts)
    t.t1 = new(pkg1.T1)
    // ...
    t.ti = new(pkgI.TI)
    // ...
    t.tn = new(pkgN.TN)
    // register composing types to t.scc settings.Contexts
    err := t.scc.Apply(
        pkg1.Register(t.t1, t.scc),
        // ...
        pkgI.Register(t.ti, t.scc),
        // ...
        pkgN.Register(t.tn, t.scc),
    )
    if err != nil {
        return err
    }
    return t.occ.Register(tOptionsContextID, t, oo...)
}

func stx(scc *settings.Contexts) *T {
    return scc.Get(tSettingsContextID).(*T)
}

func SetField(value any) Option {
    return func(occ *settings.Contexts) error {
        stx(occ).field = value
        return nil
    }
}
package pkgI

import (
    "options"
    "github.com/google/uuid"
)

type Setting = settings.Setting

var tiSettingsContextID = uuid.New().String()

type TI struct { 
    scc *settings.Contexts
    field any 
}

// Register registers given TI instance ti at applying settings context.
type Register(ti *TI) Setting {
    return func(scc *settings.Contexts) error {
        return stx.Register(tiSettingsContextID, ti)
    }
}

func (ti *TI) Apply(ss ...Setting) error {
    if ti.scc == nil {
        ti.scc := new(settings.Contexts)
        _ = ti.scc.Register(tiSettingsContextID, ti) // can't error
    }
    return = ti.scc.Apply(ss...)
}

func stx(scc *settings.Contexts) *TI {
    return scc.Get(tiSettingsContextID).(*TI)
}

type SetField(value any) Settings {
    return func(scc *settings.Contexts) error {
        stx(scc).field = value
        return nil
    }
}

What we get for this effort is that we can do this

package main

import (
    "pkg"
    "pkgI"
)

func main() {
    t := new(pkg.T)
    err := t.Apply(
        pkg.SetField("value of pkg.T-option"),
        pkgI.SetField("value of pkgI.TI-option"),
    )
    // ...
}

I.e. we have an uniform, consistent and transparent way of setting up a configurable type composed of other configurable types. The limit here is that we can't have two instances of the same composing type in this involved in this process. E.g. a car could not - among other things - be composed of four instances of the type wheel in this manner.

design patterns

See pkg/view as an exemplary use-case.

location and abbreviations

Let pkg be a package that uses options. Then everything that concerns options is defined in pkg/settings.go. An instance of settings.Contexts is typically abbreviated with occ and a type instance in the function of an options context with otx. The later abbreviation is also used for a convenience function which casts an "untyped" options context to its underlying type.

finalizer

If there are settings which are optional or depend on each other it might be desirable at the end of an initial options application to execute a finalizing option ensuring that all needed aspects of a type are set to reasonable defaults. For this provided settings.Contexts implements Contexts.Finalize which takes a function as arguments which will be called at the end of an Contexts.Apply-call. Is there more than one finalizer set they are processed: 'last in first out'.

internal options for testing

Let pkg be a package. Then one may define in pkg/internal/settings.go

package internal

type Setting = settings.Setting

var TSettingsContextID = uuid.New().String()

type TSettings interface {
    M_1( /* ... */ ) // ...

    // ...

    M_n( /* ... */ ) // ...
}

func RegisterT(t any, cb func(TSettings)) settings.Settings

And define in pkg/settings.go

package pkg

import (
    "errors"

    "git.sr.ht/~slukits/eventsourced/pkg/internal"
    "git.sr.ht/~slukits/settings"
)

func init() {
    internal.RegisterT =
        func(t any, cb func(internal.TSettings)) settings.Setting {
            t_, ok := t.(*T)
            return func(occ *settings.Contexts) error {
                if !ok {
                    return errors.New("pkg: T: register: type error")
                }
                to := &tSettings{t: t_}
                err := occ.Register(internal.TSettingsContextID, to)
                if err == nil && cb != nil { 
                    // this if makes it easier to have full coverage ...
                    cb(to)
                }
                // ... while still reporting unlikely but possible error
                return err
            }
        }
}

type tSettings struct { // implements internal.TOptions
    t *T
}

func (t *tSettings) M_1( /* ... */ ) { // ... }

// ...

func (t *tSettings) M_n( /* ... */ ) { // ... }

In this way the testing backend can register an instance of T in its settings.Contexts instance and decide how it wants the features of the TOptions implementation be accessible by a testing backend consumer.

Documentation

Index

Constants

View Source
const PncOptionContextDoesntExist = "option-context doesn't exist"

Variables

View Source
var ErrContextExists = errors.New("option-context with given ID already registered")

Functions

This section is empty.

Types

type Contexts

type Contexts struct {
	// contains filtered or unexported fields
}

Contexts collects options contexts so that various sets of options of different types can be used together:

var tOptionContextID = uuid.New().String()

type T struct { field any }

func (t *T) SetOptions(oo ...settings.Option) error {
	occ := &settings.Contexts{}
	if err := occ.Register(tOptionContextID, *T, oo...); err != nil {
		return err
	}
}

func tOtx(occ *settings.Context) *T {
	return occ.Get(tOptionContextID).(*T)
}

func OptionT(value any) Option {
	return func(occ *settings.Contexts) error {
		tOtx(occ).field = value
		return nil
	}
}

func NewT(cb func(*T)) Option {
	return func(occ *settings.Contexts) error {
		t := new(T)
		cb(t)
		return occ.Register(tOptionContextID, t)
	}
}

var uOptionContextID = uuid.New().String()

type U struct {
	t *T
	field any
}

// SetOptions can also handle T-options for its t-field
func (u *U) SetOptions(oo ...settings.Option) error {
	occ := &settings.Contexts{}
	if err := occ.Apply(NewT(func (t *T) { u.t = t })); err != nil {
		return err
	}
	if err := occ.Register(uOptionContextID, *T, oo...); err != nil {
		return err
	}
}

func uOtx(occ *settings.Context) *T {
	return occ.Get(uOptionContextID).(*U)
}

func OptionU(value any) Option {
	return func(occ *settings.Contexts) error {
		uOtx(occ).field = value
		return nil
	}
}

func (*Contexts) Apply

func (occ *Contexts) Apply(oo ...Setting) (err error)

Apply applies given options oo to options-contexts of given occ. Apply returns the first error that is returned by an option. Apply calls all finalizer in lifo order before it returns. Note nested Apply calls do not trigger finalizer calls. Finalizers are ignored in case of an error.

func (*Contexts) Finalize

func (occ *Contexts) Finalize(f func(*Contexts) error)

Finalize registers given function to be called at the end of an Contexts.Apply call. Contexts.Finalize must be called within an Contexts.Apply call to take effect.

func (*Contexts) Get

func (occ *Contexts) Get(ID string) any

Get returns an options context identified by given ID and panics if it is not available.

func (*Contexts) Register

func (op *Contexts) Register(ID string, otx any, oo ...Setting) error

Register registers a new options-context and fails if given ID is already registered. If optional options are given they are applied.

type Setting

type Setting func(scc *Contexts) error

Setting is the general option type to be used cross-package. Its usually a closure returned by an option-constructor:

func MyOption(value any) Setting {
	return func(occ *Contexts) error {
		// grab the context for MyOption and apply value
		return nil
	}
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL