Add configuration and CLI foundation
This commit is contained in:
@@ -5,41 +5,239 @@ import (
|
||||
"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.
|
||||
-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 Override report output path or directory.
|
||||
`
|
||||
|
||||
// Run parses the root command and executes the selected behavior.
|
||||
type Runner struct {
|
||||
Clock timeutil.Clock
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, args []string, stdout io.Writer, stderr io.Writer) error {
|
||||
_ = ctx
|
||||
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
|
||||
|
||||
fs := flag.NewFlagSet("weatherreporter", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
|
||||
help := fs.Bool("help", false, "show help")
|
||||
fs.BoolVar(help, "h", false, "show help")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
if r.Clock == nil {
|
||||
r.Clock = timeutil.SystemClock{}
|
||||
}
|
||||
|
||||
if *help {
|
||||
if len(args) == 0 || args[0] == "--help" || args[0] == "-h" {
|
||||
_, err := fmt.Fprint(stdout, helpText)
|
||||
return err
|
||||
}
|
||||
|
||||
if fs.NArg() > 0 {
|
||||
return fmt.Errorf("unknown argument %q", fs.Arg(0))
|
||||
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])
|
||||
}
|
||||
|
||||
_, err := fmt.Fprint(stdout, helpText)
|
||||
return err
|
||||
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", "", "report output path or directory")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user