domain

package
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Sep 21, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrRuleNotFound indicates a rule with the given ID was not found.
	ErrRuleNotFound = errors.New("validation rule not found")

	// ErrInvalidRule indicates a rule has invalid configuration.
	ErrInvalidRule = errors.New("invalid validation rule")

	// ErrValidatorNotFound indicates a validator with the given type was not registered.
	ErrValidatorNotFound = errors.New("validator not found")

	// ErrRecordNotFound indicates the record to validate was not found.
	ErrRecordNotFound = errors.New("record not found")

	// ErrTableNotFound indicates the table does not exist.
	ErrTableNotFound = errors.New("table not found")

	// ErrFieldNotFound indicates the field does not exist in the record.
	ErrFieldNotFound = errors.New("field not found")

	// ErrAutoFixFailed indicates automatic fix attempt failed.
	ErrAutoFixFailed = errors.New("auto-fix failed")

	// ErrHardValidationFailed indicates a hard validation failed (should block save).
	ErrHardValidationFailed = errors.New("hard validation failed")

	// ErrInvalidParameters indicates rule parameters are invalid for the validator type.
	ErrInvalidParameters = errors.New("invalid validation parameters")

	// ErrSchemaMapperNotSet indicates no schema mapper was configured.
	ErrSchemaMapperNotSet = errors.New("schema mapper not set")

	// ErrRuleLoaderNotSet indicates no rule loader was configured.
	ErrRuleLoaderNotSet = errors.New("rule loader not set")
)

Functions

func Compare

func Compare(op Operator, actual, expected interface{}) (bool, error)

Compare applies op to actual, comparing against expected where applicable. Returns true when the condition holds, false when it doesn't. Unknown operators or type-incompatible arguments return an error rather than silently mis-matching.

Type handling is deliberately loose to match how rule parameters arrive: JSON numbers become float64, strings stay strings, arrays become []interface{}. Callers don't pre-normalize.

Types

type AutoFixConfig

type AutoFixConfig struct {
	Strategy string                 `json:"strategy"` // copy_from_coordinate, set_default, capitalize, etc.
	Params   map[string]interface{} `json:"params"`
}

AutoFixConfig defines how to automatically fix a validation error.

func (*AutoFixConfig) IsEnabled

func (a *AutoFixConfig) IsEnabled() bool

IsEnabled returns true if auto-fix is configured.

type Condition

type Condition struct {
	Field    string      `json:"field"`
	Operator string      `json:"operator"` // equals, not_equals, in, not_in, exists, not_exists, greater_than, less_than, contains, matches
	Value    interface{} `json:"value"`
	Relation string      `json:"relation,omitempty"` // optional named relation; empty = check the record's own field
}

Condition gates a rule's applicability. A rule with N conditions applies only when all N are satisfied. Empty condition list means the rule always applies (to every record of its target table).

A condition normally checks a field on the record being validated. When Relation is set, the condition instead checks a field on the record reached via that named relation from the bundle's Relations catalog. This lets a rule scoped on one table gate on fields belonging to a related table (e.g. a taxon-scoped rule that only fires when the owning name has a specific code).

func (*Condition) Eval

func (c *Condition) Eval(vctx *ValidationContext) (bool, error)

Eval evaluates the condition against the current ValidationContext. When c.Relation is empty, reads the field from vctx.Record. Otherwise it resolves the relation via vctx.RelationResolver and reads the field from the related record. Missing resolver on a relation-scoped condition is an error; missing related row is treated as field-not-present.

c.Value may be a literal (string, number, list, …) or a same-record self-reference in the shape {"field": "other_col"}. Self-references always resolve against vctx.Record — even when c.Relation is set — so rules can compare a related row's field against a field on the current record (or against a fixed value).

type ConditionResolver

type ConditionResolver interface {
	LookupRelated(ctx context.Context, db *sql.DB, sourceTable, sourceID, relation string) (map[string]interface{}, error)
}

ConditionResolver looks up a related record by name. Supplied by the consumer (typically wrapping joins.RelationResolver) so condition.go stays free of any cross-table SQL specifics.

type Enforcement

type Enforcement string

Enforcement expresses whether a rule violation blocks a write (hard) or is recorded as a warning that lets the write proceed (soft). It is a property of the rule, not the individual result.

const (
	EnforcementHard Enforcement = "hard"
	EnforcementSoft Enforcement = "soft"
)

func ParseEnforcement

func ParseEnforcement(s string) Enforcement

ParseEnforcement normalises a string to an Enforcement value. Empty input or unrecognised values fall back to soft, so a rule missing the field is treated as non-blocking rather than silently blocking writes.

type Field

type Field struct {
	ID                    string    `json:"id,omitempty"`
	Name                  string    `json:"name"`
	DisplayName           string    `json:"display_name,omitempty"`
	Type                  FieldType `json:"field_type"`
	IsRequired            bool      `json:"is_required,omitempty"`
	IsUnique              bool      `json:"is_unique,omitempty"`
	IsSystem              bool      `json:"is_system,omitempty"`
	DefaultValue          string    `json:"default_value,omitempty"`
	ReferenceTableID      string    `json:"reference_table_id,omitempty"`
	ReferenceDisplayField string    `json:"reference_display_field,omitempty"`
	Description           string    `json:"description,omitempty"`
	SortOrder             int       `json:"sort_order,omitempty"`
}

Field is a column definition in a Table. Enough shape for gsvalidator to introspect a schema and reason about types / references; consumer-app UI hints (display styles, calculated field engines, etc.) are ignored by the bundle loader since they aren't in the JSON shape gsvalidator cares about.

type FieldType

type FieldType string

FieldType classifies a Field's storage / semantics. Values match the shipping bundle format; type names are intentionally SQLite storage-class-aligned (text / integer / real / blob) plus higher-level flavors (number/date/reference) that consumers may treat specially.

const (
	FieldTypeText      FieldType = "text"
	FieldTypeNumber    FieldType = "number"
	FieldTypeInteger   FieldType = "integer"
	FieldTypeFloat     FieldType = "float"
	FieldTypeBoolean   FieldType = "boolean"
	FieldTypeDate      FieldType = "date"
	FieldTypeDateTime  FieldType = "datetime"
	FieldTypeReference FieldType = "reference"
	FieldTypeJSON      FieldType = "json"
)

type JoinFilter

type JoinFilter struct {
	Column string      `json:"column"`
	Equals interface{} `json:"equals"`
}

JoinFilter constrains a JoinStep to rows whose Column equals the given constant. Column names a field on the table that the step just joined in (identified by its alias if set, otherwise by its unaliased table name).

type JoinStep

type JoinStep struct {
	From  string       `json:"from"`
	To    string       `json:"to"`
	Alias string       `json:"alias,omitempty"`
	Where []JoinFilter `json:"where,omitempty"`
}

JoinStep is one edge of a relation's join chain. From/To are "<table_or_alias>.<column>" strings; Alias renames the joined table for later steps in the chain. Consumers walking a multi-step join thread aliases through the SQL they build.

Where narrows the joined rows to those satisfying additional equality predicates on the just-joined table (or its alias). Each JoinFilter contributes one `AND <alias>.<column> = ?` clause to the ON clause of the step, with the constant passed as a bound parameter. Only equality against a constant is supported today — the constrained shape keeps rule-supplied joins safe by construction.

type Operator

type Operator string

Operator names the shared comparison vocabulary that validators share for value checks. Rule parameters carry an operator string (e.g. "equals", "ends_with"); the Compare helper below dispatches on it. Keeping the list centralized avoids per-validator drift where two mechanisms both accept "matches" but interpret it differently.

const (
	OpEquals      Operator = "equals"
	OpNotEquals   Operator = "not_equals"
	OpGreaterThan Operator = "greater_than"
	OpLessThan    Operator = "less_than"
	OpInSet       Operator = "in_set"
	OpNotInSet    Operator = "not_in_set"
	OpStartsWith  Operator = "starts_with"
	OpEndsWith    Operator = "ends_with"
	OpContains    Operator = "contains"
	OpMatches     Operator = "matches"    // regex
	OpIsEmpty     Operator = "is_empty"   // unary; expected ignored
	OpIsPresent   Operator = "is_present" // unary; expected ignored
)

type PackageTable

type PackageTable struct {
	Table       Table                    `json:"table"`
	InitialData []map[string]interface{} `json:"initial_data,omitempty"`
}

PackageTable wraps a Table definition with optional initial-data rows (used to seed lookup tables at package install time).

type Relation

type Relation struct {
	TargetTable string     `json:"target_table"`
	TargetAlias string     `json:"target_alias,omitempty"`
	Cardinality string     `json:"cardinality,omitempty"` // "one" (default) or "many"
	Join        []JoinStep `json:"join"`
	Description string     `json:"description,omitempty"`
}

Relation is a named cross-table join declared in a SchemaPackage's Relations block. Rules reference relations by name (via validator parameters like `related_field_equals`'s `relation` key); the consumer's SchemaMapper resolves the join against the live database using the declared join chain.

Constrained shape on purpose: each Join step is a bounded {from, to, alias?} tuple, no arbitrary WHERE clauses, no subqueries, no functions. This keeps rule-author-supplied relations bounded to safe compositions that a SQL translator can verify against a schema.

Cardinality signals whether a rule using this relation should expect exactly one target row (`one`) or many (`many`). A `related_field_equals` mechanism needs `one`; a `duplicate_detection` mechanism might use `many`.

TargetAlias is only needed when the target and source tables share a name (a self-join whose final step aliases the joined-in copy). When set, the SELECT projects the aliased row instead of the source. Blank in the common case where target and source are distinct tables.

type Requires

type Requires struct {
	Tables    []string          `json:"tables,omitempty"`
	Columns   map[string]string `json:"columns,omitempty"`   // "table.column" -> "text|integer|real|blob|numeric"
	Relations []string          `json:"relations,omitempty"` // named relations that must resolve
}

Requires declares the schema features a bundle needs at load time — tables, columns (with expected type), and named relations. Consumers verify each item against the live database before activating the bundle's rules; missing items are per-item errors. Additive-compatible on the archive side: extra tables, columns, or relations don't fail the check.

Type checks on columns are loose (matched against SQLite storage classes). This avoids false-fails on unrelated schema tweaks (nullability, default value, CHECK constraints) that don't affect whether the ruleset works.

func (*Requires) IsEmpty

func (r *Requires) IsEmpty() bool

IsEmpty reports whether the Requires block declares no requirements. Empty is valid — a schema-defining package that depends on nothing existing beforehand has no Requires block (or an explicitly empty one).

type Result

type Result struct {
	RuleID           string      `json:"rule_id"`
	RuleName         string      `json:"rule_name"`
	RecordID         string      `json:"record_id"`
	TableName        string      `json:"table_name"`
	FieldName        string      `json:"field_name"` // Required to support multiple issues per field
	ValidatorType    string      `json:"validator_type"`
	Enforcement      Enforcement `json:"enforcement,omitempty"`     // hard = blocked write; soft = advisory
	Severity         Severity    `json:"severity,omitempty"`        // error/warn/info/debug for display
	ValidationType   string      `json:"validation_type,omitempty"` // legacy mirror of Severity for older readers
	Passed           bool        `json:"passed"`                    // False for failures, true for passes
	Message          string      `json:"message"`
	ActualValue      interface{} `json:"actual_value,omitempty"`
	ExpectedValue    interface{} `json:"expected_value,omitempty"`
	AutoFixAvailable bool        `json:"auto_fix_available"`
	AutoFixStrategy  string      `json:"auto_fix_strategy,omitempty"`
	Timestamp        time.Time   `json:"timestamp"`
}

Result represents the outcome of a single validation rule execution. Multiple results can exist for the same field (multiple validation issues).

func NewResult

func NewResult(ctx *ValidationContext, rule *Rule) *Result

NewResult builds a Result pre-populated with the fields every validator stamps identically: identifiers copied from the rule and context, plus the enforcement/severity/legacy validation_type triple derived from the rule. Individual validators still set Passed, Message, ActualValue, etc. on the returned pointer.

func (*Result) IsDebug

func (r *Result) IsDebug() bool

IsDebug returns true for a debug-severity result.

func (*Result) IsError

func (r *Result) IsError() bool

IsError returns true for a failed hard-enforcement rule.

func (*Result) IsFailure

func (r *Result) IsFailure() bool

IsFailure returns true if validation failed (regardless of hard/soft).

func (*Result) IsHardFailure

func (r *Result) IsHardFailure() bool

IsHardFailure returns true when the result comes from a hard-enforcement rule that failed and should block the write.

func (*Result) IsInfo

func (r *Result) IsInfo() bool

IsInfo returns true for an info-severity result.

func (*Result) IsWarning

func (r *Result) IsWarning() bool

IsWarning returns true for a failed rule that carries warn severity.

type Rule

type Rule struct {
	ID             string                 `json:"rule_id"`
	Name           string                 `json:"rule_name"`
	Description    string                 `json:"description"`
	TableName      string                 `json:"table_name"`
	FieldName      string                 `json:"field_name,omitempty"`
	ValidatorType  string                 `json:"validator_type"`
	Enforcement    Enforcement            `json:"enforcement,omitempty"`
	Severity       Severity               `json:"severity,omitempty"`
	Trigger        Trigger                `json:"trigger,omitempty"`         // default: on_write
	RecheckDays    int                    `json:"recheck_days,omitempty"`    // for trigger=time_based
	ValidationType string                 `json:"validation_type,omitempty"` // legacy: "hard"/"soft" or "error"/"warn"/"info"/"debug"
	Conditions     []Condition            `json:"conditions,omitempty"`
	Parameters     map[string]interface{} `json:"parameters"`
	ErrorMessage   string                 `json:"error_message"`
	WarningMessage string                 `json:"warning_message,omitempty"`
	AutoFix        *AutoFixConfig         `json:"auto_fix,omitempty"`
	IsActive       bool                   `json:"is_active"`
	Priority       int                    `json:"priority"` // Execution order (lower = higher priority)
	CreatedAt      time.Time              `json:"created_at,omitempty"`
	UpdatedAt      time.Time              `json:"updated_at,omitempty"`
}

Rule defines a validation rule with conditions and parameters. This is a core domain entity with no external dependencies.

func (*Rule) EffectiveEnforcement

func (r *Rule) EffectiveEnforcement() Enforcement

EffectiveEnforcement returns the rule's enforcement, falling back to legacy ValidationType parsing for rules loaded from older sources. "hard" and "error" both mean blocking; anything else is treated as soft.

func (*Rule) EffectiveSeverity

func (r *Rule) EffectiveSeverity() Severity

EffectiveSeverity returns the severity a failing result of this rule should carry, falling back to legacy ValidationType parsing and finally to the default severity for the effective enforcement.

func (*Rule) EffectiveTrigger

func (r *Rule) EffectiveTrigger() Trigger

EffectiveTrigger returns TriggerOnWrite when the rule doesn't set one, matching the pre-Trigger behavior for existing rules.

func (*Rule) EvalConditions

func (r *Rule) EvalConditions(vctx *ValidationContext) (bool, error)

EvalConditions checks whether the rule applies to the record named by vctx. Conditions may reference fields on the record itself or, when Relation is set, on a record reached via the named relation from the bundle's Relations catalog. Returns (false, nil) when the rule is inactive or any condition is unmet; (true, nil) when all conditions pass (or there are none). Returns an error when a relation-scoped condition can't be evaluated (missing resolver, SQL failure, etc.).

func (*Rule) Message

func (r *Rule) Message() string

Message returns the appropriate message based on severity: warning-level results prefer WarningMessage when available; other severities use ErrorMessage.

type SchemaPackage

type SchemaPackage struct {
	Name        string    `json:"name"`
	Version     string    `json:"version"`
	Domain      string    `json:"domain,omitempty"`
	Description string    `json:"description,omitempty"`
	Author      string    `json:"author,omitempty"`
	License     string    `json:"license,omitempty"`
	CreatedAt   time.Time `json:"created_at,omitempty"`

	// SchemaFingerprint carries a SHA256 hash of the normalized
	// schema for content-addressable discovery of matching
	// packages (e.g. re-import of a plain SQLite that lost its
	// metadata). Not used for load-time compatibility gating —
	// Requires does that instead. See CompatSchema in this file.
	SchemaFingerprint string `json:"schema_fingerprint,omitempty"`

	// Tables is populated on schema-defining packages. Consumers
	// apply the DDL to create tables + insert InitialData rows.
	// Ruleset-only packages leave this empty.
	Tables []PackageTable `json:"tables,omitempty"`

	// Relations is the named-join catalog rules may reference.
	// Constrained shape: each relation is a bounded join chain
	// with no arbitrary SQL.
	Relations map[string]Relation `json:"relations,omitempty"`

	// Rules are the validation rules the bundle contributes. May
	// be empty on a schema-only package.
	Rules []*Rule `json:"rules,omitempty"`

	// Requires declares the schema features (tables, columns,
	// relations) this bundle needs at load time. Consumers walk
	// the live database and verify each item resolves; missing
	// items are per-item load-time errors. Additive on the archive
	// side — extra tables/columns/relations are fine.
	Requires *Requires `json:"requires,omitempty"`
}

SchemaPackage is a portable JSON bundle carrying (optionally) a schema definition, a named-relation catalog, and one or more validation rulesets.

Consumers load bundles at startup: a schema-defining package creates tables (via a consumer-provided DDL applier), a ruleset-only package layers rules on top of an already-present schema. The Requires block declares what the bundle needs at load time; consumers verify against the live database before activating rules.

Fields unknown to the loader are silently ignored, so a bundle can carry application-specific metadata that gsvalidator doesn't reason about.

func (*SchemaPackage) GetTableByName

func (sp *SchemaPackage) GetTableByName(name string) *PackageTable

GetTableByName finds a table by name within the package. Returns nil when no table matches.

func (*SchemaPackage) Validate

func (sp *SchemaPackage) Validate() error

Validate checks that the package has the minimum fields to be usable: name and version. Deeper checks (rule integrity, relation resolvability, requires satisfaction) happen at load time against a live database.

type Severity

type Severity string

Severity classifies a violation for display and filtering. It is a property of the emitted result. A hard-enforcement rule always emits SeverityError when it fails; a soft-enforcement rule may emit warn, info, or debug.

const (
	SeverityError Severity = "error"
	SeverityWarn  Severity = "warn"
	SeverityInfo  Severity = "info"
	SeverityDebug Severity = "debug"
)

func DefaultSeverityFor

func DefaultSeverityFor(e Enforcement) Severity

DefaultSeverityFor returns the severity a violation of the given enforcement produces when the rule does not specify one explicitly.

func ParseSeverity

func ParseSeverity(s string) Severity

ParseSeverity normalises a string to a Severity value. Empty or unknown input falls back to warn.

type SystemContext

type SystemContext struct {
	CurrentDate time.Time
	CurrentYear int
}

SystemContext provides system-level context for validation.

func NewSystemContext

func NewSystemContext() SystemContext

NewSystemContext creates a new system context with current date/time.

type Table

type Table struct {
	ID          string  `json:"id,omitempty"`
	Name        string  `json:"name"`
	DisplayName string  `json:"display_name,omitempty"`
	Description string  `json:"description,omitempty"`
	IsSystem    bool    `json:"is_system,omitempty"`
	IsLookup    bool    `json:"is_lookup,omitempty"`
	Fields      []Field `json:"fields"`
}

Table is a table definition in a SchemaPackage. Enough shape for gsvalidator to reason about a schema (name, columns, primary key) without carrying the app-specific metadata (timestamps, UI display hints, standards-body tags) that consumer applications track internally. JSON tags match the shipping bundle format; unknown fields on incoming JSON are ignored.

func (*Table) GetField

func (t *Table) GetField(name string) *Field

GetField finds a field by name. Returns nil when no field matches.

type Trigger

type Trigger string

Trigger describes when a rule should be evaluated. Individual consumer applications decide how to honor each trigger — gsvalidator itself does not schedule re-evaluations; it just publishes the intent so callers can wire it in.

const (
	// TriggerOnWrite (default) — evaluate immediately after any write
	// that touches a matching record. Same behavior existing consumers
	// already implemented; keeping it unnamed by leaving the field
	// empty is also treated as OnWrite for back-compat.
	TriggerOnWrite Trigger = "on_write"
	// TriggerTimeBased — re-evaluate periodically regardless of writes.
	// Suits rules whose truth changes with the passage of time (stale
	// data, freshness reminders, expiring references). RecheckDays on
	// the rule controls the cadence; a value of 0 falls back to a
	// consumer-defined default.
	TriggerTimeBased Trigger = "time_based"
)

type ValidationContext

type ValidationContext struct {
	Ctx            context.Context
	DB             *sql.DB
	Record         map[string]interface{}
	TableName      string
	RecordID       string
	SystemContext  SystemContext
	RelatedRecords map[string][]map[string]interface{} // Cache of related records
	CodeContext    map[string]interface{}              // Free-form per-record context supplied by the consumer

	// RelationResolver resolves named relations from the bundle's
	// Relations catalog. Populated by the caller (ValidateRecordUseCase
	// sets it from an application-supplied resolver). Nil is
	// acceptable for records / rules that don't use relations;
	// relation-scoped conditions or validators error cleanly when
	// this is nil.
	RelationResolver ConditionResolver
}

ValidationContext provides access to the database and related records during validation. This is passed to validators to give them necessary context for complex validations.

func (*ValidationContext) GetFieldString

func (v *ValidationContext) GetFieldString(fieldName string) (string, bool)

GetFieldString safely retrieves a field value as string from the record.

func (*ValidationContext) GetFieldValue

func (v *ValidationContext) GetFieldValue(fieldName string) (interface{}, bool)

GetFieldValue safely retrieves a field value from the record.

func (*ValidationContext) GetRelatedRecords

func (v *ValidationContext) GetRelatedRecords(foreignKey string) ([]map[string]interface{}, bool)

GetRelatedRecords retrieves cached related records for a given foreign key.

func (*ValidationContext) SetRelatedRecords

func (v *ValidationContext) SetRelatedRecords(foreignKey string, records []map[string]interface{})

SetRelatedRecords caches related records for a given foreign key.

Jump to

Keyboard shortcuts

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