settings

package
v1.3.0 Latest Latest
Warning

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

Go to latest
Published: Sep 5, 2024 License: MPL-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package settings provides generic interfaces which should be implemented by each configuration settings major version type, and their upwards and downwards migrator types. This package also provides a generic Adapter type which can adapt version specific cfgN.Config structs into version-independent migrationuc.Settings interface, so they can be passed to the use cases layer.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func LoadFromDB added in v1.2.0

func LoadFromDB[C, S any](ctx context.Context, c Config[C, S]) error

LoadFromDB connects to the database, using the connection information from the `c` configuration argument and repo.NormalRole role, queries the database assuming that it has the c.SchemaVersion() version in order to obtain the serialized mutable settings (which must follow the same version is used by `c`). LoadFromDB also deserializes the queried settings in order to obtain an instance of S type and updates the `c` argument in place using its Mutate method. Errors will be returned by proper wrapping. In case of errors, the `c` will remain unchanged. In case of a BoundsError error, the `c` will be updated and that error will be logged as a warning (and a nil error will be returned).

func Nil2Zero

func Nil2Zero[T any](t **T)

Nil2Zero overwrites the (*t) pointer, which should be nil, in order to point to a newly allocated T instance and initializes it with the zero value of T type. If the (*t) pointer was not nil, Nil2Zero will perform no action.

func OverwriteNil

func OverwriteNil[T any](dst **T, src *T)

OverwriteNil overwrites the (*dst) pointer, which should be nil, in order to point to a newly allocated T instance and initializes it with the (*src) value. If the (*dst) pointer was not nil or if the src was nil, this function will perform no action.

func OverwriteUnconditionally added in v1.2.0

func OverwriteUnconditionally[T any](dst **T, src *T)

OverwriteUnconditionally overwrites the (*dst) pointer, which may or may not be a nil pointer, unconditionally based on the src pointer. If the src pointer is nil, (*dst) is overwritten so it becomes nil. If the src pointer is not nil, (*dst) is overwritten so it points to a newly allocated T instance which is initialized by (*src) value.

Types

type Adapter

type Adapter[C Config[C, S], S any] struct {
	Config[C, S]
}

Adapter of C and S is a generic struct which wraps and adapts an instance of Config[C, S] for S serializable settings type and any C type which exposes the Config[C, S] interface in order to provide the pkg/core/usecase/migrationuc.Settings interface.

Most methods of the Settings interface are provided by Config[C, S] interface, hence, only the MergeSettings and Serialize methods should be implemented by Adapter[C, S] struct.

func (Adapter[C, S]) Clone

func (a Adapter[C, S]) Clone() migrationuc.Settings

Clone creates a deep copy of this Adapter[C, S] instance by first cloning `a.Config` and then wrapping it in a new instance of Adapter[C, S] struct.

func (Adapter[C, S]) MergeSettings

func (a Adapter[C, S]) MergeSettings(
	ctx context.Context, s migrationuc.Settings,
) error

MergeSettings expects to receive a Settings instance which has the same settings version as the current instance, matching with the C type parameter version. For this purpose, the `s` argument is expected to implement the Dereferencer[C] interface (which is the case for Config[C, S] and also the Adapter[C, S] instances). When migrating settings to newer or older versions, some of the target version settings may be left uninitialized. This method fills them with their correspoding values from the given `s` argument. Some settings, such as the database connection information, are unconditionally taken from the `s` argument because they need to describe the destination settings values.

Boundary values are initialized based on the `s` argument and settings with out of range values will take the nearest valid values (from a minimum/maximum boundary value), logging the adjustment as a warning.

func (Adapter[C, S]) Serialize added in v1.2.0

func (a Adapter[C, S]) Serialize() (ms, minb, maxb []byte, err error)

Serialize finds out about the mutable settings of its embedded Config instance using the Serializable method, then tries to serialize it as a json string. It also obtains minimum and maximum boundary values for all mutable and immutable settings using the Bounds method and returns their serialized form as two other json strings. Any returned error belongs to the json serialization phase. This serialization decouples the configuration settings format from the database schema format versions.

type BoundsError added in v1.3.0

type BoundsError interface {
	error

	// IsBoundsError is a marker method, so the boundary values
	// violation error types can be distinguished from other error
	// types in a version-independent manner.
	IsBoundsError()
}

BoundsError indicates that some of the configuration settings have a value which is out of their acceptable range of values. BoundsError is implemented by *OutOfBoundsSettingsError from the version specific configuration packages such as cfg1 and so on.

type Config

type Config[C, S any] interface {
	migrationuc.SchemaSettings
	yaml.Marshaler
	Dereferencer[C]

	// Clone creates a deep copy of this configuration instance, so
	// its fields can be changed without updating this instance.
	Clone() C

	// MergeConfig overwrites all fields of this Config[C, S] instance
	// which are not initialized (and so have nil value) with their
	// corresponding values from the `c` argument. The version value is
	// also set to the latest known version values (keeping the major
	// version matched with the C type parameter version).
	// All database settings are copied from the `c` argument
	// unconditionally because after a migration, new tables are placed
	// in the target database which its connection information is
	// presented by the `c` argument. The database version number will
	// be set to its latest supported version too, having the same major
	// version as specified in the `c` instance.
	//
	// Boundary values are initialized based on the `c` instance and
	// settings with out of range values will take the nearest valid
	// values (from a minimum/maximum boundary value), logging the
	// adjustment as a warning.
	MergeConfig(ctx context.Context, c C) error

	// Version returns the semantic version of this Config[C, S] struct
	// contents. Returned version corresponds to one of the supported
	// config major versions. The minor and patch versions may
	// correspond to the Minor and Patch constants of the relevant
	// package or may describe an older version (newer/unknown minor
	// versions are rejected by the Load function). The patch version
	// has no special constraint since it has no visible effect.
	Version() model.SemVer

	// MajorVersion returns the major semantic version of this
	// Config[C, S] struct. This value matches with the first component
	// of the version which is returned by the Version method. However,
	// the Version method returns the complete semantic version as
	// written in a configuration file, hence, it cannot be called
	// without creating an instance of Config[C, S] first. In contrast,
	// this method only depends on the C type and so can be called with
	// a nil instance of the C type too.
	MajorVersion() uint

	// Mutate updates this Config[C, S] instance using the given S
	// instance which provides the mutable settings values.
	// The given S instance may contain mutable & invisible settings
	// (write-only) and mutable & visible settings (read-write), but
	// it may not contain the immutable settings. The provided S
	// instance is not updated itself, hence, a non-pointer variable
	// is suitable.
	//
	// If provided values do not respect the expected boundary values,
	// an error which implements BoundsError interface will be returned,
	// indicating that which settings were out of bound, however, this
	// type of error does not prevent this Config[C, S] instance to be
	// updated. When a minimum/maximum boundary value is crossed over,
	// that boundary value itself will be used as the new value of that
	// setting.
	Mutate(s S) error

	// Serializable creates and returns an instance of *S in order to
	// report the mutable settings, based on this Config[C, S] instance.
	Serializable() *S

	// Bounds creates and returns two instances of *S in order to
	// report the minimum and maximum boundary values for those settings
	// which their lower/upper limits should be restricted.
	// The boundary values may be reported for both of the mutable and
	// immutable settings (as they have an informational purpose).
	// All boundary values are obtained from this Config[C, S] instance.
	Bounds() (minb, maxb *S)
}

Config of C and S describes the expected interface of Config structs. It contains the database-related settings by embedding SchemaSettings interface, controls how the Config should be serialized to YAML using the Marshaler interface, keeps the main Config object accessible even when it is embedded by the Adapter[C, S] struct since it embeds the Dereferencer[C] interface, supports merging the destination configuration settings (only from the same version C struct) by the MergeConfig method, and reports the current configuration version by the Version method. All pkg/adapter/config/cfgN.Config structs must implement this interface.

This interface usage is twofold. First, asserting that each Config struct implements it (in its test.go file) ensures that all relevant methods are implemented with the proper types. For example, if the implementation of cfg1 package was copied in order to create cfg2 package, it is enough to update the test file assertion to Config[*cfg2.Config, cfg2.Serializable] in order to produce a compilation error while the relevant MergeConfig type is not updated. Second, it unifies different cfgN.Config versions, so they can be wrapped by a generic Adapter[C, S] struct instead of having to implement a distinct Adapter type per C and S types.

The S is the concrete serializable type which can be used for holding of the mutable settings. The C can produce a *S instance to be encoded as json and stored in the database. In reverse direction, stored mutable settings can be read from the database, decoded as a *S instance, and passed to C as a S instance in order to mutate the C fields (a non-pointer is used to simulate a const variable).

type Dereferencer

type Dereferencer[C any] interface {
	// Dereference returns an instance of the `C` type which is wrapped
	// by the current object, so the wrapped object may be fetched.
	//
	// Methods of a wrapped struct may need to refer to other types
	// based on some form of grouping, e.g., cfg1.Config can be merged
	// by instances of cfg1.Config but may not be merged by cfg2.Config
	// instances. Such wrapped structs have to be adapted by an abstract
	// interface, so they can be used uniformly.
	// Presence of the Dereference method allows the wrapped struct and
	// its wrapper adapter type to be used uniformly. Indeed, the raw
	// and wrapper adapter instances can be represented by Dereferencer
	// interface, so the wrapped C instance may be obtained. Note that
	// type assertion from an abstract interface (implemented by the
	// wrapper adapter type) requires pre-knowledge about the adapter
	// type, while the Dereferencer[C] interface can be provided by
	// any adapter implementation simply by embedding the C instance.
	Dereference() C
}

Dereferencer is a generic interface which its C type parameter represents a wrapped type (e.g., pkg1.Config struct). This interface can be implemented by a wrapper type which wants to be dereferenced and return its wrapped object. The ideal place for implementing the Dereference method is on the wrapped object's type itself, so any type which embeds it may inherit the Dereference method and implement the Dereferencer automatically.

type DownMigrator

type DownMigrator[C Config[C, S], S, D any] interface {
	repo.Settler[C]

	// MigrateDown migrates the contained config object, with C type,
	// into its previous/downer major version (if any) and wraps it
	// with another downwards migrator implementation, with the D type.
	// If there is no older major version, an error will be returned.
	// After reaching to the target major version, the Settler method
	// from the repo.Settler[C] interface can be used to obtain the C
	// config instance.
	MigrateDown(ctx context.Context) (D, error)
}

DownMigrator of C, S, and D describes the expected interface of a Config downwards migrator implementation. The C type parameter specifies a Config implementation like *cfg1.Config and so it exposes the Config[C, S] generic interface itself. The downwards migrators are recognized by one main function, namely MigrateDown, which converts the contained config object (with type C) to its previous version (if any) and then wraps it with another downwards migrator type which is represented by the D type parameter and returns it, so the config object can be migrated one major version downwards at a time. If there is no downer/previous version, an error will be returned. All pkg/adapter/config/down/dnmigN.Migrator structs must implement this interface. It also embeds the repo.Settler[C] interface, so after migrating downwards enough and when the target major version was achieved, the Settler method can be called and the ultimate C config instance can be fetched.

Similar to the Config[C, S] which was useful to assert that each Config struct implements the relevant methods based on the C and S config and serializable type parameters, the DownMigrator[C, S, D] is useful to assert that a Settler and MigrateDown methods with proper return value types are implemented by the corresponding dnmigN.Migrator structs (which may be copied from the previous migrator versions, whenever a newer major version is required, and should get a compilation error as while as its methods are not updated properly).

However, despite the Config[C, S] interface, it is not possible to use the DownMigrator[C, S, D] for unification of all dnmigN.Migrator structs. The reason is in the limited generic types support in the Golang as it is not possible to deduce a list of type parameters at compile time or provide a variety list of type parameters. Therefore, for each dnmigN.Migrator type which has X older major versions, it is required to define D using X nested type parameters. The appropriate C type and the nested DownMigrator[C, S, D] (for a D type parameter which depends on the X older DownMigrator types) are defined as "C" and "S" and "Type" type aliases in each dnmigN package.

The goal is to adapt each DownMigrator[C, S, D] in order to provide the pkg/core/repo.DownMigrator[pkg/core/usecase/migrationuc.Settings] interface, so it can be passed to the use cases layer independent of its configuration settings format version. Because a D instance (with "any" constraint) cannot be adapted without knowing about the inner types of D type parameter, a distinct Adapter struct is implemented in each dnmigN package.

type Duration

type Duration time.Duration

Duration is a specialization of the time.Duration which produces a more human-readable representation when marshaled using its Marshal method.

func (*Duration) LogValue added in v1.3.0

func (d *Duration) LogValue() slog.Value

LogValue implements slog.LogValuer and returns a DurationValue if this Duration is not nil, otherwise, it returns a StringValue with the constant "nil-duration" value.

func (*Duration) Marshal

func (d *Duration) Marshal() *string

Marshal returns a string representation of the `d` time duration. If d is nil, nil will be returned, so it can be used by higher-level Marshal methods for creation of an alternative struct (to be encoded to YAML instead of the actual data types with help of a top-level MarshalYAML method). If d is not nil, it will be encoded as a string according to the time.Duration string representation format, e.g., 2h3m4s, with this difference that zero trailing values will be ignored. That is, no 0s or 0m0s suffix may be included, for sake of more readability. A zero time duration will be encoded as 0h. The returned pointer will refer to a newly allocated string variable.

See pkg/adapter/config/cfg2.*Config.Marshal for an example usage.

func (*Duration) MarshalText added in v1.2.0

func (d *Duration) MarshalText() ([]byte, error)

MarshalText implements encoding.TextMarshaler interface and serializes `d` duration using its Marshal method. This interface is required for json serialization.

func (*Duration) UnmarshalText

func (d *Duration) UnmarshalText(data []byte) error

UnmarshalText reifies the encoding.TextUnmarshaler interface, so a byte slice (e.g., read from a YAML file) can be decoded as a time duration. The format of the `data` argument should conform to the time.ParseDuration expected format. In absence of errors, a nil error will be returned and only then, `d` receiver will be updated to contain the decoded duration.

type OutOfRangeError added in v1.3.0

type OutOfRangeError[T cmp.Ordered] struct {
	Value        *T   // The actual out-of-range value
	LessThanMin  bool // true if and only if min boundary is violated
	InvalidRange bool // true if and only if min is greater than max
}

OutOfRangeError indicates that a Value was out of its acceptable range, either less than its minimum valid value or greater than its maximum valid value.

func VerifyRange added in v1.3.0

func VerifyRange[T cmp.Ordered](
	value **T, minb, maxb *T,
) *OutOfRangeError[T]

VerifyRange verifies the given value ensuring that it is either nil or is within the provided minb/maxb boundary values, if the boundary values were given as non-nil values themselves. In case of a wrong value, in addition to the returned error, the value itself will be updated in order to take the minb or maxb value and fall in the acceptable range of values.

func (*OutOfRangeError[T]) Error added in v1.3.0

func (e *OutOfRangeError[T]) Error() string

Error implements error interface and returns a string reporting that minimum or maximum boundary value was not respected.

type UpMigrator

type UpMigrator[C Config[C, S], S, U any] interface {
	repo.Settler[C]

	// MigrateUp migrates the contained config object, with C type,
	// into its next/upper major version (if any) and wraps it with
	// another upwards migrator implementation, with U type.
	// If there is no upper major version, an error will be returned.
	// After reaching to the target major version, the Settler method
	// from the repo.Settler[C] interface can be used to obtain the C
	// config instance.
	MigrateUp(ctx context.Context) (U, error)
}

UpMigrator of C, S, and U describes the expected interface of a Config upwards migrator implementation. The C type parameter specifies a Config implementation like *cfg1.Config and so it exposes the Config[C, S] generic interface itself. The upwards migrators are recognized by one main function, MigrateUp, which converts the contained config object (with type C) to its next version (if any) and then wraps it with another upwards migrator type which is represented by the U type parameter and returns it, so the config object can be migrated one major version upwards at a time. If there is no upper/next version, an error will be returned. All pkg/adapter/config/up/upmigN.Migrator structs must implement this interface. It also embeds the repo.Settler[C] interface, so after migrating upwards enough and when the target major version was achieved, the Settler method can be called and the ultimate C config instance can be fetched.

Similar to the Config[C, S] which was useful to assert that each Config struct implements the relevant methods based on the C and S config and serializable type parameters, the UpMigrator[C, S, U] is useful to assert that a Settler and MigrateUp methods with proper return value types are implemented by the corresponding upmigN.Migrator structs (which may be copied from the previous migrator versions, whenever a newer major version is required, and should get a compilation error as while as its methods are not updated properly).

However, despite the Config[C, S] interface, it is not possible to use the UpMigrator[C, S, U] for unification of all upmigN.Migrator structs. The reason is in the limited generic types support in the Golang as it is not possible to deduce a list of type parameters at compile time or provide a variety list of type parameters. Therefore, for each upmigN.Migrator type which has X subsequently released major versions, it is required to define U using X nested type parameters. The appropriate C type and the nested UpMigrator[C, S, U] (for a U type parameter which depends on the X subsequent UpMigrator types) are defined as "C" and "S" and "Type" type aliases in each upmigN package.

The goal is to adapt each UpMigrator[C, S, U] in order to provide the pkg/core/repo.UpMigrator[pkg/core/usecase/migrationuc.Settings] interface, so it can be passed to the use cases layer independent of its configuration settings format version. Because a U instance (with "any" constraint) cannot be adapted without knowing about the inner types of U type parameter, a distinct Adapter struct is implemented in each upmigN package.

Jump to

Keyboard shortcuts

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