-
Notifications
You must be signed in to change notification settings - Fork 0
/
context.go
179 lines (169 loc) · 6.4 KB
/
context.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
// Ari is an environment for Array Relational Interactive programming.
//
// Ari's code base consists of two parts:
//
// - Go library at `github.com/semperos/ari`
// - Go CLI application at `github.com/semperos/ari/cmd/ari`
//
// ## Library ari
//
// The ari library at `github.com/semperos/ari` extends the Goal programming language
// with new functions for:
//
// - Unit testing in Goal
// - Date & time handling (via `time`)
// - HTTP client (via `github.com/go-resty/resty/v2`) and server (via `net/http`)
// - SQL client (via `database/sql`)
//
// Goal functions defined in Go must return a valid Goal value. The ari types that
// satisfy `goal.BV` represent such values in this code base. Their Goal string
// representation in Goal is not meant to be readable by the language, but instead are
// designed to expose as much information as possible to avoid the need to create other
// means by which to interrogate that state.
//
// Ari attempts to keep the names of Go functions and fields intact across the
// board, including from Goal code. For example, the options for configuring
// an HTTP client in Goal have names like `Body` and `QueryParam` to match what the
// underlying go-resty code expects. Even though this results in non-idiomatic
// names in Goal, it has been done to reduce overall cognitive load.
//
// ## CLI ari
//
// The CLI application at `github.com/semperos/ari/cmd/ari` provides a rich REPL with
// multi-line editing, history, and auto-complete functionality. This interface is ideal when
// first learning Goal, using Goal as an ad hoc calculator, or when building a REPL-
// based user experience.
//
// To drive ari's Goal REPL from an editor, you should prefer the `--raw` REPL,
// which does not have line editing, history, or auto-complete features, but which has
// better performance. You can use the Goal `ac` function with a string argument for glob matching
// or a regular expression argument for loose regex matching of all bindings in your Goal
// environment, optionally passing those results to `help` to view their help strings.
//
// Concretely, the CLI app adds the following on top of the base ari library:
//
// - A rich REPL using `github.com/knz/bubbline` (built on `github.com/charmbracelet/bubbletea`)
// with dedicated Goal and SQL modes, system commands starting with `)`, multiple output
// formats, basic profiling and debugging capabilities, and ability to programmatically
// change the REPL prompt.
// - A `help` Goal verb that allows (re)defining help strings for globals/keywords in Goal
// - A dependency on `github.com/marcboeker/go-duckdb` to use DuckDB as the SQL database
// - TUI functions for basic terminal styling
// - Common configuration options can be saved to a `$HOME/.config/ari/ari-config.yaml` file
// for reuse, which are overridden when CLI arguments are provided.
package ari
import (
"os"
"codeberg.org/anaseto/goal"
"github.com/semperos/ari/vendored/help"
)
type Help struct {
Dictionary map[string]map[string]string
Func func(string) string
}
type Context struct {
// GoalContext is needed to evaluate Goal programs and introspect the Goal execution environment.
GoalContext *goal.Context
// HTTPClient exposed for testing purposes.
HTTPClient *HTTPClient
// SQLDatabase keeps track of open database connections as well as the data source name.
SQLDatabase *SQLDatabase
// Help stores documentation information for identifiers.
// The top-level keys must match a modes Name output;
// the inner maps are a mapping from mode-specific identifiers
// to a string that describes them and which is user-facing.
Help Help
}
// Initialize a Goal language context with Ari's extensions.
func newGoalContext(ariContext *Context, help Help, sqlDatabase *SQLDatabase) (*goal.Context, error) {
goalContext := goal.NewContext()
goalContext.Log = os.Stderr
goalRegisterVariadics(ariContext, goalContext, help, sqlDatabase)
err := goalLoadExtendedPreamble(goalContext)
if err != nil {
return nil, err
}
return goalContext, nil
}
// Initialize a Goal language context with Ari's extensions.
func newUniversalGoalContext(ariContext *Context, help Help) (*goal.Context, error) {
goalContext := goal.NewContext()
goalContext.Log = os.Stderr
goalRegisterUniversalVariadics(ariContext, goalContext, help)
err := goalLoadExtendedPreamble(goalContext)
if err != nil {
return nil, err
}
return goalContext, nil
}
// Initialize SQL struct, but don't open the DB yet.
//
// Call SQLDatabase.open to open the database.
func newSQLDatabase(dataSourceName string) *SQLDatabase {
return &SQLDatabase{DataSource: dataSourceName, DB: nil, IsOpen: false}
}
func newHelp() map[string]map[string]string {
defaultSQLHelp := "A SQL keyword"
goalHelp := GoalKeywordsHelp()
sqlKeywords := SQLKeywords()
sqlHelp := make(map[string]string, len(sqlKeywords))
for _, x := range sqlKeywords {
sqlHelp[x] = defaultSQLHelp
}
help := make(map[string]map[string]string, 0)
help["goal"] = goalHelp
help["sql"] = sqlHelp
return help
}
// Initialize a new Context without connecting to the database.
func NewContext(dataSourceName string) (*Context, error) {
ctx := Context{}
helpDictionary := newHelp()
ariHelpFunc := func(s string) string {
goalHelp, ok := helpDictionary["goal"]
if !ok {
panic(`Developer Error: Dictionary in Help must have a \"goal\" entry.`)
}
help, found := goalHelp[s]
if found {
return help
}
return ""
}
helpFunc := help.Wrap(ariHelpFunc, help.HelpFunc())
help := Help{Dictionary: helpDictionary, Func: helpFunc}
sqlDatabase := newSQLDatabase(dataSourceName)
goalContext, err := newGoalContext(&ctx, help, sqlDatabase)
if err != nil {
return nil, err
}
ctx.GoalContext = goalContext
ctx.SQLDatabase = sqlDatabase
ctx.Help = help
return &ctx, nil
}
// Initialize a new Context that can be used across platforms, including WASM.
func NewUniversalContext() (*Context, error) {
ctx := Context{}
helpDictionary := newHelp()
ariHelpFunc := func(s string) string {
goalHelp, ok := helpDictionary["goal"]
if !ok {
panic(`Developer Error: Dictionary in Help must have a \"goal\" entry.`)
}
help, found := goalHelp[s]
if found {
return help
}
return ""
}
helpFunc := help.Wrap(ariHelpFunc, help.HelpFunc())
help := Help{Dictionary: helpDictionary, Func: helpFunc}
goalContext, err := newUniversalGoalContext(&ctx, help)
if err != nil {
return nil, err
}
ctx.GoalContext = goalContext
ctx.Help = help
return &ctx, nil
}