Compare commits

..

16 Commits

Author SHA1 Message Date
xuu
dfae0ddbcc
chore(lsm): cleanup and add tools
Some checks failed
Go Bump / bump (push) Successful in 33s
Go Test / build (push) Failing after 45s
2024-11-09 09:32:29 -07:00
xuu
cf99e18a39
chore: fixes to lsm 2024-11-03 14:19:09 -07:00
xuu
36460a131e
forward iteration 2024-11-02 09:00:17 -06:00
xuu
1d987d238d
Merge branch 'add-lsm'
Some checks failed
Go Bump / bump (push) Successful in 48s
Go Test / build (push) Failing after 2m14s
2024-06-10 21:43:00 -06:00
xuu
58ae783bf0
Merge remote-tracking branch 'github/main'
Some checks failed
Go Bump / bump (push) Successful in 37s
Go Test / build (push) Failing after 2m7s
2024-06-10 21:24:07 -06:00
xuu
f5027d9bfd
chore: cleanup and add lsm
Some checks failed
Go Bump / bump (push) Failing after 29s
Go Test / build (push) Failing after 13s
Go Test / build (pull_request) Failing after 1m23s
2024-06-10 21:12:11 -06:00
xuu
b1bff4cbf0
add libsql support 2024-04-19 10:56:27 -06:00
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
xuu
3be012e780
chore: save sst code
All checks were successful
Go Test / build (pull_request) Successful in 1m47s
2024-01-15 11:26:54 -07:00
11fa6ae522
Merge pull request #3 from sour-is/dependabot/go_modules/google.golang.org/grpc-1.58.3
build(deps): bump google.golang.org/grpc from 1.58.0 to 1.58.3
2023-10-31 11:37:31 -06:00
xuu
59eaef2ae3
chore(lsm): add initial range search 2023-10-28 19:40:29 -06:00
xuu
ddd21b39a6
chore: add lsm/sstable 2023-10-28 09:00:49 -06:00
dependabot[bot]
cef8659a52
build(deps): bump google.golang.org/grpc from 1.58.0 to 1.58.3
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.58.0 to 1.58.3.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.58.0...v1.58.3)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-25 21:20:00 +00:00
57 changed files with 8513 additions and 404 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
test.db
*.mercury
sour.is-mercury
.vscode/

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.23.1
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.14
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/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
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 go.nhat.io/otelsql v0.12.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0
go.opentelemetry.io/otel/exporters/prometheus v0.41.0 go.opentelemetry.io/contrib/instrumentation/runtime v0.48.0
go.opentelemetry.io/otel/metric v1.18.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1 // indirect
go.opentelemetry.io/otel/sdk v1.18.0 go.opentelemetry.io/otel/exporters/prometheus v0.45.2
go.opentelemetry.io/otel/trace v1.18.0 go.opentelemetry.io/otel/metric v1.23.1
go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.opentelemetry.io/otel/sdk v1.23.1
golang.org/x/net v0.17.0 // indirect go.opentelemetry.io/otel/trace v1.23.1
golang.org/x/sys v0.13.0 // indirect go.opentelemetry.io/proto/otlp v1.1.0 // indirect
golang.org/x/text v0.13.0 // indirect golang.org/x/exp v0.0.0-20240213143201-ec583247a57a
google.golang.org/grpc v1.58.0 // indirect golang.org/x/net v0.23.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/grpc v1.61.1 // indirect
google.golang.org/protobuf v1.33.0 // indirect
modernc.org/sqlite v1.29.1
) )

263
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,216 @@ 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/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
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/vektah/gqlparser/v2 v2.5.14 h1:dzLq75BJe03jjQm6n56PdH1oweB8ana42wj7E4jRy70=
go.opentelemetry.io/otel v1.18.0 h1:TgVozPGZ01nHyDZxK5WGPFB9QexeTMXEH7+tIClWfzs= github.com/vektah/gqlparser/v2 v2.5.14/go.mod h1:WQQjFc+I1YIzoPvZBhUQX7waZgg3pMLi0r8KymvAE2w=
go.opentelemetry.io/otel v1.18.0/go.mod h1:9lWqYO0Db579XzVuCKFNPDl4s73Voa+zEck3wHaAYQI= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0 h1:IAtl+7gua134xcV3NieDhJHjjOVeJhXAnYf/0hswjUY= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0/go.mod h1:w+pXobnBzh95MNIkeIuAKcHe/Uu/CX2PKIvBP6ipKRA= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0 h1:6pu8ttx76BxHf+xz/H77AUZkPF3cwWzXqAUsXhVKI18= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0/go.mod h1:IOmXxPrxoxFMXdNy7lfDmE8MzE61YPcurbUm0SMjerI= go.nhat.io/otelsql v0.12.0 h1:/rBhWZiwHFLpCm5SGdafm+Owm0OmGmnF31XWxgecFtY=
go.opentelemetry.io/otel/exporters/prometheus v0.41.0 h1:A3/bhjP5SmELy8dcpK+uttHeh9Qrh+YnS16/VzrztRQ= go.nhat.io/otelsql v0.12.0/go.mod h1:39Hc9/JDfCl7NGrBi1uPP3QPofqwnC/i5SFd7gtDMWM=
go.opentelemetry.io/otel/exporters/prometheus v0.41.0/go.mod h1:mKuXEMi9suyyNJQ99SZCO0mpWGFe0MIALtjd3r6uo7Q= go.opentelemetry.io/contrib v1.23.0 h1:5f6bvGoHE/7lcolc1jCA4Vzq2tnPs4tfqL1M/yfjbOA=
go.opentelemetry.io/otel/metric v1.18.0 h1:JwVzw94UYmbx3ej++CwLUQZxEODDj/pOuTCvzhtRrSQ= go.opentelemetry.io/contrib v1.23.0/go.mod h1:usW9bPlrjHiJFbK0a6yK/M5wNHs3nLmtrT3vzhoD3co=
go.opentelemetry.io/otel/metric v1.18.0/go.mod h1:nNSpsVDjWGfb7chbRLUNW+PBNdcSTHD4Uu5pfFMOI0k= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 h1:doUP+ExOpH3spVTLS0FcWGLnQrPct/hD/bCPbDRUEAU=
go.opentelemetry.io/otel/sdk v1.18.0 h1:e3bAB0wB3MljH38sHzpV/qWrOTCFrdZF2ct9F8rBkcY= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0/go.mod h1:rdENBZMT2OE6Ne/KLwpiXudnAsbdrdBaqBvTN8M8BgA=
go.opentelemetry.io/otel/sdk v1.18.0/go.mod h1:1RCygWV7plY2KmdskZEDDBs4tJeHG92MdHZIluiYs/M= go.opentelemetry.io/contrib/instrumentation/runtime v0.48.0 h1:dJlCKeq+zmO5Og4kgxqPvvJrzuD/mygs1g/NYM9dAsU=
go.opentelemetry.io/otel/sdk/metric v0.41.0 h1:c3sAt9/pQ5fSIUfl0gPtClV3HhE18DCVzByD33R/zsk= go.opentelemetry.io/contrib/instrumentation/runtime v0.48.0/go.mod h1:p+hpBCpLHpuUrR0lHgnHbUnbCBll1IhrcMIlycC+xYs=
go.opentelemetry.io/otel/sdk/metric v0.41.0/go.mod h1:PmOmSt+iOklKtIg5O4Vz9H/ttcRFSNTgii+E1KGyn1w= go.opentelemetry.io/otel v1.23.1 h1:Za4UzOqJYS+MUczKI320AtqZHZb7EqxO00jAHE0jmQY=
go.opentelemetry.io/otel/trace v1.18.0 h1:NY+czwbHbmndxojTEKiSMHkG2ClNH2PwmcHrdo0JY10= go.opentelemetry.io/otel v1.23.1/go.mod h1:Td0134eafDLcTS4y+zQ26GE8u3dEuRBiBCTUIRHaikA=
go.opentelemetry.io/otel/trace v1.18.0/go.mod h1:T2+SGJGuYZY3bjj5rgh/hN7KIrlpWC5nS8Mjvzckz+0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1 h1:o8iWeVFa1BcLtVEV0LzrCxV2/55tB3xLxADr6Kyoey4=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1/go.mod h1:SEVfdK4IoBnbT2FXNM/k8yC08MrfbhWk3U4ljM8B3HE=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.23.1 h1:cfuy3bXmLJS7M1RZmAL6SuhGtKUp2KEsrm00OlAXkq4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.23.1/go.mod h1:22jr92C6KwlwItJmQzfixzQM3oyyuYLCfHiMY+rpsPU=
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.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
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.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.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.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.33.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=

View File

@ -1,189 +0,0 @@
// grug math is an unbounded precision math library for integers. It is not designed to be performant in any means. But as an example of how one works.
package math
import "strconv"
type Number []rune
func NewNumber() *Number {
return &Number{}
}
func (*Number) FromString(s string) *Number {
for _, a := range s {
if !(a >= '0' && a <= '9') {
return nil
}
}
var n Number = []rune(s)
i:=0
for range n[:len(n)-1] {
if n[i] == 0 || n[i] == '0' {
i++
} else {
break
}
}
n = n[i:]
return &n
}
func (*Number) FromInt(i int) *Number {
s := strconv.Itoa(i)
var n Number = []rune(s)
return &n
}
func (n *Number) String() string {
if n == nil || len(*n) == 0 {
return "NaN"
}
return string(*n)
}
func (n *Number) Add(a *Number) *Number {
if n == nil || a == nil {
return nil
}
lenN, lenA := len(*n), len(*a)
sum := make(Number, max(lenN, lenA)+1)
for i := range sum[:len(sum)-1] {
ii := len(sum) - i - 1
switch {
case lenN == lenA:
j := (*n)[lenN-i-1]
k := (*a)[lenA-i-1]
sum[ii-1], sum[ii] = add(j, k, sum[ii])
case lenN > lenA:
j := (*n)[lenN-i-1]
k := '0'
if i < lenA {
k = (*a)[lenA-i-1]
}
sum[ii-1], sum[ii] = add(j, k, sum[ii])
case lenN < lenA:
j := '0'
if i < lenN {
j = (*n)[lenN-i-1]
}
k := (*a)[lenA-i-1]
sum[ii-1], sum[ii] = add(j, k, sum[ii])
}
}
// Trim the extra 0 if present
if sum[0] == 0 || sum[0] == '0' {
sum = sum[1:]
}
return &sum
}
func (n *Number) Sub(s *Number) *Number {
if n == nil || s == nil {
return nil
}
lenN, lenA := len(*n), len(*s)
sum := make(Number, max(lenN, lenA)+1)
for i := range sum[:len(sum)-1] {
ii := len(sum) - i - 1
switch {
case lenN == lenA:
j := (*n)[lenN-i-1]
k := (*s)[lenA-i-1]
c := '0'
if i+1 < lenN {
c = (*n)[lenN-i-2]
}
sum[ii-1], sum[ii] = sub(j, k, c)
case lenN > lenA:
j := (*n)[lenN-i-1]
k := '0'
if i < lenA {
k = (*s)[lenA-i-1]
}
c := '0'
if i+1 < lenN {
c = (*n)[lenN-i-2]
}
sum[ii-1], sum[ii] = sub(j, k, c)
if i+1 < lenN {
(*n)[lenN-i-2] =sum[ii-1]
}
case lenN < lenA:
j := '0'
if i < lenN {
j = (*n)[lenN-i-1]
}
k := (*s)[lenA-i-1]
c := '0'
if i+1 < lenN {
c = (*n)[lenN-i-2]
}
sum[ii-1], sum[ii] = sub(j, k, c)
}
}
// Trim the extra 0 if present
i:=0
for range sum[:len(sum)-1] {
if sum[i] == 0 || sum[i] == '0' {
i++
} else {
break
}
}
sum = sum[i:]
return &sum
}
func friends(r rune) (int32, int32) {
return 1, 10 - int32(r-'0')
}
func add(a, b, c rune) (rune, rune) {
up, dn := friends(b)
if c == 0 {
c = '0'
}
a = a + c - '0'
if a-dn < '0' {
return '0', a + b - '0'
}
return c + up, a - dn
}
func sub(a, b, c rune) (rune, rune) {
dn, up := friends(b)
if c == 0 {
c = '0'
}
if a+up > '9' {
return c, a - b + '0'
}
return c - dn, a + up
}

View File

@ -1,74 +0,0 @@
package math_test
import (
"testing"
"github.com/matryer/is"
"go.sour.is/pkg/grug/math"
)
func TestNumber(t *testing.T) {
is := is.New(t)
n := math.NewNumber().FromInt(100)
is.Equal(n.String(), "100")
n = n.FromString("00001")
is.Equal(n.String(), "1")
n = n.FromString("1x0")
is.True(n==nil)
is.Equal(n.String(), "NaN")
n = n.FromString("200")
is.True(n!=nil)
is.Equal(n.String(), "200")
n = (&math.Number{}).FromString("300")
is.True(n!=nil)
is.Equal(n.String(), "300")
}
func TestAdd(t *testing.T) {
is := is.New(t)
n := math.NewNumber().FromString("100")
n = n.Add(nil)
is.Equal(n.String(), "NaN")
n = n.FromInt(100)
n = n.Add(n.FromString("900"))
is.Equal(n.String(), "1000")
n = n.Add(math.NewNumber())
is.Equal(n.String(), "1000")
n = n.Add(n.FromString("10"))
is.Equal(n.String(), "1010")
n = n.Add(n.FromString("10000"))
is.Equal(n.String(), "11010")
n = n.Add(n.FromString("9000"))
is.Equal(n.String(), "20010")
}
func TestSub(t *testing.T) {
is := is.New(t)
n := math.NewNumber()
// n = n.FromString("100")
// n = n.Sub(n.FromInt(100))
// is.Equal(n.String(), "0")
// n = n.FromString("200")
// n = n.Sub(n.FromInt(100))
// is.Equal(n.String(), "100")
n = n.FromString("100")
n = n.Sub(n.FromInt(50))
is.Equal(n.String(), "50")
}

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)
}

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

@ -0,0 +1,275 @@
package source
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"go.sour.is/pkg/ident"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/mercury"
)
const identNS = "ident."
const identSFX = ".credentials"
type registry interface {
GetIndex(ctx context.Context, search mercury.Search) (c mercury.Config, err error)
GetConfig(ctx context.Context, search mercury.Search) (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())
id.ed25519 = []byte(s.FirstValue("ed25519").First())
default:
id.display = s.FirstValue("displayName").First()
}
}
return nil
}
func (id *mercuryIdent) ToConfig() mercury.Config {
space := id.Space()
list := func(values ...mercury.Value) []mercury.Value { return values }
value := func(space string, seq uint64, name string, values ...string) mercury.Value {
return mercury.Value{
Space: space,
Seq: seq,
Name: name,
Values: values,
}
}
return mercury.Config{
&mercury.Space{
Space: space,
List: list(
value(space, 1, "displayName", id.display),
value(space, 2, "lastLogin", time.UnixMilli(int64(id.Session().SessionID.Time())).Format(time.RFC3339)),
),
},
&mercury.Space{
Space: space + identSFX,
List: list(
value(space+identSFX, 1, "passwd", string(id.passwd)),
value(space+identSFX, 1, "ed25519", string(id.ed25519)),
),
},
}
}
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, mercury.ParseSearch("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, mercury.ParseSearch("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, mercury.ParseSearch("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}
_, err := s.r.GetIndex(ctx, mercury.ParseSearch( id.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

@ -38,9 +38,11 @@ func initMetrics(ctx context.Context, name string) (context.Context, func() erro
goversion := "" goversion := ""
pkg := "" pkg := ""
host := "" host := ""
version := "0.0.1"
if info, ok := debug.ReadBuildInfo(); ok { if info, ok := debug.ReadBuildInfo(); ok {
goversion = info.GoVersion goversion = info.GoVersion
pkg = info.Path pkg = info.Path
version = info.Main.Version
} }
if h, err := os.Hostname(); err == nil { if h, err := os.Hostname(); err == nil {
host = h host = h
@ -69,7 +71,7 @@ func initMetrics(ctx context.Context, name string) (context.Context, func() erro
) )
meter := provider.Meter(name, meter := provider.Meter(name,
api.WithInstrumentationVersion("0.0.1"), api.WithInstrumentationVersion(version),
api.WithInstrumentationAttributes( api.WithInstrumentationAttributes(
attribute.String("app", name), attribute.String("app", name),
attribute.String("host", host), attribute.String("host", host),

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...)

162
libsql_embed/open.go Normal file
View File

@ -0,0 +1,162 @@
package libsqlembed
import (
"context"
"database/sql"
"database/sql/driver"
"errors"
"fmt"
"io"
"log"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/tursodatabase/go-libsql"
)
func init() {
sql.Register("libsql+embed", &db{conns: make(map[string]*connector)})
}
type db struct {
conns map[string]*connector
mu sync.RWMutex
}
type connector struct {
*libsql.Connector
dsn string
dir string
driver *db
removeDir bool
}
var _ io.Closer = (*connector)(nil)
func (c *connector) Close() error {
log.Println("closing db connection", c.dir)
defer log.Println("closed db connection", c.dir)
c.driver.mu.Lock()
delete(c.driver.conns, c.dsn)
c.driver.mu.Unlock()
if c.removeDir {
defer os.RemoveAll(c.dir)
}
log.Println("sync db")
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) {
// log.Println("connector", dsn)
if dsn == "" {
return nil, fmt.Errorf("no dsn")
}
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 {
log.Println("refresh: ", refresh)
opts = append(opts, libsql.WithSyncInterval(time.Duration(refresh)*time.Minute))
}
if readWrite, err := strconv.ParseBool(u.Query().Get("readYourWrites")); err == nil {
log.Println("read your writes: ", readWrite)
opts = append(opts, libsql.WithReadYourWrites(readWrite))
}
if key := u.Query().Get("key"); key != "" {
opts = append(opts, libsql.WithEncryption(key))
}
var dir string
var removeDir bool
if dir = u.Query().Get("store"); dir == "" {
removeDir = true
dir, err = os.MkdirTemp("", "libsql-*")
log.Println("creating temporary directory:", dir)
if err != nil {
return nil, fmt.Errorf("creating temporary directory: %w", err)
}
} else {
stat, err := os.Stat(dir)
if errors.Is(err, os.ErrNotExist) {
if err = os.MkdirAll(dir, 0700); err != nil {
return nil, err
}
} else {
if !stat.IsDir() {
return nil, fmt.Errorf("store not directory")
}
}
}
dbPath := filepath.Join(dir, dbname)
c, err := libsql.NewEmbeddedReplicaConnector(
dbPath,
primary.String(),
opts...)
if err != nil {
return nil, fmt.Errorf("creating connector: %w", err)
}
log.Println("sync db")
if err := c.Sync(); err != nil {
return nil, fmt.Errorf("syncing database: %w", err)
}
connector := &connector{c, dsn, dir, db, removeDir}
db.conns[dsn] = connector
return connector, nil
}
func (db *db) Open(dsn string) (driver.Conn, error) {
log.Println("open", dsn)
c, err := db.OpenConnector(dsn)
if err != nil {
return nil, err
}
return c.Connect(context.Background())
}

286
lsm/cli/main.go Normal file
View File

@ -0,0 +1,286 @@
package main
import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"io"
"iter"
"net/http"
"net/url"
"os"
"time"
"github.com/docopt/docopt-go"
"go.sour.is/pkg/lsm"
)
var usage = `
Usage:
lsm create <archive> <files>...
lsm append <archive> <files>...
lsm read <archive> [<start> [<end>]]
lsm serve <archive>
lsm client <archive> [<start> [<end>]]`
type args struct {
Create bool
Append bool
Read bool
Serve bool
Client bool
Archive string `docopt:"<archive>"`
Files []string `docopt:"<files>"`
Start int64 `docopt:"<start>"`
End int64 `docopt:"<end>"`
}
func main() {
opts, err := docopt.ParseDoc(usage)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
args := args{}
err = opts.Bind(&args)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
err = run(Console, args)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
type console struct {
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
}
var Console = console{os.Stdin, os.Stdout, os.Stderr}
func (c console) Write(b []byte) (int, error) {
return c.Stdout.Write(b)
}
func run(console console, a args) error {
fmt.Fprintln(console, "lsm")
switch {
case a.Create:
f, err := os.OpenFile(a.Archive, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
return lsm.WriteLogFile(f, fileReaders(a.Files))
case a.Append:
f, err := os.OpenFile(a.Archive, os.O_RDWR, 0644)
if err != nil {
return err
}
defer f.Close()
return lsm.AppendLogFile(f, fileReaders(a.Files))
case a.Read:
fmt.Fprintln(console, "reading", a.Archive)
f, err := os.Open(a.Archive)
if err != nil {
return err
}
defer f.Close()
return readContent(f, console, a.Start, a.End)
case a.Serve:
fmt.Fprintln(console, "serving", a.Archive)
b, err := base64.RawStdEncoding.DecodeString(a.Archive)
now := time.Now()
if err != nil {
return err
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.ServeContent(w, r, "", now, bytes.NewReader(b))
})
return http.ListenAndServe(":8080", nil)
case a.Client:
r, err := OpenHttpReader(context.Background(), a.Archive, 0)
if err != nil {
return err
}
defer r.Close()
defer func() {fmt.Println("bytes read", r.bytesRead)}()
return readContent(r, console, a.Start, a.End)
}
return errors.New("unknown command")
}
func readContent(r io.ReaderAt, console console, start, end int64) error {
lg, err := lsm.ReadLogFile(r)
if err != nil {
return err
}
for bi, rd := range lg.Iter(uint64(start)) {
if end > 0 && int64(bi.Index) >= end {
break
}
fmt.Fprintf(console, "=========================\n%+v:\n", bi)
wr := base64.NewEncoder(base64.RawStdEncoding, console)
io.Copy(wr, rd)
fmt.Fprintln(console, "\n=========================")
}
if lg.Err != nil {
return lg.Err
}
for bi, rd := range lg.Rev(lg.Count()) {
if end > 0 && int64(bi.Index) >= end {
break
}
fmt.Fprintf(console, "=========================\n%+v:\n", bi)
wr := base64.NewEncoder(base64.RawStdEncoding, console)
io.Copy(wr, rd)
fmt.Fprintln(console, "\n=========================")
}
return lg.Err
}
func fileReaders(names []string) iter.Seq[io.Reader] {
return iter.Seq[io.Reader](func(yield func(io.Reader) bool) {
for _, name := range names {
f, err := os.Open(name)
if err != nil {
continue
}
if !yield(f) {
f.Close()
return
}
f.Close()
}
})
}
type HttpReader struct {
ctx context.Context
uri url.URL
tmpfile *os.File
pos int64
end int64
bytesRead int
}
func OpenHttpReader(ctx context.Context, uri string, end int64) (*HttpReader, error) {
u, err := url.Parse(uri)
if err != nil {
return nil, err
}
return &HttpReader{ctx: ctx, uri: *u, end: end}, nil
}
func (r *HttpReader) Read(p []byte) (int, error) {
n, err := r.ReadAt(p, r.pos)
if err != nil {
return n, err
}
r.pos += int64(n)
r.bytesRead += n
return n, nil
}
func (r *HttpReader) Seek(offset int64, whence int) (int64, error) {
switch whence {
case io.SeekStart:
r.pos = offset
case io.SeekCurrent:
r.pos += offset
case io.SeekEnd:
r.pos = r.end + offset
}
return r.pos, nil
}
func (r *HttpReader) Close() error {
r.ctx.Done()
return nil
}
// ReadAt implements io.ReaderAt. It reads data from the internal buffer starting
// from the specified offset and writes it into the provided data slice. If the
// offset is negative, it returns an error. If the requested read extends beyond
// the buffer's length, it returns the data read so far along with an io.EOF error.
func (r *HttpReader) ReadAt(data []byte, offset int64) (int, error) {
if err := r.ctx.Err(); err != nil {
return 0, err
}
if offset < 0 {
return 0, errors.New("negative offset")
}
if r.end > 0 && offset > r.end {
return 0, io.EOF
}
dlen := len(data) + int(offset)
if r.end > 0 && r.end+int64(dlen) > r.end {
dlen = int(r.end)
}
end := ""
if r.end > 0 {
end = fmt.Sprintf("/%d", r.end)
}
req, err := http.NewRequestWithContext(r.ctx, "GET", r.uri.String(), nil)
if err != nil {
return 0, err
}
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d%s", offset, dlen, end))
fmt.Fprintln(Console.Stderr, req)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusRequestedRangeNotSatisfiable {
fmt.Fprintln(Console.Stderr, "requested range not satisfiable")
return 0, io.EOF
}
if resp.StatusCode == http.StatusOK {
r.tmpfile, err = os.CreateTemp("", "httpReader")
if err != nil {
return 0, err
}
defer os.Remove(r.tmpfile.Name())
n, err := io.Copy(r.tmpfile, resp.Body)
if err != nil {
return 0, err
}
r.bytesRead += int(n)
defer fmt.Fprintln(Console.Stderr, "wrote ", n, " bytes to ", r.tmpfile.Name())
resp.Body.Close()
r.tmpfile.Seek(offset, 0)
return io.ReadFull(r.tmpfile, data)
}
n, err := io.ReadFull(resp.Body, data)
if n == 0 && err != nil {
return n, err
}
r.bytesRead += n
defer fmt.Fprintln(Console.Stderr, "read ", n, " bytes")
return n, nil
}

104
lsm/cli/main_test.go Normal file
View File

@ -0,0 +1,104 @@
package main
import (
"bytes"
"os"
"testing"
)
func TestCreate(t *testing.T) {
tests := []struct {
name string
args args
wantErr bool
wantOutput string
}{
{
name: "no input files",
args: args{
Create: true,
Archive: "test.txt",
Files: []string{},
},
wantErr: false,
wantOutput: "creating test.txt from []\nwrote 0 files\n",
},
{
name: "one input file",
args: args{
Create: true,
Archive: "test.txt",
Files: []string{"test_input.txt"},
},
wantErr: false,
wantOutput: "creating test.txt from [test_input.txt]\nwrote 1 files\n",
},
{
name: "multiple input files",
args: args{
Create: true,
Archive: "test.txt",
Files: []string{"test_input1.txt", "test_input2.txt"},
},
wantErr: false,
wantOutput: "creating test.txt from [test_input1.txt test_input2.txt]\nwrote 2 files\n",
},
{
name: "non-existent input files",
args: args{
Create: true,
Archive: "test.txt",
Files: []string{"non_existent_file.txt"},
}, wantErr: false,
wantOutput: "creating test.txt from [non_existent_file.txt]\nwrote 0 files\n",
},
{
name: "invalid command",
args: args{
Create: false,
Archive: "test.txt",
Files: []string{},
},
wantErr: true,
wantOutput: "",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Create a temporary directory for the input files
tmpDir, err := os.MkdirTemp("", "lsm2-cli-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
os.Chdir(tmpDir)
// Create the input files
for _, file := range tc.args.Files {
if file == "non_existent_file.txt" {
continue
}
if err := os.WriteFile(file, []byte(file), 0o644); err != nil {
t.Fatal(err)
}
}
// Create a buffer to capture the output
var output bytes.Buffer
// Call the create function
err = run(console{Stdout: &output}, tc.args)
// Check the output
if output.String() != tc.wantOutput {
t.Errorf("run() output = %q, want %q", output.String(), tc.wantOutput)
}
// Check for errors
if tc.wantErr && err == nil {
t.Errorf("run() did not return an error")
}
})
}
}

634
lsm/sst.go Normal file
View File

@ -0,0 +1,634 @@
package lsm
import (
"encoding/binary"
"errors"
"fmt"
"hash/fnv"
"io"
"iter"
"slices"
)
// [Sour.is|size] [size|hash][data][hash|flag|size]... [prev|count|flag|size]
// Commit1: [magic>|<end]{10} ... [<count][<size][<flag]{3..30}
// +---------|--------------------------------> end = seek to end of file
// <---|-------------+ size = seek to magic header
// <---|-------------+10 size + 10 = seek to start of file
// <-----------------------------T+10----------------> 10 + size + trailer = full file size
// Commit2: [magic>|<end]{10} ... [<count][<size][<flag]{3..30} ... [<prev][<count][<size][<flag]{4..40}
// <---|---------+
// <-------------+T----------------->
// +--------|------------------------------------------------------------------------->
// <-------------------------------------|----------------+
// prev = seek to last commit <---|-+
// prev + trailer = size of commit <----T+--------------------------------->
// Block: [hash>|<end]{10} ... [<size][<flag]{2..20}
// +---------|------------------------> end = seek to end of block
// <---|-+ size = seek to end of header
// <-------------------|-+10 size + 10 = seek to start of block
// <---------------------T+10---------------> size + 10 + trailer = full block size
const (
TypeUnknown uint64 = iota
TypeSegment
TypeCommit
TypePrevCommit
headerSize = 10
maxCommitSize = 4 * binary.MaxVarintLen64
minCommitSize = 3
maxBlockSize = 2 * binary.MaxVarintLen64
minBlockSize = 2
)
var (
Magic = [10]byte([]byte("Sour.is\x00\x00\x00"))
Version = uint8(1)
hash = fnv.New64a
ErrDecode = errors.New("decode")
)
type header struct {
end uint64
extra []byte
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
// It decodes the input binary data into the header struct.
// The function expects the input data to be of a specific size (headerSize),
// otherwise it returns an error indicating bad data.
// It reads the 'end' field from the binary data, updates the 'extra' field,
// and reverses the byte order of 'extra' in place.
func (h *header) UnmarshalBinary(data []byte) error {
if len(data) != headerSize {
return fmt.Errorf("%w: bad data", ErrDecode)
}
h.extra = make([]byte, headerSize)
copy(h.extra, data)
var bytesRead int
h.end, bytesRead = binary.Uvarint(h.extra)
reverse(h.extra)
h.extra = h.extra[:min(8,headerSize-bytesRead)]
return nil
}
type Commit struct {
flag uint64 // flag values
size uint64 // size of the trailer
count uint64 // number of entries
prev uint64 // previous commit
tsize int
}
// Append marshals the trailer into binary form and appends it to data.
// It returns the new slice.
func (h *Commit) AppendTrailer(data []byte) []byte {
h.flag |= TypeCommit
// if h.prev > 0 {
// h.flag |= TypePrevCommit
// }
size := len(data)
data = binary.AppendUvarint(data, h.size)
data = binary.AppendUvarint(data, h.flag)
data = binary.AppendUvarint(data, h.count)
// if h.prev > 0 {
// data = binary.AppendUvarint(data, h.prev)
// }
reverse(data[size:])
return data
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
// It reads a trailer from binary data, and sets the fields
// of the receiver to the values found in the header.
func (h *Commit) UnmarshalBinary(data []byte) error {
if len(data) < minCommitSize {
return fmt.Errorf("%w: bad data", ErrDecode)
}
var n int
h.size, n = binary.Uvarint(data)
data = data[n:]
h.tsize += n
h.flag, n = binary.Uvarint(data)
data = data[n:]
h.tsize += n
h.count, n = binary.Uvarint(data)
data = data[n:]
h.tsize += n
// h.prev = h.size
if h.flag&TypePrevCommit == TypePrevCommit {
h.prev, n = binary.Uvarint(data)
h.tsize += n
}
return nil
}
type Block struct {
header
size uint64
flag uint64
tsize int
}
func (h *Block) AppendHeader(data []byte) []byte {
size := len(data)
data = append(data, make([]byte, 10)...)
copy(data, h.extra)
if h.size == 0 {
return data
}
hdata := binary.AppendUvarint(make([]byte, 0, 10), h.end)
reverse(hdata)
copy(data[size+10-len(hdata):], hdata)
return data
}
// AppendTrailer marshals the footer into binary form and appends it to data.
// It returns the new slice.
func (h *Block) AppendTrailer(data []byte) []byte {
size := len(data)
h.flag |= TypeSegment
data = binary.AppendUvarint(data, h.size)
data = binary.AppendUvarint(data, h.flag)
reverse(data[size:])
return data
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
// It reads a footer from binary data, and sets the fields
// of the receiver to the values found in the footer.
func (h *Block) UnmarshalBinary(data []byte) error {
if len(data) < minBlockSize {
return fmt.Errorf("%w: bad data", ErrDecode)
}
var n int
h.size, n = binary.Uvarint(data)
data = data[n:]
h.tsize += n
h.flag, n = binary.Uvarint(data)
h.tsize += n
return nil
}
type logFile struct {
header
Commit
}
func (h *logFile) AppendMagic(data []byte) []byte {
size := len(data)
data = append(data, Magic[:]...)
if h.end == 0 {
return data
}
hdata := binary.AppendUvarint(make([]byte, 0, 10), h.end)
reverse(hdata)
copy(data[size+10-len(hdata):], hdata)
return data
}
// WriteLogFile writes a log file to w, given a list of segments.
// The caller is responsible for calling WriteAt on the correct offset.
// The function will return an error if any of the segments fail to write.
// The offset is the initial offset of the first segment, and will be
// incremented by the length of the segment on each write.
//
// The log file is written with the following format:
// - A header with the magic, version, and flag (Dirty)
// - A series of segments, each with:
// - A footer with the length and hash of the segment
// - The contents of the segment
// - A header with the magic, version, flag (Clean), and end offset
func WriteLogFile(w io.WriterAt, segments iter.Seq[io.Reader]) error {
_, err := w.WriteAt(Magic[:], 0)
if err != nil {
return err
}
lf := &LogWriter{
WriterAt: w,
}
return lf.writeIter(segments)
}
type rw interface {
io.ReaderAt
io.WriterAt
}
func AppendLogFile(rw rw, segments iter.Seq[io.Reader]) error {
logFile, err := ReadLogFile(rw)
if err != nil {
return err
}
lf := &LogWriter{
WriterAt: rw,
logFile: logFile.logFile,
}
return lf.writeIter(segments)
}
func (lf *LogWriter) writeIter(segments iter.Seq[io.Reader]) error {
lf.size = 0
for s := range segments {
n, err := lf.writeBlock(s)
if err != nil {
return err
}
lf.end += n
lf.size += n
lf.count++
}
// Write the footer to the log file.
// The footer is written at the current end of file position.
n, err := lf.WriteAt(lf.AppendTrailer(make([]byte, 0, maxCommitSize)), int64(lf.end)+10)
if err != nil {
// If there is an error, return it.
return err
}
lf.end += uint64(n)
_, err = lf.WriteAt(lf.AppendMagic(make([]byte, 0, 10)), 0)
return err
}
type LogWriter struct {
logFile
io.WriterAt
}
// writeBlock writes a segment to the log file at the current end of file position.
// The segment is written in chunks of 1024 bytes, and the hash of the segment
func (lf *LogWriter) writeBlock(segment io.Reader) (uint64, error) {
h := hash()
block := Block{}
start := int64(lf.end) + 10
end := start
bytesWritten := 0
// Write the header to the log file.
// The footer is written at the current end of file position.
n, err := lf.WriteAt(make([]byte, headerSize), start)
bytesWritten += n
end += int64(n)
if err != nil {
// If there is an error, return it.
return uint64(bytesWritten), err
}
// Write the segment to the log file.
// The segment is written in chunks of 1024 bytes.
for {
// Read a chunk of the segment.
buf := make([]byte, 1024)
n, err := segment.Read(buf)
if err != nil {
// If the segment is empty, break the loop.
if err == io.EOF {
break
}
// If there is an error, return it.
return uint64(bytesWritten), err
}
// Compute the hash of the chunk.
h.Write(buf[:n])
// Write the chunk to the log file.
// The chunk is written at the current end of file position.
n, err = lf.WriteAt(buf[:n], end)
bytesWritten += n
if err != nil {
// If there is an error, return it.
return uint64(bytesWritten), err
}
// Update the length of the segment.
end += int64(n)
block.size += uint64(n)
}
block.extra = h.Sum(nil)
block.end += block.size
// Write the footer to the log file.
// The footer is written at the current end of file position.
n, err = lf.WriteAt(block.AppendTrailer(make([]byte, 0, maxBlockSize)), end)
bytesWritten += n
if err != nil {
// If there is an error, return it.
return uint64(bytesWritten), err
}
end += int64(n)
block.end += uint64(n)
// Update header to the log file.
// The footer is written at the current end of file position.
_, err = lf.WriteAt(block.AppendHeader(make([]byte, 0, headerSize)), start)
if err != nil {
// If there is an error, return it.
return uint64(bytesWritten), err
}
return uint64(bytesWritten), nil
}
// reverse reverses a slice in-place.
func reverse[T any](b []T) {
l := len(b)
for i := 0; i < l/2; i++ {
b[i], b[l-i-1] = b[l-i-1], b[i]
}
}
type LogReader struct {
logFile
io.ReaderAt
Err error
}
// ReadLogFile reads a log file from the given io.ReaderAt. It returns a pointer to a LogFile, or an error if the file
// could not be read.
func ReadLogFile(reader io.ReaderAt) (*LogReader, error) {
header := make([]byte, headerSize)
n, err := rsr(reader, 0, 10).ReadAt(header, 0)
if err != nil {
return nil, err
}
header = header[:n]
logFile := &LogReader{ReaderAt: reader}
err = logFile.header.UnmarshalBinary(header)
if err != nil {
return nil, err
}
if logFile.end == 0 {
return logFile, nil
}
commit := make([]byte, maxCommitSize)
n, err = rsr(reader, 10, int64(logFile.end)).ReadAt(commit, 0)
if n == 0 && err != nil {
return nil, err
}
commit = commit[:n]
err = logFile.Commit.UnmarshalBinary(commit)
return logFile, err
}
// Iterate reads the log file and calls the given function for each segment.
// It passes an io.Reader that reads from the current segment. It will stop
// calling the function if the function returns false.
func (lf *LogReader) Iter(begin uint64) iter.Seq2[blockInfo, io.Reader] {
var commits []*Commit
for commit := range lf.iterCommits() {
commits = append(commits, &commit)
}
if lf.Err != nil {
return func(yield func(blockInfo, io.Reader) bool) {}
}
reverse(commits)
return func(yield func(blockInfo, io.Reader) bool) {
start := int64(10)
var adj uint64
for _, commit := range commits {
size := int64(commit.size)
it := iterBlocks(io.NewSectionReader(lf, start, size), size)
for bi, block := range it {
bi.Commit = *commit
bi.Index += adj
bi.Start += uint64(start)
if begin <= bi.Index {
if !yield(bi, block) {
return
}
}
}
start += size + int64(commit.tsize)
adj = commit.count
}
}
}
type blockInfo struct{
Index uint64
Commit Commit
Start uint64
Size uint64
Hash []byte
}
func iterBlocks(r io.ReaderAt, end int64) iter.Seq2[blockInfo, io.Reader] {
var start int64
var i uint64
var bi blockInfo
return func(yield func(blockInfo, io.Reader) bool) {
buf := make([]byte, maxBlockSize)
for start < end {
block := &Block{}
buf = buf[:10]
n, err := rsr(r, int64(start), 10).ReadAt(buf, 0)
if n == 0 && err != nil {
return
}
start += int64(n)
if err := block.header.UnmarshalBinary(buf); err != nil {
return
}
buf = buf[:maxBlockSize]
n, err = rsr(r, int64(start), int64(block.end)).ReadAt(buf, 0)
if n == 0 && err != nil {
return
}
buf = buf[:n]
err = block.UnmarshalBinary(buf)
if err != nil {
return
}
bi.Index = i
bi.Start = uint64(start)
bi.Size = block.size
bi.Hash = block.extra
if !yield(bi, io.NewSectionReader(r, int64(start), int64(block.size))) {
return
}
i++
start += int64(block.end)
}
}
}
func (lf *LogReader) iterCommits() iter.Seq[Commit] {
if lf.end == 0 {
return slices.Values([]Commit(nil))
}
offset := lf.end - lf.size - uint64(lf.tsize)
return func(yield func(Commit) bool) {
if !yield(lf.Commit) {
return
}
buf := make([]byte, maxCommitSize)
for offset > 10 {
commit := Commit{}
buf = buf[:10]
n, err := rsr(lf, 10, int64(offset)).ReadAt(buf, 0)
if n == 0 && err != nil {
lf.Err = err
return
}
buf = buf[:n]
err = commit.UnmarshalBinary(buf)
if err != nil {
lf.Err = err
return
}
if !yield(commit) {
return
}
offset -= commit.size + uint64(commit.tsize)
}
}
}
func (lf *LogReader) Rev(begin uint64) iter.Seq2[blockInfo, io.Reader] {
end := lf.end + 10
bi := blockInfo{}
bi.Index = lf.count-1
return func(yield func(blockInfo, io.Reader) bool) {
buf := make([]byte, maxBlockSize)
for commit := range lf.iterCommits() {
end -= uint64(commit.tsize)
start := end - commit.size
bi.Commit = commit
for start < end {
block := &Block{}
buf = buf[:maxBlockSize]
n, err := rsr(lf, int64(start), int64(commit.size)).ReadAt(buf, 0)
if n == 0 && err != nil {
lf.Err = err
return
}
buf = buf[:n]
err = block.UnmarshalBinary(buf)
if err != nil {
lf.Err = err
return
}
if begin >= bi.Index {
bi.Start = uint64(end-block.size)-uint64(block.tsize)
bi.Size = block.size
buf = buf[:10]
_, err = rsr(lf, int64(bi.Start)-10, 10).ReadAt(buf, 0)
if err != nil {
lf.Err = err
return
}
err = block.header.UnmarshalBinary(buf)
if err != nil {
lf.Err = err
return
}
bi.Hash = block.extra
if !yield(bi, io.NewSectionReader(lf, int64(bi.Start), int64(bi.Size))) {
return
}
}
end -= block.size + 10 + uint64(block.tsize)
bi.Index--
}
}
}
}
func (lf *LogReader) Count() uint64 {
return lf.count
}
func (lf *LogReader) Size() uint64 {
return lf.end + 10
}
func rsr(r io.ReaderAt, offset, size int64) *revSegmentReader {
r = io.NewSectionReader(r, offset, size)
return &revSegmentReader{r, size}
}
type revSegmentReader struct {
io.ReaderAt
size int64
}
func (r *revSegmentReader) ReadAt(data []byte, offset int64) (int, error) {
if offset < 0 {
return 0, errors.New("negative offset")
}
if offset > int64(r.size) {
return 0, io.EOF
}
o := r.size - int64(len(data)) - offset
d := int64(len(data))
if o < 0 {
d = max(0, d+o)
}
i, err := r.ReaderAt.ReadAt(data[:d], max(0, o))
reverse(data[:i])
return i, err
}

300
lsm/sst_test.go Normal file
View File

@ -0,0 +1,300 @@
package lsm
import (
"bytes"
"encoding/base64"
"errors"
"io"
"iter"
"slices"
"testing"
"github.com/docopt/docopt-go"
"github.com/matryer/is"
)
// TestWriteLogFile tests AppendLogFile and WriteLogFile against a set of test cases.
//
// Each test case contains a slice of slices of io.Readers, which are passed to
// AppendLogFile and WriteLogFile in order. The test case also contains the
// expected encoded output as a base64 string, as well as the expected output
// when the file is read back using ReadLogFile.
//
// The test case also contains the expected output when the file is read back in
// reverse order using ReadLogFile.Rev().
//
// The test cases are as follows:
//
// - nil reader: Passes a nil slice of io.Readers to WriteLogFile.
// - err reader: Passes a slice of io.Readers to WriteLogFile which returns an
// error when read.
// - single reader: Passes a single io.Reader to WriteLogFile.
// - multiple readers: Passes a slice of multiple io.Readers to WriteLogFile.
// - multiple commit: Passes multiple slices of io.Readers to AppendLogFile.
// - multiple commit 3x: Passes multiple slices of io.Readers to AppendLogFile
// three times.
//
// The test uses the is package from github.com/matryer/is to check that the
// output matches the expected output.
func TestWriteLogFile(t *testing.T) {
type test struct {
name string
in [][]io.Reader
enc string
out [][]byte
rev [][]byte
}
tests := []test{
{
name: "nil reader",
in: nil,
enc: "U291ci5pcwAAAwACAA",
out: [][]byte{},
rev: [][]byte{},
},
{
name: "err reader",
in: nil,
enc: "U291ci5pcwAAAwACAA",
out: [][]byte{},
rev: [][]byte{},
},
{
name: "single reader",
in: [][]io.Reader{
{
bytes.NewBuffer([]byte{1, 2, 3, 4})}},
enc: "U291ci5pcwAAE756XndRZXhdAAYBAgMEAQQBAhA",
out: [][]byte{{1, 2, 3, 4}},
rev: [][]byte{{1, 2, 3, 4}}},
{
name: "multiple readers",
in: [][]io.Reader{
{
bytes.NewBuffer([]byte{1, 2, 3, 4}),
bytes.NewBuffer([]byte{5, 6, 7, 8})}},
enc: "U291ci5pcwAAI756XndRZXhdAAYBAgMEAQRhQyZWDDn5BQAGBQYHCAEEAgIg",
out: [][]byte{{1, 2, 3, 4}, {5, 6, 7, 8}},
rev: [][]byte{{5, 6, 7, 8}, {1, 2, 3, 4}}},
{
name: "multiple commit",
in: [][]io.Reader{
{
bytes.NewBuffer([]byte{1, 2, 3, 4})},
{
bytes.NewBuffer([]byte{5, 6, 7, 8})}},
enc: "U291ci5pcwAAJr56XndRZXhdAAYBAgMEAQQBAhBhQyZWDDn5BQAGBQYHCAEEAgIQ",
out: [][]byte{{1, 2, 3, 4}, {5, 6, 7, 8}},
rev: [][]byte{{5, 6, 7, 8}, {1, 2, 3, 4}}},
{
name: "multiple commit",
in: [][]io.Reader{
{
bytes.NewBuffer([]byte{1, 2, 3, 4}),
bytes.NewBuffer([]byte{5, 6, 7, 8})},
{
bytes.NewBuffer([]byte{9, 10, 11, 12})},
},
enc: "U291ci5pcwAANr56XndRZXhdAAYBAgMEAQRhQyZWDDn5BQAGBQYHCAEEAgIgA4Buuio8Ro0ABgkKCwwBBAMCEA",
out: [][]byte{{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}},
rev: [][]byte{{9, 10, 11, 12}, {5, 6, 7, 8}, {1, 2, 3, 4}}},
{
name: "multiple commit 3x",
in: [][]io.Reader{
{
bytes.NewBuffer([]byte{1, 2, 3}),
bytes.NewBuffer([]byte{4, 5, 6}),
},
{
bytes.NewBuffer([]byte{7, 8, 9}),
},
{
bytes.NewBuffer([]byte{10, 11, 12}),
bytes.NewBuffer([]byte{13, 14, 15}),
},
},
enc: "U291ci5pcwAAVNCqYhhnLPWrAAUBAgMBA7axWhhYd+HsAAUEBQYBAwICHr9ryhhdbkEZAAUHCAkBAwMCDy/UIhidCwCqAAUKCwwBA/NCwhh6wXgXAAUNDg8BAwUCHg",
out: [][]byte{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11, 12}, {13, 14, 15}},
rev: [][]byte{{13, 14, 15}, {10, 11, 12}, {7, 8, 9}, {4, 5, 6}, {1, 2, 3}}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
is := is.New(t)
buf := &buffer{}
buffers := 0
if len(test.in) == 0 {
err := WriteLogFile(buf, slices.Values([]io.Reader{}))
is.NoErr(err)
}
for i, in := range test.in {
buffers += len(in)
if i == 0 {
err := WriteLogFile(buf, slices.Values(in))
is.NoErr(err)
} else {
err := AppendLogFile(buf, slices.Values(in))
is.NoErr(err)
}
}
is.Equal(base64.RawStdEncoding.EncodeToString(buf.Bytes()), test.enc)
files, err := ReadLogFile(bytes.NewReader(buf.Bytes()))
is.NoErr(err)
is.Equal(files.Size(), uint64(len(buf.Bytes())))
i := 0
for bi, fp := range files.Iter(0) {
buf, err := io.ReadAll(fp)
is.NoErr(err)
hash := hash()
hash.Write(buf)
is.Equal(bi.Hash, hash.Sum(nil)[:len(bi.Hash)])
is.True(len(test.out) > int(bi.Index))
is.Equal(buf, test.out[bi.Index])
i++
}
is.NoErr(files.Err)
is.Equal(i, buffers)
i = 0
for bi, fp := range files.Rev(files.Count()) {
buf, err := io.ReadAll(fp)
is.NoErr(err)
hash := hash()
hash.Write(buf)
is.Equal(bi.Hash, hash.Sum(nil)[:len(bi.Hash)])
is.Equal(buf, test.rev[i])
is.Equal(buf, test.out[bi.Index])
i++
}
is.NoErr(files.Err)
is.Equal(i, buffers)
is.Equal(files.Count(), uint64(i))
})
}
}
// TestArgs tests that the CLI arguments are correctly parsed.
func TestArgs(t *testing.T) {
is := is.New(t)
usage := `Usage: lsm2 create <archive> <files>...`
arguments, err := docopt.ParseArgs(usage, []string{"create", "archive", "file1", "file2"}, "1.0")
is.NoErr(err)
var params struct {
Create bool `docopt:"create"`
Archive string `docopt:"<archive>"`
Files []string `docopt:"<files>"`
}
err = arguments.Bind(&params)
is.NoErr(err)
is.Equal(params.Create, true)
is.Equal(params.Archive, "archive")
is.Equal(params.Files, []string{"file1", "file2"})
}
func BenchmarkIterate(b *testing.B) {
block := make([]byte, 1024)
buf := &buffer{}
b.Run("write", func(b *testing.B) {
WriteLogFile(buf, func(yield func(io.Reader) bool) {
for range (b.N) {
if !yield(bytes.NewBuffer(block)) {
break
}
}
})
})
b.Run("read", func(b *testing.B) {
lf, _ := ReadLogFile(buf)
b.Log(lf.Count())
for range (b.N) {
for _, fp := range lf.Iter(0) {
_, _ = io.Copy(io.Discard, fp)
break
}
}
})
b.Run("rev", func(b *testing.B) {
lf, _ := ReadLogFile(buf)
b.Log(lf.Count())
for range (b.N) {
for _, fp := range lf.Rev(lf.Count()) {
_, _ = io.Copy(io.Discard, fp)
break
}
}
})
}
type buffer struct {
buf []byte
}
// Bytes returns the underlying byte slice of the bufferWriterAt.
func (b *buffer) Bytes() []byte {
return b.buf
}
// WriteAt implements io.WriterAt. It appends data to the internal buffer
// if the offset is beyond the current length of the buffer. It will
// return an error if the offset is negative.
func (b *buffer) WriteAt(data []byte, offset int64) (written int, err error) {
if offset < 0 {
return 0, errors.New("negative offset")
}
currentLength := int64(len(b.buf))
if currentLength < offset+int64(len(data)) {
b.buf = append(b.buf, make([]byte, offset+int64(len(data))-currentLength)...)
}
written = copy(b.buf[offset:], data)
return
}
// ReadAt implements io.ReaderAt. It reads data from the internal buffer starting
// from the specified offset and writes it into the provided data slice. If the
// offset is negative, it returns an error. If the requested read extends beyond
// the buffer's length, it returns the data read so far along with an io.EOF error.
func (b *buffer) ReadAt(data []byte, offset int64) (int, error) {
if offset < 0 {
return 0, errors.New("negative offset")
}
if offset > int64(len(b.buf)) || len(b.buf[offset:]) < len(data) {
return copy(data, b.buf[offset:]), io.EOF
}
return copy(data, b.buf[offset:]), nil
}
// IterOne takes an iterator that yields values of type T along with a value of
// type I, and returns an iterator that yields only the values of type T. It
// discards the values of type I.
func IterOne[I, T any](it iter.Seq2[I, T]) iter.Seq[T] {
return func(yield func(T) bool) {
for i, v := range it {
_ = i
if !yield(v) {
return
}
}
}
}

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
}

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

@ -0,0 +1,295 @@
package app
import (
"context"
"fmt"
"os"
"os/user"
"sort"
"strings"
"go.sour.is/pkg/ident"
"go.sour.is/pkg/mercury"
"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)
}
func getSearch(spec mercury.Search) mercury.NamespaceSearch {
return spec.NamespaceSearch
}
// Index returns nil
func (app *mercuryEnviron) GetIndex(ctx context.Context, spec mercury.Search) (lis mercury.Config, err error) {
search := getSearch(spec)
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, spec mercury.Search) (lis mercury.Config, err error) {
search := getSearch(spec)
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{} })
}

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;
}
}

420
mercury/registry.go Normal file
View File

@ -0,0 +1,420 @@
package mercury
import (
"context"
"fmt"
"log"
"path/filepath"
"sort"
"strconv"
"strings"
"go.sour.is/pkg/ident"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/set"
"golang.org/x/sync/errgroup"
)
type GetIndex interface {
GetIndex(context.Context, Search) (Config, error)
}
type GetConfig interface {
GetConfig(context.Context, Search) (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 Search
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 {
log.Println("configure: ", space)
if strings.HasPrefix(space, "mercury.source.") {
space = strings.TrimPrefix(space, "mercury.source.")
handler, name, _ := strings.Cut(space, ".")
matches := c.FirstValue("match")
readonly := c.HasTag("readonly")
for _, match := range matches.Values {
ps := strings.Fields(match)
priority, err := strconv.Atoi(ps[0])
if err != nil {
return err
}
err = r.add(name, handler, strings.Join(ps[1:],"|"), priority, c, readonly)
if err != nil {
return err
}
}
}
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
}
err = r.add(name, handler, strings.Join(ps[1:],"|"), priority, c, false)
if err != nil {
return err
}
}
}
}
r.sortMatchers()
return nil
}
// Register add a handler to registry
func (r *registry) add(name, handler, match string, priority int, cfg *Space, readonly bool) error {
log.Println("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: ParseSearch(match), Priority: priority, Handler: hdlr},
)
}
if hdlr, ok := hdlr.(GetConfig); ok {
r.matchers.getConfig = append(
r.matchers.getConfig,
matcher[GetConfig]{Name: name, Match: ParseSearch(match), Priority: priority, Handler: hdlr},
)
}
if hdlr, ok := hdlr.(WriteConfig); !readonly && ok {
r.matchers.writeConfig = append(
r.matchers.writeConfig,
matcher[WriteConfig]{Name: name, Match: ParseSearch(match), Priority: priority, Handler: hdlr},
)
}
if hdlr, ok := hdlr.(GetRules); ok {
r.matchers.getRules = append(
r.matchers.getRules,
matcher[GetRules]{Name: name, Match: ParseSearch(match), Priority: priority, Handler: hdlr},
)
}
if hdlr, ok := hdlr.(GetNotify); ok {
r.matchers.getNotify = append(
r.matchers.getNotify,
matcher[GetNotify]{Name: name, Match: ParseSearch(match), Priority: priority, Handler: hdlr},
)
}
if hdlr, ok := hdlr.(SendNotify); ok {
r.matchers.sendNotify = append(
r.matchers.sendNotify,
matcher[SendNotify]{Name: name, Match: ParseSearch(match), Priority: priority, Handler: hdlr},
)
}
return nil
}
func getMatches(search Search, matchers matchers) []Search {
matches := make([]Search, len(matchers.getIndex))
for _, n := range search.NamespaceSearch {
for i, hdlr := range matchers.getIndex {
if hdlr.Match.Match(n.Raw()) {
matches[i].NamespaceSearch = append(matches[i].NamespaceSearch, n)
matches[i].Count = search.Count
matches[i].Cursor = search.Cursor // need to decode cursor for the match
matches[i].Fields = search.Fields
matches[i].Find = search.Find
}
}
}
return matches
}
// GetIndex query each handler that match namespace.
func (r *registry) GetIndex(ctx context.Context, search Search) (c Config, err error) {
ctx, span := lg.Span(ctx)
defer span.End()
matches := getMatches(search, r.matchers)
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])
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, search Search) (Config, error) {
ctx, span := lg.Span(ctx)
defer span.End()
matches := getMatches(search, r.matchers)
m := make(SpaceMap)
for i, hdlr := range r.matchers.getConfig {
if len(matches[i].NamespaceSearch) == 0 {
continue
}
span.AddEvent(fmt.Sprintf("QUERY %s %s", hdlr.Name, hdlr.Match))
lis, err := hdlr.Handler.GetConfig(ctx, matches[i])
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 := ParseSearch(space)
log.Print("PRE: ", ns)
//ns = rules.ReduceSearch(ns)
log.Print("POST: ", ns)
lis, err := Registry.GetConfig(ctx, ns)
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 := ParseSearch(space)
ns.NamespaceSearch = rules.ReduceSearch(ns.NamespaceSearch)
span.AddEvent(ns.String())
lis, err := Registry.GetIndex(ctx, ns)
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)
}
}

202
mercury/spec.go Normal file
View File

@ -0,0 +1,202 @@
package mercury
import (
"log"
"path/filepath"
"strconv"
"strings"
)
// Search implements a parsed namespace search
// It parses the input and generates an AST to inform the driver how to select values.
// * => all spaces
// mercury.* => all prefixed with `mercury.`
// mercury.config => only space `mercury.config`
// mercury.source.*#readonly => all prefixed with `mercury.source.` AND has tag `readonly`
// test.*|mercury.* => all prefixed with `test.` AND `mercury.`
// test.* find bin=eq=bar => all prefixed with `test.` AND has an attribute bin that equals bar
// test.* fields foo,bin => all prefixed with `test.` only show fields foo and bin
// - count 20 => start a cursor with 20 results
// - count 20 after <cursor> => continue after cursor for 20 results
// cursor encodes start points for each of the matched sources
type Search struct {
NamespaceSearch
Find []ops
Fields []string
Count uint64
Offset uint64
Cursor string
}
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 ParseSearch(text string) (search Search) {
ns, text, _ := strings.Cut(text, " ")
var 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))
}
}
search.NamespaceSearch = lis
field, text, next := strings.Cut(text, " ")
text = strings.TrimSpace(text)
for next {
switch strings.ToLower(field) {
case "find":
field, text, _ = strings.Cut(text, " ")
text = strings.TrimSpace(text)
search.Find = simpleParse(field)
case "fields":
field, text, _ = strings.Cut(text, " ")
text = strings.TrimSpace(text)
search.Fields = strings.Split(field, ",")
case "count":
field, text, _ = strings.Cut(text, " ")
text = strings.TrimSpace(text)
search.Count, _ = strconv.ParseUint(field, 10, 64)
case "offset":
field, text, _ = strings.Cut(text, " ")
text = strings.TrimSpace(text)
search.Offset, _ = strconv.ParseUint(field, 10, 64)
case "after":
field, text, _ = strings.Cut(text, " ")
text = strings.TrimSpace(text)
search.Cursor = field
}
field, text, next = strings.Cut(text, " ")
text = strings.TrimSpace(text)
}
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
}
type ops struct {
Left string
Op string
Right string
}
func simpleParse(in string) (out []ops) {
items := strings.Split(in, ",")
for _, i := range items {
log.Println(i)
eq := strings.Split(i, "=")
switch len(eq) {
case 2:
out = append(out, ops{eq[0], "eq", eq[1]})
case 3:
if eq[1] == "" {
eq[1] = "eq"
}
out = append(out, ops{eq[0], eq[1], eq[2]})
}
}
return
}

109
mercury/spec_test.go Normal file
View File

@ -0,0 +1,109 @@
package mercury_test
import (
"fmt"
"testing"
"github.com/matryer/is"
"go.sour.is/pkg/mercury"
"go.sour.is/pkg/mercury/sql"
sq "github.com/Masterminds/squirrel"
)
var MAX_FILTER int = 40
func TestNamespaceParse(t *testing.T) {
var tests = []struct {
getWhere func(mercury.Search) sq.Sqlizer
in string
out string
args []any
}{
{
getWhere: getWhere,
in: "d42.bgp.kapha.*|trace:d42.bgp.kapha",
out: "(column LIKE ? OR ? LIKE column || '%')",
args: []any{"d42.bgp.kapha.%", "d42.bgp.kapha"},
},
{
getWhere: getWhere,
in: "d42.bgp.kapha.*|d42.bgp.kapha",
out: "(column LIKE ? OR column = ?)",
args: []any{"d42.bgp.kapha.%", "d42.bgp.kapha"},
},
{
getWhere: mkWhere(t, sql.GetWhereSQ),
in: "d42.bgp.kapha.* find active=eq=true",
out: `SELECT * FROM spaces JOIN ( SELECT DISTINCT id FROM mercury_values mv, json_each(mv."values") vs WHERE (json_valid("values") AND name = ? AND vs.value = ?) ) r000 USING (id) WHERE (space LIKE ?)`,
args: []any{"active", "true", "d42.bgp.kapha.%"},
},
{
getWhere: mkWhere(t, sql.GetWhereSQ),
in: "d42.bgp.kapha.* count 10 offset 5",
out: `SELECT * FROM spaces WHERE (space LIKE ?) LIMIT 10 OFFSET 5`,
args: []any{"d42.bgp.kapha.%"},
},
{
getWhere: mkWhere(t, sql.GetWhereSQ),
in: "d42.bgp.kapha.* fields a,b,c",
out: `SELECT * FROM spaces WHERE (space LIKE ?)`,
args: []any{"d42.bgp.kapha.%"},
},
{
getWhere: mkWhere(t, sql.GetWhereSQ),
in: "dn42.* find @type=in=[person,net]",
out: `SELECT `,
args: []any{"d42.bgp.kapha.%"},
},
}
//SELECT * FROM spaces JOIN ( SELECT DISTINCT id FROM mercury_values mv, json_valid("values") vs WHERE (json_valid("values") AND name = ? AND vs.value = ?) ) r000 USING (id) WHERE (space LIKE ?) !=
//SELECT * FROM spaces JOIN ( SELECT DISTINCT mv.id FROM mercury_values mv, json_each(mv."values") vs WHERE (json_valid("values") AND name = ? AND vs.value = ?) ) r000 USING (id) WHERE (space LIKE ?)
for i, tt := range tests {
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
is := is.New(t)
out := mercury.ParseSearch(tt.in)
sql, args, err := tt.getWhere(out).ToSql()
is.NoErr(err)
is.Equal(sql, tt.out)
is.Equal(args, tt.args)
})
}
}
func getWhere(search mercury.Search) sq.Sqlizer {
var where sq.Or
space := "column"
for _, m := range search.NamespaceSearch {
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
}
func mkWhere(t *testing.T, where func(search mercury.Search) (func(sq.SelectBuilder) sq.SelectBuilder, error)) func(search mercury.Search) sq.Sqlizer {
t.Helper()
return func(search mercury.Search) sq.Sqlizer {
w, err := where(search)
if err != nil {
t.Log(err)
t.Fail()
}
return w(sq.Select("*").From("spaces"))
}
}

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;

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

@ -0,0 +1,128 @@
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
}
*e = append(*e, splitComma(string(str))...)
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(ctx)
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()
}

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

@ -0,0 +1,44 @@
package sql
import (
"database/sql"
"strings"
"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" || strings.HasPrefix(driver, "libsql") {
system = semconv.DBSystemSqlite
}
if driver == "postgres" {
var err error
// Register the otelsql wrapper for the provided postgres driver.
driver, 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(driver, 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()
}

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

@ -0,0 +1,594 @@
package sql
import (
"context"
"database/sql"
"errors"
"fmt"
"log"
"slices"
"strings"
sq "github.com/Masterminds/squirrel"
"go.sour.is/pkg/lg"
"go.sour.is/pkg/mercury"
"golang.org/x/exp/maps"
)
var MAX_FILTER int = 40
type sqlHandler struct {
name string
db *sql.DB
paceholderFormat sq.PlaceholderFormat
listFormat [2]rune
readonly bool
getWhere func(search mercury.Search) (func(sq.SelectBuilder) sq.SelectBuilder, error)
}
var (
_ mercury.GetIndex = (*sqlHandler)(nil)
_ mercury.GetConfig = (*sqlHandler)(nil)
_ mercury.GetRules = (*sqlHandler)(nil)
_ mercury.WriteConfig = (*sqlHandler)(nil)
)
func Register() func(context.Context) error {
var hdlrs []*sqlHandler
mercury.Registry.Register("sql", func(s *mercury.Space) any {
var dsn string
var opts strings.Builder
var dbtype string
var readonly bool = slices.Contains(s.Tags, "readonly")
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", "libsql", "libsql+embed":
h := &sqlHandler{s.Space, db, sq.Question, [2]rune{'[', ']'}, readonly, GetWhereSQ}
hdlrs = append(hdlrs, h)
return h
case "postgres":
h := &sqlHandler{s.Space, db, sq.Dollar, [2]rune{'{', '}'}, readonly, GetWherePG}
hdlrs = append(hdlrs, h)
return h
default:
return fmt.Errorf("unsupported dbtype: %s", dbtype)
}
})
return func(ctx context.Context) error {
var errs error
for _, h := range hdlrs {
// if err = ctx.Err(); err != nil {
// return errors.Join(errs, err)
// }
errs = errors.Join(errs, h.db.Close())
}
return errs
}
}
type Space struct {
mercury.Space
id uint64
}
type Value struct {
mercury.Value
id uint64
}
func (p *sqlHandler) GetIndex(ctx context.Context, search mercury.Search) (mercury.Config, error) {
ctx, span := lg.Span(ctx)
defer span.End()
where, err := p.getWhere(search)
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.Search) (config mercury.Config, err error) {
ctx, span := lg.Span(ctx)
defer span.End()
where, err := p.getWhere(search)
if err != nil {
return nil, err
}
lis, err := p.listSpace(ctx, nil, where)
if err != nil {
log.Println(err)
return nil, err
}
if len(lis) == 0 {
return nil, nil
}
spaceIDX := make([]uint64, len(lis))
spaceMap := make(map[uint64]int, len(lis))
config = make(mercury.Config, len(lis))
for i, s := range lis {
spaceIDX[i] = s.id
config[i] = &s.Space
spaceMap[s.id] = i
}
query := sq.Select(`"id"`, `"name"`, `"seq"`, `"notes"`, `"tags"`, `"values"`).
From("mercury_values").
Where(sq.Eq{"id": spaceIDX}).
OrderBy("id asc", "seq asc").
PlaceholderFormat(p.paceholderFormat)
span.AddEvent(p.name)
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 Value
err = rows.Scan(
&s.id,
&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.id]; ok {
lis[u].List = append(lis[u].List, s.Value)
}
}
err = rows.Err()
span.RecordError(err)
span.AddEvent(fmt.Sprint("read index ", len(lis)))
// log.Println(config.String())
return config, err
}
func (p *sqlHandler) listSpace(ctx context.Context, tx sq.BaseRunner, where func(sq.SelectBuilder) sq.SelectBuilder) ([]*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").
OrderBy("space asc").
PlaceholderFormat(p.paceholderFormat)
query = where(query)
span.AddEvent(p.name)
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()
if p.readonly {
return fmt.Errorf("readonly database")
}
// 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
where := func(qry sq.SelectBuilder) sq.SelectBuilder { return qry.Where(sq.Eq{"space": maps.Keys(names)}) }
lis, err := p.listSpace(ctx, tx, where)
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(p.paceholderFormat).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(p.paceholderFormat).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(p.paceholderFormat)
span.AddEvent(p.name)
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(p.paceholderFormat).
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(p.name)
span.AddEvent(lg.LogQuery(query.ToSql()))
err := query.
RunWith(tx).
QueryRowContext(ctx).
Scan(&id)
if err != nil {
span.AddEvent(p.name)
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(p.paceholderFormat).
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)
span.AddEvent(p.name)
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)
span.AddEvent(p.name)
span.AddEvent(lg.LogQuery(insert.ToSql()))
_, err = insert.ExecContext(ctx)
if err != nil {
// log.Error(err)
return
}
}
return
}
func GetWherePG(search mercury.Search) (func(sq.SelectBuilder) sq.SelectBuilder, error) {
var where sq.Or
space := "space"
for _, m := range search.NamespaceSearch {
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)
}
}
var joins []sq.SelectBuilder
for i, o := range search.Find {
log.Println(o)
if i > MAX_FILTER {
err := fmt.Errorf("too many filters [%d]", MAX_FILTER)
return nil, err
}
q := sq.Select("DISTINCT id").From("mercury_values")
switch o.Op {
case "key":
q = q.Where(sq.Eq{"name": o.Left})
case "nkey":
q = q.Where(sq.NotEq{"name": o.Left})
case "eq":
q = q.Where("name = ? AND ? = any (values)", o.Left, o.Right)
case "neq":
q = q.Where("name = ? AND ? != any (values)", o.Left, o.Right)
case "gt":
q = q.Where("name = ? AND ? > any (values)", o.Left, o.Right)
case "lt":
q = q.Where("name = ? AND ? < any (values)", o.Left, o.Right)
case "ge":
q = q.Where("name = ? AND ? >= any (values)", o.Left, o.Right)
case "le":
q = q.Where("name = ? AND ? <= any (values)", o.Left, o.Right)
// case "like":
// q = q.Where("name = ? AND value LIKE ?", o.Left, o.Right)
// case "in":
// q = q.Where(sq.Eq{"name": o.Left, "value": strings.Split(o.Right, " ")})
}
joins = append(joins, q)
}
return func(s sq.SelectBuilder) sq.SelectBuilder {
for i, q := range joins {
s = s.JoinClause(q.Prefix("JOIN (").Suffix(fmt.Sprintf(`) r%03d USING (id)`, i)))
}
if search.Count > 0 {
s = s.Limit(search.Count)
}
return s.Where(where)
}, nil
}
func GetWhereSQ(search mercury.Search) (func(sq.SelectBuilder) sq.SelectBuilder, error) {
var where sq.Or
var errs error
id := "id"
space := "space"
name := "name"
values_each := `json_valid("values")`
values_valid := `json_valid("values")`
if errs != nil {
return nil, errs
}
for _, m := range search.NamespaceSearch {
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)
}
}
var joins []sq.SelectBuilder
for i, o := range search.Find {
log.Println(o)
if i > MAX_FILTER {
err := fmt.Errorf("too many filters [%d]", MAX_FILTER)
return nil, err
}
q := sq.Select("DISTINCT " + id).From(`mercury_values mv, ` + values_each + ` vs`)
switch o.Op {
case "key":
q = q.Where(sq.Eq{name: o.Left})
case "nkey":
q = q.Where(sq.NotEq{name: o.Left})
case "eq":
q = q.Where(sq.And{sq.Expr(values_valid), sq.Eq{name: o.Left, `vs.value`: o.Right}})
case "neq":
q = q.Where(sq.And{sq.Expr(values_valid), sq.Eq{name: o.Left}, sq.NotEq{`vs.value`: o.Right}})
case "gt":
q = q.Where(sq.And{sq.Expr(values_valid), sq.Eq{name: o.Left}, sq.Gt{`vs.value`: o.Right}})
case "lt":
q = q.Where(sq.And{sq.Expr(values_valid), sq.Eq{name: o.Left}, sq.Lt{`vs.value`: o.Right}})
case "ge":
q = q.Where(sq.And{sq.Expr(values_valid), sq.Eq{name: o.Left}, sq.GtOrEq{`vs.value`: o.Right}})
case "le":
q = q.Where(sq.And{sq.Expr(values_valid), sq.Eq{name: o.Left}, sq.LtOrEq{`vs.value`: o.Right}})
case "like":
q = q.Where(sq.And{sq.Expr(values_valid), sq.Eq{name: o.Left}, sq.Like{`vs.value`: o.Right}})
case "in":
q = q.Where(sq.Eq{name: o.Left, "vs.value": strings.Split(o.Right, " ")})
}
joins = append(joins, q)
}
return func(s sq.SelectBuilder) sq.SelectBuilder {
for i, q := range joins {
s = s.JoinClause(q.Prefix("JOIN (").Suffix(fmt.Sprintf(`) r%03d USING (id)`, i)))
}
if search.Count > 0 {
s = s.Limit(search.Count)
}
if search.Offset > 0 {
s = s.Offset(search.Offset)
}
return s.Where(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())
}

98
rsql/dbcolumns.go Normal file
View File

@ -0,0 +1,98 @@
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)
tag, _, _ := strings.Cut(field.Tag.Get("db"), ",")
json := field.Tag.Get("json")
json, _, _ = strings.Cut(json, ",")
if tag == "" {
tag = json
}
graphql := field.Tag.Get("graphql")
graphql, _, _ = strings.Cut(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
}
func QuoteCols(cols []string) []string {
lis := make([]string, len(cols))
for i := range cols {
lis[i] = `"` + cols[i] + `"`
}
return lis
}

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
@ -113,7 +114,6 @@ func (s *Harness) Run(ctx context.Context, appName, version string) error {
err := g.Wait() err := g.Wait()
if err != nil { if err != nil {
log.Printf("Shutdown due to error: %s", err) log.Printf("Shutdown due to error: %s", err)
} }
return err return err
} }

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
}