refactor: split source files that exceeded the ~300-line ceiling
Mechanical moves only — operations, store, history_view, and settings_view are now split along their existing seams so every file stays within the 250+20% guideline. Document the new layout in ARCHITECTURE.md and close the ROADMAP item. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gitea.mixdep.ru/mix/gosentry/src/domain"
|
||||
)
|
||||
|
||||
// normalizeJob trims user-entered fields and applies the same defaults the job
|
||||
// dialog used, so callers do not have to.
|
||||
func normalizeJob(job *domain.Job) {
|
||||
job.Name = strings.TrimSpace(job.Name)
|
||||
job.Folder = strings.TrimSpace(job.Folder)
|
||||
job.Schedule = strings.TrimSpace(job.Schedule)
|
||||
job.Command = strings.TrimSpace(job.Command)
|
||||
job.Arguments = strings.TrimSpace(job.Arguments)
|
||||
}
|
||||
|
||||
// validateJob enforces the minimum executable definition: name, schedule, and
|
||||
// command must be present. Folder is optional. The schedule string itself is not
|
||||
// rejected for being unparseable — that surfaces later as an "Invalid schedule"
|
||||
// next-run, matching the prior behavior.
|
||||
func validateJob(job domain.Job) error {
|
||||
if job.Name == "" || job.Schedule == "" || job.Command == "" {
|
||||
return errors.New("name, schedule, and command are required")
|
||||
}
|
||||
policy := strings.TrimSpace(job.OverlapPolicy)
|
||||
if policy != "" && policy != string(domain.OverlapPolicySkip) && policy != string(domain.OverlapPolicyQueue) {
|
||||
return errors.New("overlap policy must be 'skip', 'queue', or empty")
|
||||
}
|
||||
if job.TimeoutSeconds != nil && *job.TimeoutSeconds < 0 {
|
||||
return errors.New("timeout must be zero (no timeout) or a positive number of seconds, or unset to inherit the global default")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasFileName reports whether a path ends in something that can be a file name.
|
||||
// It is a syntax check only — an existing directory whose name looks like a file
|
||||
// name still passes, and fails at write time — but it catches the shapes a user
|
||||
// types when they mean a folder: a trailing separator, "." and "..".
|
||||
func hasFileName(path string) bool {
|
||||
if strings.HasSuffix(path, "/") || strings.HasSuffix(path, string(filepath.Separator)) {
|
||||
return false
|
||||
}
|
||||
switch filepath.Base(path) {
|
||||
case ".", "..", string(filepath.Separator):
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// validateConfig rejects settings that would break persistence or cleanup.
|
||||
func validateConfig(config domain.Config) error {
|
||||
jobsFile := strings.TrimSpace(config.JobsFile)
|
||||
if jobsFile == "" {
|
||||
return errors.New("jobs file is required")
|
||||
}
|
||||
// A path that names only a folder would be written to as if it were a file
|
||||
// and fail later with an opaque OS error, so require a file name here.
|
||||
if !hasFileName(jobsFile) {
|
||||
return errors.New("jobs file must include a file name")
|
||||
}
|
||||
if strings.TrimSpace(config.LogsDir) == "" {
|
||||
return errors.New("logs directory is required")
|
||||
}
|
||||
// 0 means "keep everything" (see runner.CleanupLogs); only a negative count
|
||||
// is rejected, the same three-state shape as DefaultTimeoutSeconds below.
|
||||
if config.MaxLogFiles < 0 {
|
||||
return errors.New("max log files must be zero (unlimited) or a positive number")
|
||||
}
|
||||
if config.MaxLogAgeDays < 0 {
|
||||
return errors.New("max log age days must be zero (unlimited) or a positive number")
|
||||
}
|
||||
if config.ExecutionMode != domain.ExecutionModeParallel && config.ExecutionMode != domain.ExecutionModeSequential {
|
||||
return errors.New("execution mode must be 'parallel' or 'sequential'")
|
||||
}
|
||||
if config.OverlapPolicy != domain.OverlapPolicySkip && config.OverlapPolicy != domain.OverlapPolicyQueue {
|
||||
return errors.New("overlap policy must be 'skip' or 'queue'")
|
||||
}
|
||||
if config.DefaultTimeoutSeconds < 0 {
|
||||
return errors.New("default timeout must not be negative (0 means no timeout)")
|
||||
}
|
||||
// Empty Theme is accepted and normalized to the branded theme on load, so
|
||||
// older configs (and hand-built ones) stay valid without an explicit theme.
|
||||
if config.Theme != "" && config.Theme != domain.ThemeSystem && config.Theme != domain.ThemeGoSentry {
|
||||
return errors.New("theme must be 'system' or 'gosentry'")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user