Compare commits

...

4 Commits

Author SHA1 Message Date
xuu
1f8b4ab24f
chore: mercury changes 2024-04-05 13:26:14 -06:00
xuu
d4e021386b
chore: add mercury 2024-04-05 13:26:11 -06:00
xuu
eb63312542
chore: update deps
All checks were successful
Go Bump / bump (push) Successful in 35s
Go Test / build (push) Successful in 43s
2024-04-05 13:22:53 -06:00
xuu
0a4986d476
chore: add libsql driver
Some checks failed
Go Test / build (push) Failing after 23s
Go Bump / bump (push) Successful in 44s
2024-04-05 12:41:30 -06:00
53 changed files with 7185 additions and 139 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
test.db
*.mercury
sour.is-mercury

23
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,23 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${fileDirname}",
"cwd": "${workspaceFolder}"
},
{
"name": "Attach to Process",
"type": "go",
"request": "attach",
"mode": "local",
"processId": 0
}
]
}

45
cmd/testsql/main.go Normal file
View File

@ -0,0 +1,45 @@
package main
import (
"database/sql"
"log"
_ "modernc.org/sqlite"
)
func main() {
db, err := sql.Open("sqlite", "./test.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
_, err = db.Exec(`drop table if exists foo`)
if err != nil {
log.Fatal(err)
}
_, err = db.Exec(`create table foo (bar jsonb)`)
if err != nil {
log.Fatal(err)
}
_, err = db.Exec(`insert into foo (bar) values ('["one"]')`)
if err != nil {
log.Fatal(err)
}
rows, err := db.Query(`select j.value from foo, json_each(bar) j `)
if err != nil {
log.Fatal(err)
}
for rows.Next() {
var s string
err = rows.Scan(&s)
if err != nil {
log.Fatal(err)
}
log.Println("GOT: ", s)
}
}

View File

@ -127,9 +127,6 @@ func (c *cron) run(ctx context.Context, now time.Time) {
case <-timer.C: case <-timer.C:
} }
span.AddEvent("Cron Run: " + now.Format(time.RFC822))
// fmt.Println("Cron Run: ", now.Format(time.RFC822))
c.state.Use(ctx, func(ctx context.Context, state *state) error { c.state.Use(ctx, func(ctx context.Context, state *state) error {
run = append(run, state.queue...) run = append(run, state.queue...)
state.queue = state.queue[:0] state.queue = state.queue[:0]
@ -148,6 +145,9 @@ func (c *cron) run(ctx context.Context, now time.Time) {
return return
} }
span.AddEvent("Cron Run: " + now.Format(time.RFC822))
// fmt.Println("Cron Run: ", now.Format(time.RFC822))
wg, _ := errgroup.WithContext(ctx) wg, _ := errgroup.WithContext(ctx)
for i := range run { for i := range run {

37
env/env.go vendored
View File

@ -9,35 +9,16 @@ import (
"strings" "strings"
) )
func Default(name, defaultValue string) string { func Default(name, defaultValue string) (s string) {
name = strings.TrimSpace(name) name = strings.TrimSpace(name)
defaultValue = strings.TrimSpace(defaultValue) s = strings.TrimSpace(defaultValue)
if v := strings.TrimSpace(os.Getenv(name)); v != "" {
slog.Info("env", name, v) if v, ok := os.LookupEnv(name); ok {
return v s = strings.TrimSpace(v)
} slog.Info("env", slog.String(name, v))
slog.Info("env", name, defaultValue+" (default)") return
return defaultValue
} }
type secret string slog.Info("env", slog.String(name, s+" (default)"))
return
func (s secret) String() string {
if s == "" {
return "(nil)"
}
return "***"
}
func (s secret) Secret() string {
return string(s)
}
func Secret(name, defaultValue string) secret {
name = strings.TrimSpace(name)
defaultValue = strings.TrimSpace(defaultValue)
if v := strings.TrimSpace(os.Getenv(name)); v != "" {
slog.Info("env", name, secret(v))
return secret(v)
}
slog.Info("env", name, secret(defaultValue).String()+" (default)")
return secret(defaultValue)
} }

35
env/secret.go vendored Normal file
View File

@ -0,0 +1,35 @@
package env
import (
"os"
"log/slog"
"strings"
)
type secret string
func (s secret) String() string {
if s == "" {
return "(nil)"
}
return "***"
}
func (s secret) Secret() string {
return string(s)
}
func Secret(name, defaultValue string) (s secret) {
name = strings.TrimSpace(name)
s = secret(strings.TrimSpace(defaultValue))
if v, ok := os.LookupEnv(name); ok {
s = secret(strings.TrimSpace(v))
slog.Info("env", slog.String(name, s.String()))
return
}
slog.Info("env", slog.String(name, s.String()+" (default)"))
return
}

92
go.mod
View File

@ -1,55 +1,81 @@
module go.sour.is/pkg module go.sour.is/pkg
go 1.21 go 1.22.0
require ( require (
github.com/99designs/gqlgen v0.17.34 github.com/99designs/gqlgen v0.17.44
github.com/golang-jwt/jwt/v4 v4.5.0 github.com/golang-jwt/jwt/v4 v4.5.0
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.1
github.com/matryer/is v1.4.1 github.com/matryer/is v1.4.1
github.com/ravilushqa/otelgqlgen v0.13.1 github.com/ravilushqa/otelgqlgen v0.15.0
github.com/vektah/gqlparser/v2 v2.5.6 github.com/tursodatabase/go-libsql v0.0.0-20240322134723-08771dcdd2f1
go.opentelemetry.io/otel v1.18.0 github.com/vektah/gqlparser/v2 v2.5.11
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0 go.opentelemetry.io/otel v1.23.1
go.opentelemetry.io/otel/sdk/metric v0.41.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.23.1
go.opentelemetry.io/otel/sdk/metric v1.23.1
go.sour.is/passwd v0.2.0
go.uber.org/multierr v1.11.0 go.uber.org/multierr v1.11.0
golang.org/x/sync v0.3.0 golang.org/x/sync v0.6.0
) )
require ( require (
github.com/agnivade/levenshtein v1.1.1 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.3 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/libsql/sqlite-antlr4-parser v0.0.0-20230802215326-5cb5bb604475 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.47.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect
go.opentelemetry.io/contrib v1.16.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect github.com/sosodev/duration v1.2.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect go.opentelemetry.io/contrib v1.23.0 // indirect
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240213162025-012b6fc9bca9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.41.0 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
) )
require ( require (
github.com/BurntSushi/toml v1.3.2
github.com/Masterminds/squirrel v1.5.4
github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect
github.com/prometheus/client_golang v1.17.0 github.com/oklog/ulid/v2 v2.1.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 github.com/prometheus/client_golang v1.18.0
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0 github.com/tursodatabase/go-libsql v0.0.0-20240322134723-08771dcdd2f1
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0 // indirect go.nhat.io/otelsql v0.12.0
go.opentelemetry.io/otel/exporters/prometheus v0.41.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0
go.opentelemetry.io/otel/metric v1.18.0 go.opentelemetry.io/contrib/instrumentation/runtime v0.48.0
go.opentelemetry.io/otel/sdk v1.18.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1 // indirect
go.opentelemetry.io/otel/trace v1.18.0 go.opentelemetry.io/otel/exporters/prometheus v0.45.2
go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.opentelemetry.io/otel/metric v1.23.1
golang.org/x/net v0.17.0 // indirect go.opentelemetry.io/otel/sdk v1.23.1
golang.org/x/sys v0.13.0 // indirect go.opentelemetry.io/otel/trace v1.23.1
golang.org/x/text v0.13.0 // indirect go.opentelemetry.io/proto/otlp v1.1.0 // indirect
google.golang.org/grpc v1.58.0 // indirect golang.org/x/exp v0.0.0-20240213143201-ec583247a57a
google.golang.org/protobuf v1.31.0 // indirect golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/grpc v1.61.1 // indirect
google.golang.org/protobuf v1.32.0 // indirect
modernc.org/sqlite v1.29.1
) )

259
go.sum
View File

@ -1,13 +1,26 @@
github.com/99designs/gqlgen v0.17.34 h1:5cS5/OKFguQt+Ws56uj9FlG2xm1IlcJWNF2jrMIKYFQ= cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/99designs/gqlgen v0.17.34/go.mod h1:Axcd3jIFHBVcqzixujJQr1wGqE+lGTpz6u4iZBZg1G8= github.com/99designs/gqlgen v0.17.44 h1:OS2wLk/67Y+vXM75XHbwRnNYJcbuJd4OBL76RX3NQQA=
github.com/99designs/gqlgen v0.17.44/go.mod h1:UTCu3xpK2mLI5qcMNw+HKDiEL77it/1XtAjisC4sLwM=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 h1:goHVqTbFX3AIo0tzGr14pgfAW2ZfPChKO21Z9MGf/gk=
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs=
github.com/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
@ -17,106 +30,212 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f h1:16RtHeWGkJMc80Etb8RPCcKevXGldr57+LOyZt8zOlg=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f/go.mod h1:ijRvpgDJDI262hYq/IQVYgf8hd8IHUs93Ol0kvMBAx4=
github.com/golang/lint v0.0.0-20170918230701-e5d664eb928e/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.1.1-0.20171103154506-982329095285/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/hashicorp/golang-lru/v2 v2.0.3 h1:kmRrRLlInXvng0SmLxmQpQkpbYAvcXm7NPDrgxJa9mE= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.3/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/gregjones/httpcache v0.0.0-20170920190843-316c5e0ff04e/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v0.0.0-20170914154624-68e816d1c783/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc=
github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE=
github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
github.com/libsql/sqlite-antlr4-parser v0.0.0-20230802215326-5cb5bb604475 h1:6PfEMwfInASh9hkN83aR0j4W/eKaAZt/AURtXAXlas0=
github.com/libsql/sqlite-antlr4-parser v0.0.0-20230802215326-5cb5bb604475/go.mod h1:20nXSmcf0nAscrzqsXeC2/tA3KkV2eCiJqYuyAgl+ss=
github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= github.com/prometheus/common v0.47.0 h1:p5Cz0FNHo7SnWOmWmoRozVcjEp0bIVU8cV7OShpjL1k=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/common v0.47.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/ravilushqa/otelgqlgen v0.13.1 h1:V+zFE75iDd2/CSzy5kKnb+Fi09SsE5535wv9U2nUEFE= github.com/ravilushqa/otelgqlgen v0.15.0 h1:U85nrlweMXTGaMChUViYM39/MXBZVeVVlpuHq+6eECQ=
github.com/ravilushqa/otelgqlgen v0.13.1/go.mod h1:ZIyWykK2paCuNi9k8gk5edcNSwDJuxZaW90vZXpafxw= github.com/ravilushqa/otelgqlgen v0.15.0/go.mod h1:o+1Eju0VySmgq2BP8Vupz2YrN21Bj7D7imBqu3m2uB8=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sosodev/duration v1.2.0 h1:pqK/FLSjsAADWY74SyWDCjOcd5l7H8GSnnOGEB9A1Us=
github.com/sosodev/duration v1.2.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/spf13/afero v0.0.0-20170901052352-ee1bd8ee15a1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/jwalterweatherman v0.0.0-20170901151539-12bd96e66386/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.1-0.20170901120850-7aff26db30c1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/vektah/gqlparser/v2 v2.5.6 h1:Ou14T0N1s191eRMZ1gARVqohcbe1e8FrcONScsq8cRU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/vektah/gqlparser/v2 v2.5.6/go.mod h1:z8xXUff237NntSuH8mLFijZ+1tjV1swDbpDqjJmk6ME= github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ=
go.opentelemetry.io/contrib v1.16.1 h1:EpASvVyGx6/ZTlmXzxYfTMZxHROelCeXXa2uLiwltcs= github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU=
go.opentelemetry.io/contrib v1.16.1/go.mod h1:gIzjwWFoGazJmtCaDgViqOSJPde2mCWzv60o0bWPcZs= github.com/tursodatabase/go-libsql v0.0.0-20240322134723-08771dcdd2f1 h1:lQwP++jvwcQiPqqIXIvabCIvfh8bzibpjYvT4s0jWmA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 h1:KfYpVmrjI7JuToy5k8XV3nkapjWx48k4E4JOtVstzQI= github.com/tursodatabase/go-libsql v0.0.0-20240322134723-08771dcdd2f1/go.mod h1:sb520Yr+GHBsfL43FQgQ+rLFfuJkItgRWlTgbIQHVxA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0/go.mod h1:SeQhzAEccGVZVEy7aH87Nh0km+utSpo1pTv6eMMop48= github.com/vektah/gqlparser/v2 v2.5.11 h1:JJxLtXIoN7+3x6MBdtIP59TP1RANnY7pXOaDnADQSf8=
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0 h1:EbmAUG9hEAMXyfWEasIt2kmh/WmXUznUksChApTgBGc= github.com/vektah/gqlparser/v2 v2.5.11/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc=
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0/go.mod h1:rD9feqRYP24P14t5kmhNMqsqm1jvKmpx2H2rKVw52V8= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
go.opentelemetry.io/otel v1.18.0 h1:TgVozPGZ01nHyDZxK5WGPFB9QexeTMXEH7+tIClWfzs= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
go.opentelemetry.io/otel v1.18.0/go.mod h1:9lWqYO0Db579XzVuCKFNPDl4s73Voa+zEck3wHaAYQI= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0 h1:IAtl+7gua134xcV3NieDhJHjjOVeJhXAnYf/0hswjUY= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0/go.mod h1:w+pXobnBzh95MNIkeIuAKcHe/Uu/CX2PKIvBP6ipKRA= go.nhat.io/otelsql v0.12.0 h1:/rBhWZiwHFLpCm5SGdafm+Owm0OmGmnF31XWxgecFtY=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0 h1:6pu8ttx76BxHf+xz/H77AUZkPF3cwWzXqAUsXhVKI18= go.nhat.io/otelsql v0.12.0/go.mod h1:39Hc9/JDfCl7NGrBi1uPP3QPofqwnC/i5SFd7gtDMWM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0/go.mod h1:IOmXxPrxoxFMXdNy7lfDmE8MzE61YPcurbUm0SMjerI= go.opentelemetry.io/contrib v1.23.0 h1:5f6bvGoHE/7lcolc1jCA4Vzq2tnPs4tfqL1M/yfjbOA=
go.opentelemetry.io/otel/exporters/prometheus v0.41.0 h1:A3/bhjP5SmELy8dcpK+uttHeh9Qrh+YnS16/VzrztRQ= go.opentelemetry.io/contrib v1.23.0/go.mod h1:usW9bPlrjHiJFbK0a6yK/M5wNHs3nLmtrT3vzhoD3co=
go.opentelemetry.io/otel/exporters/prometheus v0.41.0/go.mod h1:mKuXEMi9suyyNJQ99SZCO0mpWGFe0MIALtjd3r6uo7Q= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 h1:doUP+ExOpH3spVTLS0FcWGLnQrPct/hD/bCPbDRUEAU=
go.opentelemetry.io/otel/metric v1.18.0 h1:JwVzw94UYmbx3ej++CwLUQZxEODDj/pOuTCvzhtRrSQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0/go.mod h1:rdENBZMT2OE6Ne/KLwpiXudnAsbdrdBaqBvTN8M8BgA=
go.opentelemetry.io/otel/metric v1.18.0/go.mod h1:nNSpsVDjWGfb7chbRLUNW+PBNdcSTHD4Uu5pfFMOI0k= go.opentelemetry.io/contrib/instrumentation/runtime v0.48.0 h1:dJlCKeq+zmO5Og4kgxqPvvJrzuD/mygs1g/NYM9dAsU=
go.opentelemetry.io/otel/sdk v1.18.0 h1:e3bAB0wB3MljH38sHzpV/qWrOTCFrdZF2ct9F8rBkcY= go.opentelemetry.io/contrib/instrumentation/runtime v0.48.0/go.mod h1:p+hpBCpLHpuUrR0lHgnHbUnbCBll1IhrcMIlycC+xYs=
go.opentelemetry.io/otel/sdk v1.18.0/go.mod h1:1RCygWV7plY2KmdskZEDDBs4tJeHG92MdHZIluiYs/M= go.opentelemetry.io/otel v1.23.1 h1:Za4UzOqJYS+MUczKI320AtqZHZb7EqxO00jAHE0jmQY=
go.opentelemetry.io/otel/sdk/metric v0.41.0 h1:c3sAt9/pQ5fSIUfl0gPtClV3HhE18DCVzByD33R/zsk= go.opentelemetry.io/otel v1.23.1/go.mod h1:Td0134eafDLcTS4y+zQ26GE8u3dEuRBiBCTUIRHaikA=
go.opentelemetry.io/otel/sdk/metric v0.41.0/go.mod h1:PmOmSt+iOklKtIg5O4Vz9H/ttcRFSNTgii+E1KGyn1w= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1 h1:o8iWeVFa1BcLtVEV0LzrCxV2/55tB3xLxADr6Kyoey4=
go.opentelemetry.io/otel/trace v1.18.0 h1:NY+czwbHbmndxojTEKiSMHkG2ClNH2PwmcHrdo0JY10= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1/go.mod h1:SEVfdK4IoBnbT2FXNM/k8yC08MrfbhWk3U4ljM8B3HE=
go.opentelemetry.io/otel/trace v1.18.0/go.mod h1:T2+SGJGuYZY3bjj5rgh/hN7KIrlpWC5nS8Mjvzckz+0= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.23.1 h1:cfuy3bXmLJS7M1RZmAL6SuhGtKUp2KEsrm00OlAXkq4=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.23.1/go.mod h1:22jr92C6KwlwItJmQzfixzQM3oyyuYLCfHiMY+rpsPU=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.opentelemetry.io/otel/exporters/prometheus v0.45.2 h1:pe2Jqk1K18As0RCw7J08QhgXNqr+6npx0a5W4IgAFA8=
go.opentelemetry.io/otel/exporters/prometheus v0.45.2/go.mod h1:B38pscHKI6bhFS44FDw0eFU3iqG3ASNIvY+fZgR5sAc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.40.0 h1:hf7JSONqAuXT1PDYYlVhKNMPLe4060d+4RFREcv7X2c=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.40.0/go.mod h1:IxD5qbw/XcnFB7i5k4d7J1aW5iBU2h4DgSxtk4YqR4c=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.17.0 h1:Ut6hgtYcASHwCzRHkXEtSsM251cXJPW+Z9DyLwEn6iI=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.17.0/go.mod h1:TYeE+8d5CjrgBa0ZuRaDeMpIC1xZ7atg4g+nInjuSjc=
go.opentelemetry.io/otel/metric v1.23.1 h1:PQJmqJ9u2QaJLBOELl1cxIdPcpbwzbkjfEyelTl2rlo=
go.opentelemetry.io/otel/metric v1.23.1/go.mod h1:mpG2QPlAfnK8yNhNJAxDZruU9Y1/HubbC+KyH8FaCWI=
go.opentelemetry.io/otel/sdk v1.23.1 h1:O7JmZw0h76if63LQdsBMKQDWNb5oEcOThG9IrxscV+E=
go.opentelemetry.io/otel/sdk v1.23.1/go.mod h1:LzdEVR5am1uKOOwfBWFef2DCi1nu3SA8XQxx2IerWFk=
go.opentelemetry.io/otel/sdk/metric v1.23.1 h1:T9/8WsYg+ZqIpMWwdISVVrlGb/N0Jr1OHjR/alpKwzg=
go.opentelemetry.io/otel/sdk/metric v1.23.1/go.mod h1:8WX6WnNtHCgUruJ4TJ+UssQjMtpxkpX0zveQC8JG/E0=
go.opentelemetry.io/otel/trace v1.23.1 h1:4LrmmEd8AU2rFvU1zegmvqW7+kWarxtNOPyeL6HmYY8=
go.opentelemetry.io/otel/trace v1.23.1/go.mod h1:4IpnpJFwr1mo/6HL8XIPJaE9y0+u1KcVmuW7dwFSVrI=
go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
go.sour.is/passwd v0.2.0 h1:eFLrvcayaS2e8ItjTU/tzBWtt1Am9xH97uBGqCCxdkk=
go.sour.is/passwd v0.2.0/go.mod h1:xDqWTLiztFhr1KvUh//lvmJfMg+9piWt7K+d1JX3n0s=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g= google.golang.org/api v0.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= google.golang.org/genproto v0.0.0-20170918111702-1e559d0a00ee/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s=
google.golang.org/grpc v1.58.0 h1:32JY8YpPMSR45K+c3o6b8VL73V+rR8k+DeMIr4vRH8o= google.golang.org/genproto/googleapis/api v0.0.0-20240213162025-012b6fc9bca9 h1:4++qSzdWBUy9/2x8L5KZgwZw+mjJZ2yDSCGMVM0YzRs=
google.golang.org/grpc v1.58.0/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/genproto/googleapis/api v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:PVreiBMirk8ypES6aw9d4p6iiBNSIfZEBqr3UGoAi2E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 h1:hZB7eLIaYlW9qXRfCq/qDaPdbeY3757uARz5Vvfv+cY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:YUWgXUFRPfoYK1IHMuxH5K6nPEXSCzIMljnQ59lLRCk=
google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY=
google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk=
modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/sqlite v1.29.1 h1:19GY2qvWB4VPw0HppFlZCPAbmxFU41r+qjKZQdQ1ryA=
modernc.org/sqlite v1.29.1/go.mod h1:hG41jCYxOAOoO6BRK66AdRlmOcDzXf7qnwlwjUIOqa0=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

6
go.work Normal file
View File

@ -0,0 +1,6 @@
go 1.22.0
use (
.
../go-tools
)

334
go.work.sum Normal file
View File

@ -0,0 +1,334 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4=
cloud.google.com/go/accessapproval v1.7.5/go.mod h1:g88i1ok5dvQ9XJsxpUInWWvUBrIZhyPDPbk4T01OoJ0=
cloud.google.com/go/accesscontextmanager v1.8.5/go.mod h1:TInEhcZ7V9jptGNqN3EzZ5XMhT6ijWxTGjzyETwmL0Q=
cloud.google.com/go/aiplatform v1.60.0/go.mod h1:eTlGuHOahHprZw3Hio5VKmtThIOak5/qy6pzdsqcQnM=
cloud.google.com/go/analytics v0.23.0/go.mod h1:YPd7Bvik3WS95KBok2gPXDqQPHy08TsCQG6CdUCb+u0=
cloud.google.com/go/apigateway v1.6.5/go.mod h1:6wCwvYRckRQogyDDltpANi3zsCDl6kWi0b4Je+w2UiI=
cloud.google.com/go/apigeeconnect v1.6.5/go.mod h1:MEKm3AiT7s11PqTfKE3KZluZA9O91FNysvd3E6SJ6Ow=
cloud.google.com/go/apigeeregistry v0.8.3/go.mod h1:aInOWnqF4yMQx8kTjDqHNXjZGh/mxeNlAf52YqtASUs=
cloud.google.com/go/appengine v1.8.5/go.mod h1:uHBgNoGLTS5di7BvU25NFDuKa82v0qQLjyMJLuPQrVo=
cloud.google.com/go/area120 v0.8.5/go.mod h1:BcoFCbDLZjsfe4EkCnEq1LKvHSK0Ew/zk5UFu6GMyA0=
cloud.google.com/go/artifactregistry v1.14.7/go.mod h1:0AUKhzWQzfmeTvT4SjfI4zjot72EMfrkvL9g9aRjnnM=
cloud.google.com/go/asset v1.17.2/go.mod h1:SVbzde67ehddSoKf5uebOD1sYw8Ab/jD/9EIeWg99q4=
cloud.google.com/go/assuredworkloads v1.11.5/go.mod h1:FKJ3g3ZvkL2D7qtqIGnDufFkHxwIpNM9vtmhvt+6wqk=
cloud.google.com/go/automl v1.13.5/go.mod h1:MDw3vLem3yh+SvmSgeYUmUKqyls6NzSumDm9OJ3xJ1Y=
cloud.google.com/go/baremetalsolution v1.2.4/go.mod h1:BHCmxgpevw9IEryE99HbYEfxXkAEA3hkMJbYYsHtIuY=
cloud.google.com/go/batch v1.8.0/go.mod h1:k8V7f6VE2Suc0zUM4WtoibNrA6D3dqBpB+++e3vSGYc=
cloud.google.com/go/beyondcorp v1.0.4/go.mod h1:Gx8/Rk2MxrvWfn4WIhHIG1NV7IBfg14pTKv1+EArVcc=
cloud.google.com/go/bigquery v1.59.1/go.mod h1:VP1UJYgevyTwsV7desjzNzDND5p6hZB+Z8gZJN1GQUc=
cloud.google.com/go/billing v1.18.2/go.mod h1:PPIwVsOOQ7xzbADCwNe8nvK776QpfrOAUkvKjCUcpSE=
cloud.google.com/go/binaryauthorization v1.8.1/go.mod h1:1HVRyBerREA/nhI7yLang4Zn7vfNVA3okoAR9qYQJAQ=
cloud.google.com/go/certificatemanager v1.7.5/go.mod h1:uX+v7kWqy0Y3NG/ZhNvffh0kuqkKZIXdvlZRO7z0VtM=
cloud.google.com/go/channel v1.17.5/go.mod h1:FlpaOSINDAXgEext0KMaBq/vwpLMkkPAw9b2mApQeHc=
cloud.google.com/go/cloudbuild v1.15.1/go.mod h1:gIofXZSu+XD2Uy+qkOrGKEx45zd7s28u/k8f99qKals=
cloud.google.com/go/clouddms v1.7.4/go.mod h1:RdrVqoFG9RWI5AvZ81SxJ/xvxPdtcRhFotwdE79DieY=
cloud.google.com/go/cloudtasks v1.12.6/go.mod h1:b7c7fe4+TJsFZfDyzO51F7cjq7HLUlRi/KZQLQjDsaY=
cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/contactcenterinsights v1.13.0/go.mod h1:ieq5d5EtHsu8vhe2y3amtZ+BE+AQwX5qAy7cpo0POsI=
cloud.google.com/go/container v1.31.0/go.mod h1:7yABn5s3Iv3lmw7oMmyGbeV6tQj86njcTijkkGuvdZA=
cloud.google.com/go/containeranalysis v0.11.4/go.mod h1:cVZT7rXYBS9NG1rhQbWL9pWbXCKHWJPYraE8/FTSYPE=
cloud.google.com/go/datacatalog v1.19.3/go.mod h1:ra8V3UAsciBpJKQ+z9Whkxzxv7jmQg1hfODr3N3YPJ4=
cloud.google.com/go/dataflow v0.9.5/go.mod h1:udl6oi8pfUHnL0z6UN9Lf9chGqzDMVqcYTcZ1aPnCZQ=
cloud.google.com/go/dataform v0.9.2/go.mod h1:S8cQUwPNWXo7m/g3DhWHsLBoufRNn9EgFrMgne2j7cI=
cloud.google.com/go/datafusion v1.7.5/go.mod h1:bYH53Oa5UiqahfbNK9YuYKteeD4RbQSNMx7JF7peGHc=
cloud.google.com/go/datalabeling v0.8.5/go.mod h1:IABB2lxQnkdUbMnQaOl2prCOfms20mcPxDBm36lps+s=
cloud.google.com/go/dataplex v1.14.2/go.mod h1:0oGOSFlEKef1cQeAHXy4GZPB/Ife0fz/PxBf+ZymA2U=
cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4=
cloud.google.com/go/dataproc/v2 v2.4.0/go.mod h1:3B1Ht2aRB8VZIteGxQS/iNSJGzt9+CA0WGnDVMEm7Z4=
cloud.google.com/go/dataqna v0.8.5/go.mod h1:vgihg1mz6n7pb5q2YJF7KlXve6tCglInd6XO0JGOlWM=
cloud.google.com/go/datastore v1.15.0/go.mod h1:GAeStMBIt9bPS7jMJA85kgkpsMkvseWWXiaHya9Jes8=
cloud.google.com/go/datastream v1.10.4/go.mod h1:7kRxPdxZxhPg3MFeCSulmAJnil8NJGGvSNdn4p1sRZo=
cloud.google.com/go/deploy v1.17.1/go.mod h1:SXQyfsXrk0fBmgBHRzBjQbZhMfKZ3hMQBw5ym7MN/50=
cloud.google.com/go/dialogflow v1.49.0/go.mod h1:dhVrXKETtdPlpPhE7+2/k4Z8FRNUp6kMV3EW3oz/fe0=
cloud.google.com/go/dlp v1.11.2/go.mod h1:9Czi+8Y/FegpWzgSfkRlyz+jwW6Te9Rv26P3UfU/h/w=
cloud.google.com/go/documentai v1.25.0/go.mod h1:ftLnzw5VcXkLItp6pw1mFic91tMRyfv6hHEY5br4KzY=
cloud.google.com/go/domains v0.9.5/go.mod h1:dBzlxgepazdFhvG7u23XMhmMKBjrkoUNaw0A8AQB55Y=
cloud.google.com/go/edgecontainer v1.1.5/go.mod h1:rgcjrba3DEDEQAidT4yuzaKWTbkTI5zAMu3yy6ZWS0M=
cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU=
cloud.google.com/go/essentialcontacts v1.6.6/go.mod h1:XbqHJGaiH0v2UvtuucfOzFXN+rpL/aU5BCZLn4DYl1Q=
cloud.google.com/go/eventarc v1.13.4/go.mod h1:zV5sFVoAa9orc/52Q+OuYUG9xL2IIZTbbuTHC6JSY8s=
cloud.google.com/go/filestore v1.8.1/go.mod h1:MbN9KcaM47DRTIuLfQhJEsjaocVebNtNQhSLhKCF5GM=
cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ=
cloud.google.com/go/functions v1.16.0/go.mod h1:nbNpfAG7SG7Duw/o1iZ6ohvL7mc6MapWQVpqtM29n8k=
cloud.google.com/go/gkebackup v1.3.5/go.mod h1:KJ77KkNN7Wm1LdMopOelV6OodM01pMuK2/5Zt1t4Tvc=
cloud.google.com/go/gkeconnect v0.8.5/go.mod h1:LC/rS7+CuJ5fgIbXv8tCD/mdfnlAadTaUufgOkmijuk=
cloud.google.com/go/gkehub v0.14.5/go.mod h1:6bzqxM+a+vEH/h8W8ec4OJl4r36laxTs3A/fMNHJ0wA=
cloud.google.com/go/gkemulticloud v1.1.1/go.mod h1:C+a4vcHlWeEIf45IB5FFR5XGjTeYhF83+AYIpTy4i2Q=
cloud.google.com/go/grafeas v0.3.4/go.mod h1:A5m316hcG+AulafjAbPKXBO/+I5itU4LOdKO2R/uDIc=
cloud.google.com/go/gsuiteaddons v1.6.5/go.mod h1:Lo4P2IvO8uZ9W+RaC6s1JVxo42vgy+TX5a6hfBZ0ubs=
cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI=
cloud.google.com/go/iap v1.9.4/go.mod h1:vO4mSq0xNf/Pu6E5paORLASBwEmphXEjgCFg7aeNu1w=
cloud.google.com/go/ids v1.4.5/go.mod h1:p0ZnyzjMWxww6d2DvMGnFwCsSxDJM666Iir1bK1UuBo=
cloud.google.com/go/iot v1.7.5/go.mod h1:nq3/sqTz3HGaWJi1xNiX7F41ThOzpud67vwk0YsSsqs=
cloud.google.com/go/kms v1.15.7/go.mod h1:ub54lbsa6tDkUwnu4W7Yt1aAIFLnspgh0kPGToDukeI=
cloud.google.com/go/language v1.12.3/go.mod h1:evFX9wECX6mksEva8RbRnr/4wi/vKGYnAJrTRXU8+f8=
cloud.google.com/go/lifesciences v0.9.5/go.mod h1:OdBm0n7C0Osh5yZB7j9BXyrMnTRGBJIZonUMxo5CzPw=
cloud.google.com/go/logging v1.9.0/go.mod h1:1Io0vnZv4onoUnsVUQY3HZ3Igb1nBchky0A0y7BBBhE=
cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s=
cloud.google.com/go/managedidentities v1.6.5/go.mod h1:fkFI2PwwyRQbjLxlm5bQ8SjtObFMW3ChBGNqaMcgZjI=
cloud.google.com/go/maps v1.6.4/go.mod h1:rhjqRy8NWmDJ53saCfsXQ0LKwBHfi6OSh5wkq6BaMhI=
cloud.google.com/go/mediatranslation v0.8.5/go.mod h1:y7kTHYIPCIfgyLbKncgqouXJtLsU+26hZhHEEy80fSs=
cloud.google.com/go/memcache v1.10.5/go.mod h1:/FcblbNd0FdMsx4natdj+2GWzTq+cjZvMa1I+9QsuMA=
cloud.google.com/go/metastore v1.13.4/go.mod h1:FMv9bvPInEfX9Ac1cVcRXp8EBBQnBcqH6gz3KvJ9BAE=
cloud.google.com/go/monitoring v1.18.0/go.mod h1:c92vVBCeq/OB4Ioyo+NbN2U7tlg5ZH41PZcdvfc+Lcg=
cloud.google.com/go/networkconnectivity v1.14.4/go.mod h1:PU12q++/IMnDJAB+3r+tJtuCXCfwfN+C6Niyj6ji1Po=
cloud.google.com/go/networkmanagement v1.9.4/go.mod h1:daWJAl0KTFytFL7ar33I6R/oNBH8eEOX/rBNHrC/8TA=
cloud.google.com/go/networksecurity v0.9.5/go.mod h1:KNkjH/RsylSGyyZ8wXpue8xpCEK+bTtvof8SBfIhMG8=
cloud.google.com/go/notebooks v1.11.3/go.mod h1:0wQyI2dQC3AZyQqWnRsp+yA+kY4gC7ZIVP4Qg3AQcgo=
cloud.google.com/go/optimization v1.6.3/go.mod h1:8ve3svp3W6NFcAEFr4SfJxrldzhUl4VMUJmhrqVKtYA=
cloud.google.com/go/orchestration v1.8.5/go.mod h1:C1J7HesE96Ba8/hZ71ISTV2UAat0bwN+pi85ky38Yq8=
cloud.google.com/go/orgpolicy v1.12.1/go.mod h1:aibX78RDl5pcK3jA8ysDQCFkVxLj3aOQqrbBaUL2V5I=
cloud.google.com/go/osconfig v1.12.5/go.mod h1:D9QFdxzfjgw3h/+ZaAb5NypM8bhOMqBzgmbhzWViiW8=
cloud.google.com/go/oslogin v1.13.1/go.mod h1:vS8Sr/jR7QvPWpCjNqy6LYZr5Zs1e8ZGW/KPn9gmhws=
cloud.google.com/go/phishingprotection v0.8.5/go.mod h1:g1smd68F7mF1hgQPuYn3z8HDbNre8L6Z0b7XMYFmX7I=
cloud.google.com/go/policytroubleshooter v1.10.3/go.mod h1:+ZqG3agHT7WPb4EBIRqUv4OyIwRTZvsVDHZ8GlZaoxk=
cloud.google.com/go/privatecatalog v0.9.5/go.mod h1:fVWeBOVe7uj2n3kWRGlUQqR/pOd450J9yZoOECcQqJk=
cloud.google.com/go/pubsub v1.36.1/go.mod h1:iYjCa9EzWOoBiTdd4ps7QoMtMln5NwaZQpK1hbRfBDE=
cloud.google.com/go/pubsublite v1.8.1/go.mod h1:fOLdU4f5xldK4RGJrBMm+J7zMWNj/k4PxwEZXy39QS0=
cloud.google.com/go/recaptchaenterprise/v2 v2.9.2/go.mod h1:trwwGkfhCmp05Ll5MSJPXY7yvnO0p4v3orGANAFHAuU=
cloud.google.com/go/recommendationengine v0.8.5/go.mod h1:A38rIXHGFvoPvmy6pZLozr0g59NRNREz4cx7F58HAsQ=
cloud.google.com/go/recommender v1.12.1/go.mod h1:gf95SInWNND5aPas3yjwl0I572dtudMhMIG4ni8nr+0=
cloud.google.com/go/redis v1.14.2/go.mod h1:g0Lu7RRRz46ENdFKQ2EcQZBAJ2PtJHJLuiiRuEXwyQw=
cloud.google.com/go/resourcemanager v1.9.5/go.mod h1:hep6KjelHA+ToEjOfO3garMKi/CLYwTqeAw7YiEI9x8=
cloud.google.com/go/resourcesettings v1.6.5/go.mod h1:WBOIWZraXZOGAgoR4ukNj0o0HiSMO62H9RpFi9WjP9I=
cloud.google.com/go/retail v1.16.0/go.mod h1:LW7tllVveZo4ReWt68VnldZFWJRzsh9np+01J9dYWzE=
cloud.google.com/go/run v1.3.4/go.mod h1:FGieuZvQ3tj1e9GnzXqrMABSuir38AJg5xhiYq+SF3o=
cloud.google.com/go/scheduler v1.10.6/go.mod h1:pe2pNCtJ+R01E06XCDOJs1XvAMbv28ZsQEbqknxGOuE=
cloud.google.com/go/secretmanager v1.11.5/go.mod h1:eAGv+DaCHkeVyQi0BeXgAHOU0RdrMeZIASKc+S7VqH4=
cloud.google.com/go/security v1.15.5/go.mod h1:KS6X2eG3ynWjqcIX976fuToN5juVkF6Ra6c7MPnldtc=
cloud.google.com/go/securitycenter v1.24.4/go.mod h1:PSccin+o1EMYKcFQzz9HMMnZ2r9+7jbc+LvPjXhpwcU=
cloud.google.com/go/servicedirectory v1.11.4/go.mod h1:Bz2T9t+/Ehg6x+Y7Ycq5xiShYLD96NfEsWNHyitj1qM=
cloud.google.com/go/shell v1.7.5/go.mod h1:hL2++7F47/IfpfTO53KYf1EC+F56k3ThfNEXd4zcuiE=
cloud.google.com/go/spanner v1.56.0/go.mod h1:DndqtUKQAt3VLuV2Le+9Y3WTnq5cNKrnLb/Piqcj+h0=
cloud.google.com/go/speech v1.21.1/go.mod h1:E5GHZXYQlkqWQwY5xRSLHw2ci5NMQNG52FfMU1aZrIA=
cloud.google.com/go/storage v1.37.0/go.mod h1:i34TiT2IhiNDmcj65PqwCjcoUX7Z5pLzS8DEmoiFq1k=
cloud.google.com/go/storagetransfer v1.10.4/go.mod h1:vef30rZKu5HSEf/x1tK3WfWrL0XVoUQN/EPDRGPzjZs=
cloud.google.com/go/talent v1.6.6/go.mod h1:y/WQDKrhVz12WagoarpAIyKKMeKGKHWPoReZ0g8tseQ=
cloud.google.com/go/texttospeech v1.7.5/go.mod h1:tzpCuNWPwrNJnEa4Pu5taALuZL4QRRLcb+K9pbhXT6M=
cloud.google.com/go/tpu v1.6.5/go.mod h1:P9DFOEBIBhuEcZhXi+wPoVy/cji+0ICFi4TtTkMHSSs=
cloud.google.com/go/trace v1.10.5/go.mod h1:9hjCV1nGBCtXbAE4YK7OqJ8pmPYSxPA0I67JwRd5s3M=
cloud.google.com/go/translate v1.10.1/go.mod h1:adGZcQNom/3ogU65N9UXHOnnSvjPwA/jKQUMnsYXOyk=
cloud.google.com/go/video v1.20.4/go.mod h1:LyUVjyW+Bwj7dh3UJnUGZfyqjEto9DnrvTe1f/+QrW0=
cloud.google.com/go/videointelligence v1.11.5/go.mod h1:/PkeQjpRponmOerPeJxNPuxvi12HlW7Em0lJO14FC3I=
cloud.google.com/go/vision/v2 v2.8.0/go.mod h1:ocqDiA2j97pvgogdyhoxiQp2ZkDCyr0HWpicywGGRhU=
cloud.google.com/go/vmmigration v1.7.5/go.mod h1:pkvO6huVnVWzkFioxSghZxIGcsstDvYiVCxQ9ZH3eYI=
cloud.google.com/go/vmwareengine v1.1.1/go.mod h1:nMpdsIVkUrSaX8UvmnBhzVzG7PPvNYc5BszcvIVudYs=
cloud.google.com/go/vpcaccess v1.7.5/go.mod h1:slc5ZRvvjP78c2dnL7m4l4R9GwL3wDLcpIWz6P/ziig=
cloud.google.com/go/webrisk v1.9.5/go.mod h1:aako0Fzep1Q714cPEM5E+mtYX8/jsfegAuS8aivxy3U=
cloud.google.com/go/websecurityscanner v1.6.5/go.mod h1:QR+DWaxAz2pWooylsBF854/Ijvuoa3FCyS1zBa1rAVQ=
cloud.google.com/go/workflows v1.12.4/go.mod h1:yQ7HUqOkdJK4duVtMeBCAOPiN1ZF1E9pAMX51vpwB/w=
github.com/99designs/gqlgen v0.17.41/go.mod h1:GQ6SyMhwFbgHR0a8r2Wn8fYgEwPxxmndLFPhU63+cJE=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apache/arrow/go/v14 v14.0.2/go.mod h1:u3fgh3EdgN/YQ8cVQRguVW3R+seMybFg8QBQ5LU+eBY=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04=
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-pkcs11 v0.2.1-0.20230907215043-c6f79328ddf9/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY=
github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
github.com/gopherjs/gopherjs v0.0.0-20220221023154-0b2280d3ff96/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lyft/protoc-gen-star/v2 v2.0.3/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/matryer/moq v0.3.3/go.mod h1:RJ75ZZZD71hejp39j4crZLsEDszGk6iH4v4YsWFKH4s=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mmcloughlin/professor v0.0.0-20170922221822-6b97112ab8b3/go.mod h1:LQkXsHRSPIEklPCq8OMQAzYNS2NGtYStdNE/ej1oJU8=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8qy1rU=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/wblakecaldwell/profiler v0.0.0-20150908040756-6111ef1313a1/go.mod h1:3+0F8oLB1rQlbIcRAuqDgGdzNi9X69un/aPz4cUAFV4=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.einride.tech/aip v0.66.0/go.mod h1:qAhMsfT7plxBX+Oy7Huol6YUvZ0ZzdUz26yZsQwfl1M=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib v1.21.1/go.mod h1:usW9bPlrjHiJFbK0a6yK/M5wNHs3nLmtrT3vzhoD3co=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw=
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
go.opentelemetry.io/otel v1.23.0/go.mod h1:YCycw9ZeKhcJFrb34iVSkyT0iczq/zYDtZYFufObyB0=
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
go.opentelemetry.io/otel/metric v1.23.0/go.mod h1:MqUW2X2a6Q8RN96E2/nqNoT+z9BSms20Jb7Bbp+HiTo=
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
go.opentelemetry.io/otel/trace v1.23.0/go.mod h1:GSGTbIClEsuZrGIzoEHqsVfxgn5UkggkflQwDScNUsk=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM=
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/api v0.162.0/go.mod h1:6SulDkfoBIg4NFmCuZ39XeeAgSHCPecfSUuDyYlAHs0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY=
google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k=
google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro=
google.golang.org/genproto v0.0.0-20240205150955-31a09d347014/go.mod h1:xEgQu1e4stdSSsxPDK8Azkrk/ECl5HvdPf6nbZrTS5M=
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4=
google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go.mod h1:CmlNWB9lSezaYELKS5Ym1r44VrrbPUa7JTvw+6MbpJ0=
google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA=
google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014/go.mod h1:rbHMSEDyoYX62nRVLOCc4Qt1HbsdytAYoVwgjiOhF3I=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:SCz6T5xjNXM4QFPRwxHcfChp7V+9DcXR3ay2TkHR8Tg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014/go.mod h1:SaPjaZGWb0lPqs6Ittu0spdfrOArqji4ZdeP5IC/9N4=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=

115
ident/ident.go Normal file
View File

@ -0,0 +1,115 @@
package ident
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"sort"
"github.com/oklog/ulid/v2"
"go.sour.is/passwd"
)
// Ident interface for a logged in user
type Ident interface {
Identity() string
Session() *SessionInfo
}
type SessionInfo struct {
SessionID ulid.ULID
Active bool
}
func (s *SessionInfo) Session() *SessionInfo { return s }
// Handler handler function to read ident from HTTP request
type Handler interface {
ReadIdent(r *http.Request) (Ident, error)
}
type HandleGet interface {
GetIdent(context.Context /* identity */, string) (Ident, error)
}
type HandleRegister interface {
RegisterIdent(ctx context.Context, identity, displayName string, passwd []byte) (Ident, error)
}
type source struct {
Handler
priority int
}
var contextKey = struct{ key string }{"ident"}
func FromContext(ctx context.Context) Ident {
if id, ok := ctx.Value(contextKey).(Ident); ok {
return id
}
return Anonymous
}
type IDM struct {
rand io.Reader
sources []source
pwd *passwd.Passwd
}
func NewIDM(pwd *passwd.Passwd, rand io.Reader) *IDM {
return &IDM{pwd: pwd, rand: rand}
}
func (idm *IDM) Add(p int, h Handler) {
idm.sources = append(idm.sources, source{priority: p, Handler: h})
sort.Slice(idm.sources, func(i, j int) bool { return idm.sources[i].priority < idm.sources[j].priority })
}
func (idm *IDM) Passwd(pass, hash []byte) ([]byte, error) {
return idm.pwd.Passwd(pass, hash)
}
// ReadIdent read ident from a list of ident handlers
func (idm *IDM) ReadIdent(r *http.Request) (Ident, error) {
var errs error
for _, source := range idm.sources {
u, err := source.ReadIdent(r)
errs = errors.Join(errs, err)
if u != nil && u.Session().Active {
return u, errs
}
}
return Anonymous, errs
}
func (idm *IDM) RegisterIdent(ctx context.Context, identity, displayName string, passwd []byte) (Ident, error) {
for _, source := range idm.sources {
if source, ok := source.Handler.(HandleRegister); ok {
return source.RegisterIdent(ctx, identity, displayName, passwd)
}
}
return nil, fmt.Errorf("no HandleRegister source registered")
}
func (idm *IDM) GetIdent(ctx context.Context, identity string) (Ident, error) {
for _, source := range idm.sources {
if source, ok := source.Handler.(HandleGet); ok {
return source.GetIdent(ctx, identity)
}
}
return nil, fmt.Errorf("no HandleGet source registered")
}
func (idm *IDM) NewSessionInfo() (session SessionInfo, err error) {
session.SessionID, err = ulid.New(ulid.Now(), idm.rand)
if err != nil {
return
}
session.Active = true
return session, nil
}

75
ident/null-user.go Normal file
View File

@ -0,0 +1,75 @@
package ident
import "net/http"
// nullUser implements a null ident
type nullUser struct {
identity string
aspect string
displayName string
SessionInfo
}
// Anonymous is a logged out user
var Anonymous = NewNullUser("anon", "none", "Guest User", false)
// NewNullUser creates a null user ident
func NewNullUser(ident, aspect, name string, active bool) *nullUser {
return &nullUser{ident, aspect, name, SessionInfo{Active: active}}
}
func (id nullUser) String() string {
return "id: " + id.identity + " dn: " + id.displayName
}
// GetIdentity returns identity
func (m nullUser) Identity() string {
return m.identity
}
// GetAspect returns aspect
func (m nullUser) Aspect() string {
return m.aspect
}
// HasRole returns true if matches role
func (m nullUser) Role(r ...string) bool {
return m.Active
}
// HasGroup returns true if matches group
func (m nullUser) Group(g ...string) bool {
return m.Active
}
// GetGroups returns empty list
func (m nullUser) Groups() []string {
return []string{}
}
// GetRoles returns empty list
func (m nullUser) Roles() []string {
return []string{}
}
// GetMeta returns empty list
func (m nullUser) Meta() map[string]string {
return make(map[string]string)
}
// IsActive returns true if active
func (m nullUser) IsActive() bool {
return m.Active
}
// GetDisplay returns display name
func (m nullUser) Display() string {
return m.displayName
}
// MakeHandlerFunc returns handler func
func (m nullUser) HandlerFunc() func(r *http.Request) Ident {
return func(r *http.Request) Ident {
return &m
}
}

249
ident/routes.go Normal file
View File

@ -0,0 +1,249 @@
package ident
import (
"context"
"fmt"
"net/http"
"go.sour.is/pkg/lg"
)
var (
loginForm = func(nick string, valid bool) string {
indicator := ""
if !valid {
indicator = `class="invalid"`
}
if nick != "" {
nick = `value="` + nick + `"`
}
return `
<form id="login" hx-post="ident/session" hx-target="#login" hx-swap="outerHTML">
<input required id="login-identity" name="identity" type="text" ` + nick + `placeholder="Identity..." />
<input required id="login-passwd" name="passwd" type="password" ` + indicator + ` placeholder="Password..." />
<button type="submit">Login</button>
<button hx-get="ident/register">Register</button>
</form>`
}
logoutForm = func(id Ident) string {
display := id.Identity()
if id, ok := id.(interface{ DisplayName() string }); ok {
display = id.DisplayName()
}
return `<button id="login" hx-delete="ident/session" hx-target="#login" hx-swap="outerHTML">` + display + ` (logout)</button>`
}
registerForm = `
<form id="login" hx-post="ident/register" hx-target="#login" hx-swap="outerHTML">
<input required id="register-display" name="displayName" type="text" placeholder="Display Name..." />
<input required id="register-identity" name="identity" type="text" placeholder="Identity..." />
<input required id="register-passwd" name="passwd" type="password" placeholder="Password..." />
<button type="submit">Register</button>
<button hx-get="ident" hx-target="#login" hx-swap="outerHTML">Close</button>
</form>`
)
type sessionIF interface {
ReadIdent(r *http.Request) (Ident, error)
CreateSession(context.Context, http.ResponseWriter, Ident) error
DestroySession(context.Context, http.ResponseWriter, Ident) error
}
type root struct {
idm *IDM
session sessionIF
}
func NewHTTP(idm *IDM, session sessionIF) *root {
idm.Add(0, session)
return &root{
idm: idm,
session: session,
}
}
func (s *root) RegisterHTTP(mux *http.ServeMux) {
mux.HandleFunc("/ident", s.sessionHTTP)
mux.HandleFunc("/ident/register", s.registerHTTP)
mux.HandleFunc("/ident/session", s.sessionHTTP)
}
func (s *root) RegisterAPIv1(mux *http.ServeMux) {
mux.HandleFunc("GET /ident", s.sessionV1)
mux.HandleFunc("POST /ident", s.registerV1)
mux.HandleFunc("/ident/session", s.sessionV1)
}
func (s *root) RegisterMiddleware(hdlr http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, span := lg.Span(r.Context())
defer span.End()
r = r.WithContext(ctx)
id, err := s.idm.ReadIdent(r)
span.RecordError(err)
if id == nil {
id = Anonymous
}
r = r.WithContext(context.WithValue(r.Context(), contextKey, id))
hdlr.ServeHTTP(w, r)
})
}
func (s *root) sessionV1(w http.ResponseWriter, r *http.Request) {
ctx, span := lg.Span(r.Context())
defer span.End()
var id Ident = FromContext(ctx)
switch r.Method {
case http.MethodGet:
if id == nil {
http.Error(w, "NO_AUTH", http.StatusUnauthorized)
return
}
fmt.Fprint(w, id)
case http.MethodPost:
if !id.Session().Active {
http.Error(w, "NO_AUTH", http.StatusUnauthorized)
return
}
err := s.session.CreateSession(ctx, w, id)
if err != nil {
span.RecordError(err)
http.Error(w, "ERR", http.StatusInternalServerError)
return
}
fmt.Fprint(w, id)
case http.MethodDelete:
if !id.Session().Active {
http.Error(w, "NO_AUTH", http.StatusUnauthorized)
return
}
err := s.session.DestroySession(ctx, w, FromContext(ctx))
if err != nil {
span.RecordError(err)
http.Error(w, "ERR", http.StatusInternalServerError)
return
}
http.Error(w, "GONE", http.StatusGone)
default:
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
}
func (s *root) registerV1(w http.ResponseWriter, r *http.Request) {
ctx, span := lg.Span(r.Context())
defer span.End()
r.ParseForm()
identity := r.Form.Get("identity")
display := r.Form.Get("displayName")
passwd, err := s.idm.Passwd([]byte(r.Form.Get("passwd")), nil)
if err != nil {
http.Error(w, "ERR", http.StatusInternalServerError)
return
}
_, err = s.idm.RegisterIdent(ctx, identity, display, passwd)
if err != nil {
http.Error(w, "ERR", http.StatusInternalServerError)
return
}
http.Error(w, "OK "+identity, http.StatusCreated)
}
func (s *root) sessionHTTP(w http.ResponseWriter, r *http.Request) {
ctx, span := lg.Span(r.Context())
defer span.End()
id := FromContext(ctx)
switch r.Method {
case http.MethodGet:
if id.Session().Active {
fmt.Fprint(w, logoutForm(id))
return
}
fmt.Fprint(w, loginForm("", true))
case http.MethodPost:
if !id.Session().Active {
http.Error(w, loginForm("", false), http.StatusOK)
return
}
err := s.session.CreateSession(ctx, w, id)
span.RecordError(err)
if err != nil {
http.Error(w, "ERROR", http.StatusInternalServerError)
return
}
fmt.Fprint(w, logoutForm(id))
case http.MethodDelete:
err := s.session.DestroySession(ctx, w, FromContext(ctx))
span.RecordError(err)
if err != nil {
http.Error(w, loginForm("", true), http.StatusUnauthorized)
return
}
fmt.Fprint(w, loginForm("", true))
default:
http.Error(w, "ERROR", http.StatusMethodNotAllowed)
}
}
func (s *root) registerHTTP(w http.ResponseWriter, r *http.Request) {
ctx, span := lg.Span(r.Context())
defer span.End()
switch r.Method {
case http.MethodGet:
fmt.Fprint(w, registerForm)
return
case http.MethodPost:
// break
default:
http.Error(w, "ERR", http.StatusMethodNotAllowed)
return
}
r.ParseForm()
identity := r.Form.Get("identity")
display := r.Form.Get("displayName")
passwd, err := s.idm.Passwd([]byte(r.Form.Get("passwd")), nil)
if err != nil {
http.Error(w, "ERR", http.StatusInternalServerError)
return
}
id, err := s.idm.RegisterIdent(ctx, identity, display, passwd)
if err != nil {
http.Error(w, "ERR", http.StatusInternalServerError)
return
}
if !id.Session().Active {
http.Error(w, loginForm("", false), http.StatusUnauthorized)
return
}
err = s.session.CreateSession(ctx, w, id)
span.RecordError(err)
if err != nil {
http.Error(w, "ERROR", http.StatusInternalServerError)
return
}
http.Error(w, logoutForm(id), http.StatusCreated)
}

280
ident/source/mercury.go Normal file
View File

@ -0,0 +1,280 @@
package source
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/mercury"
"go.sour.is/pkg/ident"
)
const identNS = "ident."
const identSFX = ".credentials"
type registry interface {
GetIndex(ctx context.Context, match, search string) (c mercury.Config, err error)
GetConfig(ctx context.Context, match, search, fields string) (mercury.Config, error)
WriteConfig(ctx context.Context, spaces mercury.Config) error
}
type mercuryIdent struct {
identity string
display string
passwd []byte
ed25519 []byte
ident.SessionInfo
}
func (id *mercuryIdent) Identity() string { return id.identity }
func (id *mercuryIdent) DisplayName() string { return id.display }
func (id *mercuryIdent) Space() string { return identNS + "@" + id.identity }
func (id *mercuryIdent) FromConfig(cfg mercury.Config) error {
if id == nil {
return fmt.Errorf("nil ident")
}
for _, s := range cfg {
if !strings.HasPrefix(s.Space, identNS) {
continue
}
if id.identity == "" {
_, id.identity, _ = strings.Cut(s.Space, ".@")
id.identity, _, _ = strings.Cut(id.identity, ".")
}
switch {
case strings.HasSuffix(s.Space, ".credentials"):
id.passwd = []byte(s.FirstValue("passwd").First())
default:
id.display = s.FirstValue("displayName").First()
}
}
return nil
}
func (id *mercuryIdent) ToConfig() mercury.Config {
space := id.Space()
return mercury.Config{
&mercury.Space{
Space: space,
List: []mercury.Value{
{
Space: space,
Seq: 1,
Name: "displayName",
Values: []string{id.display},
},
{
Space: space,
Seq: 2,
Name: "lastLogin",
Values: []string{time.UnixMilli(int64(id.Session().SessionID.Time())).Format(time.RFC3339)},
},
},
},
&mercury.Space{
Space: space + identSFX,
List: []mercury.Value{
{
Space: space + identSFX,
Seq: 1,
Name: "passwd",
Values: []string{string(id.passwd)},
},
},
},
}
}
func (id *mercuryIdent) String() string {
return "id: " + id.identity + " sp: " + id.Space() + " dn: " + id.display // + " ps: " + string(id.passwd)
}
func (id *mercuryIdent) HasRole(r ...string) bool {
return false
}
type mercurySource struct {
r registry
idm *ident.IDM
}
func NewMercury(r registry, pwd *ident.IDM) *mercurySource {
return &mercurySource{r, pwd}
}
func (s *mercurySource) ReadIdent(r *http.Request) (ident.Ident, error) {
if id, err := s.readIdentBasic(r); id != nil {
return id, err
}
if id, err := s.readIdentURL(r); id != nil {
return id, err
}
if id, err := s.readIdentHTTP(r); id != nil {
return id, err
}
return nil, fmt.Errorf("no auth")
}
func (s *mercurySource) readIdentURL(r *http.Request) (ident.Ident, error) {
ctx, span := lg.Span(r.Context())
defer span.End()
pass, ok := r.URL.User.Password()
if !ok {
return nil, nil
}
id := &mercuryIdent{
identity: r.URL.User.Username(),
passwd: []byte(pass),
}
space := id.Space()
c, err := s.r.GetConfig(ctx, "trace:"+space+identSFX, "", "")
if err != nil {
span.RecordError(err)
return id, err
}
var current mercuryIdent
current.FromConfig(c)
if len(current.passwd) == 0 {
return nil, fmt.Errorf("not registered")
}
_, err = s.idm.Passwd(id.passwd, current.passwd)
if err != nil {
return id, err
}
current.SessionInfo, err = s.idm.NewSessionInfo()
if err != nil {
return id, err
}
err = s.r.WriteConfig(ctx, current.ToConfig())
if err != nil {
return &current, err
}
return &current, nil
}
func (s *mercurySource) readIdentBasic(r *http.Request) (ident.Ident, error) {
ctx, span := lg.Span(r.Context())
defer span.End()
user, pass, ok := r.BasicAuth()
if !ok {
return nil, nil
}
id := &mercuryIdent{
identity: user,
passwd: []byte(pass),
}
space := id.Space()
c, err := s.r.GetConfig(ctx, "trace:"+space+identSFX, "", "")
if err != nil {
span.RecordError(err)
return id, err
}
var current mercuryIdent
current.FromConfig(c)
if len(current.passwd) == 0 {
return nil, fmt.Errorf("not registered")
}
_, err = s.idm.Passwd(id.passwd, current.passwd)
if err != nil {
return id, err
}
current.SessionInfo, err = s.idm.NewSessionInfo()
if err != nil {
return id, err
}
err = s.r.WriteConfig(ctx, current.ToConfig())
if err != nil {
return &current, err
}
return &current, nil
}
func (s *mercurySource) readIdentHTTP(r *http.Request) (ident.Ident, error) {
ctx, span := lg.Span(r.Context())
defer span.End()
if r.Method != http.MethodPost {
return nil, fmt.Errorf("method not allowed")
}
r.ParseForm()
id := &mercuryIdent{
identity: r.Form.Get("identity"),
passwd: []byte(r.Form.Get("passwd")),
}
if id.identity == "" {
return nil, nil
}
space := id.Space()
c, err := s.r.GetConfig(ctx, "trace:"+space+identSFX, "", "")
if err != nil {
span.RecordError(err)
return id, err
}
var current mercuryIdent
current.FromConfig(c)
if len(current.passwd) == 0 {
return nil, fmt.Errorf("not registered")
}
_, err = s.idm.Passwd(id.passwd, current.passwd)
if err != nil {
return id, err
}
current.SessionInfo, err = s.idm.NewSessionInfo()
if err != nil {
return id, err
}
err = s.r.WriteConfig(ctx, current.ToConfig())
if err != nil {
return &current, err
}
return &current, nil
}
func (s *mercurySource) RegisterIdent(ctx context.Context, identity, display string, passwd []byte) (ident.Ident, error) {
ctx, span := lg.Span(ctx)
defer span.End()
id := &mercuryIdent{identity: identity, display: display, passwd: passwd}
space := id.Space()
_, err := s.r.GetIndex(ctx, space, "")
if err != nil {
return nil, err
}
id.SessionInfo, err = s.idm.NewSessionInfo()
if err != nil {
return id, err
}
err = s.r.WriteConfig(ctx, id.ToConfig())
if err != nil {
return nil, err
}
return id, nil
}

83
ident/source/session.go Normal file
View File

@ -0,0 +1,83 @@
package source
import (
"context"
"net/http"
"time"
"github.com/oklog/ulid/v2"
"go.sour.is/pkg/ident"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/locker"
)
const CookieName = "sour.is-ident"
type sessions map[ulid.ULID]ident.Ident
type session struct {
cookieName string
sessions *locker.Locked[sessions]
}
func NewSession(cookieName string) *session {
return &session{
cookieName: cookieName,
sessions: locker.New(make(sessions)),
}
}
func (s *session) ReadIdent(r *http.Request) (ident.Ident, error) {
ctx, span := lg.Span(r.Context())
defer span.End()
cookie, err := r.Cookie(s.cookieName)
span.RecordError(err)
if err != nil {
return nil, nil
}
sessionID, err := ulid.Parse(cookie.Value)
span.RecordError(err)
var id ident.Ident = ident.Anonymous
if err == nil {
err = s.sessions.Use(ctx, func(ctx context.Context, sessions sessions) error {
if session, ok := sessions[sessionID]; ok {
id = session
}
return nil
})
}
span.RecordError(err)
return id, err
}
func (s *session) CreateSession(ctx context.Context, w http.ResponseWriter, id ident.Ident) error {
http.SetCookie(w, &http.Cookie{
Name: s.cookieName,
Value: id.Session().SessionID.String(),
Expires: time.Time{},
Path: "/",
Secure: false,
HttpOnly: true,
})
return s.sessions.Use(ctx, func(ctx context.Context, sessions sessions) error {
sessions[id.Session().SessionID] = id
return nil
})
}
func (s *session) DestroySession(ctx context.Context, w http.ResponseWriter, id ident.Ident) error {
session := id.Session()
session.Active = false
http.SetCookie(w, &http.Cookie{Name: s.cookieName, MaxAge: -1})
return s.sessions.Use(ctx, func(ctx context.Context, sessions sessions) error {
delete(sessions, session.SessionID)
return nil
})
}

View File

@ -58,17 +58,56 @@ type wrapSpan struct {
trace.Span trace.Span
} }
func LogQuery(q string, args []any, err error) (string, trace.EventOption) {
var attrs []attribute.KeyValue
for k, v := range args {
var attr attribute.KeyValue
switch v:=v.(type) {
case int64:
attr = attribute.Int64(
fmt.Sprintf("$%d", k),
v,
)
case string:
attr = attribute.String(
fmt.Sprintf("$%d", k),
v,
)
default:
attr = attribute.String(
fmt.Sprintf("$%d", k),
fmt.Sprint(v),
)
}
attrs = append(attrs, attr)
}
return q, trace.WithAttributes(attrs...)
}
func (w wrapSpan) AddEvent(name string, options ...trace.EventOption) { func (w wrapSpan) AddEvent(name string, options ...trace.EventOption) {
w.Span.AddEvent(name, options...) w.Span.AddEvent(name, options...)
cfg := trace.NewEventConfig(options...) cfg := trace.NewEventConfig(options...)
attrs := cfg.Attributes() attrs := cfg.Attributes()
args := make([]any, len(attrs)*2) args := make([]any, len(attrs))
for i, a := range attrs { for i, a := range attrs {
args[2*i] = a.Key switch a.Value.Type() {
args[2*i+1] = a.Value case attribute.BOOL:
args[i] = slog.Bool(string(a.Key), a.Value.AsBool())
case attribute.INT64:
args[i] = slog.Int64(string(a.Key), a.Value.AsInt64())
case attribute.FLOAT64:
args[i] = slog.Float64(string(a.Key), a.Value.AsFloat64())
case attribute.STRING:
args[i] = slog.String(string(a.Key), a.Value.AsString())
default:
args[i] = slog.Any(string(a.Key), a.Value.AsInterface())
}
} }
slog.Debug(name, args...) slog.Debug(name, args...)

120
libsql_embed/open.go Normal file
View File

@ -0,0 +1,120 @@
package libsqlembed
import (
"context"
"database/sql"
"database/sql/driver"
"fmt"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/tursodatabase/go-libsql"
)
func init() {
sql.Register("libsql+embed", &db{})
}
type db struct {
conns map[string]connector
mu sync.RWMutex
}
type connector struct {
*libsql.Connector
dsn string
dir string
driver *db
}
func (c *connector) Close() error {
c.driver.mu.Lock()
delete(c.driver.conns, c.dsn)
c.driver.mu.Unlock()
defer os.RemoveAll(c.dir)
if err := c.Connector.Sync(); err != nil {
return fmt.Errorf("syncing database: %w", err)
}
return c.Connector.Close()
}
func (db *db) OpenConnector(dsn string) (driver.Connector, error) {
if c, ok := func() (connector, bool) {
db.mu.RLock()
defer db.mu.RUnlock()
c, ok := db.conns[dsn]
return c, ok
}(); ok {
return c, nil
}
db.mu.Lock()
defer db.mu.Unlock()
u, err := url.Parse(dsn)
if err != nil {
return nil, err
}
var primary url.URL
primary.Scheme = strings.TrimSuffix(u.Scheme, "+embed")
primary.Host = u.Host
dbname, _, _ := strings.Cut(u.Host, ".")
authToken := u.Query().Get("authToken")
if authToken == "" {
return nil, fmt.Errorf("missing authToken")
}
opts := []libsql.Option{
libsql.WithAuthToken(authToken),
}
if refresh, err := strconv.ParseInt(u.Query().Get("refresh"),10,64); err == nil {
opts = append(opts, libsql.WithSyncInterval(time.Duration(refresh)*time.Minute))
}
if readWrite, err := strconv.ParseBool(u.Query().Get("readYourWrites")); err == nil {
opts = append(opts, libsql.WithReadYourWrites(readWrite))
}
if key := u.Query().Get("key"); key != "" {
opts = append(opts, libsql.WithEncryption(key))
}
dir, err := os.MkdirTemp("", "libsql-*")
if err != nil {
return nil, fmt.Errorf("creating temporary directory: %w", err)
}
dbPath := filepath.Join(dir, dbname)
c, err := libsql.NewEmbeddedReplicaConnector(
dbPath,
primary.String(),
opts...)
if err != nil {
return nil, fmt.Errorf("creating connector: %w", err)
}
connector := connector{c, dsn, dir, db}
db.conns[dsn] = connector
return connector, nil
}
func (db *db) Open(dsn string) (driver.Conn, error) {
c, err := db.OpenConnector(dsn)
if err != nil {
return nil, err
}
return c.Connect(context.Background())
}

157
mercury/app/app_test.go Normal file
View File

@ -0,0 +1,157 @@
package app
import (
"context"
"reflect"
"testing"
"go.sour.is/pkg/mercury"
"go.sour.is/pkg/ident"
)
type mockUser struct {
roles map[string]struct{}
ident.SessionInfo
}
func (m *mockUser) Identity() string { return "user" }
func (m *mockUser) HasRole(roles ...string) bool {
var found bool
for _, role := range roles {
if _, ok := m.roles[role]; ok {
found = true
}
}
return found
}
func Test_appConfig_GetRules(t *testing.T) {
type args struct {
u ident.Ident
}
tests := []struct {
name string
args args
wantLis mercury.Rules
}{
{"normal", args{&mockUser{}}, nil},
{
"admin",
args{
&mockUser{
SessionInfo: ident.SessionInfo{Active: true},
roles: map[string]struct{}{"admin": {}},
},
},
mercury.Rules{
mercury.Rule{
Role: "read",
Type: "NS",
Match: "mercury.source.*",
},
mercury.Rule{
Role: "read",
Type: "NS",
Match: "mercury.priority",
},
mercury.Rule{
Role: "read",
Type: "NS",
Match: "mercury.host",
},
mercury.Rule{
Role: "read",
Type: "NS",
Match: "mercury.environ",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := mercuryEnviron{}
if gotLis, _ := a.GetRules(context.TODO(), tt.args.u); !reflect.DeepEqual(gotLis, tt.wantLis) {
t.Errorf("appConfig.GetRules() = %v, want %v", gotLis, tt.wantLis)
}
})
}
}
// func Test_appConfig_GetIndex(t *testing.T) {
// type args struct {
// search mercury.NamespaceSearch
// in1 *rsql.Program
// }
// tests := []struct {
// name string
// args args
// wantLis mercury.Config
// }{
// {"nil", args{
// nil,
// nil,
// }, nil},
// {"app.settings", args{
// mercury.ParseNamespace("app.settings"),
// nil,
// }, mercury.Config{&mercury.Space{Space: "app.settings"}}},
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// a := mercuryEnviron{}
// if gotLis, _ := a.GetIndex(tt.args.search, tt.args.in1); !reflect.DeepEqual(gotLis, tt.wantLis) {
// t.Errorf("appConfig.GetIndex() = %#v, want %#v", gotLis, tt.wantLis)
// }
// })
// }
// }
// func Test_appConfig_GetObjects(t *testing.T) {
// cfg, err := mercury.ParseText(strings.NewReader(`
// @mercury.source.mercury-settings.default
// match :0 *
// `))
// type args struct {
// search mercury.NamespaceSearch
// in1 *rsql.Program
// in2 []string
// }
// tests := []struct {
// name string
// args args
// wantLis mercury.Config
// }{
// {"nil", args{
// nil,
// nil,
// nil,
// }, nil},
// {"app.settings", args{
// mercury.ParseNamespace("app.settings"),
// nil,
// nil,
// }, mercury.Config{
// &mercury.Space{
// Space: "app.settings",
// List: []mercury.Value{{
// Space: "app.settings",
// Name: "app.setting",
// Values: []string{"TRUE"}},
// },
// },
// }},
// }
// for _, tt := range tests {
// cfg, err :=
// t.Run(tt.name, func(t *testing.T) {
// a := appConfig{cfg: }
// if gotLis, _ := a.GetConfig(tt.args.search, tt.args.in1, tt.args.in2); !reflect.DeepEqual(gotLis, tt.wantLis) {
// t.Errorf("appConfig.GetIndex() = %#v, want %#v", gotLis, tt.wantLis)
// }
// })
// }
// }

View File

@ -0,0 +1,98 @@
package app
import (
"context"
"strings"
"go.sour.is/pkg/mercury"
"go.sour.is/pkg/ident"
)
type mercuryDefault struct {
name string
cfg mercury.SpaceMap
}
var (
_ mercury.GetRules = (*mercuryDefault)(nil)
_ mercury.GetIndex = (*mercuryEnviron)(nil)
_ mercury.GetConfig = (*mercuryEnviron)(nil)
_ mercury.GetRules = (*mercuryEnviron)(nil)
)
// GetRules returns default rules for user role.
func (app *mercuryDefault) GetRules(ctx context.Context, id ident.Ident) (lis mercury.Rules, err error) {
identity := id.Identity()
lis = append(lis,
mercury.Rule{
Role: "write",
Type: "NS",
Match: "mercury.@" + identity,
},
mercury.Rule{
Role: "write",
Type: "NS",
Match: "mercury.@" + identity + ".*",
},
)
groups := groups(identity, &app.cfg)
if s, ok := app.cfg.Space("mercury.policy."+app.name); ok {
for _, p := range s.List {
if groups.Has(p.Name) {
for _, r := range p.Values {
fds := strings.Fields(r)
if len(fds) < 3 {
continue
}
lis = append(lis, mercury.Rule{
Role: fds[0],
Type: fds[1],
Match: fds[2],
})
}
}
}
}
if u, ok := id.(hasRole); groups.Has("admin") || ok && u.HasRole("admin") {
lis = append(lis,
mercury.Rule{
Role: "admin",
Type: "NS",
Match: "*",
},
mercury.Rule{
Role: "write",
Type: "NS",
Match: "*",
},
mercury.Rule{
Role: "admin",
Type: "GR",
Match: "*",
},
)
} else if u.HasRole("write") {
lis = append(lis,
mercury.Rule{
Role: "write",
Type: "NS",
Match: "*",
},
)
} else if u.HasRole("read") {
lis = append(lis,
mercury.Rule{
Role: "read",
Type: "NS",
Match: "*",
},
)
}
return lis, nil
}

288
mercury/app/environ.go Normal file
View File

@ -0,0 +1,288 @@
package app
import (
"context"
"fmt"
"os"
"os/user"
"sort"
"strings"
"go.sour.is/pkg/mercury"
"go.sour.is/pkg/ident"
"go.sour.is/pkg/rsql"
"go.sour.is/pkg/set"
)
const (
mercurySource = "mercury.source.*"
mercuryPriority = "mercury.priority"
mercuryHost = "mercury.host"
appDotEnviron = "mercury.environ"
)
var (
mercuryPolicy = func(id string) string { return "mercury.@" + id + ".policy" }
)
func Register(name string, cfg mercury.SpaceMap) {
for _, c := range cfg {
c.Tags = append(c.Tags, "RO")
}
mercury.Registry.Register("mercury-default", func(s *mercury.Space) any { return &mercuryDefault{name: name, cfg: cfg} })
mercury.Registry.Register("mercury-environ", func(s *mercury.Space) any { return &mercuryEnviron{cfg: cfg, lookup: mercury.Registry.GetRules} })
}
type hasRole interface {
HasRole(r ...string) bool
}
type mercuryEnviron struct {
cfg mercury.SpaceMap
lookup func(context.Context, ident.Ident) (mercury.Rules, error)
}
// Index returns nil
func (app *mercuryEnviron) GetIndex(ctx context.Context, search mercury.NamespaceSearch, _ *rsql.Program) (lis mercury.Config, err error) {
if search.Match(mercurySource) {
for _, s := range app.cfg.ToArray() {
if search.Match(s.Space) {
lis = append(lis, &mercury.Space{Space: s.Space, Tags: []string{"RO"}})
}
}
}
if search.Match(mercuryPriority) {
lis = append(lis, &mercury.Space{Space: mercuryPriority, Tags: []string{"RO"}})
}
if search.Match(mercuryHost) {
lis = append(lis, &mercury.Space{Space: mercuryHost, Tags: []string{"RO"}})
}
if search.Match(appDotEnviron) {
lis = append(lis, &mercury.Space{Space: appDotEnviron, Tags: []string{"RO"}})
}
if id := ident.FromContext(ctx); id != nil {
identity := id.Identity()
match := mercuryPolicy(identity)
if search.Match(match) {
lis = append(lis, &mercury.Space{Space: match, Tags: []string{"RO"}})
}
}
return
}
// Objects returns nil
func (app *mercuryEnviron) GetConfig(ctx context.Context, search mercury.NamespaceSearch, _ *rsql.Program, _ []string) (lis mercury.Config, err error) {
if search.Match(mercurySource) {
for _, s := range app.cfg.ToArray() {
if search.Match(s.Space) {
lis = append(lis, s)
}
}
}
if search.Match(mercuryPriority) {
space := mercury.Space{
Space: mercuryPriority,
Tags: []string{"RO"},
}
// for i, key := range mercury.Registry {
// space.List = append(space.List, mercury.Value{
// Space: appDotPriority,
// Seq: uint64(i),
// Name: key.Match,
// Values: []string{fmt.Sprint(key.Priority)},
// })
// }
lis = append(lis, &space)
}
if search.Match(mercuryHost) {
if usr, err := user.Current(); err == nil {
space := mercury.Space{
Space: mercuryHost,
Tags: []string{"RO"},
}
hostname, _ := os.Hostname()
wd, _ := os.Getwd()
grp, _ := usr.GroupIds()
space.List = []mercury.Value{
{
Space: mercuryHost,
Seq: 1,
Name: "hostname",
Values: []string{hostname},
},
{
Space: mercuryHost,
Seq: 2,
Name: "username",
Values: []string{usr.Username},
},
{
Space: mercuryHost,
Seq: 3,
Name: "uid",
Values: []string{usr.Uid},
},
{
Space: mercuryHost,
Seq: 4,
Name: "gid",
Values: []string{usr.Gid},
},
{
Space: mercuryHost,
Seq: 5,
Name: "display",
Values: []string{usr.Name},
},
{
Space: mercuryHost,
Seq: 6,
Name: "home",
Values: []string{usr.HomeDir},
},
{
Space: mercuryHost,
Seq: 7,
Name: "groups",
Values: grp,
},
{
Space: mercuryHost,
Seq: 8,
Name: "pid",
Values: []string{fmt.Sprintf("%v", os.Getpid())},
},
{
Space: mercuryHost,
Seq: 9,
Name: "wd",
Values: []string{wd},
},
}
lis = append(lis, &space)
}
}
if search.Match(appDotEnviron) {
env := os.Environ()
space := mercury.Space{
Space: appDotEnviron,
Tags: []string{"RO"},
}
sort.Strings(env)
for i, s := range env {
key, val, _ := strings.Cut(s, "=")
vals := []string{val}
if strings.Contains(key, "PATH") || strings.Contains(key, "XDG") {
vals = strings.Split(val, ":")
}
space.List = append(space.List, mercury.Value{
Space: appDotEnviron,
Seq: uint64(i),
Name: key,
Values: vals,
})
}
lis = append(lis, &space)
}
if id := ident.FromContext(ctx); id != nil {
identity := id.Identity()
groups := groups(identity, &app.cfg)
match := mercuryPolicy(identity)
if search.Match(match) {
space := &mercury.Space{
Space: match,
Tags: []string{"RO"},
}
lis = append(lis, space)
rules, err := app.lookup(ctx, id)
if err != nil {
space.AddNotes(err.Error())
} else {
k := mercury.NewValue("groups")
k.AddValues(strings.Join(groups.Values(), " "))
space.AddKeys(k)
k = mercury.NewValue("rules")
for _, r := range rules {
k.AddValues(strings.Join([]string{r.Role, r.Type, r.Match}, " "))
}
space.AddKeys(k)
}
}
}
return
}
// Rules returns nil
func (app *mercuryEnviron) GetRules(ctx context.Context, id ident.Ident) (lis mercury.Rules, err error) {
identity := id.Identity()
lis = append(lis,
mercury.Rule{
Role: "read",
Type: "NS",
Match: mercuryPolicy(identity),
},
)
groups := groups(identity, &app.cfg)
if u, ok := id.(hasRole); groups.Has("admin") || ok && u.HasRole("admin") {
lis = append(lis,
mercury.Rule{
Role: "read",
Type: "NS",
Match: mercurySource,
},
mercury.Rule{
Role: "read",
Type: "NS",
Match: mercuryPriority,
},
mercury.Rule{
Role: "read",
Type: "NS",
Match: mercuryHost,
},
mercury.Rule{
Role: "read",
Type: "NS",
Match: appDotEnviron,
},
)
}
return lis, nil
}
func groups(identity string, cfg *mercury.SpaceMap) set.Set[string] {
groups := set.New[string]()
if s, ok := cfg.Space("mercury.groups"); ok {
for _, g := range s.List {
for _, v := range g.Values {
for _, u := range strings.Fields(v) {
if u == identity {
groups.Add(g.Name)
}
}
}
}
}
return groups
}

63
mercury/http/notify.go Normal file
View File

@ -0,0 +1,63 @@
package http
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/mercury"
)
type httpNotify struct{}
func (httpNotify) SendNotify(ctx context.Context, n mercury.Notify) error {
ctx, span := lg.Span(ctx)
defer span.End()
cl := &http.Client{}
caCertPool, err := x509.SystemCertPool()
if err != nil {
caCertPool = x509.NewCertPool()
}
// Setup HTTPS client
tlsConfig := &tls.Config{
RootCAs: caCertPool,
}
transport := &http.Transport{TLSClientConfig: tlsConfig}
cl.Transport = transport
var req *http.Request
req, err = http.NewRequestWithContext(ctx, n.Method, n.URL, bytes.NewBufferString(""))
if err != nil {
span.RecordError(err)
return err
}
req.Header.Set("content-type", "application/json")
span.AddEvent(fmt.Sprint("URL: ", n.URL))
res, err := cl.Do(req)
if err != nil {
span.RecordError(err)
return err
}
res.Body.Close()
span.AddEvent(fmt.Sprint(res.Status))
if res.StatusCode != 200 {
span.RecordError(err)
err = fmt.Errorf("unable to read config")
return err
}
return nil
}
func Register() {
mercury.Registry.Register("http-notify", func(s *mercury.Space) any { return httpNotify{} })
}

727
mercury/mercury.go Normal file
View File

@ -0,0 +1,727 @@
package mercury
import (
"fmt"
"path/filepath"
"strings"
"golang.org/x/exp/maps"
)
type Config []*Space
func NewConfig(spaces ...*Space) Config {
return spaces
}
func (c *Config) AddSpace(spaces ...*Space) *Config {
*c = append(*c, spaces...)
return c
}
// Len implements Len for sort.interface
func (lis Config) Len() int {
return len(lis)
}
// Less implements Less for sort.interface
func (lis Config) Less(i, j int) bool {
return lis[i].Space < lis[j].Space
}
// Swap implements Swap for sort.interface
func (lis Config) Swap(i, j int) { lis[i], lis[j] = lis[j], lis[i] }
// StringList returns the space names as a list
func (lis Config) StringList() string {
var buf strings.Builder
for _, o := range lis {
if len(o.Notes) > 0 {
buf.WriteString("# ")
buf.WriteString(strings.Join(o.Notes, "\n# "))
buf.WriteRune('\n')
}
buf.WriteRune('@')
buf.WriteString(o.Space)
if len(o.Tags) > 0 {
buf.WriteRune(' ')
buf.WriteString(strings.Join(o.Tags, " "))
}
buf.WriteRune('\n')
}
return buf.String()
}
// ToSpaceMap formats as SpaceMap
func (lis Config) ToSpaceMap() SpaceMap {
out := make(SpaceMap)
for _, c := range lis {
out[c.Space] = c
}
return out
}
// String format config as string
func (lis Config) String() string {
var buf strings.Builder
for i, o := range lis {
attLen := 0
tagLen := 0
if i > 0 {
buf.WriteRune('\n')
}
for _, v := range o.List {
l := len(v.Name)
if attLen <= l {
attLen = l
}
t := len(strings.Join(v.Tags, " "))
if tagLen <= t {
tagLen = t
}
}
if len(o.Notes) > 0 {
buf.WriteString("# ")
buf.WriteString(strings.Join(o.Notes, "\n# "))
buf.WriteRune('\n')
}
buf.WriteRune('@')
buf.WriteString(o.Space)
if len(o.Tags) > 0 {
buf.WriteRune(' ')
buf.WriteString(strings.Join(o.Tags, " "))
}
buf.WriteRune('\n')
for _, v := range o.List {
if len(v.Notes) > 0 {
buf.WriteString("# ")
buf.WriteString(strings.Join(v.Notes, "\n# "))
buf.WriteString("\n")
}
buf.WriteString(v.Name)
buf.WriteString(strings.Repeat(" ", attLen-len(v.Name)+1))
if len(v.Tags) > 0 {
t := strings.Join(v.Tags, " ")
buf.WriteString(t)
buf.WriteString(strings.Repeat(" ", tagLen-len(t)+1))
} else {
buf.WriteString(strings.Repeat(" ", tagLen+1))
}
switch len(v.Values) {
case 0:
buf.WriteString("\n")
case 1:
buf.WriteString(" :")
buf.WriteString(v.Values[0])
buf.WriteString("\n")
default:
buf.WriteString(" :")
buf.WriteString(v.Values[0])
buf.WriteString("\n")
for _, s := range v.Values[1:] {
buf.WriteString(strings.Repeat(" ", attLen+tagLen+3))
buf.WriteString(":")
buf.WriteString(s)
buf.WriteString("\n")
}
}
}
for _, line := range o.Trailer {
buf.WriteString(line)
buf.WriteRune('\n')
}
}
return buf.String()
}
// EnvString format config as environ
func (lis Config) EnvString() string {
var buf strings.Builder
for _, o := range lis {
for _, v := range o.List {
buf.WriteString(o.Space)
for _, t := range o.Tags {
buf.WriteRune(' ')
buf.WriteString(t)
}
buf.WriteRune(':')
buf.WriteString(v.Name)
for _, t := range v.Tags {
buf.WriteRune(' ')
buf.WriteString(t)
}
switch len(v.Values) {
case 0:
buf.WriteRune('=')
buf.WriteRune('\n')
case 1:
buf.WriteRune('=')
buf.WriteString(v.Values[0])
buf.WriteRune('\n')
default:
buf.WriteRune('+')
buf.WriteRune('=')
buf.WriteString(v.Values[0])
buf.WriteRune('\n')
for _, s := range v.Values[1:] {
buf.WriteString(o.Space)
buf.WriteRune(':')
buf.WriteString(v.Name)
buf.WriteRune('+')
buf.WriteRune('=')
buf.WriteString(s)
buf.WriteRune('\n')
}
}
}
}
return buf.String()
}
// INIString format config as ini
func (lis Config) INIString() string {
var buf strings.Builder
for _, o := range lis {
for _, note := range o.Notes {
buf.WriteString("; ")
buf.WriteString(note)
buf.WriteRune('\n')
}
buf.WriteRune('[')
buf.WriteString(o.Space)
buf.WriteRune(']')
buf.WriteRune('\n')
for _, v := range o.List {
for _, note := range v.Notes {
buf.WriteString("; ")
buf.WriteString(note)
buf.WriteRune('\n')
}
buf.WriteString(v.Name)
switch len(v.Values) {
case 0:
buf.WriteRune('=')
buf.WriteRune('\n')
case 1:
buf.WriteRune('=')
buf.WriteString(v.Values[0])
buf.WriteRune('\n')
default:
buf.WriteRune('[')
buf.WriteRune('0')
buf.WriteRune(']')
buf.WriteRune('=')
buf.WriteString(v.Values[0])
buf.WriteRune('\n')
for i, s := range v.Values[1:] {
buf.WriteString(v.Name)
buf.WriteRune('[')
buf.WriteString(fmt.Sprintf("%d", i))
buf.WriteRune(']')
buf.WriteRune('=')
buf.WriteString(s)
buf.WriteRune('\n')
}
}
}
for _, line := range o.Trailer {
buf.WriteString("; ")
buf.WriteString(line)
buf.WriteRune('\n')
}
buf.WriteRune('\n')
}
return buf.String()
}
// String format config as string
func (lis Config) HTMLString() string {
var buf strings.Builder
for i, o := range lis {
attLen := 0
tagLen := 0
if i > 0 {
buf.WriteRune('\n')
}
for _, v := range o.List {
l := len(v.Name)
if attLen <= l {
attLen = l
}
t := len(strings.Join(v.Tags, " "))
if tagLen <= t {
tagLen = t
}
}
if len(o.Notes) > 0 {
buf.WriteString("<i>")
buf.WriteString("# ")
buf.WriteString(strings.Join(o.Notes, "\n# "))
buf.WriteString("</i>")
buf.WriteRune('\n')
}
buf.WriteString("<strong>")
buf.WriteRune('@')
buf.WriteString(o.Space)
buf.WriteString("</strong>")
if len(o.Tags) > 0 {
buf.WriteRune(' ')
buf.WriteString("<em>")
buf.WriteString(strings.Join(o.Tags, " "))
buf.WriteString("</em>")
}
buf.WriteRune('\n')
for _, v := range o.List {
if len(v.Notes) > 0 {
buf.WriteString("<i>")
buf.WriteString("# ")
buf.WriteString(strings.Join(v.Notes, "\n# "))
buf.WriteString("</i>")
buf.WriteString("\n")
}
buf.WriteString("<dfn>")
buf.WriteString(v.Name)
buf.WriteString("</dfn>")
buf.WriteString(strings.Repeat(" ", attLen-len(v.Name)+1))
if len(v.Tags) > 0 {
t := strings.Join(v.Tags, " ")
buf.WriteString("<em>")
buf.WriteString(t)
buf.WriteString(strings.Repeat(" ", tagLen-len(t)+1))
buf.WriteString("</em>")
} else {
buf.WriteString(strings.Repeat(" ", tagLen+1))
}
switch len(v.Values) {
case 0:
buf.WriteString("\n")
case 1:
buf.WriteString(" :")
buf.WriteString(v.Values[0])
buf.WriteString("\n")
default:
buf.WriteString(" :")
buf.WriteString(v.Values[0])
buf.WriteString("\n")
for _, s := range v.Values[1:] {
buf.WriteString(strings.Repeat(" ", attLen+tagLen+3))
buf.WriteString(":")
buf.WriteString(s)
buf.WriteString("\n")
}
}
}
for _, line := range o.Trailer {
buf.WriteString("<small>")
buf.WriteString(line)
buf.WriteString("</small>")
buf.WriteRune('\n')
}
}
return buf.String()
}
// Space stores a registry of spaces
type Space struct {
Space string `json:"space"`
Tags []string `json:"tags,omitempty"`
Notes []string `json:"notes,omitempty"`
List []Value `json:"list,omitempty"`
Trailer []string `json:"trailer,omitempty"`
}
func NewSpace(space string) *Space {
return &Space{Space: space}
}
// HasTag returns true if needle is found
// If the needle ends with a / it will be treated
// as a prefix for tag meta data.
func (s *Space) HasTag(needle string) bool {
isPrefix := strings.HasSuffix(needle, "/")
for i := range s.Tags {
switch isPrefix {
case true:
if strings.HasPrefix(s.Tags[i], needle) {
return true
}
case false:
if s.Tags[i] == needle {
return true
}
}
}
return false
}
// GetTagMeta retuns the value after a '/' in a tag.
// Tags are in the format 'name' or 'name/value'
// This function returns the value.
func (s *Space) GetTagMeta(needle string, offset int) string {
if !strings.HasSuffix(needle, "/") {
needle += "/"
}
for i := range s.Tags {
if strings.HasPrefix(s.Tags[i], needle) {
if offset > 0 {
offset--
continue
}
return strings.TrimPrefix(s.Tags[i], needle)
}
}
return ""
}
// FirstTagMeta returns the first meta tag value.
func (s *Space) FirstTagMeta(needle string) string {
return s.GetTagMeta(needle, 0)
}
// GetValues that match name
func (s *Space) GetValues(name string) (lis []Value) {
for i := range s.List {
if s.List[i].Name == name {
lis = append(lis, s.List[i])
}
}
return
}
// FirstValue that matches name
func (s *Space) FirstValue(name string) Value {
for i := range s.List {
if s.List[i].Name == name {
return s.List[i]
}
}
return Value{}
}
func (s *Space) SetTags(tags ...string) *Space {
s.Tags = tags
return s
}
func (s *Space) AddTags(tags ...string) *Space {
s.Tags = append(s.Tags, tags...)
return s
}
func (s *Space) SetNotes(notes ...string) *Space {
s.Notes = notes
return s
}
func (s *Space) AddNotes(notes ...string) *Space {
s.Notes = append(s.Notes, notes...)
return s
}
func (s *Space) SetKeys(keys ...*Value) *Space {
for i := range keys {
k := *keys[i]
k.Seq = uint64(i)
s.List = append(s.List, k)
}
return s
}
func (s *Space) AddKeys(keys ...*Value) *Space {
l := uint64(len(s.List))
for i := range keys {
k := *keys[i]
k.Seq = uint64(i) + l
s.List = append(s.List, k)
}
return s
}
// SpaceMap generic map of space values
type SpaceMap map[string]*Space
func (m SpaceMap) Space(name string) (*Space, bool) {
s, ok := m[name]
return s, ok
}
// Rule is a type of rule
type Rule struct {
Role string
Type string
Match string
}
// Rules is a list of rules
type Rules []Rule
// GetNamespaceSearch returns a default search for users rules.
func (r Rules) GetNamespaceSearch() (lis NamespaceSearch) {
for _, o := range r {
if o.Type == "NS" && (o.Role == "read" || o.Role == "write") {
lis = append(lis, NamespaceStar(o.Match))
}
}
return
}
// Check if name matches rule
func (r Rule) Check(name string) bool {
ok, err := filepath.Match(r.Match, name)
if err != nil {
return false
}
return ok
}
// CheckNamespace verifies user has access
func (r Rules) CheckNamespace(search NamespaceSearch) bool {
for _, ns := range search {
if !r.GetRoles("NS", ns.Value()).HasRole("read", "write") {
return false
}
}
return true
}
func (r Rules) Less(i, j int) bool {
si, sj := scoreRule(r[i]), scoreRule(r[j])
if si != sj {
return si < sj
}
return len(r[i].Match) < len(r[j].Match)
}
func (r Rules) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r Rules) Len() int { return len(r) }
func scoreRule(r Rule) int {
score := 0
if r.Type == "GR" {
score += 1000
}
switch r.Role {
case "admin":
score += 100
case "write":
score += 50
case "read":
score += 10
}
return score
}
// ReduceSearch verifies user has access
func (r Rules) ReduceSearch(search NamespaceSearch) (out NamespaceSearch) {
rules := r.GetNamespaceSearch()
skip := make(map[string]struct{})
out = make(NamespaceSearch, 0, len(rules))
for _, rule := range rules {
if _, ok := skip[rule.Raw()]; ok {
continue
}
for _, ck := range search {
if _, ok := skip[ck.Raw()]; ok {
continue
} else if rule.Match(ck.Raw()) {
skip[ck.Raw()] = struct{}{}
out = append(out, ck)
} else if ck.Match(rule.Raw()) {
out = append(out, rule)
}
}
}
return
}
// Roles is a list of roles for a resource
type Roles map[string]struct{}
// GetRoles returns a list of Roles
func (r Rules) GetRoles(typ, name string) (lis Roles) {
lis = make(Roles)
for _, o := range r {
if typ == o.Type && o.Check(name) {
lis[o.Role] = struct{}{}
}
}
return
}
// HasRole is a valid role
func (r Roles) HasRole(roles ...string) bool {
for _, role := range roles {
if _, ok := r[role]; ok {
return true
}
}
return false
}
// ToArray converts SpaceMap to ArraySpace
func (m SpaceMap) ToArray() Config {
a := make(Config, 0, len(m))
for _, s := range m {
a = append(a, s)
}
return a
}
func (m *SpaceMap) MergeMap(s SpaceMap) {
m.Merge(maps.Values(s)...)
}
func (m *SpaceMap) Merge(lis ...*Space) {
for _, s := range lis {
// Only accept first version based on priority.
if _, ok := (*m)[s.Space]; ok {
continue
}
(*m)[s.Space] = s
// // Merge values together.
// c, ok := (*m)[s.Space]
// if ok {
// c = &Space{}
// }
// c.Notes = append(c.Notes, s.Notes...)
// c.Tags = append(c.Tags, s.Tags...)
// last := c.List[len(c.List)-1].Seq
// for i := range s.List {
// v := s.List[i]
// v.Seq += last
// c.List = append(c.List, v)
// }
// (*m)[s.Space] = c
}
}
// Value stores the attributes for space values
type Value struct {
Space string `json:"-" db:"space"`
Seq uint64 `json:"-" db:"seq"`
Name string `json:"name"`
Values []string `json:"values"`
Notes []string `json:"notes"`
Tags []string `json:"tags"`
}
// func (v *Value) ID() string {
// return gql.FmtID("MercurySpace:%v:%v", v.Space, v.Seq)
// }
// HasTag returns true if needle is found
// If the needle ends with a / it will be treated
// as a prefix for tag meta data.
func (v Value) HasTag(needle string) bool {
isPrefix := strings.HasSuffix(needle, "/")
for i := range v.Tags {
switch isPrefix {
case true:
if strings.HasPrefix(v.Tags[i], needle) {
return true
}
case false:
if v.Tags[i] == needle {
return true
}
}
}
return false
}
// GetTagMeta retuns the value after a '/' in a tag.
// Tags are in the format 'name' or 'name/value'
// This function returns the value.
func (v Value) GetTagMeta(needle string, offset int) string {
if !strings.HasSuffix(needle, "/") {
needle += "/"
}
for i := range v.Tags {
if strings.HasPrefix(v.Tags[i], needle) {
if offset > 0 {
offset--
continue
}
return strings.TrimPrefix(v.Tags[i], needle)
}
}
return ""
}
// FirstTagMeta returns the first meta tag value.
func (v Value) FirstTagMeta(needle string) string {
return v.GetTagMeta(needle, 0)
}
// First value in array.
func (v Value) First() string {
if len(v.Values) < 1 {
return ""
}
return v.Values[0]
}
// Join values with newlines.
func (v Value) Join() string {
return strings.Join(v.Values, "\n")
}
func NewValue(name string) *Value {
return &Value{Name: name}
}
func (v *Value) SetTags(tags ...string) *Value {
v.Tags = tags
return v
}
func (v *Value) AddTags(tags ...string) *Value {
v.Tags = append(v.Tags, tags...)
return v
}
func (v *Value) SetNotes(notes ...string) *Value {
v.Notes = notes
return v
}
func (v *Value) AddNotes(notes ...string) *Value {
v.Notes = append(v.Notes, notes...)
return v
}
func (v *Value) SetValues(values ...string) *Value {
v.Values = values
return v
}
func (v *Value) AddValues(values ...string) *Value {
v.Values = append(v.Values, values...)
return v
}

27
mercury/mqtt/notify.go Normal file
View File

@ -0,0 +1,27 @@
package mqtt
import (
"context"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/mercury"
)
type mqttNotify struct{}
func (mqttNotify) SendNotify(ctx context.Context, n mercury.Notify) {
_, span := lg.Span(ctx)
defer span.End()
// var m mqtt.Message
// m, err = mqtt.NewMessage(n.URL, n)
// if err != nil {
// return
// }
// log.Debug(n)
// err = mqtt.Publish(m)
// return
}
func Register() {
mercury.Registry.Register("mqtt-notify", func(s *mercury.Space) any { return &mqttNotify{} })
}

119
mercury/namespace.go Normal file
View File

@ -0,0 +1,119 @@
package mercury
import (
"path/filepath"
"strings"
)
// NamespaceSpec implements a parsed namespace search
type NamespaceSpec interface {
Value() string
String() string
Raw() string
Match(string) bool
}
// NamespaceSearch list of namespace specs
type NamespaceSearch []NamespaceSpec
// ParseNamespace returns a list of parsed values
func ParseNamespace(ns string) (lis NamespaceSearch) {
for _, part := range strings.Split(ns, ";") {
if strings.HasPrefix(part, "trace:") {
lis = append(lis, NamespaceTrace(part[6:]))
} else if strings.Contains(part, "*") {
lis = append(lis, NamespaceStar(part))
} else {
lis = append(lis, NamespaceNode(part))
}
}
return
}
// String output string value
func (n NamespaceSearch) String() string {
lis := make([]string, 0, len(n))
for _, v := range n {
lis = append(lis, v.String())
}
return strings.Join(lis, ";")
}
// Match returns true if any match.
func (n NamespaceSearch) Match(s string) bool {
for _, m := range n {
ok, err := filepath.Match(m.Raw(), s)
if err != nil {
return false
}
if ok {
return true
}
}
return false
}
// NamespaceNode implements a node search value
type NamespaceNode string
// String output string value
func (n NamespaceNode) String() string { return string(n) }
// Quote return quoted value.
// func (n NamespaceNode) Quote() string { return `'` + n.Value() + `'` }
// Value to return the value
func (n NamespaceNode) Value() string { return string(n) }
// Raw return raw value.
func (n NamespaceNode) Raw() string { return string(n) }
// Match returns true if any match.
func (n NamespaceNode) Match(s string) bool { return match(n, s) }
// NamespaceTrace implements a trace search value
type NamespaceTrace string
// String output string value
func (n NamespaceTrace) String() string { return "trace:" + string(n) }
// Quote return quoted value.
// func (n NamespaceTrace) Quote() string { return `'` + n.Value() + `'` }
// Value to return the value
func (n NamespaceTrace) Value() string { return strings.Replace(string(n), "*", "%", -1) }
// Raw return raw value.
func (n NamespaceTrace) Raw() string { return string(n) }
// Match returns true if any match.
func (n NamespaceTrace) Match(s string) bool { return match(n, s) }
// NamespaceStar implements a trace search value
type NamespaceStar string
// String output string value
func (n NamespaceStar) String() string { return string(n) }
// Quote return quoted value.
// func (n NamespaceStar) Quote() string { return `'` + n.Value() + `'` }
// Value to return the value
func (n NamespaceStar) Value() string { return strings.Replace(string(n), "*", "%", -1) }
// Raw return raw value.
func (n NamespaceStar) Raw() string { return string(n) }
// Match returns true if any match.
func (n NamespaceStar) Match(s string) bool { return match(n, s) }
func match(n NamespaceSpec, s string) bool {
ok, err := filepath.Match(n.Raw(), s)
if err != nil {
return false
}
return ok
}

59
mercury/namespace_test.go Normal file
View File

@ -0,0 +1,59 @@
package mercury_test
import (
"fmt"
"testing"
"github.com/matryer/is"
"go.sour.is/pkg/mercury"
sq "github.com/Masterminds/squirrel"
)
func TestNamespaceParse(t *testing.T) {
var tests = []struct {
in string
out string
args []any
}{
{
in: "d42.bgp.kapha.*;trace:d42.bgp.kapha",
out: "(column LIKE ? OR ? LIKE column || '%')",
args: []any{"d42.bgp.kapha.%", "d42.bgp.kapha"},
},
{
in: "d42.bgp.kapha.*,d42.bgp.kapha",
out: "(column LIKE ? OR column = ?)",
args: []any{"d42.bgp.kapha.%", "d42.bgp.kapha"},
},
}
for i, tt := range tests {
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
is := is.New(t)
out := mercury.ParseNamespace(tt.in)
sql, args, err := getWhere(out).ToSql()
is.NoErr(err)
is.Equal(sql, tt.out)
is.Equal(args, tt.args)
})
}
}
func getWhere(search mercury.NamespaceSearch) sq.Sqlizer {
var where sq.Or
space := "column"
for _, m := range search {
switch m.(type) {
case mercury.NamespaceNode:
where = append(where, sq.Eq{space: m.Value()})
case mercury.NamespaceStar:
where = append(where, sq.Like{space: m.Value()})
case mercury.NamespaceTrace:
e := sq.Expr(`? LIKE `+space+` || '%'`, m.Value())
where = append(where, e)
}
}
return where
}

127
mercury/parse.go Normal file
View File

@ -0,0 +1,127 @@
package mercury
import (
"bufio"
"io"
"log"
"strings"
)
func ParseText(body io.Reader) (config SpaceMap, err error) {
config = make(SpaceMap)
var space string
var name string
var tags []string
var notes []string
var seq uint64
scanner := bufio.NewScanner(body)
for scanner.Scan() {
line := scanner.Text()
if len(line) == 0 {
continue
}
if strings.HasPrefix(line, "#") {
notes = append(notes, strings.TrimPrefix(line, "# "))
continue
}
if strings.HasPrefix(line, "@") {
var c *Space
var ok bool
sp := strings.Fields(strings.TrimPrefix(line, "@"))
space = sp[0]
if c, ok = config[space]; !ok {
c = &Space{Space: space}
}
c.Notes = append(make([]string, 0, len(notes)), notes...)
c.Tags = append(make([]string, 0, len(sp[1:])), sp[1:]...)
config[space] = c
notes = notes[:0]
tags = tags[:0]
continue
}
if strings.HasPrefix(line, "----") && strings.HasSuffix(line, "----") {
var trailer []string
trailer = append(trailer, line)
for scanner.Scan() {
line = scanner.Text()
trailer = append(trailer, line)
if strings.HasPrefix(line, "----") && strings.HasSuffix(line, "----") {
break
}
}
c, ok := config[space]
if !ok {
c = &Space{Space: space}
}
log.Println(trailer)
c.Trailer = append(c.Trailer, trailer...)
config[space] = c
continue
}
if space == "" {
continue
}
sp := strings.SplitN(line, ":", 2)
if len(sp) < 2 {
continue
}
if strings.TrimSpace(sp[0]) == "" {
c, ok := config[space]
if !ok {
c = &Space{Space: space}
}
c.List[len(c.List)-1].Values = append(c.List[len(c.List)-1].Values, sp[1])
config[space] = c
continue
}
fields := strings.Fields(sp[0])
name = fields[0]
if len(fields) > 1 {
tags = fields[1:]
}
c, ok := config[space]
if !ok {
c = &Space{Space: space}
}
seq++
c.List = append(
c.List,
Value{
Seq: seq,
Name: name,
Tags: append(make([]string, 0, len(tags)), tags...),
Notes: append(make([]string, 0, len(notes)), notes...),
Values: []string{sp[1]},
},
)
config[space] = c
notes = notes[:0]
tags = tags[:0]
}
if err = scanner.Err(); err != nil {
return nil, err
}
return
}

28
mercury/parse_test.go Normal file
View File

@ -0,0 +1,28 @@
package mercury_test
import (
"strings"
"testing"
"github.com/matryer/is"
"go.sour.is/pkg/mercury"
)
func TestParseText(t *testing.T) {
is := is.New(t)
sm, err := mercury.ParseText(strings.NewReader(`
@test.sign
key :value1
-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZ+OuJYdd3UiUbyBuO1RlsQR20a
Qm5mKneuMxRjGo3zkAAAAEZmlsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtzc2gtZWQyNTUx
OQAAAED8T4C6WILXYZ1KxqDIlVhlrAEjr1Vc+tn8ypcVM3bN7iOexVvuUuvm90nr8eEwKU
acrdDxmq2S+oysQbK+pMUE
-----END SSH SIGNATURE-----
`))
is.NoErr(err)
for _, c := range sm {
is.Equal(len(c.Trailer), 6)
}
}

BIN
mercury/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

47
mercury/public/index.html Normal file
View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<title>☿ Mercury ☿</title>
<script src="https://unpkg.com/htmx.org@2.0.0-alpha1/dist/htmx.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css" />
<link rel="stylesheet" href="/style.css" />
</head>
<body>
<header>
<nav style="position: absolute; top:0; right:50px" hx-trigger="load" hx-get="/ident"></nav>
<h1>☿ Mercury ☿</h1>
</header>
<div class="container">
<div>
<form class="search" hx-get="/api/v1/mercury/config" hx-target="#config-results"
hx-headers='{"Accept": "text/html"}''>
<div>@</div>
<input id="space-config" name="space" type="text" placeholder="Space...">
<button type="submit">Load</button>
</form>
<code tabindex="0"><pre id="config-results"></pre></code>
</div>
<div>
<form class="edit" hx-post="/api/v1/mercury/config" hx-target="#space-saved" hx-encoding="multipart/form-data">
<button type="submit">Save</button>
<br />
<textarea name="content" rows="45" wrap="off"
onkeyup="if (this.scrollHeight > this.clientHeight) this.style.height = this.scrollHeight + ' px';"
style="overflow:auto; transition: height 0.2s ease-out;"></textarea>
</form>
<pre id="space-saved"></pre>
</div>
</div>
<footer>
sour.is 🅭2024
<span hx-trigger="load" hx-get="/api/v1/app-info"></span>
</footer>
</body>
</html>

218
mercury/public/style.css Normal file
View File

@ -0,0 +1,218 @@
* {
font-weight: lighter;
font-family: 'fira code', monospace;
}
body {
margin: 0;
min-height: 100vh;
background: rgb(210, 221, 240);
}
header {
margin: 0 50px;
background: rgb(63, 94, 251);
background: radial-gradient(circle, rgba(63, 94, 251, 1) 0%, rgba(252, 70, 107, 1) 100%);
height: 100px;
user-select: none;
}
h1 {
text-align: center;
color: white;
line-height: 100px;
margin: 0;
}
input,
textarea {
font-size: small;
border: 2px solid cornflowerblue;
}
input.invalid {
border-color: red;
}
button {
border: 2px solid cornflowerblue;
}
code,
pre {
font-size: small;
-webkit-user-select: text;
user-select: text;
}
code strong {
font-weight: bold;
}
code dfn {
color: green
}
code i {
color: grey
}
code em {
color: orangered;
}
code small {
font-size-adjust: 50%;
color: orange;
}
code:focus {
animation: select 100ms step-end forwards;
}
footer {
position: fixed;
font-size: x-small;
bottom: 0;
left: 0;
right: 0;
height: 1em;
padding: 4px;
color: white;
border-top: 1px solid black;
background: cornflowerblue;
}
footer>span {
float: right;
}
.container {
margin: 0 50px;
display: grid;
min-height: 100vh;
}
.container .open {
grid-template-columns: 1fr 1fr;
}
.container>div {
overflow: auto;
padding: 10px;
background-color: white;
border: 0px ;
}
.container>div>code {
-webkit-user-select: all;
/* for Safari */
user-select: all;
}
.search {
display: flex
}
.search>div {
background-color: lightgrey;
border-radius: 5px 0px 0px 5px;
border: 2px solid cornflowerblue;
border-right: 0;
user-select: none;
padding: 0 2px;
}
.search>button {
border-left: 0;
}
.search>div {
line-height: 30px;
}
.search>input {
flex-grow: 5;
}
.search>button {
flex-grow: 2;
border-radius: 0px 5px 5px 0px;
}
.edit {
display: grid;
grid-template-columns: 1fr;
}
.edit>button {
line-height: 28px;
border-bottom: 0;
border-radius: 5px 5px 0 0;
}
.edit>textarea {
margin-top: 0;
border-radius: 0 0 5px 5px;
}
@keyframes select {
to {
-webkit-user-select: text;
user-select: text;
}
}
@media (prefers-color-scheme: dark) {
html, body {
color: white;
background: #111
}
header {
background: #111;
background: radial-gradient(circle, rgba(2, 0, 36, 1) 0%, rgba(7, 80, 29, 1) 35%, rgba(0, 0, 0, 1) 100%);
}
h1 {
color: white;
}
.container>div {
background: #111;
/* background: linear-gradient(304deg, rgba(2, 0, 36, 1) 0%, rgba(77, 77, 77, 1) 18%, rgba(0, 0, 0, 1) 100%); */
border: 2px solid rgb(117, 117, 117);
}
.search>div {
background-color: grey;
border-color: rgb(117, 117, 117);
}
button {
color: white;
background: rgba(7, 80, 29, 1);
border: 2px solid rgb(117, 117, 117);
}
button:hover {
background: rgb(11, 121, 44);
}
button:active {
background: rgb(5, 59, 21);
}
input,
textarea {
color: white;
background-color: #111;
border: 2px solid rgb(117, 117, 117);
}
footer {
color: white;
border-top: 1px solid white;
background: #222;
}
}

416
mercury/registry.go Normal file
View File

@ -0,0 +1,416 @@
package mercury
import (
"context"
"fmt"
"path/filepath"
"sort"
"strconv"
"strings"
"go.sour.is/pkg/ident"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/rsql"
"go.sour.is/pkg/set"
"golang.org/x/sync/errgroup"
)
type GetIndex interface {
GetIndex(context.Context, NamespaceSearch, *rsql.Program) (Config, error)
}
type GetConfig interface {
GetConfig(context.Context, NamespaceSearch, *rsql.Program, []string) (Config, error)
}
type WriteConfig interface {
WriteConfig(context.Context, Config) error
}
type GetRules interface {
GetRules(context.Context, ident.Ident) (Rules, error)
}
type GetNotify interface {
GetNotify(context.Context, string) (ListNotify, error)
}
type SendNotify interface {
SendNotify(context.Context, Notify) error
}
// type nobody struct{}
// func (nobody) IsActive() bool { return true }
// func (nobody) Identity() string { return "xuu" }
// func (nobody) HasRole(r ...string) bool { return true }
func (reg *registry) accessFilter(rules Rules, lis Config) (out Config, err error) {
accessList := make(map[string]struct{})
for _, o := range lis {
if _, ok := accessList[o.Space]; ok {
out = append(out, o)
continue
}
if role := rules.GetRoles("NS", o.Space); role.HasRole("read", "write") && !role.HasRole("deny") {
accessList[o.Space] = struct{}{}
out = append(out, o)
}
}
return
}
// HandlerItem a single handler matching
type matcher[T any] struct {
Name string
Match NamespaceSearch
Priority int
Handler T
}
type matchers struct {
getIndex []matcher[GetIndex]
getConfig []matcher[GetConfig]
writeConfig []matcher[WriteConfig]
getRules []matcher[GetRules]
getNotify []matcher[GetNotify]
sendNotify []matcher[SendNotify]
}
// registry a list of handlers
type registry struct {
handlers map[string]func(*Space) any
matchers matchers
}
func (m matcher[T]) String() string {
return fmt.Sprintf("%d: %s", m.Priority, m.Match)
}
// Registry handler
var Registry *registry = &registry{}
func (r registry) String() string {
var buf strings.Builder
for h := range r.handlers {
buf.WriteString(h)
buf.WriteRune('\n')
}
return buf.String()
}
func (r *registry) resetMatchers() {
r.matchers.getIndex = r.matchers.getIndex[:0]
r.matchers.getConfig = r.matchers.getConfig[:0]
r.matchers.writeConfig = r.matchers.writeConfig[:0]
r.matchers.getRules = r.matchers.getRules[:0]
r.matchers.getNotify = r.matchers.getNotify[:0]
r.matchers.sendNotify = r.matchers.sendNotify[:0]
}
func (r *registry) sortMatchers() {
sort.Slice(r.matchers.getConfig, func(i, j int) bool { return r.matchers.getConfig[i].Priority < r.matchers.getConfig[j].Priority })
sort.Slice(r.matchers.getIndex, func(i, j int) bool { return r.matchers.getIndex[i].Priority < r.matchers.getIndex[j].Priority })
sort.Slice(r.matchers.writeConfig, func(i, j int) bool { return r.matchers.writeConfig[i].Priority < r.matchers.writeConfig[j].Priority })
sort.Slice(r.matchers.getRules, func(i, j int) bool { return r.matchers.getRules[i].Priority < r.matchers.getRules[j].Priority })
sort.Slice(r.matchers.getNotify, func(i, j int) bool { return r.matchers.getNotify[i].Priority < r.matchers.getNotify[j].Priority })
sort.Slice(r.matchers.sendNotify, func(i, j int) bool { return r.matchers.sendNotify[i].Priority < r.matchers.sendNotify[j].Priority })
}
func (r *registry) Register(name string, h func(*Space) any) {
if r.handlers == nil {
r.handlers = make(map[string]func(*Space) any)
}
r.handlers[name] = h
}
func (r *registry) Configure(m SpaceMap) error {
r.resetMatchers()
for space, c := range m {
if strings.HasPrefix(space, "mercury.source.") {
space = strings.TrimPrefix(space, "mercury.source.")
handler, name, _ := strings.Cut(space, ".")
matches := c.FirstValue("match")
for _, match := range matches.Values {
ps := strings.Fields(match)
priority, err := strconv.Atoi(ps[0])
if err != nil {
return err
}
r.add(name, handler, ps[1], priority, c)
}
}
if strings.HasPrefix(space, "mercury.output.") {
space = strings.TrimPrefix(space, "mercury.output.")
handler, name, _ := strings.Cut(space, ".")
matches := c.FirstValue("match")
for _, match := range matches.Values {
ps := strings.Fields(match)
priority, err := strconv.Atoi(ps[0])
if err != nil {
return err
}
r.add(name, handler, ps[1], priority, c)
}
}
}
r.sortMatchers()
return nil
}
// Register add a handler to registry
func (r *registry) add(name, handler, match string, priority int, cfg *Space) error {
// log.Infos("mercury regster", "match", match, "pri", priority)
mkHandler, ok := r.handlers[handler]
if !ok {
return fmt.Errorf("handler not registered: %s", handler)
}
hdlr := mkHandler(cfg)
if err, ok := hdlr.(error); ok {
return fmt.Errorf("%w: failed to config %s as handler: %s", err, name, handler)
}
if hdlr == nil {
return fmt.Errorf("failed to config %s as handler: %s", name, handler)
}
if hdlr, ok := hdlr.(GetIndex); ok {
r.matchers.getIndex = append(
r.matchers.getIndex,
matcher[GetIndex]{Name: name, Match: ParseNamespace(match), Priority: priority, Handler: hdlr},
)
}
if hdlr, ok := hdlr.(GetConfig); ok {
r.matchers.getConfig = append(
r.matchers.getConfig,
matcher[GetConfig]{Name: name, Match: ParseNamespace(match), Priority: priority, Handler: hdlr},
)
}
if hdlr, ok := hdlr.(WriteConfig); ok {
r.matchers.writeConfig = append(
r.matchers.writeConfig,
matcher[WriteConfig]{Name: name, Match: ParseNamespace(match), Priority: priority, Handler: hdlr},
)
}
if hdlr, ok := hdlr.(GetRules); ok {
r.matchers.getRules = append(
r.matchers.getRules,
matcher[GetRules]{Name: name, Match: ParseNamespace(match), Priority: priority, Handler: hdlr},
)
}
if hdlr, ok := hdlr.(GetNotify); ok {
r.matchers.getNotify = append(
r.matchers.getNotify,
matcher[GetNotify]{Name: name, Match: ParseNamespace(match), Priority: priority, Handler: hdlr},
)
}
if hdlr, ok := hdlr.(SendNotify); ok {
r.matchers.sendNotify = append(
r.matchers.sendNotify,
matcher[SendNotify]{Name: name, Match: ParseNamespace(match), Priority: priority, Handler: hdlr},
)
}
return nil
}
// GetIndex query each handler that match namespace.
func (r *registry) GetIndex(ctx context.Context, match, search string) (c Config, err error) {
ctx, span := lg.Span(ctx)
defer span.End()
spec := ParseNamespace(match)
pgm := rsql.DefaultParse(search)
matches := make([]NamespaceSearch, len(r.matchers.getIndex))
for _, n := range spec {
for i, hdlr := range r.matchers.getIndex {
if hdlr.Match.Match(n.Raw()) {
matches[i] = append(matches[i], n)
}
}
}
wg, ctx := errgroup.WithContext(ctx)
slots := make(chan Config, len(r.matchers.getConfig))
wg.Go(func() error {
i := 0
for lis := range slots {
c = append(c, lis...)
i++
if i > len(slots) {
break
}
}
return nil
})
for i, hdlr := range r.matchers.getIndex {
i, hdlr := i, hdlr
wg.Go(func() error {
span.AddEvent(fmt.Sprintf("INDEX %s %s", hdlr.Name, hdlr.Match))
lis, err := hdlr.Handler.GetIndex(ctx, matches[i], pgm)
slots <- lis
return err
})
}
err = wg.Wait()
if err != nil {
return nil, err
}
return
}
// Search query each handler with a key=value search
// GetConfig query each handler that match for fully qualified namespaces.
func (r *registry) GetConfig(ctx context.Context, match, search, fields string) (Config, error) {
ctx, span := lg.Span(ctx)
defer span.End()
spec := ParseNamespace(match)
pgm := rsql.DefaultParse(search)
flds := strings.Split(fields, ",")
matches := make([]NamespaceSearch, len(r.matchers.getConfig))
for _, n := range spec {
for i, hdlr := range r.matchers.getConfig {
if hdlr.Match.Match(n.Raw()) {
matches[i] = append(matches[i], n)
}
}
}
m := make(SpaceMap)
for i, hdlr := range r.matchers.getConfig {
if len(matches[i]) == 0 {
continue
}
span.AddEvent(fmt.Sprintf("QUERY %s %s", hdlr.Name, hdlr.Match))
lis, err := hdlr.Handler.GetConfig(ctx, matches[i], pgm, flds)
if err != nil {
return nil, err
}
m.Merge(lis...)
}
return m.ToArray(), nil
}
// WriteConfig write objects to backends
func (r *registry) WriteConfig(ctx context.Context, spaces Config) error {
ctx, span := lg.Span(ctx)
defer span.End()
matches := make([]Config, len(r.matchers.writeConfig))
for _, s := range spaces {
for i, hdlr := range r.matchers.writeConfig {
if hdlr.Match.Match(s.Space) {
matches[i] = append(matches[i], s)
break
}
}
}
for i, hdlr := range r.matchers.writeConfig {
if len(matches[i]) == 0 {
continue
}
span.AddEvent(fmt.Sprint("WRITE MATCH", hdlr.Name, hdlr.Match))
err := hdlr.Handler.WriteConfig(ctx, matches[i])
if err != nil {
return err
}
}
return nil
}
// GetRules query each of the handlers for rules.
func (r *registry) GetRules(ctx context.Context, user ident.Ident) (Rules, error) {
ctx, span := lg.Span(ctx)
defer span.End()
s := set.New[Rule]()
for _, hdlr := range r.matchers.getRules {
span.AddEvent(fmt.Sprint("RULES", hdlr.Name, hdlr.Match))
lis, err := hdlr.Handler.GetRules(ctx, user)
if err != nil {
return nil, err
}
s.Add(lis...)
}
var rules Rules = s.Values()
sort.Sort(rules)
return rules, nil
}
// GetNotify query each of the handlers for rules.
func (r *registry) GetNotify(ctx context.Context, event string) (ListNotify, error) {
ctx, span := lg.Span(ctx)
defer span.End()
s := set.New[Notify]()
for _, hdlr := range r.matchers.getNotify {
span.AddEvent(fmt.Sprint("GET NOTIFY", hdlr.Name, hdlr.Match))
lis, err := hdlr.Handler.GetNotify(ctx, event)
if err != nil {
return nil, err
}
s.Add(lis...)
}
return s.Values(), nil
}
func (r *registry) SendNotify(ctx context.Context, n Notify) (err error) {
ctx, span := lg.Span(ctx)
defer span.End()
for _, hdlr := range r.matchers.sendNotify {
span.AddEvent(fmt.Sprint("SEND NOTIFY", hdlr.Name, hdlr.Match))
err := hdlr.Handler.SendNotify(ctx, n)
if err != nil {
return err
}
}
return
}
// Check if name matches notify
func (n Notify) Check(name string) bool {
ok, err := filepath.Match(n.Match, name)
if err != nil {
return false
}
return ok
}
// Notify stores the attributes for a registry space
type Notify struct {
Name string
Match string
Event string
Method string
URL string
}
// ListNotify array of notify
type ListNotify []Notify
// Find returns list of notify that match name.
func (ln ListNotify) Find(name string) (lis ListNotify) {
lis = make(ListNotify, 0, len(ln))
for _, o := range ln {
if o.Check(name) {
lis = append(lis, o)
}
}
return
}

277
mercury/routes.go Normal file
View File

@ -0,0 +1,277 @@
package mercury
import (
"embed"
"encoding/json"
"fmt"
"io/fs"
"log"
"net/http"
"sort"
"strings"
"time"
"github.com/BurntSushi/toml"
"github.com/golang/gddo/httputil"
"go.sour.is/pkg/ident"
"go.sour.is/pkg/lg"
)
type root struct{}
func NewHTTP() *root {
return &root{}
}
//go:embed public
var public embed.FS
func (s *root) RegisterHTTP(mux *http.ServeMux) {
// mux.Handle("/", http.FileServer(http.Dir("./mercury/public")))
public, _ := fs.Sub(public, "public")
mux.Handle("/", http.FileServerFS(public))
}
func (s *root) RegisterAPIv1(mux *http.ServeMux) {
mux.HandleFunc("GET /mercury", s.indexV1)
// mux.HandleFunc("/mercury/config", s.configV1)
mux.HandleFunc("GET /mercury/config", s.configV1)
mux.HandleFunc("POST /mercury/config", s.storeV1)
}
func (s *root) RegisterWellKnown(mux *http.ServeMux) {
s.RegisterAPIv1(mux)
}
func (s *root) configV1(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
s.storeV1(w, r)
return
}
ctx, span := lg.Span(r.Context())
defer span.End()
var id ident.Ident = ident.FromContext(ctx)
if !id.Session().Active {
span.RecordError(fmt.Errorf("NO_AUTH"))
http.Error(w, "NO_AUTH", http.StatusUnauthorized)
return
}
rules, err := Registry.GetRules(ctx, id)
if err != nil {
span.RecordError(err)
http.Error(w, "ERR: "+err.Error(), http.StatusInternalServerError)
return
}
space := r.URL.Query().Get("space")
if space == "" {
space = "*"
}
log.Print("SPC: ", space)
ns := ParseNamespace(space)
log.Print("PRE: ", ns)
//ns = rules.ReduceSearch(ns)
log.Print("POST: ", ns)
lis, err := Registry.GetConfig(ctx, ns.String(), "", "")
if err != nil {
http.Error(w, "ERR: "+err.Error(), http.StatusInternalServerError)
return
}
lis, err = Registry.accessFilter(rules, lis)
if err != nil {
span.RecordError(err)
http.Error(w, "ERR: "+err.Error(), http.StatusInternalServerError)
return
}
sort.Sort(lis)
var content string
switch httputil.NegotiateContentType(r, []string{
"text/plain",
"text/html",
"application/environ",
"application/ini",
"application/json",
"application/toml",
}, "text/plain") {
case "text/plain":
content = lis.String()
case "text/html":
content = lis.HTMLString()
case "application/environ":
content = lis.EnvString()
case "application/ini":
content = lis.INIString()
case "application/json":
json.NewEncoder(w).Encode(lis)
case "application/toml":
w.WriteHeader(200)
m := make(map[string]map[string][]string)
for _, o := range lis {
if _, ok := m[o.Space]; !ok {
m[o.Space] = make(map[string][]string)
}
for _, v := range o.List {
m[o.Space][v.Name] = append(m[o.Space][v.Name], v.Values...)
}
}
err := toml.NewEncoder(w).Encode(m)
if err != nil {
// log.Error(err)
http.Error(w, "ERR", http.StatusInternalServerError)
return
}
}
fmt.Fprint(w, content)
}
func (s *root) storeV1(w http.ResponseWriter, r *http.Request) {
ctx, span := lg.Span(r.Context())
defer span.End()
var id = ident.FromContext(ctx)
if !id.Session().Active {
span.RecordError(fmt.Errorf("NO_AUTH"))
http.Error(w, "NO_AUTH", http.StatusUnauthorized)
return
}
var config SpaceMap
var err error
contentType, _, _ := strings.Cut(r.Header.Get("Content-Type"), ";")
switch contentType {
case "text/plain":
config, err = ParseText(r.Body)
r.Body.Close()
case "application/x-www-form-urlencoded":
r.ParseForm()
config, err = ParseText(strings.NewReader(r.Form.Get("content")))
case "multipart/form-data":
r.ParseMultipartForm(1 << 20)
config, err = ParseText(strings.NewReader(r.Form.Get("content")))
default:
http.Error(w, "PARSE_ERR", http.StatusUnsupportedMediaType)
return
}
if err != nil {
span.RecordError(err)
http.Error(w, "PARSE_ERR", 400)
return
}
{
rules, err := Registry.GetRules(ctx, id)
if err != nil {
span.RecordError(err)
http.Error(w, "ERR: "+err.Error(), http.StatusInternalServerError)
return
}
notify, err := Registry.GetNotify(ctx, "updated")
if err != nil {
span.RecordError(err)
http.Error(w, "ERR", http.StatusInternalServerError)
return
}
_ = rules
var notifyActive = make(map[string]struct{})
var filteredConfigs Config
for ns, c := range config {
if !rules.GetRoles("NS", ns).HasRole("write") {
span.AddEvent(fmt.Sprint("SKIP", ns))
continue
}
span.AddEvent(fmt.Sprint("SAVE", ns))
for _, n := range notify.Find(ns) {
notifyActive[n.Name] = struct{}{}
}
filteredConfigs = append(filteredConfigs, c)
}
err = Registry.WriteConfig(ctx, filteredConfigs)
if err != nil {
span.RecordError(err)
http.Error(w, "ERR", http.StatusInternalServerError)
return
}
span.AddEvent(fmt.Sprint("SEND NOTIFYS ", notifyActive))
for _, n := range notify {
if _, ok := notifyActive[n.Name]; ok {
err = Registry.SendNotify(ctx, n)
if err != nil {
span.RecordError(err)
http.Error(w, "ERR", http.StatusInternalServerError)
return
}
}
}
span.AddEvent("DONE!")
}
w.WriteHeader(202)
fmt.Fprint(w, "OK")
}
func (s *root) indexV1(w http.ResponseWriter, r *http.Request) {
ctx, span := lg.Span(r.Context())
defer span.End()
var id = ident.FromContext(ctx)
timer := time.Now()
defer func() { fmt.Println(time.Since(timer)) }()
if !id.Session().Active {
span.RecordError(fmt.Errorf("NO_AUTH"))
http.Error(w, "NO_AUTH", http.StatusUnauthorized)
return
}
rules, err := Registry.GetRules(ctx, id)
if err != nil {
span.RecordError(err)
http.Error(w, "ERR: "+err.Error(), http.StatusInternalServerError)
return
}
span.AddEvent(fmt.Sprint(rules))
space := r.URL.Query().Get("space")
if space == "" {
space = "*"
}
ns := ParseNamespace(space)
ns = rules.ReduceSearch(ns)
span.AddEvent(ns.String())
lis, err := Registry.GetIndex(ctx, ns.String(), "")
if err != nil {
span.RecordError(err)
http.Error(w, "ERR: "+err.Error(), http.StatusInternalServerError)
return
}
sort.Sort(lis)
switch httputil.NegotiateContentType(r, []string{
"text/plain",
"application/json",
}, "text/plain") {
case "text/plain":
_, err = fmt.Fprint(w, lis.StringList())
span.RecordError(err)
case "application/json":
err = json.NewEncoder(w).Encode(lis)
span.RecordError(err)
}
}

118
mercury/sql/init-pg.sql Normal file
View File

@ -0,0 +1,118 @@
CREATE SEQUENCE IF NOT EXISTS mercury_spaces_id_seq;
CREATE TABLE IF NOT EXISTS mercury_spaces
(
space character varying NOT NULL,
id integer NOT NULL DEFAULT nextval('mercury_spaces_id_seq'::regclass),
notes character varying[] NOT NULL DEFAULT '{}'::character varying[],
tags character varying[] NOT NULL DEFAULT '{}'::character varying[],
trailer character varying[] NOT NULL DEFAULT '{}'::character varying[],
CONSTRAINT mercury_namespace_pk PRIMARY KEY (id)
);
CREATE UNIQUE INDEX IF NOT EXISTS mercury_namespace_space_uindex
ON mercury_spaces USING btree
(space ASC NULLS LAST);
CREATE TABLE IF NOT EXISTS mercury_values
(
id integer NOT NULL,
seq integer NOT NULL,
name character varying NOT NULL,
"values" character varying[] NOT NULL DEFAULT '{}'::character varying[],
tags character varying[] NOT NULL DEFAULT '{}'::character varying[],
notes character varying[] NOT NULL DEFAULT '{}'::character varying[],
CONSTRAINT mercury_values_pk PRIMARY KEY (id, seq)
);
CREATE INDEX IF NOT EXISTS mercury_values_name_index
ON mercury_values USING btree
(name ASC NULLS LAST);
CREATE OR REPLACE VIEW mercury_registry_vw
AS
SELECT
s.id,
v.seq,
s.space,
v.name,
v."values",
v.notes,
v.tags,
s.trailer
FROM mercury_spaces s
JOIN mercury_values v ON s.id = v.id;
CREATE OR REPLACE VIEW mercury_groups_vw
AS
SELECT DISTINCT
unnest(vw."values") AS user_id,
vw.name AS group_id
FROM mercury_registry_vw vw
WHERE vw.space::text = 'mercury.groups'::text;
CREATE OR REPLACE VIEW mercury_group_rules_vw
AS
WITH
tt as (
SELECT DISTINCT
vw.name AS group_id,
unnest(vw."values") AS rules
FROM mercury_registry_vw vw
WHERE vw.space::text = 'mercury.policy'::text
)
SELECT tt.group_id,
split_part(tt.rules::text, ' '::text, 1) AS role,
split_part(tt.rules::text, ' '::text, 2) AS type,
split_part(tt.rules::text, ' '::text, 3) AS match
FROM tt;
CREATE OR REPLACE VIEW mercury_user_rules_vw
AS
WITH
tt as (
SELECT DISTINCT
vw.name AS group_id,
unnest(vw."values") AS rules
FROM mercury_registry_vw vw
WHERE vw.space::text = 'mercury.policy'::text
)
SELECT
g.user_id,
split_part(tt.rules::text, ' '::text, 1) AS role,
split_part(tt.rules::text, ' '::text, 2) AS type,
split_part(tt.rules::text, ' '::text, 3) AS match
FROM mercury_groups_vw g
JOIN tt ON g.group_id::text = tt.group_id::text;
CREATE OR REPLACE VIEW mercury_rules_vw
AS
SELECT
'U-'::text || vw.user_id::text AS id,
vw.role,
vw.type,
vw.match
FROM mercury_user_rules_vw vw
UNION
SELECT
'G-'::text || vw.group_id::text AS id,
vw.role,
vw.type,
vw.match
FROM mercury_group_rules_vw vw;
CREATE OR REPLACE VIEW mercury_notify_vw
AS
WITH
tt as (
SELECT DISTINCT
vw.name,
unnest(vw."values") AS rules
FROM mercury_registry_vw vw
WHERE vw.space::text = 'mercury.notify'::text
)
SELECT
tt.name,
split_part(tt.rules::text, ' '::text, 1) AS match,
split_part(tt.rules::text, ' '::text, 2) AS event,
split_part(tt.rules::text, ' '::text, 3) AS method,
split_part(tt.rules::text, ' '::text, 4) AS url
FROM tt;

120
mercury/sql/init-sql3.sql Normal file
View File

@ -0,0 +1,120 @@
CREATE TABLE IF NOT EXISTS mercury_spaces
(
space character varying NOT NULL unique,
id integer NOT NULL CONSTRAINT mercury_namespace_pk PRIMARY KEY autoincrement,
notes json NOT NULL DEFAULT '[]',
tags json NOT NULL DEFAULT '[]',
trailer json NOT NULL DEFAULT '[]'
);
CREATE TABLE IF NOT EXISTS mercury_values
(
id integer NOT NULL,
seq integer NOT NULL,
name character varying NOT NULL,
"values" json NOT NULL DEFAULT '[]',
tags json NOT NULL DEFAULT '[]',
notes json NOT NULL DEFAULT '[]',
CONSTRAINT mercury_values_pk PRIMARY KEY (id, seq)
);
drop view if exists mercury_registry_vw;
CREATE VIEW if not exists mercury_registry_vw
AS
SELECT
s.id,
v.seq,
s.space,
v.name,
v."values",
v.notes,
v.tags,
s.trailer
FROM mercury_spaces s
JOIN mercury_values v ON s.id = v.id;
drop view if exists mercury_groups_vw;
CREATE VIEW if not exists mercury_groups_vw
AS
SELECT DISTINCT
j.value AS user_id,
vw.name AS group_id
FROM mercury_registry_vw vw, json_each(vw."values") j
WHERE vw.space = 'mercury.groups';
drop view if exists mercury_group_rules_vw;
CREATE VIEW if not exists mercury_group_rules_vw
AS
WITH
tt as (
SELECT DISTINCT
vw.name AS group_id,
j.value AS rules
FROM mercury_registry_vw vw, json_each(vw."values") j
WHERE vw.space = 'mercury.policy'
)
SELECT tt.group_id,
tt.rules rule,
'' AS role,
'' AS type,
''AS match
FROM tt;
drop view if exists mercury_user_rules_vw;
CREATE VIEW if not exists mercury_user_rules_vw
AS
WITH
tt as (
SELECT DISTINCT
vw.name AS group_id,
j.value AS rules
FROM mercury_registry_vw vw, json_each(vw."values") j
WHERE vw.space = 'mercury.policy'
)
SELECT
g.user_id,
tt.rules rule,
'' AS role,
'' AS type,
'' AS match
FROM mercury_groups_vw g
JOIN tt ON g.group_id = tt.group_id;
drop view if exists mercury_rules_vw;
CREATE VIEW if not exists mercury_rules_vw
AS
SELECT
'U-' || vw.user_id AS id,
vw.rule,
vw.role,
vw.type,
vw.match
FROM mercury_user_rules_vw vw
UNION
SELECT
'G-' || vw.group_id AS id,
vw.rule,
vw.role,
vw.type,
vw.match
FROM mercury_group_rules_vw vw;
drop view if exists mercury_notify_vw;
CREATE VIEW if not exists mercury_notify_vw
AS
WITH
tt as (
SELECT DISTINCT
vw.name,
j.value AS rules
FROM mercury_registry_vw vw, json_each(vw."values") j
WHERE vw.space = 'mercury.notify'
)
SELECT
tt.name,
tt.rules rule,
substr(tt.rules, 1, instr(tt.rules, ' ')-1) AS match,
substr(tt.rules, instr(tt.rules, ' ')+1, instr(substr(tt.rules, instr(tt.rules, ' ')+1), ' ')-1) AS event,
'' AS method,
'' as url
FROM tt;

130
mercury/sql/list-string.go Normal file
View File

@ -0,0 +1,130 @@
package sql
import (
"database/sql/driver"
"fmt"
"strings"
"unicode"
"unicode/utf8"
)
type valueFn func() (v driver.Value, err error)
func (fn valueFn) Value() (v driver.Value, err error) {
return fn()
}
type scanFn func(value any) error
func (fn scanFn) Scan(v any) error {
return fn(v)
}
func listScan(e *[]string, ends [2]rune) scanFn {
return func(value any) error {
var str string
switch v := value.(type) {
case string:
str = v
case []byte:
str = string(v)
case []rune:
str = string(v)
default:
return fmt.Errorf("array must be uint64, got: %T", value)
}
if e == nil {
*e = []string{}
}
str = trim(str, ends[0], ends[1])
if len(str) == 0 {
return nil
}
for _, s := range splitComma(string(str)) {
*e = append(*e, s)
}
return nil
}
}
func listValue(e []string, ends [2]rune) valueFn {
return func() (value driver.Value, err error) {
var b strings.Builder
if len(e) == 0 {
return string(ends[:]), nil
}
_, err = b.WriteRune(ends[0])
if err != nil {
return
}
var arr []string
for _, s := range e {
arr = append(arr, `"`+s+`"`)
}
_, err = b.WriteString(strings.Join(arr, ","))
if err != nil {
return
}
_, err = b.WriteRune(ends[1])
if err != nil {
return
}
return b.String(), nil
}
}
func splitComma(s string) []string {
lastQuote := rune(0)
f := func(c rune) bool {
switch {
case c == lastQuote:
lastQuote = rune(0)
return false
case lastQuote != rune(0):
return false
case unicode.In(c, unicode.Quotation_Mark):
lastQuote = c
return false
default:
return c == ','
}
}
lis := strings.FieldsFunc(s, f)
var out []string
for _, s := range lis {
s = trim(s, '"', '"')
out = append(out, s)
}
return out
}
func trim(s string, start, end rune) string {
r0, size0 := utf8.DecodeRuneInString(s)
if size0 == 0 {
return s
}
if r0 != start {
return s
}
r1, size1 := utf8.DecodeLastRuneInString(s)
if size1 == 0 {
return s
}
if r1 != end {
return s
}
return s[size0 : len(s)-size1]
}

55
mercury/sql/notify.go Normal file
View File

@ -0,0 +1,55 @@
package sql
import (
"context"
"strings"
"github.com/Masterminds/squirrel"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/mercury"
)
// Notify stores the attributes for a registry space
type Notify struct {
Name string `json:"name" view:"mercury_notify_vw"`
Match string `json:"match"`
Event string `json:"event"`
Method string `json:"-" db:"method"`
URL string `json:"-" db:"url"`
}
// GetNotify get list of rules
func (pgm *sqlHandler) GetNotify(ctx context.Context, event string) (lis mercury.ListNotify, err error) {
ctx, span := lg.Span(ctx)
defer span.End()
rows, err := squirrel.Select(`"name"`, `"match"`, `"event"`, `"method"`, `"url"`, `"rule"`).
From("mercury_notify_vw").
Where(squirrel.Eq{"event": event}).
PlaceholderFormat(squirrel.Dollar).
RunWith(pgm.db).
QueryContext(context.TODO())
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var s mercury.Notify
var rule string
err = rows.Scan(&s.Name, &s.Match, &s.Event, &s.Method, &s.URL, &rule)
if err != nil {
return nil, err
}
if rule != "" {
s.Match, rule, _ = strings.Cut(rule, " ")
s.Event, rule, _ = strings.Cut(rule, " ")
s.Method, s.URL, _ = strings.Cut(rule, " ")
}
lis = append(lis, s)
}
return lis, rows.Err()
}

40
mercury/sql/otel.go Normal file
View File

@ -0,0 +1,40 @@
package sql
import (
"database/sql"
"go.nhat.io/otelsql"
semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
)
func openDB(driver, dsn string) (*sql.DB, error) {
system := semconv.DBSystemPostgreSQL
if driver == "sqlite" {
system = semconv.DBSystemSqlite
}
// Register the otelsql wrapper for the provided postgres driver.
driverName, err := otelsql.Register(driver,
otelsql.AllowRoot(),
otelsql.TraceQueryWithoutArgs(),
otelsql.TraceRowsClose(),
otelsql.TraceRowsAffected(),
// otelsql.WithDatabaseName("my_database"), // Optional.
otelsql.WithSystem(system), // Optional.
)
if err != nil {
return nil, err
}
// Connect to a Postgres database using the postgres driver wrapper.
db, err := sql.Open(driverName, dsn)
if err != nil {
return nil, err
}
if err := otelsql.RecordStats(db); err != nil {
return nil, err
}
return db, nil
}

99
mercury/sql/rules.go Normal file
View File

@ -0,0 +1,99 @@
package sql
import (
"context"
"fmt"
"strings"
"github.com/Masterminds/squirrel"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/mercury"
"go.sour.is/pkg/ident"
)
type grouper interface {
GetGroups() []string
}
// GetRules get list of rules
func (p *sqlHandler) GetRules(ctx context.Context, user ident.Ident) (lis mercury.Rules, err error) {
ctx, span := lg.Span(ctx)
defer span.End()
var ids []string
ids = append(ids, "U-"+user.Identity())
switch u := user.(type) {
case grouper:
for _, g := range u.GetGroups() {
ids = append(ids, "G-"+g)
}
}
if groups, err := p.getGroups(ctx, user.Identity()); err != nil {
for _, g := range groups {
ids = append(ids, "G-"+g)
}
}
query := squirrel.Select(`"role"`, `"type"`, `"match"`, `"rule"`).
From("mercury_rules_vw").
Where(squirrel.Eq{"id": ids}).
PlaceholderFormat(squirrel.Dollar)
rows, err := query.
RunWith(p.db).
QueryContext(ctx)
if err != nil {
span.RecordError(err)
return nil, err
}
defer rows.Close()
for rows.Next() {
var s mercury.Rule
var rule string
err = rows.Scan(&s.Role, &s.Type, &s.Match, &rule)
if err != nil {
span.RecordError(err)
return nil, err
}
if rule != "" {
s.Role, rule, _ = strings.Cut(rule, " ")
s.Type, s.Match, _ = strings.Cut(rule, " ")
}
lis = append(lis, s)
}
err = rows.Err()
span.RecordError(err)
span.AddEvent(fmt.Sprint("read rules ", len(lis)))
return lis, err
}
// getGroups get list of groups
func (pgm *sqlHandler) getGroups(ctx context.Context, user string) (lis []string, err error) {
ctx, span := lg.Span(ctx)
defer span.End()
rows, err := squirrel.Select("group_id").
From("mercury_groups_vw").
Where(squirrel.Eq{"user_id": user}).
PlaceholderFormat(squirrel.Dollar).
RunWith(pgm.db).
QueryContext(ctx)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var s string
err = rows.Scan(&s)
if err != nil {
return nil, err
}
lis = append(lis, s)
}
return lis, rows.Err()
}

426
mercury/sql/sql.go Normal file
View File

@ -0,0 +1,426 @@
package sql
import (
"context"
"database/sql"
"fmt"
"log"
"strings"
sq "github.com/Masterminds/squirrel"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/mercury"
"go.sour.is/pkg/rsql"
"golang.org/x/exp/maps"
)
type sqlHandler struct {
db *sql.DB
paceholderFormat sq.PlaceholderFormat
listFormat [2]rune
}
var (
_ mercury.GetIndex = (*sqlHandler)(nil)
_ mercury.GetConfig = (*sqlHandler)(nil)
_ mercury.GetRules = (*sqlHandler)(nil)
_ mercury.WriteConfig = (*sqlHandler)(nil)
)
func Register() {
mercury.Registry.Register("sql", func(s *mercury.Space) any {
var dsn string
var opts strings.Builder
var dbtype string
for _, c := range s.List {
if c.Name == "match" {
continue
}
if c.Name == "dbtype" {
dbtype = c.First()
continue
}
if c.Name == "dsn" {
dsn = c.First()
break
}
fmt.Fprintln(&opts, c.Name, "=", c.First())
}
if dsn == "" {
dsn = opts.String()
}
db, err := openDB(dbtype, dsn)
if err != nil {
return err
}
if err = db.Ping(); err != nil {
return err
}
switch dbtype {
case "sqlite":
return &sqlHandler{db, sq.Dollar, [2]rune{'[', ']'}}
case "postgres":
return &sqlHandler{db, sq.Dollar, [2]rune{'{', '}'}}
default:
return fmt.Errorf("unsupported dbtype: %s", dbtype)
}
})
}
type Space struct {
mercury.Space
ID uint64
}
type Value struct {
mercury.Value
ID uint64
}
func (p *sqlHandler) GetIndex(ctx context.Context, search mercury.NamespaceSearch, pgm *rsql.Program) (mercury.Config, error) {
ctx, span := lg.Span(ctx)
defer span.End()
cols := rsql.GetDbColumns(mercury.Space{})
where, err := getWhere(search, cols)
if err != nil {
return nil, err
}
lis, err := p.listSpace(ctx, nil, where)
if err != nil {
log.Println(err)
return nil, err
}
config := make(mercury.Config, len(lis))
for i, s := range lis {
config[i] = &s.Space
}
return config, nil
}
func (p *sqlHandler) GetConfig(ctx context.Context, search mercury.NamespaceSearch, pgm *rsql.Program, fields []string) (mercury.Config, error) {
ctx, span := lg.Span(ctx)
defer span.End()
idx, err := p.GetIndex(ctx, search, pgm)
if err != nil {
return nil, err
}
spaceMap := make(map[string]int, len(idx))
for u, s := range idx {
spaceMap[s.Space] = u
}
where, err := getWhere(search, rsql.GetDbColumns(mercury.Value{}))
if err != nil {
return nil, err
}
query := sq.Select(`"space"`, `"name"`, `"seq"`, `"notes"`, `"tags"`, `"values"`).
From("mercury_registry_vw").
Where(where).
OrderBy("space asc", "name asc").
PlaceholderFormat(p.paceholderFormat)
span.AddEvent(lg.LogQuery(query.ToSql()))
rows, err := query.RunWith(p.db).
QueryContext(ctx)
if err != nil {
log.Println(err)
return nil, err
}
defer rows.Close()
for rows.Next() {
var s mercury.Value
err = rows.Scan(
&s.Space,
&s.Name,
&s.Seq,
listScan(&s.Notes, p.listFormat),
listScan(&s.Tags, p.listFormat),
listScan(&s.Values, p.listFormat),
)
if err != nil {
return nil, err
}
if u, ok := spaceMap[s.Space]; ok {
idx[u].List = append(idx[u].List, s)
}
}
err = rows.Err()
span.RecordError(err)
span.AddEvent(fmt.Sprint("read index ", len(idx)))
return idx, err
}
func (p *sqlHandler) listSpace(ctx context.Context, tx sq.BaseRunner, where sq.Sqlizer) ([]*Space, error) {
ctx, span := lg.Span(ctx)
defer span.End()
if tx == nil {
tx = p.db
}
query := sq.Select(`"id"`, `"space"`, `"notes"`, `"tags"`, `"trailer"`).
From("mercury_spaces").
Where(where).
OrderBy("space asc").
PlaceholderFormat(sq.Dollar)
span.AddEvent(lg.LogQuery(query.ToSql()))
rows, err := query.RunWith(tx).
QueryContext(ctx)
if err != nil {
log.Println(err)
return nil, err
}
defer rows.Close()
var lis []*Space
for rows.Next() {
var s Space
err = rows.Scan(
&s.ID,
&s.Space.Space,
listScan(&s.Space.Notes, p.listFormat),
listScan(&s.Space.Tags, p.listFormat),
listScan(&s.Trailer, p.listFormat),
)
if err != nil {
return nil, err
}
lis = append(lis, &s)
}
err = rows.Err()
span.RecordError(err)
span.AddEvent(fmt.Sprint("read config ", len(lis)))
return lis, err
}
// WriteConfig writes a config map to database
func (p *sqlHandler) WriteConfig(ctx context.Context, config mercury.Config) (err error) {
ctx, span := lg.Span(ctx)
defer span.End()
// Delete spaces that are present in input but are empty.
deleteSpaces := make(map[string]struct{})
// get names of each space
var names = make(map[string]int)
for i, v := range config {
names[v.Space] = i
if len(v.Tags) == 0 && len(v.Notes) == 0 && len(v.List) == 0 {
deleteSpaces[v.Space] = struct{}{}
}
}
tx, err := p.db.Begin()
if err != nil {
return err
}
defer func() {
if err != nil && tx != nil {
tx.Rollback()
}
}()
// get current spaces
lis, err := p.listSpace(ctx, tx, sq.Eq{"space": maps.Keys(names)})
if err != nil {
return
}
// determine which are being updated
var deleteIDs []uint64
var updateIDs []uint64
var currentNames = make(map[string]struct{}, len(lis))
var updateSpaces []*mercury.Space
var insertSpaces []*mercury.Space
for _, s := range lis {
spaceName := s.Space.Space
currentNames[spaceName] = struct{}{}
if _, ok := deleteSpaces[spaceName]; ok {
deleteIDs = append(deleteIDs, s.ID)
continue
}
updateSpaces = append(updateSpaces, config[names[spaceName]])
updateIDs = append(updateIDs, s.ID)
}
for _, s := range config {
spaceName := s.Space
if _, ok := currentNames[spaceName]; !ok {
insertSpaces = append(insertSpaces, s)
}
}
// delete spaces
if ids := deleteIDs; len(ids) > 0 {
_, err = sq.Delete("mercury_spaces").Where(sq.Eq{"id": ids}).RunWith(tx).PlaceholderFormat(sq.Dollar).ExecContext(ctx)
if err != nil {
return err
}
}
// delete values
if ids := append(updateIDs, deleteIDs...); len(ids) > 0 {
_, err = sq.Delete("mercury_values").Where(sq.Eq{"id": ids}).RunWith(tx).PlaceholderFormat(sq.Dollar).ExecContext(ctx)
if err != nil {
return err
}
}
var newValues []*Value
// update spaces
for i, u := range updateSpaces {
query := sq.Update("mercury_spaces").
Where(sq.Eq{"id": updateIDs[i]}).
Set("tags", listValue(u.Tags, p.listFormat)).
Set("notes", listValue(u.Notes, p.listFormat)).
Set("trailer", listValue(u.Trailer, p.listFormat)).
PlaceholderFormat(sq.Dollar)
span.AddEvent(lg.LogQuery(query.ToSql()))
_, err := query.RunWith(tx).ExecContext(ctx)
if err != nil {
return err
}
// log.Debugf("UPDATED %d SPACES", len(updateSpaces))
for _, v := range u.List {
newValues = append(newValues, &Value{Value: v, ID: updateIDs[i]})
}
}
// insert spaces
for _, s := range insertSpaces {
var id uint64
query := sq.Insert("mercury_spaces").
PlaceholderFormat(sq.Dollar).
Columns("space", "tags", "notes", "trailer").
Values(
s.Space,
listValue(s.Tags, p.listFormat),
listValue(s.Notes, p.listFormat),
listValue(s.Trailer, p.listFormat),
).
Suffix("RETURNING \"id\"")
span.AddEvent(lg.LogQuery(query.ToSql()))
err := query.
RunWith(tx).
QueryRowContext(ctx).
Scan(&id)
if err != nil {
s, v, _ := query.ToSql()
log.Println(s, v, err)
return err
}
for _, v := range s.List {
newValues = append(newValues, &Value{Value: v, ID: id})
}
}
// write all values to db.
err = p.writeValues(ctx, tx, newValues)
// log.Debugf("WROTE %d ATTRS", len(attrs))
tx.Commit()
tx = nil
return
}
// writeValues writes the values to db
func (p *sqlHandler) writeValues(ctx context.Context, tx sq.BaseRunner, lis []*Value) (err error) {
ctx, span := lg.Span(ctx)
defer span.End()
if len(lis) == 0 {
return nil
}
newInsert := func() sq.InsertBuilder {
return sq.Insert("mercury_values").
RunWith(tx).
PlaceholderFormat(sq.Dollar).
Columns(
`"id"`,
`"seq"`,
`"name"`,
`"values"`,
`"notes"`,
`"tags"`,
)
}
chunk := int(65000 / 3)
insert := newInsert()
for i, s := range lis {
insert = insert.Values(
s.ID,
s.Seq,
s.Name,
listValue(s.Values, p.listFormat),
listValue(s.Notes, p.listFormat),
listValue(s.Tags, p.listFormat),
)
// log.Debug(s.Name)
if i > 0 && i%chunk == 0 {
// log.Debugf("inserting %v rows into %v", i%chunk, d.Table)
// log.Debug(insert.ToSql())
span.AddEvent(lg.LogQuery(insert.ToSql()))
_, err = insert.ExecContext(ctx)
if err != nil {
// log.Error(err)
return
}
insert = newInsert()
}
}
if len(lis)%chunk > 0 {
// log.Debugf("inserting %v rows into %v", len(lis)%chunk, d.Table)
// log.Debug(insert.ToSql())
span.AddEvent(lg.LogQuery(insert.ToSql()))
_, err = insert.ExecContext(ctx)
if err != nil {
// log.Error(err)
return
}
}
return
}
func getWhere(search mercury.NamespaceSearch, d *rsql.DbColumns) (sq.Sqlizer, error) {
var where sq.Or
space, err := d.Col("space")
if err != nil {
return nil, err
}
for _, m := range search {
switch m.(type) {
case mercury.NamespaceNode:
where = append(where, sq.Eq{space: m.Value()})
case mercury.NamespaceStar:
where = append(where, sq.Like{space: m.Value()})
case mercury.NamespaceTrace:
e := sq.Expr(`? LIKE `+space+` || '%'`, m.Value())
where = append(where, e)
}
}
return where, nil
}

View File

@ -8,6 +8,7 @@ type mux struct {
*http.ServeMux *http.ServeMux
api *http.ServeMux api *http.ServeMux
wellknown *http.ServeMux wellknown *http.ServeMux
handler http.Handler
} }
func (mux *mux) Add(fns ...interface{ RegisterHTTP(*http.ServeMux) }) { func (mux *mux) Add(fns ...interface{ RegisterHTTP(*http.ServeMux) }) {
@ -24,16 +25,30 @@ func (mux *mux) Add(fns ...interface{ RegisterHTTP(*http.ServeMux) }) {
// log.Printf("WellKnown: %T", fn) // log.Printf("WellKnown: %T", fn)
fn.RegisterWellKnown(mux.wellknown) fn.RegisterWellKnown(mux.wellknown)
} }
if fn, ok := fn.(interface{ RegisterMiddleware(http.Handler) http.Handler }); ok {
hdlr := mux.handler
// log.Printf("WellKnown: %T", fn)
mux.handler = fn.RegisterMiddleware(hdlr)
} }
} }
}
func (mux *mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
mux.handler.ServeHTTP(w, r)
}
func New() *mux { func New() *mux {
mux := &mux{ mux := &mux{
api: http.NewServeMux(), api: http.NewServeMux(),
wellknown: http.NewServeMux(), wellknown: http.NewServeMux(),
ServeMux: http.NewServeMux(), ServeMux: http.NewServeMux(),
} }
mux.Handle("/v1/", http.StripPrefix("/v1", mux.api))
mux.Handle("/api/v1/", http.StripPrefix("/api/v1", mux.api)) mux.Handle("/api/v1/", http.StripPrefix("/api/v1", mux.api))
mux.Handle("/.well-known/", http.StripPrefix("/.well-known", mux.wellknown)) mux.Handle("/.well-known/", http.StripPrefix("/.well-known", mux.wellknown))
mux.handler = mux.ServeMux
return mux return mux
} }

255
rsql/ast.go Normal file
View File

@ -0,0 +1,255 @@
package rsql
import (
"bytes"
"strings"
)
// Node is the smallest unit of ast
type Node interface {
TokenLiteral() string
String() string
}
// Statement is a executable tree
type Statement interface {
Node
statementNode()
}
// Expression is a portion of tree
type Expression interface {
Node
expressionNode()
}
// Identifier is a variable name
type Identifier struct {
Token Token
Value string
}
func (i *Identifier) expressionNode() {}
// TokenLiteral returns the literal value of a token
func (i *Identifier) TokenLiteral() string { return i.Token.Literal }
// String returns a string representation of value
func (i *Identifier) String() string { return i.Value }
// Integer is a numeric value
type Integer struct {
Token Token
Value int64
}
func (i *Integer) expressionNode() {}
// TokenLiteral returns the literal value of a token
func (i *Integer) TokenLiteral() string { return i.Token.Literal }
// String returns a string representation of value
func (i *Integer) String() string { return i.Token.Literal }
// Float is a floating point value
type Float struct {
Token Token
Value float64
}
func (i *Float) expressionNode() {}
// TokenLiteral returns the literal value of a token
func (i *Float) TokenLiteral() string { return i.Token.Literal }
// String returns a string representation of value
func (i *Float) String() string { return i.Token.Literal }
// Bool is a boolean value
type Bool struct {
Token Token
Value bool
}
func (i *Bool) expressionNode() {}
// TokenLiteral returns the literal value of a token
func (i *Bool) TokenLiteral() string { return i.Token.Literal }
// String returns a string representation of value
func (i *Bool) String() string { return i.Token.Literal }
// Null is an empty value
type Null struct {
Token Token
}
func (i *Null) expressionNode() {}
// TokenLiteral returns the literal value of a token
func (i *Null) TokenLiteral() string { return i.Token.Literal }
// String returns a string representation of value
func (i *Null) String() string { return i.Token.Literal }
// String is an array of codepoints
type String struct {
Token Token
Value string
}
func (i *String) expressionNode() {}
// TokenLiteral returns the literal value of a token
func (i *String) TokenLiteral() string { return i.Token.Literal }
// String returns a string representation of value
func (i *String) String() string {
var out bytes.Buffer
out.WriteRune('"')
out.WriteString(i.Value)
out.WriteRune('"')
return out.String()
}
// Array is an array of tokens
type Array struct {
Token Token
Elements []Expression
}
func (a *Array) expressionNode() {}
// TokenLiteral returns the literal value of a token
func (a *Array) TokenLiteral() string { return a.Token.Literal }
// String returns a string representation of value
func (a *Array) String() string {
var out bytes.Buffer
var elements []string
for _, el := range a.Elements {
elements = append(elements, el.String())
}
out.WriteRune('(')
out.WriteString(strings.Join(elements, ","))
out.WriteRune(')')
return out.String()
}
// Program is a collection of statements
type Program struct {
Statements []Statement
Args map[string]string
}
func (p *Program) expressionNode() {}
// TokenLiteral returns the literal value of a token
func (p *Program) TokenLiteral() string {
if len(p.Statements) > 0 {
return p.Statements[0].TokenLiteral()
}
return ""
}
// String returns a string representation of value
func (p *Program) String() string {
var out bytes.Buffer
for _, s := range p.Statements {
out.WriteString(s.String())
}
if len(p.Args) > 0 {
out.WriteRune('|')
for a, b := range p.Args {
out.WriteRune(' ')
out.WriteString(a)
out.WriteRune(':')
out.WriteString(b)
}
}
return out.String()
}
// ExpressionStatement is a collection of expressions
type ExpressionStatement struct {
Token Token
Expression Expression
}
func (ExpressionStatement) statementNode() {}
// TokenLiteral returns the literal value of a token
func (e ExpressionStatement) TokenLiteral() string { return e.Token.Literal }
// String returns a string representation of value
func (e ExpressionStatement) String() string {
if e.Expression != nil {
return e.Expression.String()
}
return ""
}
// PrefixExpression is an expression with a preceeding operator
type PrefixExpression struct {
Token Token
Operator string
Right Expression
}
func (p *PrefixExpression) expressionNode() {}
// TokenLiteral returns the literal value of a token
func (p *PrefixExpression) TokenLiteral() string { return p.Token.Literal }
// String returns a string representation of value
func (p *PrefixExpression) String() string {
var out bytes.Buffer
out.WriteRune('(')
out.WriteString(p.Operator)
out.WriteString(p.Right.String())
out.WriteRune(')')
return out.String()
}
// InfixExpression is two expressions with a infix operator
type InfixExpression struct {
Token Token
Left Expression
Operator string
Right Expression
}
func (i *InfixExpression) expressionNode() {}
// TokenLiteral returns the literal value of a token
func (i *InfixExpression) TokenLiteral() string { return i.Token.Literal }
// String returns a string representation of value
func (i *InfixExpression) String() string {
var out bytes.Buffer
out.WriteRune('(')
if i.Left != nil {
out.WriteString(i.Left.String())
} else {
out.WriteString("nil")
}
out.WriteString(i.Operator)
if i.Right != nil {
out.WriteString(i.Right.String())
} else {
out.WriteString("nil")
}
out.WriteRune(')')
return out.String()
}

21
rsql/ast_test.go Normal file
View File

@ -0,0 +1,21 @@
package rsql
import "testing"
func TestString(t *testing.T) {
program := &Program{
Statements: []Statement {
ExpressionStatement{
Token: Token{TokEQ, "=="},
Expression: &InfixExpression{
Token: Token{TokEQ, "=="},
Left: &Identifier{Token{TokIdent,"foo"}, "foo"},
Operator: "==",
Right: &Integer{Token{TokInteger, "5"}, 5},
},
},
},
}
t.Log(program.String())
}

90
rsql/dbcolumns.go Normal file
View File

@ -0,0 +1,90 @@
package rsql
import (
"fmt"
"reflect"
"strings"
"unicode"
)
// DbColumns database model metadata
type DbColumns struct {
Cols []string
index map[string]int
Table string
View string
}
// Col returns the mapped column names
func (d *DbColumns) Col(column string) (s string, err error) {
idx, ok := d.Index(column)
if !ok {
err = fmt.Errorf("column not found on table: %v", column)
return
}
return d.Cols[idx], err
}
// Index returns the column number
func (d *DbColumns) Index(column string) (idx int, ok bool) {
idx, ok = d.index[column]
return
}
// GetDbColumns builds a metadata struct
func GetDbColumns(o interface{}) *DbColumns {
d := DbColumns{}
t := reflect.TypeOf(o)
d.index = make(map[string]int)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
sp := append(strings.Split(field.Tag.Get("db"), ","), "")
tag := sp[0]
json := field.Tag.Get("json")
if tag == "" {
tag = json
}
graphql := field.Tag.Get("graphql")
if tag == "" {
tag = graphql
}
if tag == "-" {
continue
}
if tag == "" {
tag = field.Name
}
d.index[field.Name] = len(d.Cols)
if _, ok := d.index[tag]; !ok && tag != "" {
d.index[tag] = len(d.Cols)
}
if _, ok := d.index[json]; !ok && json != "" {
d.index[json] = len(d.Cols)
}
if _, ok := d.index[graphql]; !ok && graphql != "" {
d.index[graphql] = len(d.Cols)
} else if !ok && graphql == "" {
a := []rune(field.Name)
for i := 0; i < len(a); i++ {
if unicode.IsLower(a[i]) {
break
}
a[i] = unicode.ToLower(a[i])
}
graphql = string(a)
d.index[graphql] = len(d.Cols)
}
d.Cols = append(d.Cols, tag)
}
return &d
}

258
rsql/lexer.go Normal file
View File

@ -0,0 +1,258 @@
package rsql
import (
"unicode"
"unicode/utf8"
)
type Lexer struct {
input string
position int
readPosition int
rune rune
}
// NewLexer returns a new lexing generator
func NewLexer(in string) *Lexer {
l := &Lexer{input: in}
l.readRune()
return l
}
// NextToken returns the next token from lexer
func (l *Lexer) NextToken() Token {
var tok Token
l.skipSpace()
switch l.rune {
case '-':
l.readRune()
if isNumber(l.rune) {
var isFloat bool
tok.Literal, isFloat = l.readNumber()
if isFloat {
tok.Type = TokFloat
} else {
tok.Type = TokInteger
}
} else if isLetter(l.rune) {
tok.Literal = l.readIdentifier()
tok.Type = lookupIdent(tok.Literal)
} else {
tok = newToken(TokIllegal, l.rune)
return tok
}
tok.Literal = "-" + tok.Literal
return tok
case '=':
r := l.peekRune()
if r == '=' {
r := l.rune
l.readRune()
tok.Type, tok.Literal = TokEQ, string(r)+string(l.rune)
} else if isLetter(r) {
tok = l.readFIQL()
return tok
} else {
tok = newToken(TokIllegal, l.rune)
}
case ';':
tok = newToken(TokAND, l.rune)
case ',':
tok = newToken(TokOR, l.rune)
case ')':
tok = newToken(TokRParen, l.rune)
case '(':
tok = newToken(TokLParen, l.rune)
case ']':
tok = newToken(TokRBracket, l.rune)
case '[':
tok = newToken(TokLBracket, l.rune)
case '~':
tok = newToken(TokLIKE, l.rune)
case '!':
if l.peekRune() == '=' {
r := l.rune
l.readRune()
tok.Type, tok.Literal = TokNEQ, string(r)+string(l.rune)
} else if l.peekRune() == '~' {
r := l.rune
l.readRune()
tok.Type, tok.Literal = TokNLIKE, string(r)+string(l.rune)
} else {
tok = newToken(TokIllegal, l.rune)
return tok
}
case '<':
if l.peekRune() == '=' {
r := l.rune
l.readRune()
tok.Type, tok.Literal = TokLE, string(r)+string(l.rune)
} else {
tok = newToken(TokLT, l.rune)
}
case '>':
if l.peekRune() == '=' {
r := l.rune
l.readRune()
tok.Type, tok.Literal = TokGE, string(r)+string(l.rune)
} else {
tok = newToken(TokGT, l.rune)
}
case '"', '\'':
tok.Type = TokString
tok.Literal = l.readString(l.rune)
case 0:
tok.Type, tok.Literal = TokEOF, ""
default:
if isNumber(l.rune) {
var isFloat bool
tok.Literal, isFloat = l.readNumber()
if isFloat {
tok.Type = TokFloat
} else {
tok.Type = TokInteger
}
} else if isLetter(l.rune) {
tok.Literal = l.readIdentifier()
tok.Type = lookupIdent(tok.Literal)
} else {
tok = newToken(TokIllegal, l.rune)
return tok
}
return tok
}
l.readRune()
return tok
}
func (l *Lexer) readRune() {
var size int
if l.readPosition >= len(l.input) {
l.rune = 0
} else {
l.rune, size = utf8.DecodeRuneInString(l.input[l.readPosition:])
}
l.position = l.readPosition
l.readPosition += size
}
func (l *Lexer) peekRune() rune {
if l.readPosition >= len(l.input) {
return 0
}
r, _ := utf8.DecodeRuneInString(l.input[l.readPosition:])
return r
}
func (l *Lexer) skipSpace() {
for unicode.IsSpace(l.rune) {
l.readRune()
}
}
func (l *Lexer) readIdentifier() string {
position := l.position
if isLetter(l.rune) {
l.readRune()
}
for isLetter(l.rune) || isNumber(l.rune) {
l.readRune()
}
return l.input[position:l.position]
}
func (l *Lexer) readNumber() (string, bool) {
isFloat := false
position := l.position
for isNumber(l.rune) {
if l.rune == '.' {
isFloat = true
}
l.readRune()
}
return l.input[position:l.position], isFloat
}
func (l *Lexer) readString(st rune) string {
position := l.position + 1
escape := false
for {
l.readRune()
if l.rune == '\\' {
escape = true
continue
}
if escape {
escape = false
continue
}
if l.rune == st || l.rune == 0 {
break
}
}
return l.input[position:l.position]
}
func (l *Lexer) readFIQL() Token {
l.readRune()
s := l.readIdentifier()
if l.rune != '=' {
return Token{TokIllegal, "=" + s}
}
l.readRune()
switch s {
case "eq":
return Token{TokEQ, "=" + s + "="}
case "neq":
return Token{TokNEQ, "=" + s + "="}
case "gt":
return Token{TokGT, "=" + s + "="}
case "ge":
return Token{TokGE, "=" + s + "="}
case "lt":
return Token{TokLT, "=" + s + "="}
case "le":
return Token{TokLE, "=" + s + "="}
default:
return Token{TokExtend, "=" + s + "="}
}
}
func isLetter(r rune) bool {
if unicode.IsSpace(r) {
return false
}
switch r {
case '"', '\'', '(', ')', ';', ',', '=', '!', '~', '<', '>', '[', ']':
return false
}
if '0' < r && r < '9' || r == '.' {
return false
}
return unicode.IsPrint(r)
}
func isNumber(r rune) bool {
if '0' <= r && r <= '9' || r == '.' {
return true
}
return false
}

105
rsql/lexer_test.go Normal file
View File

@ -0,0 +1,105 @@
package rsql
import (
"testing"
"github.com/matryer/is"
)
func TestReservedToken(t *testing.T) {
input := `( ) ; , == != ~ < > <= >= [ ]`
tests := []struct {
expectedType TokenType
expectedLiteral string
}{
{TokLParen, "("},
{TokRParen, ")"},
{TokAND, ";"},
{TokOR, ","},
{TokEQ, "=="},
{TokNEQ, "!="},
{TokLIKE, "~"},
{TokLT, "<"},
{TokGT, ">"},
{TokLE, "<="},
{TokGE, ">="},
{TokLBracket, "["},
{TokRBracket, "]"},
}
t.Run("Reserved Tokens", func(t *testing.T) {
is := is.New(t)
l := NewLexer(input)
for _, tt := range tests {
tok := l.NextToken()
is.Equal(tt.expectedType, tok.Type)
is.Equal(tt.expectedLiteral, tok.Literal)
}
})
}
func TestNextToken(t *testing.T) {
input := `director=='name\'s';actor=eq="name's";Year=le=2000,Year>=2010;(one <= -1.0, two != true),three=in=(1,2,3);c4==5`
tests := []struct {
expectedType TokenType
expectedLiteral string
}{
{TokIdent, `director`},
{TokEQ, `==`},
{TokString, `name\'s`},
{TokAND, `;`},
{TokIdent, `actor`},
{TokEQ, `=eq=`},
{TokString, `name's`},
{TokAND, `;`},
{TokIdent, "Year"},
{TokLE, "=le="},
{TokInteger, "2000"},
{TokOR, ","},
{TokIdent, "Year"},
{TokGE, ">="},
{TokInteger, "2010"},
{TokAND, ";"},
{TokLParen, "("},
{TokIdent, "one"},
{TokLE, "<="},
{TokFloat, "-1.0"},
{TokOR, ","},
{TokIdent, "two"},
{TokNEQ, "!="},
{TokTRUE, "true"},
{TokRParen, ")"},
{TokOR, ","},
{TokIdent, "three"},
{TokExtend, "=in="},
{TokLParen, "("},
{TokInteger, "1"},
{TokOR, ","},
{TokInteger, "2"},
{TokOR, ","},
{TokInteger, "3"},
{TokRParen, ")"},
{TokAND, ";"},
{TokIdent, "c4"},
{TokEQ, "=="},
{TokInteger, "5"},
}
t.Run("Next Token Parsing", func(t *testing.T) {
is := is.New(t)
l := NewLexer(input)
c := 0
for _, tt := range tests {
c++
tok := l.NextToken()
is.Equal(tt.expectedType, tok.Type)
is.Equal(tt.expectedLiteral, tok.Literal)
}
is.Equal(c, len(tests))
})
}

285
rsql/parser.go Normal file
View File

@ -0,0 +1,285 @@
package rsql
import (
"fmt"
"strconv"
"strings"
)
// Precidence enumerations
const (
_ = iota
PrecedenceLowest
PrecedenceAND
PrecedenceOR
PrecedenceCompare
PrecedenceHighest
)
var precidences = map[TokenType]int{
TokEQ: PrecedenceCompare,
TokNEQ: PrecedenceCompare,
TokLT: PrecedenceCompare,
TokLE: PrecedenceCompare,
TokGT: PrecedenceCompare,
TokGE: PrecedenceCompare,
TokLIKE: PrecedenceCompare,
TokOR: PrecedenceOR,
TokAND: PrecedenceAND,
}
type (
prefixParseFn func() Expression
infixParseFn func(expression Expression) Expression
)
// Parser reads lexed values and builds an AST
type Parser struct {
l *Lexer
errors []string
curToken Token
peekToken Token
prefixParseFns map[TokenType]prefixParseFn
infixParseFns map[TokenType]infixParseFn
}
// NewParser returns a parser for a given lexer
func NewParser(l *Lexer) *Parser {
p := &Parser{l: l}
p.prefixParseFns = make(map[TokenType]prefixParseFn)
p.registerPrefix(TokIdent, p.parseIdentifier)
p.registerPrefix(TokInteger, p.parseInteger)
p.registerPrefix(TokFloat, p.parseFloat)
p.registerPrefix(TokTRUE, p.parseBool)
p.registerPrefix(TokFALSE, p.parseBool)
p.registerPrefix(TokNULL, p.parseNull)
p.registerPrefix(TokString, p.parseString)
p.registerPrefix(TokLParen, p.parseGroupedExpression)
p.registerPrefix(TokLBracket, p.parseArray)
p.infixParseFns = make(map[TokenType]infixParseFn)
p.registerInfix(TokEQ, p.parseInfixExpression)
p.registerInfix(TokNEQ, p.parseInfixExpression)
p.registerInfix(TokLT, p.parseInfixExpression)
p.registerInfix(TokLE, p.parseInfixExpression)
p.registerInfix(TokGT, p.parseInfixExpression)
p.registerInfix(TokGE, p.parseInfixExpression)
p.registerInfix(TokLIKE, p.parseInfixExpression)
p.registerInfix(TokAND, p.parseInfixExpression)
p.registerInfix(TokOR, p.parseInfixExpression)
p.nextToken()
p.nextToken()
return p
}
// DefaultParse sets up a default lex/parse and returns the program
func DefaultParse(in string) *Program {
args := make(map[string]string)
in, argstr, ok := strings.Cut(in, "|")
if ok {
for _, fd := range strings.Fields(argstr) {
a, b, _ := strings.Cut(fd, ":")
args[a] = b
}
}
l := NewLexer(in)
p := NewParser(l)
return p.ParseProgram(args)
}
func (p *Parser) registerPrefix(tokenType TokenType, fn prefixParseFn) {
p.prefixParseFns[tokenType] = fn
}
func (p *Parser) registerInfix(tokenType TokenType, fn infixParseFn) {
p.infixParseFns[tokenType] = fn
}
// Errors returns a list of errors while parsing
func (p *Parser) Errors() []string {
return p.errors
}
func (p *Parser) peekError(t TokenType) {
msg := fmt.Sprintf("expected next token to be %s, got %s instad",
t, p.peekToken.Type)
p.errors = append(p.errors, msg)
}
func (p *Parser) nextToken() {
p.curToken = p.peekToken
p.peekToken = p.l.NextToken()
}
func (p *Parser) curTokenIs(t TokenType) bool {
return p.curToken.Type == t
}
func (p *Parser) peekTokenIs(t TokenType) bool {
return p.peekToken.Type == t
}
func (p *Parser) expectPeek(t TokenType) bool {
if p.peekTokenIs(t) {
p.nextToken()
return true
}
p.peekError(t)
return false
}
func (p *Parser) peekPrecedence() int {
if p, ok := precidences[p.peekToken.Type]; ok {
return p
}
return PrecedenceLowest
}
func (p *Parser) curPrecedence() int {
if p, ok := precidences[p.curToken.Type]; ok {
return p
}
return PrecedenceLowest
}
// ParseProgram builds a program AST from lexer
func (p *Parser) ParseProgram(args map[string]string) *Program {
program := &Program{Args: args}
program.Statements = []Statement{}
for p.curToken.Type != TokEOF {
stmt := p.parseStatement()
if stmt != nil {
program.Statements = append(program.Statements, stmt)
}
p.nextToken()
}
return program
}
func (p *Parser) parseStatement() Statement {
switch p.curToken.Type {
default:
return p.parseExpressionStatement()
}
}
func (p *Parser) parseExpressionStatement() *ExpressionStatement {
stmt := &ExpressionStatement{Token: p.curToken}
stmt.Expression = p.parseExpression(PrecedenceLowest)
return stmt
}
func (p *Parser) parseExpression(precedence int) Expression {
prefix := p.prefixParseFns[p.curToken.Type]
if prefix == nil {
msg := fmt.Sprintf("no prefix parse function for %s found", p.curToken.Type)
p.errors = append(p.errors, msg)
return nil
}
leftExp := prefix()
for !p.peekTokenIs(TokEOF) && precedence < p.peekPrecedence() {
infix := p.infixParseFns[p.peekToken.Type]
if infix == nil {
return leftExp
}
p.nextToken()
leftExp = infix(leftExp)
}
return leftExp
}
func (p *Parser) parseIdentifier() Expression {
return &Identifier{Token: p.curToken, Value: p.curToken.Literal}
}
func (p *Parser) parseInteger() Expression {
lit := &Integer{Token: p.curToken}
value, err := strconv.ParseInt(p.curToken.Literal, 0, 64)
if err != nil {
msg := fmt.Sprintf("could not parse %q as integer", p.curToken.Literal)
p.errors = append(p.errors, msg)
return nil
}
lit.Value = value
return lit
}
func (p *Parser) parseFloat() Expression {
lit := &Float{Token: p.curToken}
value, err := strconv.ParseFloat(p.curToken.Literal, 64)
if err != nil {
msg := fmt.Sprintf("could not parse %q as float", p.curToken.Literal)
p.errors = append(p.errors, msg)
return nil
}
lit.Value = value
return lit
}
func (p *Parser) parseBool() Expression {
return &Bool{Token: p.curToken, Value: p.curTokenIs(TokTRUE)}
}
func (p *Parser) parseString() Expression {
s := p.curToken.Literal
s = strings.Replace(s, `\'`, `'`, -1)
s = strings.Replace(s, `\"`, `"`, -1)
return &String{Token: p.curToken, Value: s}
}
func (p *Parser) parseNull() Expression {
return &Null{Token: p.curToken}
}
func (p *Parser) parseArray() Expression {
array := &Array{Token: p.curToken}
array.Elements = p.parseExpressionList(TokRBracket)
return array
}
func (p *Parser) parseExpressionList(end TokenType) []Expression {
var list []Expression
if p.peekTokenIs(end) {
p.nextToken()
return list
}
p.nextToken()
list = append(list, p.parseExpression(PrecedenceHighest))
for p.peekTokenIs(TokOR) {
p.nextToken()
p.nextToken()
list = append(list, p.parseExpression(PrecedenceHighest))
}
if !p.expectPeek(end) {
return nil
}
return list
}
func (p *Parser) parseInfixExpression(left Expression) Expression {
expression := &InfixExpression{
Token: p.curToken,
Left: left,
Operator: p.curToken.Literal,
}
precidence := p.curPrecedence()
p.nextToken()
expression.Right = p.parseExpression(precidence)
return expression
}
func (p *Parser) parseGroupedExpression() Expression {
p.nextToken()
exp := p.parseExpression(PrecedenceLowest)
if !p.expectPeek(TokRParen) {
return nil
}
return exp
}

309
rsql/parser_test.go Normal file
View File

@ -0,0 +1,309 @@
package rsql
import (
"fmt"
"strings"
"testing"
"github.com/matryer/is"
)
func typeOf(t any) string { return fmt.Sprintf("%T", t) }
func TestIdentifierExpression(t *testing.T) {
input := `foobar`
t.Run("Identifier Expressions", func(t *testing.T) {
is := is.New(t)
l := NewLexer(input)
p := NewParser(l)
program := p.ParseProgram(nil)
checkParserErrors(t, p)
is.Equal(len(program.Statements), 1)
// if len(program.Statements) != 1 {
// t.Fatalf("program has not envough statements. got=%d", len(program.Statements))
// }
})
}
func TestIntegerExpression(t *testing.T) {
input := `5`
t.Run("IntegerExpression", func(t *testing.T) {
is := is.New(t)
l := NewLexer(input)
p := NewParser(l)
program := p.ParseProgram(nil)
checkParserErrors(t, p)
is.Equal(len(program.Statements), 1)
// if len(program.Statements) != 1 {
// t.Fatalf("program has not enough statements. got=%d", len(program.Statements))
// }
stmt, ok := program.Statements[0].(*ExpressionStatement)
is.Equal(typeOf(program.Statements[0]), typeOf(&ExpressionStatement{}))
is.True(ok)
// if !ok {
// t.Fatalf("program.Statements[0] is not ExpressionStatement got=%T",
// program.Statements[0])
// }
literal, ok := stmt.Expression.(*Integer)
is.Equal(typeOf(literal), typeOf(&Integer{}))
is.True(ok)
// if !ok {
// t.Fatalf("stmt.Expression is not Integer got=%T",
// stmt.Expression)
// }
is.Equal(literal.Value, int64(5))
// if literal.Value != 5 {
// t.Errorf("literal.Value not %d. got=%d", 5, literal.Value)
// }
is.Equal(literal.TokenLiteral(), "5")
// if literal.TokenLiteral() != "5" {
// t.Errorf("literal.TokenLiteral not %v. got=%v", "5", literal.TokenLiteral())
// }
})
}
func TestInfixExpression(t *testing.T) {
tests := []struct {
input string
left string
operator string
right int64
}{
{"foo == 1", "foo", "==", 1},
{"bar > 2", "bar", ">", 2},
{"bin < 3", "bin", "<", 3},
{"baz != 4", "baz", "!=", 4},
{"buf >= 5", "buf", ">=", 5},
{"goz <= 6", "goz", "<=", 6},
}
t.Run("Infix Expressions", func(t *testing.T) {
is := is.New(t)
for _, tt := range tests {
l := NewLexer(tt.input)
p := NewParser(l)
program := p.ParseProgram(nil)
checkParserErrors(t, p)
is.Equal(len(program.Statements), 1)
// if len(program.Statements) != 1 {
// t.Fatalf("program has not envough statements. got=%d", len(program.Statements))
// }
stmt, ok := program.Statements[0].(*ExpressionStatement)
is.Equal(typeOf(stmt), typeOf(&ExpressionStatement{}))
is.True(ok)
// if !ok {
// t.Fatalf("program.Statements[0] is not ExpressionStatement got=%T",
// program.Statements[0])
// }
exp, ok := stmt.Expression.(*InfixExpression)
is.Equal(typeOf(exp), typeOf(&InfixExpression{}))
is.True(ok)
// if !ok {
// t.Fatalf("stmt.Expression is not InfixExpression got=%T",
// stmt.Expression)
// }
if !testIdentifier(t, exp.Left, tt.left) {
return
}
is.Equal(exp.Operator, tt.operator)
// if exp.Operator != tt.operator {
// t.Fatalf("exp.Operator is not '%v'. got '%v'", tt.operator, exp.Operator)
// }
if testInteger(t, exp.Right, tt.right) {
return
}
}
})
}
func TestOperatorPrecedenceParsing(t *testing.T) {
tests := []struct {
input string
expect string
}{
{
"foo == 1; bar == 2.0",
"((foo==1);(bar==2.0))",
},
{
`director=='name\'s';actor=eq="name\'s";Year=le=2000,Year>=2010;one <= -1.0, two != true`,
`((((director=="name's");(actor=eq="name's"));((Year=le=2000),(Year>=2010)));((one<=-1.0),(two!=true)))`,
},
}
t.Run("Operator Precidence Parsing", func(t *testing.T) {
is := is.New(t)
for _, tt := range tests {
l := NewLexer(tt.input)
p := NewParser(l)
program := p.ParseProgram(nil)
checkParserErrors(t, p)
actual := program.String()
is.Equal(actual, tt.expect)
// if actual != tt.expect {
// t.Errorf("expcected=%q, got=%q", tt.expect, actual)
// }
}
})
}
func TestParsingArray(t *testing.T) {
input := "[1, 2.1, true, null]"
l := NewLexer(input)
p := NewParser(l)
program := p.ParseProgram(nil)
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("program has not enough statements. got=%d", len(program.Statements))
}
stmt, ok := program.Statements[0].(*ExpressionStatement)
if !ok {
t.Fatalf("program.Statements[0] is not ExpressionStatement got=%T",
program.Statements[0])
}
array, ok := stmt.Expression.(*Array)
if !ok {
t.Fatalf("stmt.Expression is not Array got=%T",
stmt.Expression)
}
if len(array.Elements) != 4 {
t.Fatalf("len(array.Elements) not 4. got=%v", len(array.Elements))
}
testInteger(t, array.Elements[0], 1)
testFloat(t, array.Elements[1], 2.1)
testBool(t, array.Elements[2], true)
testNull(t, array.Elements[3])
}
func checkParserErrors(t *testing.T, p *Parser) {
errors := p.Errors()
if len(errors) == 0 {
return
}
t.Errorf("parser has %d errors", len(errors))
for _, msg := range errors {
t.Errorf("parser error: %q", msg)
}
t.FailNow()
}
func testInteger(t *testing.T, e Expression, value int64) bool {
literal, ok := e.(*Integer)
if !ok {
t.Errorf("stmt.Expression is not Integer got=%T", e)
return false
}
if literal.Value != value {
t.Errorf("literal.Value not %d. got=%d", value, literal.Value)
return false
}
if literal.TokenLiteral() != fmt.Sprintf("%v", value) {
t.Errorf("literal.TokenLiteral not %v. got=%v", value, literal.TokenLiteral())
return false
}
return true
}
func testFloat(t *testing.T, e Expression, value float64) bool {
literal, ok := e.(*Float)
if !ok {
t.Errorf("stmt.Expression is not Float got=%T", e)
return false
}
if literal.Value != value {
t.Errorf("literal.Value not %f. got=%f", value, literal.Value)
return false
}
if literal.TokenLiteral() != fmt.Sprintf("%v", value) {
t.Errorf("literal.TokenLiteral not %q. got=%q", fmt.Sprintf("%v", value), literal.TokenLiteral())
return false
}
return true
}
func testBool(t *testing.T, e Expression, value bool) bool {
literal, ok := e.(*Bool)
if !ok {
t.Errorf("stmt.Expression is not Float got=%T", e)
return false
}
if literal.Value != value {
t.Errorf("literal.Value not %t. got=%t", value, literal.Value)
return false
}
if literal.TokenLiteral() != fmt.Sprintf("%v", value) {
t.Errorf("literal.TokenLiteral not %v. got=%v", value, literal.TokenLiteral())
return false
}
return true
}
func testNull(t *testing.T, e Expression) bool {
literal, ok := e.(*Null)
if !ok {
t.Errorf("stmt.Expression is not Null got=%T", e)
return false
}
if literal.Token.Type != TokNULL {
t.Errorf("liternal.Token is not TokNULL got=%T", e)
return false
}
return true
}
func testIdentifier(t *testing.T, e Expression, value string) bool {
literal, ok := e.(*Identifier)
if !ok {
t.Errorf("stmt.Expression is not Integer got=%T", e)
return false
}
if literal.Value != value {
t.Errorf("literal.Value not %s. got=%s", value, literal.Value)
return false
}
if literal.TokenLiteral() != value {
t.Errorf("literal.TokenLiteral not %v. got=%v", value, literal.TokenLiteral())
return false
}
return true
}
func TestReplace(t *testing.T) {
is := is.New(t)
is.Equal(strings.Replace(`name\'s`, `\'`, `'`, -1), `name's`)
}

300
rsql/squirrel/sqlizer.go Normal file
View File

@ -0,0 +1,300 @@
package squirrel
import (
"fmt"
"strconv"
"strings"
"github.com/Masterminds/squirrel"
"go.sour.is/pkg/rsql"
)
type dbInfo interface {
Col(string) (string, error)
}
type args map[string]string
func (d *decoder) mkArgs(a args) args {
m := make(args, len(a))
for k, v := range a {
if k == "limit" || k == "offset" {
m[k] = v
continue
}
var err error
if k, err = d.dbInfo.Col(k); err == nil {
m[k] = v
}
}
return m
}
func (a args) Limit() (uint64, bool) {
if a == nil {
return 0, false
}
if v, ok := a["limit"]; ok {
i, err := strconv.ParseUint(v, 10, 64)
return i, err == nil
}
return 0, false
}
func (a args) Offset() (uint64, bool) {
if a == nil {
return 0, false
}
if v, ok := a["offset"]; ok {
i, err := strconv.ParseUint(v, 10, 64)
return i, err == nil
}
return 0, false
}
func (a args) Order() []string {
var lis []string
for k, v := range a {
if k == "limit" || k == "offset" {
continue
}
lis = append(lis, k+" "+v)
}
return lis
}
func Query(in string, db dbInfo) (squirrel.Sqlizer, args, error) {
d := decoder{dbInfo: db}
program := rsql.DefaultParse(in)
sql, err := d.decode(program)
return sql, d.mkArgs(program.Args), err
}
type decoder struct {
dbInfo dbInfo
}
func (db *decoder) decode(in *rsql.Program) (squirrel.Sqlizer, error) {
switch len(in.Statements) {
case 0:
return nil, nil
case 1:
return db.decodeStatement(in.Statements[0])
default:
a := squirrel.And{}
for _, stmt := range in.Statements {
d, err := db.decodeStatement(stmt)
if d == nil {
return nil, err
}
a = append(a, d)
}
return a, nil
}
}
func (db *decoder) decodeStatement(in rsql.Statement) (squirrel.Sqlizer, error) {
switch s := in.(type) {
case *rsql.ExpressionStatement:
return db.decodeExpression(s.Expression)
}
return nil, nil
}
func (db *decoder) decodeExpression(in rsql.Expression) (squirrel.Sqlizer, error) {
switch e := in.(type) {
case *rsql.InfixExpression:
return db.decodeInfix(e)
}
return nil, nil
}
func (db *decoder) decodeInfix(in *rsql.InfixExpression) (squirrel.Sqlizer, error) {
switch in.Token.Type {
case rsql.TokAND:
a := squirrel.And{}
left, err := db.decodeExpression(in.Left)
if err != nil {
return nil, err
}
switch v := left.(type) {
case squirrel.And:
for _, el := range v {
if el != nil {
a = append(a, el)
}
}
default:
if v != nil {
a = append(a, v)
}
}
right, err := db.decodeExpression(in.Right)
if err != nil {
return nil, err
}
switch v := right.(type) {
case squirrel.And:
for _, el := range v {
if el != nil {
a = append(a, el)
}
}
default:
if v != nil {
a = append(a, v)
}
}
return a, nil
case rsql.TokOR:
left, err := db.decodeExpression(in.Left)
if err != nil {
return nil, err
}
right, err := db.decodeExpression(in.Right)
if err != nil {
return nil, err
}
return squirrel.Or{left, right}, nil
case rsql.TokEQ:
col, err := db.dbInfo.Col(in.Left.String())
if err != nil {
return nil, err
}
v, err := db.decodeValue(in.Right)
if err != nil {
return nil, err
}
return squirrel.Eq{col: v}, nil
case rsql.TokLIKE:
col, err := db.dbInfo.Col(in.Left.String())
if err != nil {
return nil, err
}
v, err := db.decodeValue(in.Right)
if err != nil {
return nil, err
}
switch value := v.(type) {
case string:
return Like{col, strings.Replace(value, "*", "%", -1)}, nil
default:
return nil, fmt.Errorf("LIKE requires a string value")
}
case rsql.TokNEQ:
col, err := db.dbInfo.Col(in.Left.String())
if err != nil {
return nil, err
}
v, err := db.decodeValue(in.Right)
if err != nil {
return nil, err
}
return squirrel.NotEq{col: v}, nil
case rsql.TokGT:
col, err := db.dbInfo.Col(in.Left.String())
if err != nil {
return nil, err
}
v, err := db.decodeValue(in.Right)
if err != nil {
return nil, err
}
return squirrel.Gt{col: v}, nil
case rsql.TokGE:
col, err := db.dbInfo.Col(in.Left.String())
if err != nil {
return nil, err
}
v, err := db.decodeValue(in.Right)
if err != nil {
return nil, err
}
return squirrel.GtOrEq{col: v}, nil
case rsql.TokLT:
col, err := db.dbInfo.Col(in.Left.String())
if err != nil {
return nil, err
}
v, err := db.decodeValue(in.Right)
if err != nil {
return nil, err
}
return squirrel.Lt{col: v}, nil
case rsql.TokLE:
col, err := db.dbInfo.Col(in.Left.String())
if err != nil {
return nil, err
}
v, err := db.decodeValue(in.Right)
if err != nil {
return nil, err
}
return squirrel.LtOrEq{col: v}, nil
default:
return nil, nil
}
}
func (db *decoder) decodeValue(in rsql.Expression) (interface{}, error) {
switch v := in.(type) {
case *rsql.Array:
var values []interface{}
for _, el := range v.Elements {
v, err := db.decodeValue(el)
if err != nil {
return nil, err
}
values = append(values, v)
}
return values, nil
case *rsql.InfixExpression:
return db.decodeInfix(v)
case *rsql.Identifier:
return v.Value, nil
case *rsql.Integer:
return v.Value, nil
case *rsql.Float:
return v.Value, nil
case *rsql.String:
return v.Value, nil
case *rsql.Bool:
return v.Value, nil
case *rsql.Null:
return nil, nil
}
return nil, nil
}
type Like struct {
column string
value string
}
func (l Like) ToSql() (sql string, args []interface{}, err error) {
sql = fmt.Sprintf("%s LIKE(?)", l.column)
args = append(args, l.value)
return
}

View File

@ -0,0 +1,141 @@
package squirrel
import (
"fmt"
"testing"
"github.com/Masterminds/squirrel"
"github.com/matryer/is"
"go.sour.is/pkg/rsql"
)
type testTable struct {
Foo string `json:"foo"`
Bar string `json:"bar"`
Baz string `json:"baz"`
Director string `json:"director"`
Actor string `json:"actor"`
Year string `json:"year"`
Genres string `json:"genres"`
One string `json:"one"`
Two string `json:"two"`
Family string `json:"family_name"`
}
func TestQuery(t *testing.T) {
d := rsql.GetDbColumns(testTable{})
is := is.New(t)
tests := []struct {
input string
expect squirrel.Sqlizer
expectLimit *uint64
expectOffset *uint64
expectOrder []string
fail bool
}{
{input: "foo==[1, 2, 3]", expect: squirrel.Eq{"foo": []interface{}{1, 2, 3}}},
{input: "foo==1,(bar==2;baz==3)", expect: squirrel.Or{squirrel.Eq{"foo": 1}, squirrel.And{squirrel.Eq{"bar": 2}, squirrel.Eq{"baz": 3}}}},
{input: "foo==1", expect: squirrel.Eq{"foo": 1}},
{input: "foo!=1.1", expect: squirrel.NotEq{"foo": 1.1}},
{input: "foo==true", expect: squirrel.Eq{"foo": true}},
{input: "foo==null", expect: squirrel.Eq{"foo": nil}},
{input: "foo>2", expect: squirrel.Gt{"foo": 2}},
{input: "foo>=2.1", expect: squirrel.GtOrEq{"foo": 2.1}},
{input: "foo<3", expect: squirrel.Lt{"foo": 3}},
{input: "foo<=3.1", expect: squirrel.LtOrEq{"foo": 3.1}},
{input: "foo=eq=1", expect: squirrel.Eq{"foo": 1}},
{input: "foo=neq=1.1", expect: squirrel.NotEq{"foo": 1.1}},
{input: "foo=gt=2", expect: squirrel.Gt{"foo": 2}},
{input: "foo=ge=2.1", expect: squirrel.GtOrEq{"foo": 2.1}},
{input: "foo=lt=3", expect: squirrel.Lt{"foo": 3}},
{input: "foo=le=3.1", expect: squirrel.LtOrEq{"foo": 3.1}},
{input: "foo==1;bar==2", expect: squirrel.And{squirrel.Eq{"foo": 1}, squirrel.Eq{"bar": 2}}},
{input: "foo==1,bar==2", expect: squirrel.Or{squirrel.Eq{"foo": 1}, squirrel.Eq{"bar": 2}}},
{input: "foo==1,bar==2;baz=3", expect: nil},
{
input: `director=='name\'s';actor=eq="name\'s";Year=le=2000,Year>=2010;one <= -1.0, two != true`,
expect: squirrel.And{
squirrel.Eq{"director": "name's"},
squirrel.Eq{"actor": "name's"},
squirrel.Or{
squirrel.LtOrEq{"year": 2000},
squirrel.GtOrEq{"year": 2010},
},
squirrel.Or{
squirrel.LtOrEq{"one": -1.0},
squirrel.NotEq{"two": true},
},
},
},
{
input: `genres==[sci-fi,action] ; genres==[romance,animated,horror] , director~Que*Tarantino`,
expect: squirrel.And{
squirrel.Eq{"genres": []interface{}{"sci-fi", "action"}},
squirrel.Or{
squirrel.Eq{"genres": []interface{}{"romance", "animated", "horror"}},
Like{"director", "Que%Tarantino"},
},
},
},
{input: "", expect: nil},
{input: "family_name==LUNDY", expect: squirrel.Eq{"family_name": "LUNDY"}},
{input: "family_name==[1,2,null]", expect: squirrel.Eq{"family_name": []interface{}{1, 2, nil}}},
{input: "family_name=LUNDY", expect: nil},
{input: "family_name==LUNDY and family_name==SMITH", expect: squirrel.And{squirrel.Eq{"family_name": "LUNDY"}, squirrel.Eq{"family_name": "SMITH"}}},
{input: "family_name==LUNDY or family_name==SMITH", expect: squirrel.Or{squirrel.Eq{"family_name": "LUNDY"}, squirrel.Eq{"family_name": "SMITH"}}},
{input: "foo==1,family_name=LUNDY;baz==2", expect: nil},
{input: "foo ~ bar*", expect: Like{"foo", "bar%"}},
{input: "foo ~ [bar*,bin*]", expect: nil, fail: true},
{input: "foo==1|limit:10", expect: squirrel.Eq{"foo": 1}, expectLimit: ptr(uint64(10))},
{input: "foo==1|offset:2", expect: squirrel.Eq{"foo": 1}, expectOffset: ptr(uint64(2))},
{
input: "foo>=1|limit:10 offset:2 foo:desc",
expect: squirrel.GtOrEq{"foo": 1},
expectLimit: ptr(uint64(10)),
expectOffset: ptr(uint64(2)),
expectOrder: []string{"foo desc"},
},
}
for i, tt := range tests {
q, a, err := Query(tt.input, d)
if !tt.fail && err != nil {
t.Error(err)
}
if q != nil {
t.Log(q.ToSql())
}
actual := fmt.Sprintf("%#v", q)
expect := fmt.Sprintf("%#v", tt.expect)
if expect != actual {
t.Errorf("test[%d]: %v\n\tinput and expected are not the same. wanted=%s got=%s", i, tt.input, expect, actual)
}
if limit, ok := a.Limit(); tt.expectLimit != nil {
is.True(ok)
is.Equal(limit, *tt.expectLimit)
} else {
is.True(!ok)
}
if offset, ok := a.Offset(); tt.expectOffset != nil {
is.True(ok)
is.Equal(offset, *tt.expectOffset)
} else {
is.True(!ok)
}
if order := a.Order(); tt.expectOrder != nil {
is.Equal(order, tt.expectOrder)
} else {
is.Equal(len(order), 0)
}
}
}
func ptr[T any](t T) *T { return &t }

62
rsql/token.go Normal file
View File

@ -0,0 +1,62 @@
package rsql
// Tokens for RSQL FIQL
const (
TokIllegal = "TokIllegal"
TokEOF = "TokEOF"
TokIdent = "TokIdent"
TokInteger = "TokInteger"
TokString = "TokString"
TokFloat = "TokFloat"
TokExtend = "TokExtend"
TokLParen = "("
TokRParen = ")"
TokLBracket = "["
TokRBracket = "]"
TokLIKE = "~"
TokNLIKE= "!~"
TokNOT = "!"
TokLT = "<"
TokGT = ">"
TokLE = "<="
TokGE = ">="
TokEQ = "=="
TokNEQ = "!="
TokAND = ";"
TokOR = ","
TokTRUE = "true"
TokFALSE = "false"
TokNULL = "null"
)
var keywords = map[string]TokenType {
"true": TokTRUE,
"false": TokFALSE,
"null": TokNULL,
"and": TokAND,
"or": TokOR,
}
// TokenType is a token enumeration
type TokenType string
// Token is a type and literal pair
type Token struct {
Type TokenType
Literal string
}
func newToken(tokenType TokenType, ch rune) Token {
return Token{Type: tokenType, Literal: string(ch)}
}
func lookupIdent(ident string) TokenType {
if tok, ok := keywords[ident]; ok {
return tok
}
return TokIdent
}

View File

@ -35,10 +35,11 @@ func (s *Harness) Setup(ctx context.Context, apps ...application) error {
ctx, span := lg.Span(ctx) ctx, span := lg.Span(ctx)
defer span.End() defer span.End()
s.onRunning = make(chan struct{})
// setup crontab // setup crontab
c := cron.New(cron.DefaultGranularity) c := cron.New(cron.DefaultGranularity)
s.OnStart(c.Run) s.OnStart(c.Run)
s.onRunning = make(chan struct{})
s.crontab = c s.crontab = c
var err error var err error

View File

@ -6,6 +6,7 @@ import (
"strings" "strings"
"go.sour.is/pkg/math" "go.sour.is/pkg/math"
"golang.org/x/exp/maps"
) )
type Set[T comparable] map[T]struct{} type Set[T comparable] map[T]struct{}
@ -33,6 +34,9 @@ func (s Set[T]) Delete(items ...T) Set[T] {
} }
return s return s
} }
func (s Set[T]) Values() []T {
return maps.Keys(s)
}
func (s Set[T]) Equal(e Set[T]) bool { func (s Set[T]) Equal(e Set[T]) bool {
for k := range s { for k := range s {

View File

@ -4,6 +4,7 @@
package xdg package xdg
import ( import (
"errors"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -36,6 +37,9 @@ func setENV(name, value string) string {
return literal(name) return literal(name)
} }
func Get(base, suffix string) string { func Get(base, suffix string) string {
return strings.Join(paths(base, suffix), string(os.PathListSeparator))
}
func paths(base, suffix string) []string {
paths := strings.Split(os.ExpandEnv(base), string(os.PathListSeparator)) paths := strings.Split(os.ExpandEnv(base), string(os.PathListSeparator))
for i, path := range paths { for i, path := range paths {
if strings.HasPrefix(path, "~") { if strings.HasPrefix(path, "~") {
@ -43,7 +47,17 @@ func Get(base, suffix string) string {
} }
paths[i] = os.ExpandEnv(filepath.Join(path, suffix)) paths[i] = os.ExpandEnv(filepath.Join(path, suffix))
} }
return strings.Join(paths, string(os.PathListSeparator)) return paths
}
func Find(base, filename string) []string {
var files []string
for _, f := range paths(base, filename) {
if ok, _ := exists(f); !ok {
continue
}
files = append(files, f)
}
return files
} }
func getHome() string { func getHome() string {
@ -53,3 +67,17 @@ func getHome() string {
} }
return home return home
} }
func exists(name string) (bool, error) {
s, err := os.Stat(name)
if err == nil {
return true, nil
}
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
if s.IsDir() {
return false, nil
}
return false, err
}