Help us shape the future of Go! Take the 2026 Go Developer Survey

dbsetting

package
v0.46.0 Latest Latest
Warning

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

Go to latest
Published: Sep 26, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package dbsetting — bool.go

The switch-shaped member of the family. Same four behaviours as IntSpec (resolution order, seed-on-first-boot-only, fall back on read, refuse on write), minus the one that has no meaning for a boolean: there is no range to clamp to, because every parseable value is already legal.

⚠ That is the whole reason a BoolSpec is not "an IntSpec with Min 0 Max 1". A number out of range has a nearest legal neighbour and clamping to it keeps the operator's intent; an unparseable switch has none, and guessing which way an operator meant a typo'd kill-switch to point is exactly the kind of decision software must not make. Unparseable text therefore resolves to Default and says so in the log.

Package dbsetting is the shared shape for a filex setting that lives in the `settings` table, is seeded once from an environment variable, is clamped to a range on read, and is validated at the API that writes it.

It exists because that is four behaviours, and every one of them is a place a per-setting reimplementation quietly diverges: one setting clamps and the next rejects, one treats a blank row as the default and the next as zero, one documents its env var as an override when it is really a seed. Declaring a setting as an IntSpec, a BoolSpec or a StringSpec means the next one is a struct literal rather than a redesign.

The three kinds, and why they are three

  • IntSpec — a magnitude with bounds (minutes, days, MB). Out of range clamps on read and is refused on write.
  • BoolSpec — a switch (bool.go). No bounds exist, so nothing clamps; unparseable text falls back to Default rather than being guessed at.
  • StringSpec — text with a shape (string.go): a mode name, an address. Bounds are replaced by a Check hook, because "clamav 3310" is the same shape to the compiler as "clamav:3310" and only one of them can be dialled.

They share Seeder (first-boot seeding), so one SeedAll call at boot carries a mixed list.

Resolution order

stored row (settings table)  →  env var, on first boot only  →  Default

⚠⚠ The env var is a SEED, not an override. It is consumed exactly once, by Seed at boot, and only when no row exists yet. From the moment a row exists — written by that seeding or by an admin in the UI — the env var is inert: editing it in compose and restarting changes nothing, because the row wins. This is the single most surprising thing about the pattern and it belongs in every doc row that describes one of these variables.

Why seed instead of override

The value is editable in the admin UI. If the env var kept overriding, every restart would silently undo what an operator changed on the page, and the UI would be lying about what is in force. Seeding gives a fresh install a way to be provisioned from compose without taking the setting away from the person running it afterwards.

Package dbsetting — string.go

The text-shaped member of the family: a mode name, a host:port, a path.

⚠⚠ A string setting needs a validation hook and an IntSpec does not, and that difference is the reason this type exists rather than a `Value string` field somewhere. A number is validated by its bounds, which the spec already declares. Text has no bounds — "clamav:3310" and "clamav 3310" are the same shape to the type system and only one of them can be dialled. Without a hook the second is accepted at save time and discovered at SCAN time, i.e. by the file that did not get scanned. Check is therefore where the meaning of the value lives, and Canonical is the only way this package writes one.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func FormatBool

func FormatBool(v bool) string

FormatBool is the single spelling this package ever WRITES, so a row can be compared as text and a human reading the settings table sees one form.

func OneOf

func OneOf(allowed ...string) func(string) error

OneOf builds a Check that accepts only the listed values, with an error message that lists them — the common case for a mode-style setting, and one worth having in one place so every such setting says no the same way.

func ParseBool

func ParseBool(raw string) (value bool, ok bool)

ParseBool reads the spellings an operator actually types into a compose file, in either case: 1/true/t/yes/y/on and 0/false/f/no/n/off. ok is false for anything else — including the empty string, which is "not set", not "false".

⚠ It is deliberately wider than strconv.ParseBool (which rejects yes/no/on/ off) and deliberately not wider still: a value like "enabled" or "disable" is refused rather than guessed, so a typo shows up as a log line naming the key instead of as a switch silently pointing the wrong way.

func SeedAll

func SeedAll(ctx context.Context, st Store, specs ...Seeder)

SeedAll runs Seed for each spec, logging failures rather than aborting boot. Adding a setting to the family is one more entry at the call site.

⚠ A failed seed is not fatal on purpose: the setting falls back to its Default and the server comes up. A container that refuses to start because it could not write a scan interval is a worse outcome than one that starts with the default and says so in the log.

Types

type BoolSpec

type BoolSpec struct {
	// Key is the settings-table key, dotted and namespaced by feature,
	// e.g. "antivirus.enabled".
	Key string
	// EnvVar seeds the first-boot value. Empty means no seeding.
	EnvVar string
	// Default applies when there is no row, no seed, or an unreadable value.
	Default bool
}

BoolSpec declares one boolean-valued setting: a switch an admin flips.

func (BoolSpec) Resolve

func (s BoolSpec) Resolve(ctx context.Context, g Getter) bool

Resolve reads the value in force. Call it at the point of USE, like every spec in this package.

A missing row, a store error, a blank value or unparseable text all resolve to Default: none of them carries operator intent.

func (BoolSpec) Seed

func (s BoolSpec) Seed(ctx context.Context, st Store) error

Seed writes the env var's value into the settings table when, and only when, no row exists yet. Call once at boot, before anything resolves the setting.

Unparseable text is ignored entirely — there is no honest value to store — and Default applies. Returns nil when there is nothing to do, which is the normal case on every boot after the first.

func (BoolSpec) SettingKey

func (s BoolSpec) SettingKey() string

SettingKey identifies the row this spec owns (Seeder).

func (BoolSpec) Validate

func (s BoolSpec) Validate(bool) error

Validate is the write-side gate, kept for symmetry with IntSpec.Validate so a handler treats every spec the same way. It cannot fail: by the time a caller holds a Go bool the only two values that exist are both legal. The gate that matters for a switch is the JSON decoder, which refuses "maybe" before this is ever reached.

type Getter

type Getter interface {
	GetSetting(ctx context.Context, key string) (string, error)
}

Getter is the settings-table read. db.Store satisfies it.

type IntSpec

type IntSpec struct {
	// Key is the settings-table key, dotted and namespaced by feature,
	// e.g. "antivirus.save_scan_window_minutes".
	Key string
	// EnvVar seeds the first-boot value. Empty means no seeding.
	EnvVar string
	// Default applies when there is no row and no seed.
	Default int
	// Min and Max are inclusive bounds. Values outside clamp on read and
	// are refused on write.
	Min, Max int
	// Unit names what the number counts, for log and error text
	// ("minutes", "days"). Purely cosmetic.
	Unit string
	// SeedParse converts the env var's text into this setting's unit. Nil
	// means "the env var is already in this unit" (plain integer parse).
	//
	// It exists for settings whose environment variable predates the move to
	// the database and speaks a different unit — FILEX_CLAMAV_MAX is bytes
	// while the setting an admin edits is megabytes. Converting at SEED time
	// rather than renaming the variable is what keeps existing deployments
	// from losing their configuration.
	SeedParse func(raw string) (int, bool)
}

IntSpec declares one integer-valued setting.

Zero is never a legal value: every setting shaped this way is a magnitude (minutes, days, counts) whose "off" state, if it has one, is a separate and explicit switch. Min is therefore expected to be ≥ 1 and callers get told so rather than discovering it through a clamp.

func (IntSpec) Clamp

func (s IntSpec) Clamp(v int) (int, bool)

Clamp returns the value that will be in force for v, and whether v had to be changed to get there.

func (IntSpec) Resolve

func (s IntSpec) Resolve(ctx context.Context, g Getter) int

Resolve reads the value in force.

⚠ Call it at the point of USE, not once at boot: reading per use is what makes these settings live-changeable, which is the reason they are in the database rather than in the environment. Callers that have already acted on an earlier value (a scheduled job carrying a deadline, say) keep it — a changed setting affects the next decision, never one already made.

A missing row, a store error, a blank value or unparseable text all resolve to Default: none of them carries operator intent. An out-of-range NUMBER does carry intent, so it clamps to the nearest bound and logs once, because the operator has to be able to find out from the logs that the number they typed is not the number in force.

func (IntSpec) Seed

func (s IntSpec) Seed(ctx context.Context, st Store) error

Seed writes the env var's value into the settings table when, and only when, no row exists yet. Call once at boot, before anything resolves the setting.

A seed value outside the bounds is clamped and logged rather than refused: the alternative is a container that will not start because of a scan interval, which is disproportionate. Unparseable text is ignored entirely — there is no honest number to store — and Default applies.

Returns nil when there is nothing to do, which is the normal case on every boot after the first.

func (IntSpec) SettingKey

func (s IntSpec) SettingKey() string

SettingKey identifies the row this spec owns (Seeder).

func (IntSpec) Validate

func (s IntSpec) Validate(v int) error

Validate is the write-side gate: an operator typing an impossible value must be told no at the moment they save it, in the UI, rather than having it silently clamped on some later read. Clamping on read stays as the last line of defence for rows that predate a bound or were written by hand.

type Seeder

type Seeder interface {
	// SettingKey is the row the spec owns, for log text.
	SettingKey() string
	// Seed writes the environment's value, first boot only.
	Seed(ctx context.Context, st Store) error
}

Seeder is the one behaviour every spec kind shares: consume an environment variable into the settings table on a boot where no row exists yet.

It exists so a single SeedAll call at boot can carry a MIXED list — an IntSpec, a BoolSpec and a StringSpec side by side. Splitting it into SeedAllInts / SeedAllBools would put the ordering of boot-time seeding in three places and make "did this setting get seeded?" a question with three answers.

type Store

type Store interface {
	Getter
	UpsertSetting(ctx context.Context, key, value string) error
}

Store is read + write, needed for seeding. db.Store satisfies it.

type StringSpec

type StringSpec struct {
	// Key is the settings-table key, dotted and namespaced by feature,
	// e.g. "antivirus.clamd_addr".
	Key string
	// EnvVar seeds the first-boot value. Empty means no seeding.
	EnvVar string
	// Default applies when there is no row, no seed, or an unusable value.
	// It must itself pass Check — a default that cannot be saved would make
	// an unreadable row unrecoverable without SQL.
	Default string
	// Normalize canonicalises raw text before it is checked, stored or
	// compared: lowercase a mode name, trim a host. Nil means "trim
	// surrounding whitespace", which every text setting wants and none wants
	// to restate.
	//
	// ⚠ It runs on the seed, on the write and on the read, so a row written
	// by hand in a different case still resolves to the same value.
	Normalize func(raw string) string
	// Check rejects a value that is the wrong shape. Nil accepts anything.
	// It is given the NORMALIZED text and must accept Default.
	//
	// ⚠ Return an error a person can act on — it is rendered in the admin
	// form, next to the field they just typed.
	Check func(v string) error
}

StringSpec declares one text-valued setting.

func (StringSpec) Canonical

func (s StringSpec) Canonical(raw string) (string, error)

Canonical normalises raw and validates it, returning the exact text to store. It is the ONLY shape this package writes: a handler that stores s.Canonical's output can never persist a value its own Resolve would then reject.

func (StringSpec) Resolve

func (s StringSpec) Resolve(ctx context.Context, g Getter) string

Resolve reads the value in force.

A missing row, a store error or a blank value resolve to Default. A stored value that fails Check ALSO resolves to Default — and logs, loudly, because that row is the one case here where what the table says and what is running differ. (It happens: a row written by hand, or one written before Check grew stricter.) This is the string analogue of IntSpec's clamp-on-read: the last line of defence, never the intended path, since Validate refuses the same value at the API.

func (StringSpec) ResolveStrict

func (s StringSpec) ResolveStrict(ctx context.Context, g Getter) (string, error)

ResolveStrict is Resolve with the reason attached: it returns the stored text and, when that text fails Check, the error explaining why it is not in force. Resolve is the same call with the error swallowed.

⚠⚠ It exists because falling back to Default silently is fine for a value that HAS a sensible default and useless for one that does not. An empty clamd address means "not configured"; a stored address that will not parse means "you typed something wrong" — and collapsing the second into the first makes the admin page say "no address is configured" while the field beside it visibly contains one. A caller that has somewhere to SHOW the reason should ask for it.

func (StringSpec) Seed

func (s StringSpec) Seed(ctx context.Context, st Store) error

Seed writes the env var's value into the settings table when, and only when, no row exists yet. Call once at boot, before anything resolves the setting.

⚠ A seed that fails Check is IGNORED, not clamped — unlike IntSpec, which clamps an out-of-range seed to the nearest bound. There is no nearest legal string, so the alternatives are storing nonsense or storing nothing, and storing nothing leaves Default in force with a log line naming the variable.

func (StringSpec) SeedValue

func (s StringSpec) SeedValue(ctx context.Context, st Store, raw string) error

SeedValue seeds an explicitly supplied value under the same first-boot-only rule, for a setting whose first value is DERIVED rather than typed — e.g. "an address was configured, so the mode is daemon". Keeping it here rather than letting callers Upsert directly is what stops a derived seed from overwriting a row an admin has since edited.

func (StringSpec) SettingKey

func (s StringSpec) SettingKey() string

SettingKey identifies the row this spec owns (Seeder).

func (StringSpec) Validate

func (s StringSpec) Validate(raw string) error

Validate is the write-side gate: an operator typing an unusable value must be told no at the moment they save it, in the UI, rather than having it discovered later by whatever tries to use it.

Jump to

Keyboard shortcuts

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