summaryrefslogtreecommitdiff
path: root/internal/engine/engine.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-12 01:22:12 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-12 01:22:12 +0200
commit3b36a48b7ce5a53a9366f3b31f94311f178e2553 (patch)
treeecbb277ff916b719f2ee45fba017792b85d5faf9 /internal/engine/engine.go
parent42b02c47be9b285099203e44a2570636d4ca6f03 (diff)
downloadkrino-3b36a48b7ce5a53a9366f3b31f94311f178e2553.tar.gz
krino-3b36a48b7ce5a53a9366f3b31f94311f178e2553.zip
krino: matching — scan, ignore, conditions, extraction, duplicates, explain, dry run
Diffstat (limited to 'internal/engine/engine.go')
-rw-r--r--internal/engine/engine.go170
1 files changed, 170 insertions, 0 deletions
diff --git a/internal/engine/engine.go b/internal/engine/engine.go
new file mode 100644
index 0000000..eec9a60
--- /dev/null
+++ b/internal/engine/engine.go
@@ -0,0 +1,170 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+// Package engine is what every krino front end calls: it loads and compiles
+// the configuration, matches files against rules, and (from plan 3) plans
+// and applies actions. It returns data; front ends only render it.
+package engine
+
+import (
+ "os"
+ "time"
+
+ "krino/internal/cond"
+ "krino/internal/config"
+ "krino/internal/extract"
+ "krino/internal/ignore"
+)
+
+// Engine holds a loaded, compiled configuration: everything a front end
+// needs to check, match and (from plan 3) act.
+type Engine struct {
+ Config *config.Config
+ Dirs []*Dir
+ Extract *extract.Extractor
+ Now func() time.Time // time.Now; tests replace it
+ MainFile string
+}
+
+// Dir is one configured directory, with its ignore matcher and rules
+// compiled.
+type Dir struct {
+ Name string
+ Root string // absolute
+ Conf *config.Dir
+ Settings config.Resolved // built-in, then defaults, then the directory
+ Ignore *ignore.Matcher
+ Rules []*Rule
+
+ // ContentVariants is the distinct (IgnoreCase, Fold) pairs any of
+ // Rules' content tests evaluate under, in first-seen order. B2: when
+ // this holds exactly one variant, facts.Content releases a file's raw
+ // extracted text once that variant's normalised copy exists, since no
+ // other variant will ever be asked for; with more than one, both must
+ // stay memoised, as before.
+ ContentVariants []cond.Options
+}
+
+// Rule is one directory's rule, with its condition compiled.
+type Rule struct {
+ Name string
+ Conf *config.Rule
+ Settings config.Resolved // the rule's own settings over its directory's
+ Cond *cond.Cond
+}
+
+// Load reads and compiles everything. Any problem anywhere returns a nil
+// Engine and every diagnostic: krino never acts on a configuration it only
+// partly understood. Duplicate names are ignored after their first use.
+func Load(mainFile string, names ...string) (*Engine, []*config.Diag) {
+ cfg, errs := config.Load(mainFile, dedupeNames(names)...)
+ if cfg == nil {
+ return nil, errs
+ }
+
+ var dirs []*Dir
+ for _, d := range cfg.Dirs {
+ dir := &Dir{
+ Name: d.Name,
+ Root: d.Path,
+ Conf: d,
+ Settings: cfg.Resolved(d),
+ }
+ if m, err := ignore.New(d.Ignore); err != nil {
+ errs = append(errs, &config.Diag{File: d.File, Msg: err.Error()})
+ } else {
+ dir.Ignore = m
+ }
+ for _, r := range d.Rules {
+ rs := r.Settings.Over(dir.Settings)
+ c, cerrs := cond.Compile(d.File, r.When, cond.Options{
+ IgnoreCase: rs.Case == config.CaseIgnore,
+ Fold: rs.Fold,
+ })
+ if len(cerrs) > 0 {
+ errs = append(errs, cerrs...)
+ continue
+ }
+ dir.Rules = append(dir.Rules, &Rule{Name: r.Name, Conf: r, Settings: rs, Cond: c})
+ }
+ dir.ContentVariants = contentVariants(dir.Rules)
+ dirs = append(dirs, dir)
+ }
+
+ if len(errs) > 0 {
+ return nil, errs
+ }
+ return &Engine{
+ Config: cfg,
+ Dirs: dirs,
+ Extract: extract.New(),
+ Now: time.Now,
+ MainFile: mainFile,
+ }, nil
+}
+
+// dedupeNames returns names with every repeat after its first occurrence
+// removed, order preserved.
+func dedupeNames(names []string) []string {
+ var out []string
+ seen := map[string]bool{}
+ for _, n := range names {
+ if seen[n] {
+ continue
+ }
+ seen[n] = true
+ out = append(out, n)
+ }
+ return out
+}
+
+// contentVariants returns the distinct (IgnoreCase, Fold) pairs any of
+// rules' content tests evaluate under, in first-seen order — B2's per-Dir
+// ContentVariants. A rule whose condition has no content test at all
+// (Cond.UsesContent false) never calls facts.Content, so its resolved
+// case/fold settings contribute no variant here.
+func contentVariants(rules []*Rule) []cond.Options {
+ var out []cond.Options
+ seen := map[cond.Options]bool{}
+ for _, r := range rules {
+ if !r.Cond.UsesContent {
+ continue
+ }
+ opt := cond.Options{IgnoreCase: r.Settings.Case == config.CaseIgnore, Fold: r.Settings.Fold}
+ if seen[opt] {
+ continue
+ }
+ seen[opt] = true
+ out = append(out, opt)
+ }
+ return out
+}
+
+// Report is what Check reports: the files involved and each directory's
+// state.
+type Report struct {
+ MainFile string
+ LogFile string
+ Dirs []DirReport
+ Tools []extract.Tool
+}
+
+// DirReport is one directory's state in a Report.
+type DirReport struct {
+ Dir *Dir
+ Missing bool // the root is not a directory right now
+}
+
+// Check reports the engine's configuration files, each directory's current
+// state and the external tools found for content extraction.
+func (e *Engine) Check() Report {
+ r := Report{
+ MainFile: e.MainFile,
+ LogFile: e.Config.LogFile(),
+ Tools: e.Extract.Tools(),
+ }
+ for _, d := range e.Dirs {
+ fi, err := os.Stat(d.Root)
+ r.Dirs = append(r.Dirs, DirReport{Dir: d, Missing: err != nil || !fi.IsDir()})
+ }
+ return r
+}