1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
|
package main
import (
_ "embed"
"encoding/json"
"flag"
"fmt"
"html/template"
"log/slog"
"net/http"
"os"
"os/signal"
"strconv"
"strings"
"time"
"github.com/peterbourgon/ff/v3"
"go.jolheiser.com/ffjsonnet"
)
var (
//go:embed index.html
_indexTmpl string
indexTmpl = template.Must(template.New("").Parse(_indexTmpl))
//go:embed budget.json
_json []byte
)
type Event struct {
ID string `json:"id"`
AllDay bool `json:"allDay"`
Start time.Time `json:"start"`
Title string `json:"title"`
ExtendedProps map[string]any `json:"extendedProps"`
}
type Args struct {
Port int
Database string
JSON bool
LogLevel slog.Level
}
func maine() error {
args := Args{
LogLevel: slog.LevelInfo,
}
fs := flag.NewFlagSet("mint", flag.ExitOnError)
fs.IntVar(&args.Port, "port", 8080, "Port to run on")
fs.IntVar(&args.Port, "p", args.Port, "--port")
fs.StringVar(&args.Database, "database", "mint.sqlite3", "Path to database file")
fs.StringVar(&args.Database, "d", args.Database, "--database")
fs.BoolVar(&args.JSON, "json", false, "JSON logging")
fs.BoolVar(&args.JSON, "j", args.JSON, "--json")
logFn := func(s string) error {
switch strings.ToLower(s) {
case "debug":
args.LogLevel = slog.LevelDebug
case "info":
args.LogLevel = slog.LevelInfo
case "warn", "warning":
args.LogLevel = slog.LevelWarn
case "error":
args.LogLevel = slog.LevelError
default:
return fmt.Errorf("unknown log level %q: valid settings are debug, info, warn, or error")
}
return nil
}
fs.Func("log-level", "Logging level [debug, info, warn, error]", logFn)
fs.Func("l", "--log-level", logFn)
fs.String("config", "mint.jsonnet", "Config file")
if err := ff.Parse(fs, os.Args[1:],
ff.WithConfigFileFlag("config"),
ff.WithAllowMissingConfigFile(true),
ff.WithConfigFileParser(ffjsonnet.Parser),
ff.WithEnvVarPrefix("MINT"),
); err != nil {
return fmt.Errorf("could not parse CLI arguments: %w", err)
}
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
if args.JSON {
logger = slog.New(slog.NewJSONHandler(os.Stderr, nil))
}
slog.SetDefault(logger)
slog.SetLogLoggerLevel(args.LogLevel)
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
indexTmpl.Execute(w, nil)
})
mux.HandleFunc("/data", func(w http.ResponseWriter, r *http.Request) {
var budget []struct {
Title string
Amount float64
Date time.Time
}
if err := json.Unmarshal(_json, &budget); err != nil {
slog.Error("could not unmarshal budget", slog.Any("err", err))
}
var events []Event
var id int
for _, b := range budget {
events = append(events, Event{
ID: strconv.Itoa(id),
AllDay: true,
Start: b.Date,
Title: fmt.Sprintf("%s ($%.2f)", b.Title, b.Amount),
ExtendedProps: map[string]any{
"title": b.Title,
"amount": int(b.Amount * 100),
"recurring": true,
},
})
id++
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(events); err != nil {
slog.Error("could not marshal budget data", slog.Any("err", err))
}
})
go func() {
addr := fmt.Sprintf(":%d", *portFlag)
slog.Debug(fmt.Sprintf("Listening at http://localhost%s", addr))
if err := http.ListenAndServe(addr, mux); err != nil {
panic(err)
}
}()
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Kill, os.Interrupt)
<-ch
return nil
}
func main() {
if err := maine(); err != nil {
slog.Error("error during runtime", slog.Any("err", err))
}
}
|