package cli import ( "context" "flag" "fmt" "io" "gitea.maximumdirect.net/eric/weatherreporter/internal/app" "gitea.maximumdirect.net/eric/weatherreporter/internal/config" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" ) const helpText = `weatherreporter prepares weather reports from normalized forecast data. Usage: weatherreporter --help weatherreporter generate daily [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--date YYYY-MM-DD] weatherreporter generate tomorrow [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] weatherreporter generate three-day [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] weatherreporter generate weekend [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] weatherreporter generate storm [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] --start TIME --end TIME weatherreporter run morning [--config PATH] [--units VALUE] [--tz NAME] weatherreporter run evening [--config PATH] [--units VALUE] [--tz NAME] Options: -h, --help Show this help message. --config PATH Load configuration from PATH instead of /usr/local/etc/weatherreporter/config.yml. --units VALUE Override weather API units. --tz NAME Override weather API timezone. --out PATH Write an extra Markdown report copy for generate daily. ` type Runner struct { Clock timeutil.Clock } func Run(ctx context.Context, args []string, stdout io.Writer, stderr io.Writer) error { return Runner{Clock: timeutil.SystemClock{}}.Run(ctx, args, stdout, stderr) } func (r Runner) Run(ctx context.Context, args []string, stdout io.Writer, stderr io.Writer) error { _ = stderr if r.Clock == nil { r.Clock = timeutil.SystemClock{} } if len(args) == 0 || args[0] == "--help" || args[0] == "-h" { _, err := fmt.Fprint(stdout, helpText) return err } switch args[0] { case "generate": req, err := r.resolveGenerate(args[1:]) if err != nil { return err } return app.Generate(ctx, req) case "run": req, err := resolveRun(args[1:]) if err != nil { return err } return app.RunBatch(ctx, req) default: return fmt.Errorf("unknown command %q", args[0]) } } type commonOptions struct { ConfigPath string Units string Timezone string Output string } type generateOptions struct { commonOptions Date string Start string End string } func (r Runner) resolveGenerate(args []string) (app.GenerateRequest, error) { if len(args) == 0 { return app.GenerateRequest{}, fmt.Errorf("generate requires a report name") } report, ok := reportKind(args[0]) if !ok { return app.GenerateRequest{}, fmt.Errorf("unknown generate report %q", args[0]) } opts, err := parseGenerateFlags(report, args[1:]) if err != nil { return app.GenerateRequest{}, err } cfg, err := config.Load(config.LoadOptions{ Path: opts.ConfigPath, Units: opts.Units, Timezone: opts.Timezone, Output: opts.Output, }) if err != nil { return app.GenerateRequest{}, err } location, err := timeutil.LoadLocation(cfg.WeatherAPI.Timezone) if err != nil { return app.GenerateRequest{}, err } req := app.GenerateRequest{ Config: cfg, Report: report, OutputPath: opts.Output, } switch report { case app.ReportDaily: if opts.Date == "" { req.Date = timeutil.LocalDate(r.Clock.Now(), location) } else { req.Date, err = timeutil.ParseLocalDate(opts.Date, location) if err != nil { return app.GenerateRequest{}, err } } case app.ReportStorm: if opts.Start == "" { return app.GenerateRequest{}, fmt.Errorf("generate storm requires --start") } if opts.End == "" { return app.GenerateRequest{}, fmt.Errorf("generate storm requires --end") } req.StormStart, err = timeutil.ParseStormTime(opts.Start, location) if err != nil { return app.GenerateRequest{}, err } req.StormEnd, err = timeutil.ParseStormTime(opts.End, location) if err != nil { return app.GenerateRequest{}, err } if !req.StormEnd.After(req.StormStart) { return app.GenerateRequest{}, fmt.Errorf("generate storm requires --end after --start") } } return req, nil } func resolveRun(args []string) (app.BatchRequest, error) { if len(args) == 0 { return app.BatchRequest{}, fmt.Errorf("run requires a batch name") } batch, ok := batchKind(args[0]) if !ok { return app.BatchRequest{}, fmt.Errorf("unknown run batch %q", args[0]) } opts, err := parseRunFlags(args[1:]) if err != nil { return app.BatchRequest{}, err } cfg, err := config.Load(config.LoadOptions{ Path: opts.ConfigPath, Units: opts.Units, Timezone: opts.Timezone, }) if err != nil { return app.BatchRequest{}, err } return app.BatchRequest{Config: cfg, Batch: batch}, nil } func parseGenerateFlags(report app.ReportKind, args []string) (generateOptions, error) { fs := flag.NewFlagSet("generate "+string(report), flag.ContinueOnError) fs.SetOutput(io.Discard) opts := generateOptions{} addCommonFlags(fs, &opts.commonOptions, true) if report == app.ReportDaily { fs.StringVar(&opts.Date, "date", "", "report date in YYYY-MM-DD") } if report == app.ReportStorm { fs.StringVar(&opts.Start, "start", "", "storm start time") fs.StringVar(&opts.End, "end", "", "storm end time") } if err := fs.Parse(args); err != nil { return generateOptions{}, err } if fs.NArg() > 0 { return generateOptions{}, fmt.Errorf("unexpected argument %q", fs.Arg(0)) } return opts, nil } func parseRunFlags(args []string) (commonOptions, error) { fs := flag.NewFlagSet("run", flag.ContinueOnError) fs.SetOutput(io.Discard) opts := commonOptions{} addCommonFlags(fs, &opts, false) if err := fs.Parse(args); err != nil { return commonOptions{}, err } if fs.NArg() > 0 { return commonOptions{}, fmt.Errorf("unexpected argument %q", fs.Arg(0)) } return opts, nil } func addCommonFlags(fs *flag.FlagSet, opts *commonOptions, includeOutput bool) { fs.StringVar(&opts.ConfigPath, "config", "", "configuration file path") fs.StringVar(&opts.Units, "units", "", "weather API units") fs.StringVar(&opts.Timezone, "tz", "", "weather API timezone") if includeOutput { fs.StringVar(&opts.Output, "out", "", "extra Markdown report copy path") } } func reportKind(value string) (app.ReportKind, bool) { switch value { case string(app.ReportDaily): return app.ReportDaily, true case string(app.ReportTomorrow): return app.ReportTomorrow, true case string(app.ReportThreeDay): return app.ReportThreeDay, true case string(app.ReportWeekend): return app.ReportWeekend, true case string(app.ReportStorm): return app.ReportStorm, true default: return "", false } } func batchKind(value string) (app.BatchKind, bool) { switch value { case string(app.BatchMorning): return app.BatchMorning, true case string(app.BatchEvening): return app.BatchEvening, true default: return "", false } }