Compare commits
4 Commits
eb63312542
...
f5027d9bfd
Author | SHA1 | Date | |
---|---|---|---|
f5027d9bfd | |||
b1bff4cbf0 | |||
1f8b4ab24f | |||
d4e021386b |
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
test.db
|
||||
*.mercury
|
||||
sour.is-mercury
|
45
cmd/testsql/main.go
Normal file
45
cmd/testsql/main.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -127,9 +127,6 @@ func (c *cron) run(ctx context.Context, now time.Time) {
|
|||
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 {
|
||||
run = append(run, state.queue...)
|
||||
state.queue = state.queue[:0]
|
||||
|
@ -148,6 +145,9 @@ func (c *cron) run(ctx context.Context, now time.Time) {
|
|||
return
|
||||
}
|
||||
|
||||
span.AddEvent("Cron Run: " + now.Format(time.RFC822))
|
||||
// fmt.Println("Cron Run: ", now.Format(time.RFC822))
|
||||
|
||||
wg, _ := errgroup.WithContext(ctx)
|
||||
|
||||
for i := range run {
|
||||
|
|
37
env/env.go
vendored
37
env/env.go
vendored
|
@ -9,35 +9,16 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
func Default(name, defaultValue string) string {
|
||||
func Default(name, defaultValue string) (s string) {
|
||||
name = strings.TrimSpace(name)
|
||||
defaultValue = strings.TrimSpace(defaultValue)
|
||||
if v := strings.TrimSpace(os.Getenv(name)); v != "" {
|
||||
slog.Info("env", name, v)
|
||||
return v
|
||||
}
|
||||
slog.Info("env", name, defaultValue+" (default)")
|
||||
return defaultValue
|
||||
}
|
||||
s = strings.TrimSpace(defaultValue)
|
||||
|
||||
type secret string
|
||||
if v, ok := os.LookupEnv(name); ok {
|
||||
s = strings.TrimSpace(v)
|
||||
slog.Info("env", slog.String(name, v))
|
||||
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)
|
||||
slog.Info("env", slog.String(name, s+" (default)"))
|
||||
return
|
||||
}
|
||||
|
|
35
env/secret.go
vendored
Normal file
35
env/secret.go
vendored
Normal 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
|
||||
}
|
||||
|
83
go.mod
83
go.mod
|
@ -1,19 +1,21 @@
|
|||
module go.sour.is/pkg
|
||||
|
||||
go 1.21
|
||||
go 1.22.0
|
||||
|
||||
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/gorilla/websocket v1.5.0
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/matryer/is v1.4.1
|
||||
github.com/ravilushqa/otelgqlgen v0.13.1
|
||||
github.com/vektah/gqlparser/v2 v2.5.6
|
||||
go.opentelemetry.io/otel v1.18.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0
|
||||
go.opentelemetry.io/otel/sdk/metric v0.41.0
|
||||
github.com/ravilushqa/otelgqlgen v0.15.0
|
||||
github.com/tursodatabase/go-libsql v0.0.0-20240322134723-08771dcdd2f1
|
||||
github.com/vektah/gqlparser/v2 v2.5.11
|
||||
go.opentelemetry.io/otel v1.23.1
|
||||
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
|
||||
golang.org/x/sync v0.7.0
|
||||
golang.org/x/sync v0.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -21,39 +23,58 @@ require (
|
|||
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.3 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/felixge/httpsnoop 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/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // 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
|
||||
go.opentelemetry.io/contrib v1.16.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/sosodev/duration v1.2.0 // 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 (
|
||||
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/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
|
||||
github.com/prometheus/client_golang v1.17.0
|
||||
github.com/tursodatabase/go-libsql v0.0.0-20240322134723-08771dcdd2f1
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.41.0
|
||||
go.opentelemetry.io/otel/metric v1.18.0
|
||||
go.opentelemetry.io/otel/sdk v1.18.0
|
||||
go.opentelemetry.io/otel/trace v1.18.0
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.0
|
||||
github.com/prometheus/client_golang v1.18.0
|
||||
go.nhat.io/otelsql v0.12.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.48.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.45.2
|
||||
go.opentelemetry.io/otel/metric v1.23.1
|
||||
go.opentelemetry.io/otel/sdk v1.23.1
|
||||
go.opentelemetry.io/otel/trace v1.23.1
|
||||
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a
|
||||
golang.org/x/net v0.23.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/grpc v1.58.3 // indirect
|
||||
google.golang.org/grpc v1.61.1 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
modernc.org/sqlite v1.29.1
|
||||
)
|
||||
|
|
257
go.sum
257
go.sum
|
@ -1,5 +1,13 @@
|
|||
github.com/99designs/gqlgen v0.17.34 h1:5cS5/OKFguQt+Ws56uj9FlG2xm1IlcJWNF2jrMIKYFQ=
|
||||
github.com/99designs/gqlgen v0.17.34/go.mod h1:Axcd3jIFHBVcqzixujJQr1wGqE+lGTpz6u4iZBZg1G8=
|
||||
cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
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/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||
|
@ -10,6 +18,9 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig
|
|||
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/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/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
|
@ -19,120 +30,195 @@ 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/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/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
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.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
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/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/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f h1:16RtHeWGkJMc80Etb8RPCcKevXGldr57+LOyZt8zOlg=
|
||||
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.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
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.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.3 h1:kmRrRLlInXvng0SmLxmQpQkpbYAvcXm7NPDrgxJa9mE=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.3/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
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/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/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
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/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
|
||||
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.47.0 h1:p5Cz0FNHo7SnWOmWmoRozVcjEp0bIVU8cV7OShpjL1k=
|
||||
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/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/ravilushqa/otelgqlgen v0.13.1 h1:V+zFE75iDd2/CSzy5kKnb+Fi09SsE5535wv9U2nUEFE=
|
||||
github.com/ravilushqa/otelgqlgen v0.13.1/go.mod h1:ZIyWykK2paCuNi9k8gk5edcNSwDJuxZaW90vZXpafxw=
|
||||
github.com/ravilushqa/otelgqlgen v0.15.0 h1:U85nrlweMXTGaMChUViYM39/MXBZVeVVlpuHq+6eECQ=
|
||||
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/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/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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ=
|
||||
github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU=
|
||||
github.com/tursodatabase/go-libsql v0.0.0-20240322134723-08771dcdd2f1 h1:lQwP++jvwcQiPqqIXIvabCIvfh8bzibpjYvT4s0jWmA=
|
||||
github.com/tursodatabase/go-libsql v0.0.0-20240322134723-08771dcdd2f1/go.mod h1:sb520Yr+GHBsfL43FQgQ+rLFfuJkItgRWlTgbIQHVxA=
|
||||
github.com/vektah/gqlparser/v2 v2.5.6 h1:Ou14T0N1s191eRMZ1gARVqohcbe1e8FrcONScsq8cRU=
|
||||
github.com/vektah/gqlparser/v2 v2.5.6/go.mod h1:z8xXUff237NntSuH8mLFijZ+1tjV1swDbpDqjJmk6ME=
|
||||
go.opentelemetry.io/contrib v1.16.1 h1:EpASvVyGx6/ZTlmXzxYfTMZxHROelCeXXa2uLiwltcs=
|
||||
go.opentelemetry.io/contrib v1.16.1/go.mod h1:gIzjwWFoGazJmtCaDgViqOSJPde2mCWzv60o0bWPcZs=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 h1:KfYpVmrjI7JuToy5k8XV3nkapjWx48k4E4JOtVstzQI=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0/go.mod h1:SeQhzAEccGVZVEy7aH87Nh0km+utSpo1pTv6eMMop48=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0 h1:EbmAUG9hEAMXyfWEasIt2kmh/WmXUznUksChApTgBGc=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0/go.mod h1:rD9feqRYP24P14t5kmhNMqsqm1jvKmpx2H2rKVw52V8=
|
||||
go.opentelemetry.io/otel v1.18.0 h1:TgVozPGZ01nHyDZxK5WGPFB9QexeTMXEH7+tIClWfzs=
|
||||
go.opentelemetry.io/otel v1.18.0/go.mod h1:9lWqYO0Db579XzVuCKFNPDl4s73Voa+zEck3wHaAYQI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0 h1:IAtl+7gua134xcV3NieDhJHjjOVeJhXAnYf/0hswjUY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0/go.mod h1:w+pXobnBzh95MNIkeIuAKcHe/Uu/CX2PKIvBP6ipKRA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0 h1:6pu8ttx76BxHf+xz/H77AUZkPF3cwWzXqAUsXhVKI18=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0/go.mod h1:IOmXxPrxoxFMXdNy7lfDmE8MzE61YPcurbUm0SMjerI=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.41.0 h1:A3/bhjP5SmELy8dcpK+uttHeh9Qrh+YnS16/VzrztRQ=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.41.0/go.mod h1:mKuXEMi9suyyNJQ99SZCO0mpWGFe0MIALtjd3r6uo7Q=
|
||||
go.opentelemetry.io/otel/metric v1.18.0 h1:JwVzw94UYmbx3ej++CwLUQZxEODDj/pOuTCvzhtRrSQ=
|
||||
go.opentelemetry.io/otel/metric v1.18.0/go.mod h1:nNSpsVDjWGfb7chbRLUNW+PBNdcSTHD4Uu5pfFMOI0k=
|
||||
go.opentelemetry.io/otel/sdk v1.18.0 h1:e3bAB0wB3MljH38sHzpV/qWrOTCFrdZF2ct9F8rBkcY=
|
||||
go.opentelemetry.io/otel/sdk v1.18.0/go.mod h1:1RCygWV7plY2KmdskZEDDBs4tJeHG92MdHZIluiYs/M=
|
||||
go.opentelemetry.io/otel/sdk/metric v0.41.0 h1:c3sAt9/pQ5fSIUfl0gPtClV3HhE18DCVzByD33R/zsk=
|
||||
go.opentelemetry.io/otel/sdk/metric v0.41.0/go.mod h1:PmOmSt+iOklKtIg5O4Vz9H/ttcRFSNTgii+E1KGyn1w=
|
||||
go.opentelemetry.io/otel/trace v1.18.0 h1:NY+czwbHbmndxojTEKiSMHkG2ClNH2PwmcHrdo0JY10=
|
||||
go.opentelemetry.io/otel/trace v1.18.0/go.mod h1:T2+SGJGuYZY3bjj5rgh/hN7KIrlpWC5nS8Mjvzckz+0=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||
github.com/vektah/gqlparser/v2 v2.5.11 h1:JJxLtXIoN7+3x6MBdtIP59TP1RANnY7pXOaDnADQSf8=
|
||||
github.com/vektah/gqlparser/v2 v2.5.11/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc=
|
||||
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
|
||||
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||
go.nhat.io/otelsql v0.12.0 h1:/rBhWZiwHFLpCm5SGdafm+Owm0OmGmnF31XWxgecFtY=
|
||||
go.nhat.io/otelsql v0.12.0/go.mod h1:39Hc9/JDfCl7NGrBi1uPP3QPofqwnC/i5SFd7gtDMWM=
|
||||
go.opentelemetry.io/contrib v1.23.0 h1:5f6bvGoHE/7lcolc1jCA4Vzq2tnPs4tfqL1M/yfjbOA=
|
||||
go.opentelemetry.io/contrib v1.23.0/go.mod h1:usW9bPlrjHiJFbK0a6yK/M5wNHs3nLmtrT3vzhoD3co=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 h1:doUP+ExOpH3spVTLS0FcWGLnQrPct/hD/bCPbDRUEAU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0/go.mod h1:rdENBZMT2OE6Ne/KLwpiXudnAsbdrdBaqBvTN8M8BgA=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.48.0 h1:dJlCKeq+zmO5Og4kgxqPvvJrzuD/mygs1g/NYM9dAsU=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.48.0/go.mod h1:p+hpBCpLHpuUrR0lHgnHbUnbCBll1IhrcMIlycC+xYs=
|
||||
go.opentelemetry.io/otel v1.23.1 h1:Za4UzOqJYS+MUczKI320AtqZHZb7EqxO00jAHE0jmQY=
|
||||
go.opentelemetry.io/otel v1.23.1/go.mod h1:Td0134eafDLcTS4y+zQ26GE8u3dEuRBiBCTUIRHaikA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1 h1:o8iWeVFa1BcLtVEV0LzrCxV2/55tB3xLxADr6Kyoey4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1/go.mod h1:SEVfdK4IoBnbT2FXNM/k8yC08MrfbhWk3U4ljM8B3HE=
|
||||
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/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
|
||||
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
|
||||
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
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/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
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=
|
||||
google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
|
||||
google.golang.org/grpc v1.58.0 h1:32JY8YpPMSR45K+c3o6b8VL73V+rR8k+DeMIr4vRH8o=
|
||||
google.golang.org/grpc v1.58.0/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
|
||||
google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ=
|
||||
google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
|
||||
google.golang.org/api v0.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20170918111702-1e559d0a00ee/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
|
||||
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240213162025-012b6fc9bca9 h1:4++qSzdWBUy9/2x8L5KZgwZw+mjJZ2yDSCGMVM0YzRs=
|
||||
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/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
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=
|
||||
|
@ -141,3 +227,20 @@ 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/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
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=
|
||||
|
|
115
ident/ident.go
Normal file
115
ident/ident.go
Normal 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
75
ident/null-user.go
Normal 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
249
ident/routes.go
Normal 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
275
ident/source/mercury.go
Normal 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 ¤t, err
|
||||
}
|
||||
|
||||
return ¤t, 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 ¤t, err
|
||||
}
|
||||
|
||||
return ¤t, 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 ¤t, err
|
||||
}
|
||||
|
||||
return ¤t, 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
83
ident/source/session.go
Normal 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
|
||||
})
|
||||
}
|
45
lg/tracer.go
45
lg/tracer.go
|
@ -58,17 +58,56 @@ type wrapSpan struct {
|
|||
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) {
|
||||
w.Span.AddEvent(name, options...)
|
||||
|
||||
cfg := trace.NewEventConfig(options...)
|
||||
|
||||
attrs := cfg.Attributes()
|
||||
args := make([]any, len(attrs)*2)
|
||||
args := make([]any, len(attrs))
|
||||
|
||||
for i, a := range attrs {
|
||||
args[2*i] = a.Key
|
||||
args[2*i+1] = a.Value
|
||||
switch a.Value.Type() {
|
||||
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...)
|
||||
|
|
|
@ -4,7 +4,10 @@ import (
|
|||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -17,28 +20,37 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
sql.Register("libsql+embed", &db{})
|
||||
sql.Register("libsql+embed", &db{conns: make(map[string]*connector)})
|
||||
}
|
||||
|
||||
type db struct {
|
||||
conns map[string]connector
|
||||
conns map[string]*connector
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
type connector struct {
|
||||
*libsql.Connector
|
||||
dsn string
|
||||
dir string
|
||||
driver *db
|
||||
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()
|
||||
|
||||
defer os.RemoveAll(c.dir)
|
||||
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)
|
||||
}
|
||||
|
@ -47,7 +59,12 @@ func (c *connector) Close() error {
|
|||
}
|
||||
|
||||
func (db *db) OpenConnector(dsn string) (driver.Connector, error) {
|
||||
if c, ok := func() (connector, bool) {
|
||||
// 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]
|
||||
|
@ -79,20 +96,39 @@ func (db *db) OpenConnector(dsn string) (driver.Connector, error) {
|
|||
libsql.WithAuthToken(authToken),
|
||||
}
|
||||
|
||||
if refresh, err := strconv.ParseInt(u.Query().Get("refresh"),10,64); err == nil {
|
||||
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))
|
||||
}
|
||||
|
||||
dir, err := os.MkdirTemp("", "libsql-*")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating temporary directory: %w", err)
|
||||
|
||||
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)
|
||||
|
@ -105,13 +141,19 @@ func (db *db) OpenConnector(dsn string) (driver.Connector, error) {
|
|||
return nil, fmt.Errorf("creating connector: %w", err)
|
||||
}
|
||||
|
||||
connector := connector{c, dsn, dir, db}
|
||||
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
|
||||
|
|
138
lsm/marshal.go
Normal file
138
lsm/marshal.go
Normal file
|
@ -0,0 +1,138 @@
|
|||
package lsm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type entry struct {
|
||||
key string
|
||||
value uint64
|
||||
}
|
||||
|
||||
// MarshalBinary implements encoding.BinaryMarshaler.
|
||||
func (e *entry) MarshalBinary() (data []byte, err error) {
|
||||
data = make([]byte, len(e.key), len(e.key)+binary.MaxVarintLen16)
|
||||
copy(data, e.key)
|
||||
|
||||
data = binary.AppendUvarint(data, e.value)
|
||||
reverse(data[len(e.key):])
|
||||
return data, err
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
|
||||
func (e *entry) UnmarshalBinary(data []byte) error {
|
||||
// fmt.Println("unmarshal", data, string(data))
|
||||
|
||||
if len(data) < binary.MaxVarintLen16 {
|
||||
return fmt.Errorf("%w: bad data", ErrDecode)
|
||||
}
|
||||
head := make([]byte, binary.MaxVarintLen16)
|
||||
copy(head, data[max(0, len(data)-cap(head)):])
|
||||
reverse(head)
|
||||
|
||||
size := 0
|
||||
e.value, size = binary.Uvarint(head)
|
||||
if size == 0 {
|
||||
return fmt.Errorf("%w: invalid data", ErrDecode)
|
||||
}
|
||||
e.key = string(data[:len(data)-size])
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ encoding.BinaryMarshaler = (*entry)(nil)
|
||||
var _ encoding.BinaryUnmarshaler = (*entry)(nil)
|
||||
|
||||
type entries []entry
|
||||
|
||||
// MarshalBinary implements encoding.BinaryMarshaler.
|
||||
func (lis *entries) MarshalBinary() (data []byte, err error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
for _, e := range *lis {
|
||||
d, err := e.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = buf.Write(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = buf.Write(reverse(binary.AppendUvarint(make([]byte, 0, binary.MaxVarintLen32), uint64(len(d)))))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
|
||||
func (lis *entries) UnmarshalBinary(data []byte) error {
|
||||
head := make([]byte, binary.MaxVarintLen16)
|
||||
pos := uint64(len(data))
|
||||
|
||||
for pos > 0 {
|
||||
copy(head, data[max(0, pos-uint64(cap(head))):])
|
||||
length, size := binary.Uvarint(reverse(head))
|
||||
|
||||
e := entry{}
|
||||
if err := e.UnmarshalBinary(data[max(0, pos-(length+uint64(size))) : pos-uint64(size)]); err != nil {
|
||||
return err
|
||||
}
|
||||
*lis = append(*lis, e)
|
||||
|
||||
pos -= length + uint64(size)
|
||||
}
|
||||
reverse(*lis)
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ encoding.BinaryMarshaler = (*entries)(nil)
|
||||
var _ encoding.BinaryUnmarshaler = (*entries)(nil)
|
||||
|
||||
type segment struct {
|
||||
entries entries
|
||||
}
|
||||
|
||||
// MarshalBinary implements encoding.BinaryMarshaler.
|
||||
func (s *segment) MarshalBinary() (data []byte, err error) {
|
||||
head := header{
|
||||
entries: uint64(len(s.entries)),
|
||||
}
|
||||
|
||||
data, err = s.entries.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
head.datalen = uint64(len(data))
|
||||
|
||||
h := hash()
|
||||
h.Write(data)
|
||||
head.sig = h.Sum(nil)
|
||||
|
||||
return head.Append(data), err
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
|
||||
func (s *segment) UnmarshalBinary(data []byte) error {
|
||||
head, err := ReadHead(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h := hash()
|
||||
h.Write(data[:head.datalen])
|
||||
if !bytes.Equal(head.sig, h.Sum(nil)) {
|
||||
return fmt.Errorf("%w: invalid checksum", ErrDecode)
|
||||
}
|
||||
|
||||
s.entries = make(entries, 0, head.entries)
|
||||
return s.entries.UnmarshalBinary(data[:head.datalen])
|
||||
}
|
76
lsm/marshal_test.go
Normal file
76
lsm/marshal_test.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package lsm
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
)
|
||||
|
||||
func TestEncoding(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
data := segment{entries: entries{
|
||||
{"key-1", 1},
|
||||
{"key-2", 2},
|
||||
{"key-3", 3},
|
||||
{"longerkey-4", 65535},
|
||||
}}
|
||||
|
||||
b, err := data.MarshalBinary()
|
||||
is.NoErr(err)
|
||||
|
||||
var got segment
|
||||
err = got.UnmarshalBinary(b)
|
||||
is.NoErr(err)
|
||||
|
||||
is.Equal(data, got)
|
||||
}
|
||||
|
||||
func TestReverse(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
got := []byte("gnirts a si siht")
|
||||
reverse(got)
|
||||
|
||||
is.Equal(got, []byte("this is a string"))
|
||||
|
||||
got = []byte("!gnirts a si siht")
|
||||
reverse(got)
|
||||
|
||||
is.Equal(got, []byte("this is a string!"))
|
||||
}
|
||||
|
||||
func TestFile(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
entries := entries {
|
||||
{"key-1", 1},
|
||||
{"key-2", 2},
|
||||
{"key-3", 3},
|
||||
{"longerkey-4", 65535},
|
||||
}
|
||||
|
||||
f := basicFile(t, entries, entries, entries)
|
||||
|
||||
sf, err := ReadFile(f)
|
||||
is.NoErr(err)
|
||||
|
||||
is.Equal(len(sf.segments), 3)
|
||||
}
|
||||
|
||||
func basicFile(t *testing.T, lis ...entries) fs.File {
|
||||
t.Helper()
|
||||
|
||||
segments := make([][]byte, len(lis))
|
||||
var err error
|
||||
for i, entries := range lis {
|
||||
data := segment{entries: entries}
|
||||
segments[i], err = data.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
return NewFile(segments...)
|
||||
}
|
370
lsm/sst.go
Normal file
370
lsm/sst.go
Normal file
|
@ -0,0 +1,370 @@
|
|||
// SPDX-FileCopyrightText: 2023 Jon Lundy <jon@xuu.cc>
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// lsm -- Log Structured Merge-Tree
|
||||
//
|
||||
// This is a basic LSM tree using a SSTable optimized for append only writing. On disk data is organized into time ordered
|
||||
// files of segments, containing reverse sorted keys. Each segment ends with a magic value `Souris\x01`, a 4byte hash, count of
|
||||
// segment entries, and data length.
|
||||
|
||||
package lsm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"io/fs"
|
||||
"sort"
|
||||
)
|
||||
|
||||
var (
|
||||
magic = reverse(append([]byte("Souris"), '\x01'))
|
||||
hash = fnv.New32a
|
||||
hashLength = hash().Size()
|
||||
// segmentSize = 2 ^ 16 // min 2^9 = 512b, max? 2^20 = 1M
|
||||
segmentFooterLength = len(magic) + hashLength + binary.MaxVarintLen32 + binary.MaxVarintLen32
|
||||
)
|
||||
|
||||
type header struct {
|
||||
sig []byte // 4Byte signature
|
||||
entries uint64 // count of entries in segment
|
||||
datalen uint64 // length of data
|
||||
headlen uint64 // length of header
|
||||
end int64 // location of end of data/start of header (start of data is `end - datalen`)
|
||||
}
|
||||
|
||||
// ReadHead parse header from a segment. reads from the end of slice of length segmentFooterLength
|
||||
func ReadHead(data []byte) (*header, error) {
|
||||
if len(data) < len(magic)+6 {
|
||||
return nil, fmt.Errorf("%w: invalid size", ErrDecode)
|
||||
}
|
||||
|
||||
if !bytes.Equal(data[len(data)-len(magic):], magic) {
|
||||
return nil, fmt.Errorf("%w: invalid header", ErrDecode)
|
||||
}
|
||||
|
||||
head := make([]byte, 0, segmentFooterLength)
|
||||
head = reverse(append(head, data[max(0, len(data)-cap(head)-1):]...))
|
||||
size, s := binary.Uvarint(head[len(magic)+4:])
|
||||
length, i := binary.Uvarint(head[len(magic)+4+s:])
|
||||
|
||||
return &header{
|
||||
sig: head[len(magic) : len(magic)+4],
|
||||
entries: size,
|
||||
datalen: length,
|
||||
headlen: uint64(len(magic) + hashLength + s + i),
|
||||
end: int64(len(data)),
|
||||
}, nil
|
||||
}
|
||||
func (h *header) Append(data []byte) []byte {
|
||||
|
||||
length := len(data)
|
||||
data = append(data, h.sig...)
|
||||
data = binary.AppendUvarint(data, h.entries)
|
||||
data = binary.AppendUvarint(data, h.datalen)
|
||||
reverse(data[length:])
|
||||
|
||||
return append(data, magic...)
|
||||
}
|
||||
|
||||
var _ encoding.BinaryMarshaler = (*segment)(nil)
|
||||
var _ encoding.BinaryUnmarshaler = (*segment)(nil)
|
||||
|
||||
var ErrDecode = errors.New("decode")
|
||||
|
||||
func reverse[T any](b []T) []T {
|
||||
l := len(b)
|
||||
for i := 0; i < l/2; i++ {
|
||||
b[i], b[l-i-1] = b[l-i-1], b[i]
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// func clone[T ~[]E, E any](e []E) []E {
|
||||
// return append(e[0:0:0], e...)
|
||||
// }
|
||||
|
||||
type entryBytes []byte
|
||||
|
||||
// KeyValue returns the parsed key and value from an entry
|
||||
func (e entryBytes) KeyValue() ([]byte, uint64) {
|
||||
if len(e) < 2 {
|
||||
return nil, 0
|
||||
}
|
||||
head := reverse(append(e[0:0:0], e[max(0, len(e)-binary.MaxVarintLen64):]...))
|
||||
value, i := binary.Uvarint(head)
|
||||
return append(e[0:0:0], e[:len(e)-i]...), value
|
||||
}
|
||||
|
||||
// NewKeyValue packed into an entry
|
||||
func NewKeyValue(key []byte, val uint64) entryBytes {
|
||||
length := len(key)
|
||||
data := append(key[0:0:0], key...)
|
||||
data = binary.AppendUvarint(data, val)
|
||||
reverse(data[length:])
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
type listEntries []entryBytes
|
||||
|
||||
// WriteTo implements io.WriterTo.
|
||||
func (lis *listEntries) WriteTo(wr io.Writer) (int64, error) {
|
||||
if lis == nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
head := header{
|
||||
entries: uint64(len(*lis)),
|
||||
}
|
||||
h := hash()
|
||||
|
||||
wr = io.MultiWriter(wr, h)
|
||||
|
||||
var i int64
|
||||
for _, b := range *lis {
|
||||
j, err := wr.Write(b)
|
||||
i += int64(j)
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
|
||||
j, err = wr.Write(reverse(binary.AppendUvarint(make([]byte, 0, binary.MaxVarintLen32), uint64(len(b)))))
|
||||
i += int64(j)
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
}
|
||||
head.datalen = uint64(i)
|
||||
head.sig = h.Sum(nil)
|
||||
|
||||
b := head.Append([]byte{})
|
||||
j, err := wr.Write(b)
|
||||
i += int64(j)
|
||||
|
||||
return i, err
|
||||
}
|
||||
|
||||
var _ sort.Interface = listEntries{}
|
||||
|
||||
// Len implements sort.Interface.
|
||||
func (lis listEntries) Len() int {
|
||||
return len(lis)
|
||||
}
|
||||
|
||||
// Less implements sort.Interface.
|
||||
func (lis listEntries) Less(i int, j int) bool {
|
||||
iname, _ := lis[i].KeyValue()
|
||||
jname, _ := lis[j].KeyValue()
|
||||
|
||||
return bytes.Compare(iname, jname) < 0
|
||||
}
|
||||
|
||||
// Swap implements sort.Interface.
|
||||
func (lis listEntries) Swap(i int, j int) {
|
||||
lis[i], lis[j] = lis[j], lis[i]
|
||||
}
|
||||
|
||||
type segmentReader struct {
|
||||
head *header
|
||||
rd io.ReaderAt
|
||||
}
|
||||
|
||||
// FirstEntry parses the first segment entry from the end of the segment
|
||||
func (s *segmentReader) FirstEntry() (*entryBytes, error) {
|
||||
e, _, err := s.readEntryAt(-1)
|
||||
return e, err
|
||||
}
|
||||
|
||||
func (s *segmentReader) VerifyHash() (bool, error) {
|
||||
h := hash()
|
||||
data := make([]byte, s.head.datalen)
|
||||
_, err := s.rd.ReadAt(data, s.head.end-int64(s.head.datalen))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
_, err = h.Write(data)
|
||||
ok := bytes.Equal(h.Sum(nil), s.head.sig)
|
||||
|
||||
return ok, err
|
||||
}
|
||||
|
||||
// Find locates needle within a segment. if it cant find it will return the nearest key before needle.
|
||||
func (s *segmentReader) Find(needle []byte, first bool) (*entryBytes, bool, error) {
|
||||
if s == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
e, pos, err := s.readEntryAt(-1)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
last := e
|
||||
found := false
|
||||
for pos > 0 {
|
||||
key, _ := e.KeyValue()
|
||||
switch bytes.Compare(key, needle) {
|
||||
case 1: // key=ccc, needle=bbb
|
||||
return last, found, nil
|
||||
case 0: // equal
|
||||
if first {
|
||||
return e, true, nil
|
||||
}
|
||||
found = true
|
||||
fallthrough
|
||||
case -1: // key=aaa, needle=bbb
|
||||
last = e
|
||||
e, pos, err = s.readEntryAt(pos)
|
||||
if err != nil {
|
||||
return nil, found, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return last, found, nil
|
||||
}
|
||||
func (s *segmentReader) readEntryAt(pos int64) (*entryBytes, int64, error) {
|
||||
if pos < 0 {
|
||||
pos = s.head.end
|
||||
}
|
||||
head := make([]byte, binary.MaxVarintLen16)
|
||||
s.rd.ReadAt(head, pos-binary.MaxVarintLen16)
|
||||
length, hsize := binary.Uvarint(reverse(head))
|
||||
|
||||
e := make(entryBytes, length)
|
||||
_, err := s.rd.ReadAt(e, pos-int64(length)-int64(hsize))
|
||||
|
||||
return &e, pos - int64(length) - int64(hsize), err
|
||||
}
|
||||
|
||||
type logFile struct {
|
||||
rd interface {
|
||||
io.ReaderAt
|
||||
io.WriterTo
|
||||
}
|
||||
segments []segmentReader
|
||||
|
||||
fs.File
|
||||
}
|
||||
|
||||
func ReadFile(fd fs.File) (*logFile, error) {
|
||||
l := &logFile{File: fd}
|
||||
|
||||
stat, err := fd.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eof := stat.Size()
|
||||
if rd, ok := fd.(interface {
|
||||
io.ReaderAt
|
||||
io.WriterTo
|
||||
}); ok {
|
||||
l.rd = rd
|
||||
|
||||
} else {
|
||||
rd, err := io.ReadAll(fd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.rd = bytes.NewReader(rd)
|
||||
}
|
||||
|
||||
head := make([]byte, segmentFooterLength)
|
||||
for eof > 0 {
|
||||
_, err = l.rd.ReadAt(head, eof-int64(segmentFooterLength))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := segmentReader{
|
||||
rd: l.rd,
|
||||
}
|
||||
s.head, err = ReadHead(head)
|
||||
s.head.end = eof - int64(s.head.headlen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
eof -= int64(s.head.datalen) + int64(s.head.headlen)
|
||||
l.segments = append(l.segments, s)
|
||||
}
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (l *logFile) Count() int64 {
|
||||
return int64(len(l.segments))
|
||||
}
|
||||
func (l *logFile) LoadSegment(pos int64) (*segmentBytes, error) {
|
||||
if pos < 0 {
|
||||
pos = int64(len(l.segments) - 1)
|
||||
}
|
||||
if pos > int64(len(l.segments)-1) {
|
||||
return nil, ErrDecode
|
||||
}
|
||||
s := l.segments[pos]
|
||||
|
||||
b := make([]byte, s.head.datalen+s.head.headlen)
|
||||
_, err := l.rd.ReadAt(b, s.head.end-int64(len(b)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &segmentBytes{b, -1}, nil
|
||||
}
|
||||
func (l *logFile) Find(needle []byte, first bool) (*entryBytes, bool, error) {
|
||||
var cur, last segmentReader
|
||||
|
||||
for _, s := range l.segments {
|
||||
cur = s
|
||||
e, err := cur.FirstEntry()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
k, _ := e.KeyValue()
|
||||
|
||||
if first && bytes.Equal(k, needle) {
|
||||
break
|
||||
}
|
||||
if first && bytes.Compare(k, needle) > 0 {
|
||||
e, ok, err := cur.Find(needle, first)
|
||||
if ok || err != nil{
|
||||
return e, ok, err
|
||||
}
|
||||
break
|
||||
}
|
||||
if !first && bytes.Compare(k, needle) > 0 {
|
||||
break
|
||||
}
|
||||
last = s
|
||||
}
|
||||
|
||||
e, ok, err := last.Find(needle, first)
|
||||
if ok || err != nil{
|
||||
return e, ok, err
|
||||
}
|
||||
// if by mistake it was not found in the last.. check the next segment.
|
||||
return cur.Find(needle, first)
|
||||
}
|
||||
func (l *logFile) WriteTo(w io.Writer) (int64, error) {
|
||||
return l.rd.WriteTo(w)
|
||||
}
|
||||
|
||||
type segmentBytes struct {
|
||||
b []byte
|
||||
pos int
|
||||
}
|
||||
|
||||
type dataset struct {
|
||||
rd io.ReaderAt
|
||||
files []logFile
|
||||
|
||||
fs.FS
|
||||
}
|
||||
|
||||
func ReadDataset(fd fs.FS) (*dataset, error) {
|
||||
panic("not implemented")
|
||||
}
|
327
lsm/sst_test.go
Normal file
327
lsm/sst_test.go
Normal file
|
@ -0,0 +1,327 @@
|
|||
// SPDX-FileCopyrightText: 2023 Jon Lundy <jon@xuu.cc>
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package lsm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
crand "crypto/rand"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"io/fs"
|
||||
"math/rand"
|
||||
"os"
|
||||
"sort"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/matryer/is"
|
||||
)
|
||||
|
||||
func TestLargeFile(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
segCount := 4098
|
||||
|
||||
f := randFile(t, 2_000_000, segCount)
|
||||
|
||||
sf, err := ReadFile(f)
|
||||
is.NoErr(err)
|
||||
|
||||
is.True(len(sf.segments) <= segCount)
|
||||
var needle []byte
|
||||
for i, s := range sf.segments {
|
||||
e, err := s.FirstEntry()
|
||||
is.NoErr(err)
|
||||
k, v := e.KeyValue()
|
||||
needle = k
|
||||
t.Logf("Segment-%d: %s = %d", i, k, v)
|
||||
}
|
||||
t.Log(f.Stat())
|
||||
|
||||
tt, ok, err := sf.Find(needle, true)
|
||||
is.NoErr(err)
|
||||
is.True(ok)
|
||||
key, val := tt.KeyValue()
|
||||
t.Log(string(key), val)
|
||||
|
||||
tt, ok, err = sf.Find([]byte("needle"), false)
|
||||
is.NoErr(err)
|
||||
is.True(!ok)
|
||||
key, val = tt.KeyValue()
|
||||
t.Log(string(key), val)
|
||||
|
||||
tt, ok, err = sf.Find([]byte{'\xff'}, false)
|
||||
is.NoErr(err)
|
||||
is.True(!ok)
|
||||
key, val = tt.KeyValue()
|
||||
t.Log(string(key), val)
|
||||
}
|
||||
|
||||
func TestLargeFileDisk(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
segCount := 4098
|
||||
|
||||
t.Log("generate large file")
|
||||
f := randFile(t, 2_000_000, segCount)
|
||||
|
||||
fd, err := os.CreateTemp("", "sst*")
|
||||
is.NoErr(err)
|
||||
defer func() { t.Log("cleanup:", fd.Name()); fd.Close(); os.Remove(fd.Name()) }()
|
||||
|
||||
t.Log("write file:", fd.Name())
|
||||
_, err = io.Copy(fd, f)
|
||||
is.NoErr(err)
|
||||
fd.Seek(0, 0)
|
||||
|
||||
sf, err := ReadFile(fd)
|
||||
is.NoErr(err)
|
||||
|
||||
is.True(len(sf.segments) <= segCount)
|
||||
var needle []byte
|
||||
for i, s := range sf.segments {
|
||||
e, err := s.FirstEntry()
|
||||
is.NoErr(err)
|
||||
k, v := e.KeyValue()
|
||||
needle = k
|
||||
|
||||
ok, err := s.VerifyHash()
|
||||
is.NoErr(err)
|
||||
|
||||
t.Logf("Segment-%d: %s = %d %t", i, k, v, ok)
|
||||
is.True(ok)
|
||||
}
|
||||
t.Log(f.Stat())
|
||||
|
||||
tt, ok, err := sf.Find(needle, false)
|
||||
is.NoErr(err)
|
||||
is.True(ok)
|
||||
key, val := tt.KeyValue()
|
||||
t.Log(string(key), val)
|
||||
|
||||
tt, ok, err = sf.Find([]byte("needle"), false)
|
||||
is.NoErr(err)
|
||||
is.True(!ok)
|
||||
key, val = tt.KeyValue()
|
||||
t.Log(string(key), val)
|
||||
|
||||
tt, ok, err = sf.Find([]byte{'\xff'}, false)
|
||||
is.NoErr(err)
|
||||
is.True(!ok)
|
||||
key, val = tt.KeyValue()
|
||||
t.Log(string(key), val)
|
||||
}
|
||||
|
||||
func BenchmarkLargeFile(b *testing.B) {
|
||||
segCount := 4098 / 4
|
||||
f := randFile(b, 2_000_000, segCount)
|
||||
|
||||
sf, err := ReadFile(f)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
key := make([]byte, 5)
|
||||
keys := make([][]byte, b.N)
|
||||
for i := range keys {
|
||||
_, err = crand.Read(key)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
keys[i] = []byte(base64.RawURLEncoding.EncodeToString(key))
|
||||
}
|
||||
b.Log("ready", b.N)
|
||||
b.ResetTimer()
|
||||
okays := 0
|
||||
each := b.N / 10
|
||||
for n := 0; n < b.N; n++ {
|
||||
if each > 0 && n%each == 0 {
|
||||
b.Log(n)
|
||||
}
|
||||
_, ok, err := sf.Find(keys[n], false)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
if ok {
|
||||
okays++
|
||||
}
|
||||
}
|
||||
b.Log("okays=", b.N, okays)
|
||||
}
|
||||
|
||||
// TestFindRange is an initial range find for start and stop of a range of needles.
|
||||
// TODO: start the second query from where the first left off. Use an iterator?
|
||||
func TestFindRange(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
f := basicFile(t,
|
||||
entries{
|
||||
{"AD", 5},
|
||||
{"AC", 5},
|
||||
{"AB", 4},
|
||||
{"AB", 3},
|
||||
},
|
||||
entries{
|
||||
{"AB", 2},
|
||||
{"AA", 1},
|
||||
},
|
||||
)
|
||||
sf, err := ReadFile(f)
|
||||
is.NoErr(err)
|
||||
|
||||
var ok bool
|
||||
var first, last *entryBytes
|
||||
|
||||
first, ok, err = sf.Find([]byte("AB"), true)
|
||||
is.NoErr(err)
|
||||
|
||||
key, val := first.KeyValue()
|
||||
t.Log(string(key), val)
|
||||
|
||||
is.True(ok)
|
||||
is.Equal(key, []byte("AB"))
|
||||
is.Equal(val, uint64(2))
|
||||
|
||||
last, ok, err = sf.Find([]byte("AB"), false)
|
||||
is.NoErr(err)
|
||||
|
||||
key, val = last.KeyValue()
|
||||
t.Log(string(key), val)
|
||||
|
||||
is.True(ok)
|
||||
is.Equal(key, []byte("AB"))
|
||||
is.Equal(val, uint64(4))
|
||||
|
||||
|
||||
last, ok, err = sf.Find([]byte("AC"), false)
|
||||
is.NoErr(err)
|
||||
|
||||
key, val = last.KeyValue()
|
||||
t.Log(string(key), val)
|
||||
|
||||
is.True(ok)
|
||||
is.Equal(key, []byte("AC"))
|
||||
is.Equal(val, uint64(5))
|
||||
}
|
||||
|
||||
func randFile(t interface {
|
||||
Helper()
|
||||
Error(...any)
|
||||
}, size int, segments int) fs.File {
|
||||
t.Helper()
|
||||
|
||||
lis := make(listEntries, size)
|
||||
for i := range lis {
|
||||
key := make([]byte, 5)
|
||||
_, err := crand.Read(key)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
key = []byte(base64.RawURLEncoding.EncodeToString(key))
|
||||
// key := []byte(fmt.Sprintf("key-%05d", i))
|
||||
|
||||
lis[i] = NewKeyValue(key, rand.Uint64()%16_777_216)
|
||||
}
|
||||
|
||||
sort.Sort(sort.Reverse(&lis))
|
||||
each := size / segments
|
||||
if size%segments != 0 {
|
||||
each++
|
||||
}
|
||||
split := make([]listEntries, segments)
|
||||
|
||||
for i := range split {
|
||||
if (i+1)*each > len(lis) {
|
||||
split[i] = lis[i*each : i*each+len(lis[i*each:])]
|
||||
split = split[:i+1]
|
||||
break
|
||||
}
|
||||
split[i] = lis[i*each : (i+1)*each]
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
for _, s := range split {
|
||||
s.WriteTo(&b)
|
||||
}
|
||||
|
||||
return NewFile(b.Bytes())
|
||||
}
|
||||
|
||||
type fakeStat struct {
|
||||
size int64
|
||||
}
|
||||
|
||||
// IsDir implements fs.FileInfo.
|
||||
func (*fakeStat) IsDir() bool {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// ModTime implements fs.FileInfo.
|
||||
func (*fakeStat) ModTime() time.Time {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// Mode implements fs.FileInfo.
|
||||
func (*fakeStat) Mode() fs.FileMode {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// Name implements fs.FileInfo.
|
||||
func (*fakeStat) Name() string {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// Size implements fs.FileInfo.
|
||||
func (s *fakeStat) Size() int64 {
|
||||
return s.size
|
||||
}
|
||||
|
||||
// Sys implements fs.FileInfo.
|
||||
func (*fakeStat) Sys() any {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
var _ fs.FileInfo = (*fakeStat)(nil)
|
||||
|
||||
type rd interface {
|
||||
io.ReaderAt
|
||||
io.Reader
|
||||
}
|
||||
type fakeFile struct {
|
||||
stat func() fs.FileInfo
|
||||
|
||||
rd
|
||||
}
|
||||
|
||||
func (fakeFile) Close() error { return nil }
|
||||
func (f fakeFile) Stat() (fs.FileInfo, error) { return f.stat(), nil }
|
||||
|
||||
func NewFile(b ...[]byte) fs.File {
|
||||
in := bytes.Join(b, nil)
|
||||
rd := bytes.NewReader(in)
|
||||
size := int64(len(in))
|
||||
return &fakeFile{stat: func() fs.FileInfo { return &fakeStat{size: size} }, rd: rd}
|
||||
}
|
||||
func NewFileFromReader(rd *bytes.Reader) fs.File {
|
||||
return &fakeFile{stat: func() fs.FileInfo { return &fakeStat{size: int64(rd.Len())} }, rd: rd}
|
||||
}
|
||||
|
||||
type fakeFS struct {
|
||||
files map[string]*fakeFile
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Open implements fs.FS.
|
||||
func (f *fakeFS) Open(name string) (fs.File, error) {
|
||||
f.mu.RLock()
|
||||
defer f.mu.RUnlock()
|
||||
|
||||
if file, ok := f.files[name]; ok {
|
||||
return file, nil
|
||||
}
|
||||
|
||||
return nil, fs.ErrNotExist
|
||||
}
|
||||
|
||||
var _ fs.FS = (*fakeFS)(nil)
|
157
mercury/app/app_test.go
Normal file
157
mercury/app/app_test.go
Normal 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)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// }
|
98
mercury/app/default-rules.go
Normal file
98
mercury/app/default-rules.go
Normal 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
295
mercury/app/environ.go
Normal 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
63
mercury/http/notify.go
Normal 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
727
mercury/mercury.go
Normal 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
27
mercury/mqtt/notify.go
Normal 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
127
mercury/parse.go
Normal 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
28
mercury/parse_test.go
Normal 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
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
47
mercury/public/index.html
Normal 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
218
mercury/public/style.css
Normal 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
420
mercury/registry.go
Normal 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 = ®istry{}
|
||||
|
||||
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
277
mercury/routes.go
Normal 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
202
mercury/spec.go
Normal 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
109
mercury/spec_test.go
Normal 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
118
mercury/sql/init-pg.sql
Normal 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
120
mercury/sql/init-sql3.sql
Normal 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
128
mercury/sql/list-string.go
Normal 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
55
mercury/sql/notify.go
Normal 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
44
mercury/sql/otel.go
Normal 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
99
mercury/sql/rules.go
Normal 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
594
mercury/sql/sql.go
Normal 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
|
||||
}
|
|
@ -8,6 +8,7 @@ type mux struct {
|
|||
*http.ServeMux
|
||||
api *http.ServeMux
|
||||
wellknown *http.ServeMux
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
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)
|
||||
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 {
|
||||
mux := &mux{
|
||||
api: http.NewServeMux(),
|
||||
wellknown: 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("/.well-known/", http.StripPrefix("/.well-known", mux.wellknown))
|
||||
mux.handler = mux.ServeMux
|
||||
|
||||
return mux
|
||||
}
|
||||
|
|
255
rsql/ast.go
Normal file
255
rsql/ast.go
Normal 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
21
rsql/ast_test.go
Normal 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
98
rsql/dbcolumns.go
Normal 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
258
rsql/lexer.go
Normal 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
105
rsql/lexer_test.go
Normal 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
285
rsql/parser.go
Normal 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
309
rsql/parser_test.go
Normal 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
300
rsql/squirrel/sqlizer.go
Normal 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
|
||||
}
|
141
rsql/squirrel/sqlizer_test.go
Normal file
141
rsql/squirrel/sqlizer_test.go
Normal 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
62
rsql/token.go
Normal 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
|
||||
}
|
|
@ -35,10 +35,11 @@ func (s *Harness) Setup(ctx context.Context, apps ...application) error {
|
|||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
s.onRunning = make(chan struct{})
|
||||
|
||||
// setup crontab
|
||||
c := cron.New(cron.DefaultGranularity)
|
||||
s.OnStart(c.Run)
|
||||
s.onRunning = make(chan struct{})
|
||||
s.crontab = c
|
||||
|
||||
var err error
|
||||
|
@ -113,7 +114,6 @@ func (s *Harness) Run(ctx context.Context, appName, version string) error {
|
|||
err := g.Wait()
|
||||
if err != nil {
|
||||
log.Printf("Shutdown due to error: %s", err)
|
||||
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"go.sour.is/pkg/math"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
type Set[T comparable] map[T]struct{}
|
||||
|
@ -33,6 +34,9 @@ func (s Set[T]) Delete(items ...T) Set[T] {
|
|||
}
|
||||
return s
|
||||
}
|
||||
func (s Set[T]) Values() []T {
|
||||
return maps.Keys(s)
|
||||
}
|
||||
|
||||
func (s Set[T]) Equal(e Set[T]) bool {
|
||||
for k := range s {
|
||||
|
|
30
xdg/xdg.go
30
xdg/xdg.go
|
@ -4,6 +4,7 @@
|
|||
package xdg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
@ -36,6 +37,9 @@ func setENV(name, value string) string {
|
|||
return literal(name)
|
||||
}
|
||||
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))
|
||||
for i, path := range paths {
|
||||
if strings.HasPrefix(path, "~") {
|
||||
|
@ -43,7 +47,17 @@ func Get(base, suffix string) string {
|
|||
}
|
||||
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 {
|
||||
|
@ -53,3 +67,17 @@ func getHome() string {
|
|||
}
|
||||
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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user