cfg2

package
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: Mar 7, 2024 License: MPL-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package cfg2 makes it possible to load configuration settings with version 2.x.y since all minor and patch versions (which are known) with the same major version, can be loaded with one implementation. When trying to serialize and write out settings, the latest known minor and patch version will be used since older versions (with the same major version) can ignore the extra fields too.

Index

Constants

View Source
const (
	Major = 2
	Minor = 0
	Patch = 0
)

These constants define the major, minor, and patch version of the configuration settings which are supported by the Config struct.

Variables

Version is the semantic version of Config struct.

Functions

This section is empty.

Types

type Cars

type Cars struct {
	// DelayOfOPM indicates the amount of delay that an
	// old parking method should incur.
	DelayOfOPM *settings.Duration `yaml:"delay-of-old-parking-method"`
}

Cars contains the configuration settings for the cars use cases. Fields are defined as pointers, so it is possible to detect if they are or are not initialized. After migrating from some configuration settings version, some settings may be left uninitialized because they may have no corresponding items in the source settings version. Those items can be detected as nil pointers and filled by their default values using the MergeConfig method.

func (Cars) NewUseCase

func (c Cars) NewUseCase(
	p repo.Pool, r repo.Cars,
) (*carsuc.UseCase, error)

NewUseCase instantiates a new cars use case based on the settings in the c struct.

type Config

type Config struct {
	Database cfg1.Database // PostgreSQL database connection settings
	Gin      cfg1.Gin      // Gin-Gonic instantiation settings
	Usecases Usecases      // Supported use cases configuration settings

	// Vers contains the configuration file and database schema version
	// strings corresponding to this Config instance and its Database
	// target.
	Vers vers.Config `yaml:",inline"`

	// Comments contains the YAML comment lines which are written right
	// before the actual settings lines, aka head-comments.
	// These comments are preserved for top-level settings and their
	// children sequence and mapping YAML nodes. The Comments may be nil
	// which will be ignored, or may be poppulated with some comments
	// which will be preserved during a marshaling operation by the
	// multi-database migration operation. Indeed, Comments field is
	// only useful when the destination configuration file is loaded
	// during a migration operation because the MergeConfig method
	// preserves the destination Comments field, so the new comments
	// may be seen in the target config file.
	Comments *comment.Comment `yaml:"-"`
}

Config contains all settings which are required by different parts of the project following the v2.x.y format, such as adapters or use cases. It is preferred to implement Config with primitive fields or other structs which are defined locally, not models or structs which are defined in lower layers, so the configuration can be versioned and kept intact while other layers can change freely. This version (when freezed and no further minor or patch release of it was supposed acceptable) may be embedded by the future config versions (if they need to copy some parts of this config version).

func Load

func Load(data []byte) (*Config, error)

Load unmarshals the data byte slice and loads a Config instance assuming that it contains the Config settings. Extra items in the data will be ignored and missing items will take their default values. Thereafter, loaded Config will be validated and normalized in order to ensure that provided settings are acceptable (for example the major version which is reported by data settings must match with number 2 which is the major version of this config package).

If some settings should be overridden by environment variables, this method is the proper place for that replacement. However, if settings should be overridden by some information from the database, they must not be replaced here because the Load method provides those settings which are fixed by each execution (while the database contents may change continually and their loading must be performed by a separate method, such as LoadFromDB).

func LoadFromDB added in v1.2.0

func LoadFromDB(ctx context.Context, data []byte) (
	*Config, bool, error,
)

LoadFromDB parses the given data byte slice and loads a Config instance (the first return value). It also tries to establish a connection to the corresponding database which its connection information are described in the loaded Config instance. It is expected to find a serialized version of mutable settings following the same format which is used by Config (i.e., Serializable struct) in the database. The mutable settings from the database will override the settings which are read from the data byte slice. Thereafter, loaded and mutated Config will be validated and normalized in order to ensure that provided settings are acceptable.

If some settings should be overridden by environment variables, they should be updated after parsing the data byte slice and before checking the database contents (so configuration file may be updated by environment variables and both may be updated by database contents respectively). If an error prevents the configuration settings to be updated using the database contents, but the loaded static settings were valid themselves, LoadFromDB still returns the Config instance. The second return value which is a boolean reports if the Config instance is or is not being returned (like an ok flag for the first return value). Any errors will be returned as the last return value.

func (*Config) Clone

func (c *Config) Clone() *Config

Clone creates a new instance of Config and initializes its fields based on the `c` fields. Pointers are renewed too, so changes in the returned Config instance and `c` stay independent.

func (*Config) ConnectionInfo

func (c *Config) ConnectionInfo() (dbName, host string, port int)

ConnectionInfo returns the host, port, and database name of the connection information which are kept in this Config instance.

func (*Config) ConnectionPool

func (c *Config) ConnectionPool(
	ctx context.Context, r repo.Role,
) (repo.Pool, error)

ConnectionPool creates a database connection pool using the connection information which are kept in the `c` settings.

func (*Config) Dereference

func (c *Config) Dereference() *Config

Dereference returns the `c` Config instance itself.

Methods of the Config struct refer to other types based on this package Major version for complete type-safety. For example, the MergeConfig only accepts an instance of Config from this package and passing a cfg1.Config instance will be rejected at compile time. However, the use cases layer which does not know about the config version at compile time has to receive Config as an abstract interface which is common among all config versions. That abstract interface is defined as pkg/core/usecase/migrationuc.Settings which provides MergeSettings method instead of MergeConfig and accepts an instance of Settings interface instead of the Config instance. The pkg/adapter/config/settings.Adapter[Config, Serializable] is defined in order to wrap a Config instance and implement the migrationuc.Settings interface.

Presence of the Dereference method allows users of the Config struct and the Adapter[Config, Serializable] struct to use them uniformly. Indeed, both of the raw Config and its wrapper Adapter instances can be represented by pkg/adapter/config/settings.Dereferencer[Config] interface and so the wrapped Config instance may be obtained from them using the Dereference method. Note that a type assertion from the Settings interface to the Adapter instance requires pre-knowledge about the Adapter (and a Settings interface which is provided by some other adapter implementation may not be supported), while the Dereferencer[Config] interface can be provided by any adapter implementation simply by embedding the Config instance.

func (*Config) MajorVersion

func (c *Config) MajorVersion() uint

MajorVersion returns the major semantic version of this Config instance. 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 first. In contrast, this method only depends on the Config type and so can be called with a nil instance too.

func (*Config) Marshal

func (c *Config) Marshal() *Marshalled

Marshal creates an instance of the Marshalled struct and fills it with the `c` Config instance contents. The Marshalled and Config fields do correspond with each other with one difference. Any field which requires a specific MarshalYAML logic (and its default encoding logic into YAML format is not suitable) is replaced by a primitive data type, so it can contain the properly serialized version of that field.

This Marshal method encodes and replaces fields which are defined in this package and recursively calls Marshal method on those fields which are defined in other packages. Therefore, the marshaling logic can be distributed among packages, near to the relevant data types (while MarshalYAML from the yaml.Marshaler interface is only called for the top-most object and is ignored for nested types).

func (*Config) MarshalYAML

func (c *Config) MarshalYAML() (interface{}, error)

MarshalYAML computes an instance of the Marshalled struct, as created by the Marshal method, so it may be marshalled instead of the `c` Config instance. This replacement makes it possible to substitute specific settings such as a slices of numbers in a vers.Config with their alternative primitive data types and have control on the final serialization result. Thereafter, it encodes *Marshalled as a yaml node instance and saves the preserved head `c.Comments` (if any) into the resulting *yaml.Node instance (and returns it as an interface{}).

See the Marshal function for the reification details and how marshaling logic can be distributed among nested Config structs.

func (*Config) MergeConfig

func (c *Config) MergeConfig(c2 *Config) error

MergeConfig overwrites all fields of `c` which are not initialized (and have nil value) with their corresponding values from `c2` arg. The `c` config version will be set to the latest known version values as specified by Major, Minor, and Patch constants in this package. All database settings in `c` are overwritten by the `c2` values unconditionally. The database version number will be set to its latest supported version too, having the same major version as specified in `c2` instance. The Comments field takes its value from the `c2` instance, ignoring comments of the `c` instance (if any).

func (*Config) Mutate added in v1.2.0

func (c *Config) Mutate(s Serializable) error

Mutate updates this Config instance using the given Serializable instance which provides the mutable settings values. The given Serializable instance may contain mutable & invisible settings (write-only) and mutable & visible settings (read-write), but it may not contain the immutable settings (i.e., the Immutable pointer must be nil). The provided Serializable instance is not updated itself, hence, a non-pointer variable is suitable.

func (*Config) NewAppUseCase added in v1.2.0

func (c *Config) NewAppUseCase(
	p repo.Pool, s appuc.SettingsRepo, carsRepo repo.Cars,
) (*appuc.UseCase, error)

NewAppUseCase instantiates a new application management use case. Instantiated use case needs a settings repository (and access to the connection pool) in order to query and update the mutable settings. It also needs to know about the configuration file contents which should be overridden by the database contents. However, the repository instance can manage this relationship with the configuration file contents (in the adapters layer), allowing the application use case to solely deal with the model layer settings. The settings repository must take the `c` Config instance during its instantiation.

func (*Config) NewCarsUseCase added in v1.2.0

func (c *Config) NewCarsUseCase(
	p repo.Pool, r repo.Cars,
) (*carsuc.UseCase, error)

NewCarsUseCase instantiates a new cars use case based on the settings in the c struct.

func (*Config) NewSchemaRepo

func (c *Config) NewSchemaRepo() repo.Schema

NewSchemaRepo instantiates a fresh Schema repository. Role names may be optionally suffixed based on the settings and in that case, repo.Role role names which are passed to the ConnectionPool method or RenewPasswords will be suffixed automatically. Since the Schema repository has methods for creation of roles or asking to grant specific privileges to them, it needs to obtain the same role name suffix (as stored in the current SchemaSettings instance).

func (*Config) RenewPasswords

func (c *Config) RenewPasswords(
	ctx context.Context,
	change func(
		ctx context.Context, roles []repo.Role, passwords []string,
	) error,
	roles ...repo.Role,
) (finalizer func() error, err error)

RenewPasswords generates new secure passwords for the given roles and after recording them in a temporary file, will use the change function in order to update the passwords of those roles in the database too. The change function argument should perform the update operation in a transaction which may or may not be committed when RenewPasswords returns. In case of a successful commitment, the temporary passwords file should be moved over the main passwords file. The temporary passwords file is named as .pgpass.new and the main passwords file is named as .pgpass in this version. Keeping the .pgpass file (in the `c.Database.PassDir`) up-to-date, makes it possible to use ConnectionPool method again (both if the passwords are or are not updated successfully). This final file movement can be performed using the returned finalizer function.

func (*Config) SchemaInitializer

func (c *Config) SchemaInitializer(tx repo.Tx) (
	repo.SchemaInitializer, error,
)

SchemaInitializer creates a repo.SchemaInitializer instance which wraps the given transaction argument and can be used to initialize the database with development or production suitable data. The format of the created tables and their initial data rows are chosen based on the database schema version, as indicated by SchemaVersion method. All table creation and data insertion operations will be performed in the given transaction and will be persisted only if that transaction could commit successfully.

func (*Config) SchemaMigrator

func (c *Config) SchemaMigrator(tx repo.Tx) (
	repo.Migrator[repo.SchemaSettler], error,
)

SchemaMigrator creates a repo.Migrator[repo.SchemaSettler] instance which wraps the given `tx` transaction argument and can be used for

  1. loading the source database schema information with this assumption that tx belongs to the destination database and this Config instance contains the source database connection information, so it can modify the destination database within a given transaction and fill a schema with tables which represent the source database contents (not moving data items necessarily, but may create them as a foreign data wrapper, aka FDW),
  2. creating upwards or downwards migrator objects in order to transform the loaded data into their upper/lower schema versions, again with minimal data transfer and using views instead of tables as far as possible, while creating tables or even loading data into this Golang process if it is necessary, and at last
  3. obtaining a repo.SchemaSettler instance for the target schema major version, so it can persist the target schema version by creating tables and filling them with contents of the corresponding views.

func (*Config) SchemaVersion

func (c *Config) SchemaVersion() model.SemVer

SchemaVersion returns the semantic version of the database schema which its connection information are kept by this Config struct. There is no direct dependency between the configuration file and database schema versions.

func (*Config) Serializable added in v1.2.0

func (c *Config) Serializable() *Serializable

Serializable creates and returns an instance of *Serializable in order to report the mutable settings, based on this Config instance. The Immutable pointer will be nil in the returned object.

func (*Config) SetSchemaVersion

func (c *Config) SetSchemaVersion(sv model.SemVer)

SetSchemaVersion updates the semantic version of the database schema as recorded in this Config instance and reported by the SchemaVersion method.

func (*Config) SettingsPersister added in v1.2.0

func (c *Config) SettingsPersister(tx repo.Tx) (
	repo.SettingsPersister, error,
)

SettingsPersister instantiates a repo.SettingsPersister for the database schema version of the `c` Config instance, wrapping the given `tx` transaction argument. Obtained settings persister depends on the schema major version because the migration process only needs to create and fill tables for the latest minor version of some target major version. Caller needs to serialize the mutable settings independently (based on the settings format version) and then employ this persister object for its storage in the database (see the settings.Adapter.Serialize and Config.Serializable methods). A transaction (not a connection) is required because other migration operations must be performed usually in the same transaction.

func (*Config) ValidateAndNormalize

func (c *Config) ValidateAndNormalize() error

ValidateAndNormalize validates the configuration settings and returns an error if they were not acceptable. It can also modify settings in order to normalize them or replace some zero values with their expected default values (if any).

func (*Config) Version

func (c *Config) Version() model.SemVer

Version returns the semantic version of this Config struct contents which its major version is equal to 2, while its minor and patch versions may correspond to the Minor and Patch constants or may describe an older version (if the minor version of the returned semantic version was more recent than Minor constant, it could not be loaded by the Load function). By the way, no constraint exists on the patch version because it has no visible effect.

func (*Config) Visible added in v1.2.0

func (c *Config) Visible() *Visible

Visible creates and fills an instance of Visible struct with the mutable and immutable settings which can be queried by end-users. That is, the Immutable pointer will be non-nil in the returned object. Despite the Mutate and Serializable methods, the Visible method is not included in the pkg/adapter/config/settings.Config generic interface because it is only useful in the adapters layer where a repository package may query the visible settings after updating a Config instance. However, it is not required in the migration use cases as they deal with mutable settings which are exposed by the Serializable method.

type Immutable added in v1.2.0

type Immutable struct {
	// Logger reports if server-side REST API logging is enabled.
	Logger bool `json:"logger"`
}

Immutable contains settings which are immutable (and can be configured only using the configuration file or environment variables alone), but are visible by end-users (settings must be at least visible or mutable, otherwise, they may not be called a setting).

type Marshalled

type Marshalled struct {
	Database cfg1.Database
	Gin      cfg1.Gin
	Usecases struct {
		Cars struct {
			Delay *string `yaml:"delay-of-old-parking-method,omitempty"`
		}
	}
	Vers *vers.Marshalled `yaml:",inline"`
}

Marshalled struct contains a field for each one of the Config struct fields. The field names may be different for simplicity, but the yaml tag of fields are chosen to have consistent names after the serialization operation. The types of those fields are the same if their default serialization format is acceptable, otherwise, they will be serialized manually using the Marshal method and their target primitive types will be used in the Marshalled struct.

type Serializable added in v1.2.0

type Serializable struct {
	// Version indicates the format version of this Serializable and
	// is equal to the Config struct version. Although its value is
	// known from the Serializable type, but we have to store it as a
	// field in order to find it out during the deserialization and
	// application phase (by the Mutate method).
	// Therefore, the embedded Settings struct is enough at runtime.
	Version model.SemVer `json:"version"`

	Settings
}

Serializable embeds the Settings in addition to a Version field, so it can be serialized and stored in the database, while the Version field may be consulted during its deserialization in order to ensure that it belongs to the same configuration format version. The Serializable and the main Config struct are versioned together. The nested Immutable pointer must be nil because the Serializable is supposed to carry the mutable settings which are acceptable to be queried from the database and may be passed to the Mutate method.

type Settings added in v1.2.0

type Settings struct {
	Visible
}

Settings contains those settings which are mutable & invisible, that is, write-only settings. It also embeds the Visible struct so it effectively contains all kinds of settings. When fetching settings, the nested Immutable pointer can be set to nil in order to keep the mutable (visible or invisible) settings and when reporting settings, the embedded Visible struct can be reported alone (having a non-nil Immutable pointer) in order to exclude the invisible settings.

Some fields, such as Logger, were defined as a pointer in the Config struct because it was desired to detect if they were or were not initialized during a migration operation, so they could be filled by the MergeConfig method later. They had to obtain a value anyways after a call to the ValidateAndNormalize method and so nil is not a meaningful value for them. Those fields must have non-pointer types in the Settings and Visible structs, so they take a value when read from the database for example. Even if a settings manipulation use case implementation wants to allow end-users to selectively configure settings, it is the responsibility of that implementation to replace such nil values with their old settings values and we can expect to set all fields of the Settings and Visible structs collectively. By the way, such a use case increases the risk of conflicts because an end-user decides to selectively update one setting because they think that other settings have some seen values, but they have been changed concurrently. So it is preferred to ask the frontend to send the complete set of settings (whether they are set by end-user or their older seen values are left unchanged) in order to justify a PUT instead of a POST request method. Of course, that decision relies on the details of each use case and cannot be fixed in this layer.

Some fields, such as the old parking method delay, were defined as a pointer in the Config struct because they could be left uninitialized even after a call to the ValidateAndNormalize method. That is, nil is a meaningful value for them and asks the configuration instance not to pass their corresponding functional options to use cases. Those fields must have pointer types in the Settings and Visible structs, so they can be kept uninitialized even when stored in and read out from the database again. That is, even if a settings field has a non-nil value, but its corresponding field in the database has a nil value, it has to be overwritten by that nil because being uninitialized is a menaingful configuration decision which was taken and persisted in the database in that scenario.

type Usecases

type Usecases struct {
	Cars Cars // cars use cases related settings
}

Usecases contains the configuration settings for all use cases.

type Visible added in v1.2.0

type Visible struct {
	// Cars represents the visible and mutable settings for the Cars
	// use cases.
	Cars struct {
		// DelayOfOPM indicates the old parking method delay.
		DelayOfOPM *settings.Duration `json:"delay_of_opm"`
	} `json:"cars"`
	*Immutable
}

Visible contains settings which are visible by end-users. These settings may be mutable or immutable. The immutable & visible settings are managed by the embedded Immutable struct. When it is desired to serialize and transmit settings to end-users, the Immutable pointer should be non-nil and its fields should be poppulated. However, when it is desired to fetch settings from end-users and deserialize them, the Immutable pointer should be set to nil in order to abandon them.

Jump to

Keyboard shortcuts

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