config.go
1package rtw
2
3import (
4 "fmt"
5 "io"
6
7 toml "github.com/pelletier/go-toml/v2"
8)
9
10type LogLevel string
11
12const (
13 DEBUG LogLevel = "DEBUG"
14 INFO LogLevel = "INFO"
15 ERROR LogLevel = "ERROR"
16)
17
18type Server struct {
19 Host string `toml:"host" comment:"host bind address (IP or hostname, no scheme)"`
20 Port int `toml:"port" comment:"listening port"`
21 LogLevel LogLevel `toml:"loglevel" comment:"one of DEBUG, INFO, ERROR"`
22 PublicURL string `toml:"public-url" comment:"public URL of server"`
23 DataDir string `toml:"data-dir" comment:"base directory for server state (empty = current working directory, relative paths resolved against working directory)"`
24}
25
26type YSweet struct {
27 URL string `toml:"url" comment:"URL of the upstream jamsocket/ysweet server to persist documents"`
28}
29
30type Routing struct {
31 URL string `toml:"url" comment:"URL of Graphhopper-compatible routing API (e.g., https://graphhopper.com/api/1)"`
32 Token string `toml:"token,commented" comment:"authorization token"`
33}
34
35type Geocoder struct {
36 URL string `toml:"url" comment:"URL of Waldo geocoding API"`
37}
38
39type Protomaps struct {
40 APIKey string `toml:"api-key,commented" comment:"Protomaps API key (see https://protomaps.com)"`
41}
42
43type User struct {
44 Email string `toml:"email,commented"`
45 Handle string `toml:"handle,commented"`
46 HashedPassword string `toml:"hashed-password,commented" comment:"run 'echo <password> | rtw hash' to quickly compute the hashed password"`
47}
48
49type Config struct {
50 Server Server `toml:"server"`
51 YSweet YSweet `toml:"ysweet"`
52 Routing Routing `toml:"routing" comment:"configuration for Graphhopper routing API (local or hosted)"`
53 Geocoder Geocoder `toml:"geocoder" comment:"configuration for a Waldo geocoder (see https://git.kilimanjaro.io/waldo)"`
54 Protomaps Protomaps `toml:"protomaps,commented" comment:"configuration for Protomaps map tiles"`
55 SuperUsers []User `toml:"superusers,commented" comment:"users added with superuser permissions at app startup"`
56}
57
58func (c *Config) Validate() error {
59 if c.Server.Host == "" {
60 return fmt.Errorf("config error: server.host is required")
61 }
62 if c.Server.Port == 0 {
63 return fmt.Errorf("config error: server.port is required")
64 }
65 if c.Server.LogLevel == "" {
66 return fmt.Errorf("config error: server.loglevel is required")
67 }
68 switch c.Server.LogLevel {
69 case DEBUG, INFO, ERROR:
70 default:
71 return fmt.Errorf("config error: server.loglevel must be DEBUG, INFO, or ERROR")
72 }
73 if c.Server.PublicURL == "" {
74 return fmt.Errorf("config error: server.public-url is required")
75 }
76 if c.Server.DataDir == "" {
77 return fmt.Errorf("config error: server.data-dir is required")
78 }
79 if c.YSweet.URL == "" {
80 return fmt.Errorf("config error: ysweet.url is required")
81 }
82 if c.Routing.URL == "" {
83 return fmt.Errorf("config error: routing.url is required")
84 }
85 if c.Geocoder.URL == "" {
86 return fmt.Errorf("config error: geocoder.url is required")
87 }
88 return nil
89}
90
91func ReadConfig(r io.Reader) (*Config, error) {
92 var cfg Config
93 b, err := io.ReadAll(r)
94 if err != nil {
95 return nil, fmt.Errorf("error reading config file: %w", err)
96 }
97 if err := toml.Unmarshal(b, &cfg); err != nil {
98 return nil, fmt.Errorf("config error: %w", err)
99 }
100 return &cfg, nil
101}
102
103func WriteInitConfig(w io.Writer) error {
104 example := Config{
105 Server: Server{
106 Host: "127.0.0.1",
107 Port: 8080,
108 LogLevel: "INFO",
109 PublicURL: "https://rtw.run",
110 DataDir: "/var/rtw",
111 },
112 YSweet: YSweet{
113 URL: "http://127.0.0.1:9000",
114 },
115 Routing: Routing{
116 URL: "http://127.0.0.1:8989",
117 Token: "<enter-token-if-required>",
118 },
119 Geocoder: Geocoder{
120 URL: "http://127.0.0.1:8999",
121 },
122 Protomaps: Protomaps{
123 APIKey: "<your-protomaps-api-key>",
124 },
125 SuperUsers: []User{
126 {
127 Email: "[email protected]",
128 Handle: "bt",
129 HashedPassword: "use rtw hash to get this value",
130 },
131 },
132 }
133 b, err := toml.Marshal(example)
134 if err != nil {
135 return fmt.Errorf("error marshaling example config: %w", err)
136 }
137 if _, err := w.Write(b); err != nil {
138 return fmt.Errorf("error writing example config: %w", err)
139 }
140 return nil
141}