package archtest import ( "fmt" "os" "path/filepath" "runtime" "strings" "time" "gopkg.in/yaml.v3" ) const ( KindImport = "import" KindSQLWrite = "sql_write" KindPath = "path" KindEvent = "event" ) type Boundaries struct { Imports map[string][]string `yaml:"imports"` TableOwners map[string][]string `yaml:"table_owners"` PathOwners map[string][]string `yaml:"path_owners"` EventProducers map[string][]string `yaml:"event_producers"` EventConsumers map[string][]string `yaml:"event_consumers"` UnwrapOwners []string `yaml:"unwrap_owners"` Exceptions []Exception `yaml:"exceptions"` BaselineCounts map[string]int `yaml:"baseline_counts"` BlockKinds []string `yaml:"block_kinds"` } type Exception struct { Kind string `yaml:"kind"` Package string `yaml:"package"` File string `yaml:"file"` Symbol string `yaml:"symbol"` Owner string `yaml:"owner"` Reason string `yaml:"reason"` Milestone string `yaml:"milestone"` Expires string `yaml:"expires"` } type Finding struct { Kind string Package string File string Symbol string Detail string } func (f Finding) Key() string { return f.Kind + "|" + f.Package + "|" + filepath.ToSlash(f.File) + "|" + f.Symbol } func ModuleRoot() (string, error) { _, file, _, ok := runtime.Caller(0) if !ok { return "", fmt.Errorf("archtest: cannot locate caller") } dir := filepath.Dir(file) for { if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { return dir, nil } parent := filepath.Dir(dir) if parent == dir { return "", fmt.Errorf("archtest: go.mod not found") } dir = parent } } func LoadBoundaries(path string) (*Boundaries, error) { b, err := os.ReadFile(path) if err != nil { return nil, err } var out Boundaries if err := yaml.Unmarshal(b, &out); err != nil { return nil, err } if out.Imports == nil { out.Imports = map[string][]string{} } if out.TableOwners == nil { out.TableOwners = map[string][]string{} } if out.PathOwners == nil { out.PathOwners = map[string][]string{} } if out.EventProducers == nil { out.EventProducers = map[string][]string{} } if out.EventConsumers == nil { out.EventConsumers = map[string][]string{} } if out.UnwrapOwners == nil { out.UnwrapOwners = []string{} } if out.BaselineCounts == nil { out.BaselineCounts = map[string]int{} } return &out, nil } func DefaultBoundariesPath(root string) string { return filepath.Join(root, ".doc", "system-design", "module-boundaries.yaml") } func allowedHas(list []string, needle string) bool { for _, p := range list { if strings.TrimSpace(p) == needle { return true } } return false } func (e Exception) validate(now time.Time) error { if strings.TrimSpace(e.Owner) == "" { return fmt.Errorf("missing owner") } if strings.TrimSpace(e.Reason) == "" { return fmt.Errorf("missing reason") } if strings.TrimSpace(e.Milestone) == "" { return fmt.Errorf("missing milestone") } if strings.TrimSpace(e.Expires) == "" { return fmt.Errorf("missing expires") } day, err := time.ParseInLocation("2006-01-02", e.Expires, time.UTC) if err != nil { return fmt.Errorf("invalid expires %q", e.Expires) } // expires is inclusive through that calendar day (UTC). if !now.UTC().Before(day.Add(24 * time.Hour)) { return fmt.Errorf("expired on %s", e.Expires) } return nil } func (e Exception) matches(f Finding) bool { if e.Kind != f.Kind { return false } if e.Package != "" && e.Package != f.Package { return false } if e.File != "" && filepath.ToSlash(e.File) != filepath.ToSlash(f.File) { return false } if e.Symbol != "" && e.Symbol != f.Symbol { return false } return true }