-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathconfig.go
236 lines (208 loc) · 7.97 KB
/
config.go
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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
// Package config loads a tir configuration, either from a JSON file
// (e.g. $HOME/.tir.config), from command-line arguments if the config variables
// are bound with [viper.BindPFlag], or from environment variables (though the
// latter are not explicitly supported).
//
// Loading a [Config] generates a [tir.Interface] for interacting with the
// configured store and a [text.Editor] for getting user input.
//
// # Defaults
//
// In the absence of explicit configuration, [Load] yields a [Config] backed by
// a [store.File] store pointing at $HOME/.tir.json and an [edit.Tea] editor.
package config
import (
"errors"
"fmt"
"log"
"os"
"strings"
"github.com/lukasschwab/tiir/pkg/edit"
"github.com/lukasschwab/tiir/pkg/store"
"github.com/lukasschwab/tiir/pkg/text"
"github.com/lukasschwab/tiir/pkg/tir"
"github.com/spf13/viper"
)
// Values providable via a JSON config file. For example, this .tir.config file
// configures tir to use a file store rooted at /Users/me/.tir.json and the Tea
// editor:
//
// { "store": { "type": "file", "path": "/Users/me/.tir.json" }, "editor": "tea" }
//
// For comparison, this .tir.config file configures tir to talk to a server at
// tir.example.com:
//
// { "store": { "type": "http", "base_url": "https://tir.example.com", "api_secret": "YOUR_SECRET" } }
//
// And this .tir.config file configures tir to use a Turso-hosted database:
//
// { "store": { "type": "libsql", "connection_string": "libsql://[your-database].turso.io?authToken=[your-auth-token]" } }
//
// For info on where tir looks for a config, see [Load]. For info about
// how to provide configuration, see [viper].
//
// [viper]: https://github.com/spf13/viper
const (
// KeyStoreGroup is the top-level key for store configuration.
KeyStoreGroup = "store"
// KeyStoreType must match a StoreType.
KeyStoreType = KeyStoreGroup + ".type"
// KeyFileStoreLocation must be defined for file stores.
KeyFileStoreLocation = KeyStoreGroup + ".path"
// KeyHTTPStoreBaseURL must be defined for HTTP stores.
KeyHTTPStoreBaseURL = KeyStoreGroup + ".base_url"
// KeyHTTPStoreAPISecret defines an API secret to authorize requests to
// the tir server at base_url. This is an optional config variable, but a
// server that requires it will reject requests.
KeyHTTPStoreAPISecret = KeyStoreGroup + ".api_secret"
// KeyLibSQLStoreConnectionString must be defined for LibSQL stores. It
// specifies where/how to connect to LibSQL. For example:
//
// + A local SQLite file: "file://path/to/file.db"
// + A local sqld instance: "http://127.0.0.1:8080"
// + A Turso-hosted DB: "libsql://[your-database].turso.io?authToken=[your-auth-token]"
KeyLibSQLStoreConnectionString = KeyStoreGroup + ".connection_string"
// KeyEditor is the top-level key for CLI editor configuration.
KeyEditor = "editor"
)
type storeType string
// Possible values for the store.type config variable. Providing any other store
// type value will cause [Load] to fail.
const (
// StoreTypeFile selects the store.file store (default).
StoreTypeFile storeType = "file"
// StoreTypeMemory selects the store.memory store.
StoreTypeMemory storeType = "memory"
// StoreTypeMemory selects the store.http store.
StoreTypeHTTP storeType = "http"
// StoreTypeLibSQL selectes the store.libsql store (supports Turso).
StoreTypeLibSQL storeType = "libsql"
)
type editorType string
// Possible values for the editor config variable. Providing any other editor
// value will cause [Load] to fail.
const (
// EditorTypeVim selects the edit.Vim editor.
EditorTypeVim editorType = "vim"
// EditorTypeTea selects the edit.Tea editor (default).
EditorTypeTea editorType = "tea"
// EditorTypeHuh selects the edit.Huh editor.
EditorTypeHuh editorType = "huh"
)
// Enum-option to value lookups.
var (
storeFactories = map[storeType]func(*Config) (store.Interface, error){
StoreTypeFile: func(*Config) (store.Interface, error) {
filepath := viper.GetString(KeyFileStoreLocation)
if filepath == "" {
return nil, errors.New("must provide filepath for file store")
}
log.Printf("Using file store: %v", filepath)
return store.UseFile(filepath)
},
StoreTypeMemory: func(*Config) (store.Interface, error) {
log.Printf("Using memory store")
return store.UseMemory(), nil
},
StoreTypeHTTP: func(cfg *Config) (store.Interface, error) {
baseURL := viper.GetString(KeyHTTPStoreBaseURL)
if baseURL == "" {
return nil, errors.New("must provide base URL for HTTP store")
}
log.Printf("Using HTTP store: %v", baseURL)
apiSecret := cfg.GetAPISecret()
if apiSecret == "" {
log.Printf("No API secret provided; store may reject requests")
}
return store.UseHTTP(baseURL, apiSecret)
},
StoreTypeLibSQL: func(cfg *Config) (store.Interface, error) {
connectionString := viper.GetString(KeyLibSQLStoreConnectionString)
if connectionString == "" {
return nil, errors.New("must provide connection string for LibSQL store")
}
log.Printf("Using LibSQL store")
return store.UseLibSQL(connectionString)
},
}
editors = map[editorType]text.Editor{
EditorTypeVim: edit.Vim,
EditorTypeTea: edit.Tea,
EditorTypeHuh: edit.Huh,
}
)
// Load loads a tir configuration from user-provided configuration.
// Users can provide configuration via a JSON config file, via environment
// variables, or through command-line arguments with the appropriate viper
// bindings.
//
// The default [tir.Interface] is backed by a file at $HOME/.tir.json. The
// default [text.Editor] is [edit.Tea].
//
// The caller is responsible for calling [tir.Interface.Close] appropriately.
func Load() (*Config, error) {
viper.SetEnvPrefix("tir")
viper.SetConfigName(".tir.config")
viper.SetConfigType("json")
viper.AddConfigPath("/etc/tir/")
if home, err := os.UserHomeDir(); err == nil {
viper.AddConfigPath(home)
viper.SetDefault(KeyFileStoreLocation, home+"/.tir.json")
}
// Write enum-type results as strings to avoid silently borking
// viper.GetString's type indirection.
viper.SetDefault(KeyStoreType, string(StoreTypeFile))
viper.SetDefault(KeyEditor, string(EditorTypeTea))
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Config file not found; ignore error if desired
log.Printf("no config file")
} else {
return nil, fmt.Errorf("can't read config file: %w", err)
}
}
// The standard post-prefixing key for this value is unwieldy
// (e.g. TIR_STORE.API_SECRET), hence the replacement (e.g. TIR_API_SECRET).
viper.SetEnvKeyReplacer(strings.NewReplacer("STORE.", ""))
if err := viper.BindEnv(KeyStoreType); err != nil {
log.Printf("ignoring error binding %v: %v", KeyStoreType, err)
} else if err := viper.BindEnv(KeyHTTPStoreAPISecret); err != nil {
log.Printf("ignoring error binding %v: %v", KeyHTTPStoreAPISecret, err)
} else if err := viper.BindEnv(KeyLibSQLStoreConnectionString); err != nil {
log.Printf("ignoring error binding %v: %v", KeyLibSQLStoreConnectionString, err)
}
cfg := &Config{v: viper.GetViper()}
// Construct a service.
storeFactory, ok := storeFactories[cfg.getStoreType()]
if !ok {
return cfg, fmt.Errorf("invalid store type '%v'", cfg.getStoreType())
}
store, err := storeFactory(cfg)
if err != nil {
return cfg, fmt.Errorf("error generating store: %w", err)
}
cfg.App = tir.New(store)
// Construct a store.
if cfg.Editor, ok = editors[cfg.getEditorType()]; !ok {
return cfg, fmt.Errorf("invalid editor type '%v'", cfg.getEditorType())
}
return cfg, nil
}
// Config for a Service and Editor. Use [Load] to construct a Config that
// respects the [viper]-provided user configuration environment.
type Config struct {
v *viper.Viper
App tir.Interface
Editor text.Editor
}
func (cfg *Config) getStoreType() storeType {
return storeType(cfg.v.GetString(KeyStoreType))
}
func (cfg *Config) getEditorType() editorType {
return editorType(cfg.v.GetString(KeyEditor))
}
// GetAPISecret provided through [viper] or the env variable TIR_API_SECRET if
// it's present.
func (cfg *Config) GetAPISecret() string {
return cfg.v.GetString(KeyHTTPStoreAPISecret)
}