Compare commits
	
		
			1 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4181cd5fed | 
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -1,4 +0,0 @@
 | 
			
		||||
test.db
 | 
			
		||||
*.mercury
 | 
			
		||||
sour.is-mercury
 | 
			
		||||
.vscode/
 | 
			
		||||
@ -1,45 +0,0 @@
 | 
			
		||||
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,6 +127,9 @@ 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]
 | 
			
		||||
@ -145,9 +148,6 @@ 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 {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										39
									
								
								env/env.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										39
									
								
								env/env.go
									
									
									
									
										vendored
									
									
								
							@ -9,16 +9,35 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func Default(name, defaultValue string) (s string) {
 | 
			
		||||
func Default(name, defaultValue string) string {
 | 
			
		||||
	name = strings.TrimSpace(name)
 | 
			
		||||
	s = strings.TrimSpace(defaultValue)
 | 
			
		||||
 | 
			
		||||
	if v, ok := os.LookupEnv(name); ok {
 | 
			
		||||
		s = strings.TrimSpace(v)
 | 
			
		||||
		slog.Info("env", slog.String(name, v))
 | 
			
		||||
		return
 | 
			
		||||
	defaultValue = strings.TrimSpace(defaultValue)
 | 
			
		||||
	if v := strings.TrimSpace(os.Getenv(name)); v != "" {
 | 
			
		||||
		slog.Info("env", name, v)
 | 
			
		||||
		return v
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	slog.Info("env", slog.String(name, s+" (default)"))
 | 
			
		||||
	return
 | 
			
		||||
	slog.Info("env", name, defaultValue+" (default)")
 | 
			
		||||
	return defaultValue
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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) secret {
 | 
			
		||||
	name = strings.TrimSpace(name)
 | 
			
		||||
	defaultValue = strings.TrimSpace(defaultValue)
 | 
			
		||||
	if v := strings.TrimSpace(os.Getenv(name)); v != "" {
 | 
			
		||||
		slog.Info("env", name, secret(v))
 | 
			
		||||
		return secret(v)
 | 
			
		||||
	}
 | 
			
		||||
	slog.Info("env", name, secret(defaultValue).String()+" (default)")
 | 
			
		||||
	return secret(defaultValue)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										35
									
								
								env/secret.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										35
									
								
								env/secret.go
									
									
									
									
										vendored
									
									
								
							@ -1,35 +0,0 @@
 | 
			
		||||
package env
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type secret string
 | 
			
		||||
 | 
			
		||||
func (s secret) String() string {
 | 
			
		||||
	if s == "" {
 | 
			
		||||
		return "(nil)"
 | 
			
		||||
	}
 | 
			
		||||
	return "***"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s secret) Secret() string {
 | 
			
		||||
	return string(s)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Secret(name, defaultValue string) (s secret) {
 | 
			
		||||
	name = strings.TrimSpace(name)
 | 
			
		||||
	s = secret(strings.TrimSpace(defaultValue))
 | 
			
		||||
 | 
			
		||||
	if v, ok := os.LookupEnv(name); ok {
 | 
			
		||||
		s = secret(strings.TrimSpace(v))
 | 
			
		||||
		slog.Info("env", slog.String(name, s.String()))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	slog.Info("env", slog.String(name, s.String()+" (default)"))
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										92
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										92
									
								
								go.mod
									
									
									
									
									
								
							@ -1,81 +1,55 @@
 | 
			
		||||
module go.sour.is/pkg
 | 
			
		||||
 | 
			
		||||
go 1.23.1
 | 
			
		||||
go 1.21
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/99designs/gqlgen v0.17.44
 | 
			
		||||
	github.com/99designs/gqlgen v0.17.34
 | 
			
		||||
	github.com/golang-jwt/jwt/v4 v4.5.0
 | 
			
		||||
	github.com/gorilla/websocket v1.5.1
 | 
			
		||||
	github.com/gorilla/websocket v1.5.0
 | 
			
		||||
	github.com/matryer/is v1.4.1
 | 
			
		||||
	github.com/ravilushqa/otelgqlgen v0.15.0
 | 
			
		||||
	github.com/tursodatabase/go-libsql v0.0.0-20240322134723-08771dcdd2f1
 | 
			
		||||
	github.com/vektah/gqlparser/v2 v2.5.14
 | 
			
		||||
	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
 | 
			
		||||
	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
 | 
			
		||||
	go.uber.org/multierr v1.11.0
 | 
			
		||||
	golang.org/x/sync v0.6.0
 | 
			
		||||
	golang.org/x/sync v0.3.0
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/agnivade/levenshtein v1.1.1 // indirect
 | 
			
		||||
	github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 // indirect
 | 
			
		||||
	github.com/beorn7/perks v1.0.1 // indirect
 | 
			
		||||
	github.com/cespare/xxhash/v2 v2.2.0 // 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/mattn/go-isatty v0.0.20 // indirect
 | 
			
		||||
	github.com/felixge/httpsnoop v1.0.3 // indirect
 | 
			
		||||
	github.com/hashicorp/golang-lru/v2 v2.0.3 // indirect
 | 
			
		||||
	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
 | 
			
		||||
	github.com/mitchellh/mapstructure v1.5.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/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
 | 
			
		||||
	github.com/prometheus/common v0.44.0 // indirect
 | 
			
		||||
	github.com/prometheus/procfs v0.12.0 // 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
 | 
			
		||||
	go.opentelemetry.io/contrib v1.16.1 // 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
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
 | 
			
		||||
	github.com/go-logr/logr v1.4.1 // indirect
 | 
			
		||||
	github.com/go-logr/logr v1.2.4 // 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.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.61.1 // indirect
 | 
			
		||||
	google.golang.org/protobuf v1.33.0 // indirect
 | 
			
		||||
	modernc.org/sqlite v1.29.1
 | 
			
		||||
	github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
 | 
			
		||||
	github.com/prometheus/client_golang v1.17.0
 | 
			
		||||
	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
 | 
			
		||||
	golang.org/x/net v0.17.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.13.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.13.0 // indirect
 | 
			
		||||
	google.golang.org/grpc v1.58.0 // indirect
 | 
			
		||||
	google.golang.org/protobuf v1.31.0 // indirect
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										263
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										263
									
								
								go.sum
									
									
									
									
									
								
							@ -1,26 +1,13 @@
 | 
			
		||||
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/99designs/gqlgen v0.17.34 h1:5cS5/OKFguQt+Ws56uj9FlG2xm1IlcJWNF2jrMIKYFQ=
 | 
			
		||||
github.com/99designs/gqlgen v0.17.34/go.mod h1:Axcd3jIFHBVcqzixujJQr1wGqE+lGTpz6u4iZBZg1G8=
 | 
			
		||||
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=
 | 
			
		||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
 | 
			
		||||
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 h1:goHVqTbFX3AIo0tzGr14pgfAW2ZfPChKO21Z9MGf/gk=
 | 
			
		||||
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM=
 | 
			
		||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
 | 
			
		||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/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=
 | 
			
		||||
@ -30,216 +17,106 @@ 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/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
 | 
			
		||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
 | 
			
		||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 | 
			
		||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 | 
			
		||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 | 
			
		||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 | 
			
		||||
github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 | 
			
		||||
github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
 | 
			
		||||
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
 | 
			
		||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 | 
			
		||||
github.com/go-logr/logr v1.2.2/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/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/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/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/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE=
 | 
			
		||||
github.com/golang/protobuf v1.2.0/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.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/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/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/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/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/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.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/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/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
 | 
			
		||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
 | 
			
		||||
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/ravilushqa/otelgqlgen v0.13.1 h1:V+zFE75iDd2/CSzy5kKnb+Fi09SsE5535wv9U2nUEFE=
 | 
			
		||||
github.com/ravilushqa/otelgqlgen v0.13.1/go.mod h1:ZIyWykK2paCuNi9k8gk5edcNSwDJuxZaW90vZXpafxw=
 | 
			
		||||
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.11 h1:JJxLtXIoN7+3x6MBdtIP59TP1RANnY7pXOaDnADQSf8=
 | 
			
		||||
github.com/vektah/gqlparser/v2 v2.5.11/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc=
 | 
			
		||||
github.com/vektah/gqlparser/v2 v2.5.14 h1:dzLq75BJe03jjQm6n56PdH1oweB8ana42wj7E4jRy70=
 | 
			
		||||
github.com/vektah/gqlparser/v2 v2.5.14/go.mod h1:WQQjFc+I1YIzoPvZBhUQX7waZgg3pMLi0r8KymvAE2w=
 | 
			
		||||
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=
 | 
			
		||||
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=
 | 
			
		||||
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/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
			
		||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
 | 
			
		||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
 | 
			
		||||
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.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
 | 
			
		||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 | 
			
		||||
golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
 | 
			
		||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
 | 
			
		||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 | 
			
		||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 | 
			
		||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 | 
			
		||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 | 
			
		||||
golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
			
		||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
			
		||||
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
 | 
			
		||||
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
 | 
			
		||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
 | 
			
		||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
 | 
			
		||||
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/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
 | 
			
		||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
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/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
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/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/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.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
 | 
			
		||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 | 
			
		||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
 | 
			
		||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 | 
			
		||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/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=
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										189
									
								
								grug/math/math.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								grug/math/math.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,189 @@
 | 
			
		||||
// grug math is an unbounded precision math library for integers. It is not designed to be performant in any means. But as an example of how one works.
 | 
			
		||||
package math
 | 
			
		||||
 | 
			
		||||
import "strconv"
 | 
			
		||||
 | 
			
		||||
type Number []rune
 | 
			
		||||
 | 
			
		||||
func NewNumber() *Number {
 | 
			
		||||
	return &Number{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (*Number) FromString(s string) *Number {
 | 
			
		||||
	for _, a := range s {
 | 
			
		||||
		if !(a >= '0' && a <= '9') {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var n Number = []rune(s)
 | 
			
		||||
	
 | 
			
		||||
	i:=0
 | 
			
		||||
	for range n[:len(n)-1] {
 | 
			
		||||
		if n[i] == 0 || n[i] == '0' {
 | 
			
		||||
			i++
 | 
			
		||||
		} else {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	n = n[i:]
 | 
			
		||||
 | 
			
		||||
	return &n
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (*Number) FromInt(i int) *Number {
 | 
			
		||||
	s := strconv.Itoa(i)
 | 
			
		||||
	var n Number = []rune(s)
 | 
			
		||||
	return &n
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (n *Number) String() string {
 | 
			
		||||
	if n == nil || len(*n) == 0 {
 | 
			
		||||
		return "NaN"
 | 
			
		||||
	}
 | 
			
		||||
	return string(*n)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (n *Number) Add(a *Number) *Number {
 | 
			
		||||
	if n == nil || a == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lenN, lenA := len(*n), len(*a)
 | 
			
		||||
	sum := make(Number, max(lenN, lenA)+1)
 | 
			
		||||
 | 
			
		||||
	for i := range sum[:len(sum)-1] {
 | 
			
		||||
		ii := len(sum) - i - 1
 | 
			
		||||
 | 
			
		||||
		switch {
 | 
			
		||||
		case lenN == lenA:
 | 
			
		||||
			j := (*n)[lenN-i-1]
 | 
			
		||||
			k := (*a)[lenA-i-1]
 | 
			
		||||
 | 
			
		||||
			sum[ii-1], sum[ii] = add(j, k, sum[ii])
 | 
			
		||||
 | 
			
		||||
		case lenN > lenA:
 | 
			
		||||
			j := (*n)[lenN-i-1]
 | 
			
		||||
			k := '0'
 | 
			
		||||
			if i < lenA {
 | 
			
		||||
				k = (*a)[lenA-i-1]
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			sum[ii-1], sum[ii] = add(j, k, sum[ii])
 | 
			
		||||
 | 
			
		||||
		case lenN < lenA:
 | 
			
		||||
			j := '0'
 | 
			
		||||
			if i < lenN {
 | 
			
		||||
				j = (*n)[lenN-i-1]
 | 
			
		||||
			}
 | 
			
		||||
			k := (*a)[lenA-i-1]
 | 
			
		||||
 | 
			
		||||
			sum[ii-1], sum[ii] = add(j, k, sum[ii])
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Trim the extra 0 if present
 | 
			
		||||
	if sum[0] == 0 || sum[0] == '0' {
 | 
			
		||||
		sum = sum[1:]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &sum
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func (n *Number) Sub(s *Number) *Number {
 | 
			
		||||
	if n == nil || s == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lenN, lenA := len(*n), len(*s)
 | 
			
		||||
	sum := make(Number, max(lenN, lenA)+1)
 | 
			
		||||
 | 
			
		||||
	for i := range sum[:len(sum)-1] {
 | 
			
		||||
		ii := len(sum) - i - 1
 | 
			
		||||
 | 
			
		||||
		switch {
 | 
			
		||||
		case lenN == lenA:
 | 
			
		||||
			j := (*n)[lenN-i-1]
 | 
			
		||||
			k := (*s)[lenA-i-1]
 | 
			
		||||
			c := '0'
 | 
			
		||||
			if i+1 < lenN {
 | 
			
		||||
				c = (*n)[lenN-i-2]
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			sum[ii-1], sum[ii] = sub(j, k, c)
 | 
			
		||||
 | 
			
		||||
		case lenN > lenA:
 | 
			
		||||
			j := (*n)[lenN-i-1]
 | 
			
		||||
			k := '0'
 | 
			
		||||
			if i < lenA {
 | 
			
		||||
				k = (*s)[lenA-i-1]
 | 
			
		||||
			}
 | 
			
		||||
			c := '0'
 | 
			
		||||
			if i+1 < lenN {
 | 
			
		||||
				c = (*n)[lenN-i-2]
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			sum[ii-1], sum[ii] = sub(j, k, c)
 | 
			
		||||
			if i+1 < lenN {
 | 
			
		||||
				(*n)[lenN-i-2] =sum[ii-1]
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		case lenN < lenA:
 | 
			
		||||
			j := '0'
 | 
			
		||||
			if i < lenN {
 | 
			
		||||
				j = (*n)[lenN-i-1]
 | 
			
		||||
			}
 | 
			
		||||
			k := (*s)[lenA-i-1]
 | 
			
		||||
			c := '0'
 | 
			
		||||
			if i+1 < lenN {
 | 
			
		||||
				c = (*n)[lenN-i-2]
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			sum[ii-1], sum[ii] = sub(j, k, c)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Trim the extra 0 if present
 | 
			
		||||
	i:=0
 | 
			
		||||
	for range sum[:len(sum)-1] {
 | 
			
		||||
		if sum[i] == 0 || sum[i] == '0' {
 | 
			
		||||
			i++
 | 
			
		||||
		} else {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	sum = sum[i:]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	return &sum
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func friends(r rune) (int32, int32) {
 | 
			
		||||
	return 1, 10 - int32(r-'0')
 | 
			
		||||
}
 | 
			
		||||
func add(a, b, c rune) (rune, rune) {
 | 
			
		||||
	up, dn := friends(b)
 | 
			
		||||
	if c == 0 {
 | 
			
		||||
		c = '0'
 | 
			
		||||
	}
 | 
			
		||||
	a = a + c - '0'
 | 
			
		||||
 | 
			
		||||
	if a-dn < '0' {
 | 
			
		||||
		return '0', a + b - '0'
 | 
			
		||||
	}
 | 
			
		||||
	return c + up, a - dn
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func sub(a, b, c rune) (rune, rune) {
 | 
			
		||||
	dn, up := friends(b)
 | 
			
		||||
	if c == 0 {
 | 
			
		||||
		c = '0'
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if a+up > '9' {
 | 
			
		||||
		return c, a - b + '0'
 | 
			
		||||
	}
 | 
			
		||||
	return c - dn, a + up
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										74
									
								
								grug/math/math_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								grug/math/math_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,74 @@
 | 
			
		||||
package math_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/matryer/is"
 | 
			
		||||
	"go.sour.is/pkg/grug/math"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestNumber(t *testing.T) {
 | 
			
		||||
	is := is.New(t)
 | 
			
		||||
 | 
			
		||||
	n := math.NewNumber().FromInt(100)
 | 
			
		||||
	is.Equal(n.String(), "100")
 | 
			
		||||
 | 
			
		||||
	n = n.FromString("00001")
 | 
			
		||||
	is.Equal(n.String(), "1")
 | 
			
		||||
 | 
			
		||||
	n = n.FromString("1x0")
 | 
			
		||||
	is.True(n==nil)
 | 
			
		||||
	is.Equal(n.String(), "NaN")
 | 
			
		||||
 | 
			
		||||
	n = n.FromString("200")
 | 
			
		||||
	is.True(n!=nil)
 | 
			
		||||
	is.Equal(n.String(), "200")
 | 
			
		||||
 | 
			
		||||
	n = (&math.Number{}).FromString("300")
 | 
			
		||||
	is.True(n!=nil)
 | 
			
		||||
	is.Equal(n.String(), "300")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestAdd(t *testing.T) {
 | 
			
		||||
	is := is.New(t)
 | 
			
		||||
 | 
			
		||||
	n := math.NewNumber().FromString("100")
 | 
			
		||||
 | 
			
		||||
	n = n.Add(nil)
 | 
			
		||||
	is.Equal(n.String(), "NaN")
 | 
			
		||||
 | 
			
		||||
	n = n.FromInt(100)
 | 
			
		||||
	n = n.Add(n.FromString("900"))
 | 
			
		||||
	is.Equal(n.String(), "1000")
 | 
			
		||||
 | 
			
		||||
	n = n.Add(math.NewNumber())
 | 
			
		||||
	is.Equal(n.String(), "1000")
 | 
			
		||||
 | 
			
		||||
	n = n.Add(n.FromString("10"))
 | 
			
		||||
	is.Equal(n.String(), "1010")
 | 
			
		||||
 | 
			
		||||
	n = n.Add(n.FromString("10000"))
 | 
			
		||||
	is.Equal(n.String(), "11010")
 | 
			
		||||
 | 
			
		||||
	n = n.Add(n.FromString("9000"))
 | 
			
		||||
	is.Equal(n.String(), "20010")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestSub(t *testing.T) {
 | 
			
		||||
	is := is.New(t)
 | 
			
		||||
 | 
			
		||||
	n := math.NewNumber()
 | 
			
		||||
 | 
			
		||||
	// n = n.FromString("100")
 | 
			
		||||
	// n = n.Sub(n.FromInt(100))
 | 
			
		||||
	// is.Equal(n.String(), "0")
 | 
			
		||||
 | 
			
		||||
	// n = n.FromString("200")
 | 
			
		||||
	// n = n.Sub(n.FromInt(100))
 | 
			
		||||
	// is.Equal(n.String(), "100")
 | 
			
		||||
 | 
			
		||||
	n = n.FromString("100")
 | 
			
		||||
	n = n.Sub(n.FromInt(50))
 | 
			
		||||
	is.Equal(n.String(), "50")
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										115
									
								
								ident/ident.go
									
									
									
									
									
								
							
							
						
						
									
										115
									
								
								ident/ident.go
									
									
									
									
									
								
							@ -1,115 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
@ -1,75 +0,0 @@
 | 
			
		||||
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
									
									
									
									
									
								
							
							
						
						
									
										249
									
								
								ident/routes.go
									
									
									
									
									
								
							@ -1,249 +0,0 @@
 | 
			
		||||
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)
 | 
			
		||||
}
 | 
			
		||||
@ -1,275 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
@ -1,83 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@ -38,11 +38,9 @@ func initMetrics(ctx context.Context, name string) (context.Context, func() erro
 | 
			
		||||
	goversion := ""
 | 
			
		||||
	pkg := ""
 | 
			
		||||
	host := ""
 | 
			
		||||
	version := "0.0.1"
 | 
			
		||||
	if info, ok := debug.ReadBuildInfo(); ok {
 | 
			
		||||
		goversion = info.GoVersion
 | 
			
		||||
		pkg = info.Path
 | 
			
		||||
		version = info.Main.Version
 | 
			
		||||
	}
 | 
			
		||||
	if h, err := os.Hostname(); err == nil {
 | 
			
		||||
		host = h
 | 
			
		||||
@ -71,7 +69,7 @@ func initMetrics(ctx context.Context, name string) (context.Context, func() erro
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	meter := provider.Meter(name,
 | 
			
		||||
		api.WithInstrumentationVersion(version),
 | 
			
		||||
		api.WithInstrumentationVersion("0.0.1"),
 | 
			
		||||
		api.WithInstrumentationAttributes(
 | 
			
		||||
			attribute.String("app", name),
 | 
			
		||||
			attribute.String("host", host),
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										45
									
								
								lg/tracer.go
									
									
									
									
									
								
							
							
						
						
									
										45
									
								
								lg/tracer.go
									
									
									
									
									
								
							@ -58,56 +58,17 @@ 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))
 | 
			
		||||
	args := make([]any, len(attrs)*2)
 | 
			
		||||
 | 
			
		||||
	for i, a := range attrs {
 | 
			
		||||
		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())
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		args[2*i] = a.Key
 | 
			
		||||
		args[2*i+1] = a.Value
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	slog.Debug(name, args...)
 | 
			
		||||
 | 
			
		||||
@ -1,162 +0,0 @@
 | 
			
		||||
package libsqlembed
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"database/sql"
 | 
			
		||||
	"database/sql/driver"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/tursodatabase/go-libsql"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	sql.Register("libsql+embed", &db{conns: make(map[string]*connector)})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type db struct {
 | 
			
		||||
	conns map[string]*connector
 | 
			
		||||
	mu    sync.RWMutex
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type connector struct {
 | 
			
		||||
	*libsql.Connector
 | 
			
		||||
	dsn       string
 | 
			
		||||
	dir       string
 | 
			
		||||
	driver    *db
 | 
			
		||||
	removeDir bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ io.Closer = (*connector)(nil)
 | 
			
		||||
 | 
			
		||||
func (c *connector) Close() error {
 | 
			
		||||
	log.Println("closing db connection", c.dir)
 | 
			
		||||
	defer log.Println("closed db connection", c.dir)
 | 
			
		||||
 | 
			
		||||
	c.driver.mu.Lock()
 | 
			
		||||
	delete(c.driver.conns, c.dsn)
 | 
			
		||||
	c.driver.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	if c.removeDir {
 | 
			
		||||
		defer os.RemoveAll(c.dir)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Println("sync db")
 | 
			
		||||
	if err := c.Connector.Sync(); err != nil {
 | 
			
		||||
		return fmt.Errorf("syncing database: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return c.Connector.Close()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (db *db) OpenConnector(dsn string) (driver.Connector, error) {
 | 
			
		||||
	// log.Println("connector", dsn)
 | 
			
		||||
	if dsn == "" {
 | 
			
		||||
		return nil, fmt.Errorf("no dsn")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if c, ok := func() (*connector, bool) {
 | 
			
		||||
		db.mu.RLock()
 | 
			
		||||
		defer db.mu.RUnlock()
 | 
			
		||||
		c, ok := db.conns[dsn]
 | 
			
		||||
		return c, ok
 | 
			
		||||
	}(); ok {
 | 
			
		||||
		return c, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	db.mu.Lock()
 | 
			
		||||
	defer db.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	u, err := url.Parse(dsn)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var primary url.URL
 | 
			
		||||
	primary.Scheme = strings.TrimSuffix(u.Scheme, "+embed")
 | 
			
		||||
	primary.Host = u.Host
 | 
			
		||||
 | 
			
		||||
	dbname, _, _ := strings.Cut(u.Host, ".")
 | 
			
		||||
 | 
			
		||||
	authToken := u.Query().Get("authToken")
 | 
			
		||||
	if authToken == "" {
 | 
			
		||||
		return nil, fmt.Errorf("missing authToken")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	opts := []libsql.Option{
 | 
			
		||||
		libsql.WithAuthToken(authToken),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if refresh, err := strconv.ParseInt(u.Query().Get("refresh"), 10, 64); err == nil {
 | 
			
		||||
		log.Println("refresh: ", refresh)
 | 
			
		||||
		opts = append(opts, libsql.WithSyncInterval(time.Duration(refresh)*time.Minute))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if readWrite, err := strconv.ParseBool(u.Query().Get("readYourWrites")); err == nil {
 | 
			
		||||
		log.Println("read your writes: ", readWrite)
 | 
			
		||||
		opts = append(opts, libsql.WithReadYourWrites(readWrite))
 | 
			
		||||
	}
 | 
			
		||||
	if key := u.Query().Get("key"); key != "" {
 | 
			
		||||
		opts = append(opts, libsql.WithEncryption(key))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var dir string
 | 
			
		||||
	var removeDir bool
 | 
			
		||||
	if dir = u.Query().Get("store"); dir == "" {
 | 
			
		||||
		removeDir = true
 | 
			
		||||
		dir, err = os.MkdirTemp("", "libsql-*")
 | 
			
		||||
		log.Println("creating temporary directory:", dir)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("creating temporary directory: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		stat, err := os.Stat(dir)
 | 
			
		||||
		if errors.Is(err, os.ErrNotExist) {
 | 
			
		||||
			if err = os.MkdirAll(dir, 0700); err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			if !stat.IsDir() {
 | 
			
		||||
				return nil, fmt.Errorf("store not directory")
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dbPath := filepath.Join(dir, dbname)
 | 
			
		||||
 | 
			
		||||
	c, err := libsql.NewEmbeddedReplicaConnector(
 | 
			
		||||
		dbPath,
 | 
			
		||||
		primary.String(),
 | 
			
		||||
		opts...)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("creating connector: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Println("sync db")
 | 
			
		||||
	if err := c.Sync(); err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("syncing database: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	connector := &connector{c, dsn, dir, db, removeDir}
 | 
			
		||||
	db.conns[dsn] = connector
 | 
			
		||||
 | 
			
		||||
	return connector, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (db *db) Open(dsn string) (driver.Conn, error) {
 | 
			
		||||
	log.Println("open", dsn)
 | 
			
		||||
 | 
			
		||||
	c, err := db.OpenConnector(dsn)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return c.Connect(context.Background())
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										286
									
								
								lsm/cli/main.go
									
									
									
									
									
								
							
							
						
						
									
										286
									
								
								lsm/cli/main.go
									
									
									
									
									
								
							@ -1,286 +0,0 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"iter"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"os"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/docopt/docopt-go"
 | 
			
		||||
	"go.sour.is/pkg/lsm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var usage = `
 | 
			
		||||
Usage: 
 | 
			
		||||
  lsm create <archive> <files>...
 | 
			
		||||
  lsm append <archive> <files>...
 | 
			
		||||
  lsm read <archive> [<start> [<end>]]
 | 
			
		||||
  lsm serve <archive>
 | 
			
		||||
  lsm client <archive> [<start> [<end>]]`
 | 
			
		||||
 | 
			
		||||
type args struct {
 | 
			
		||||
	Create bool
 | 
			
		||||
	Append bool
 | 
			
		||||
	Read   bool
 | 
			
		||||
	Serve  bool
 | 
			
		||||
	Client bool
 | 
			
		||||
 | 
			
		||||
	Archive string   `docopt:"<archive>"`
 | 
			
		||||
	Files   []string `docopt:"<files>"`
 | 
			
		||||
	Start   int64    `docopt:"<start>"`
 | 
			
		||||
	End     int64    `docopt:"<end>"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	opts, err := docopt.ParseDoc(usage)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Println(err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	args := args{}
 | 
			
		||||
	err = opts.Bind(&args)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Println(err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	err = run(Console, args)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Println(err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type console struct {
 | 
			
		||||
	Stdin  io.Reader
 | 
			
		||||
	Stdout io.Writer
 | 
			
		||||
	Stderr io.Writer
 | 
			
		||||
}
 | 
			
		||||
var Console = console{os.Stdin, os.Stdout, os.Stderr}
 | 
			
		||||
 | 
			
		||||
func (c console) Write(b []byte) (int, error) {
 | 
			
		||||
	return c.Stdout.Write(b)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func run(console console, a args) error {
 | 
			
		||||
	fmt.Fprintln(console, "lsm")
 | 
			
		||||
	switch {
 | 
			
		||||
	case a.Create:
 | 
			
		||||
		f, err := os.OpenFile(a.Archive, os.O_CREATE|os.O_WRONLY, 0644)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		defer f.Close()
 | 
			
		||||
 | 
			
		||||
		return lsm.WriteLogFile(f, fileReaders(a.Files))
 | 
			
		||||
	case a.Append:
 | 
			
		||||
		f, err := os.OpenFile(a.Archive, os.O_RDWR, 0644)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		defer f.Close()
 | 
			
		||||
 | 
			
		||||
		return lsm.AppendLogFile(f, fileReaders(a.Files))
 | 
			
		||||
	case a.Read:
 | 
			
		||||
		fmt.Fprintln(console, "reading", a.Archive)
 | 
			
		||||
 | 
			
		||||
		f, err := os.Open(a.Archive)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		defer f.Close()
 | 
			
		||||
		return readContent(f, console, a.Start, a.End)
 | 
			
		||||
	case a.Serve:
 | 
			
		||||
		fmt.Fprintln(console, "serving", a.Archive)
 | 
			
		||||
		b, err := base64.RawStdEncoding.DecodeString(a.Archive)
 | 
			
		||||
		now := time.Now()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
			http.ServeContent(w, r, "", now, bytes.NewReader(b))
 | 
			
		||||
		})
 | 
			
		||||
		return http.ListenAndServe(":8080", nil)
 | 
			
		||||
	case a.Client:
 | 
			
		||||
		r, err := OpenHttpReader(context.Background(), a.Archive, 0)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		defer r.Close()
 | 
			
		||||
		defer func() {fmt.Println("bytes read", r.bytesRead)}()
 | 
			
		||||
		return readContent(r, console, a.Start, a.End)
 | 
			
		||||
	}
 | 
			
		||||
	return errors.New("unknown command")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func readContent(r io.ReaderAt, console console, start, end int64) error {
 | 
			
		||||
	lg, err := lsm.ReadLogFile(r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	for bi, rd := range lg.Iter(uint64(start)) {
 | 
			
		||||
		if end > 0 && int64(bi.Index) >= end {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		fmt.Fprintf(console, "=========================\n%+v:\n", bi)
 | 
			
		||||
		wr := base64.NewEncoder(base64.RawStdEncoding, console)
 | 
			
		||||
		io.Copy(wr, rd)
 | 
			
		||||
		fmt.Fprintln(console, "\n=========================")
 | 
			
		||||
	}
 | 
			
		||||
	if lg.Err != nil {
 | 
			
		||||
		return lg.Err
 | 
			
		||||
	}
 | 
			
		||||
	for bi, rd := range lg.Rev(lg.Count()) {
 | 
			
		||||
		if end > 0 && int64(bi.Index) >= end {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		fmt.Fprintf(console, "=========================\n%+v:\n", bi)
 | 
			
		||||
		wr := base64.NewEncoder(base64.RawStdEncoding, console)
 | 
			
		||||
		io.Copy(wr, rd)
 | 
			
		||||
		fmt.Fprintln(console, "\n=========================")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return lg.Err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func fileReaders(names []string) iter.Seq[io.Reader] {
 | 
			
		||||
	return iter.Seq[io.Reader](func(yield func(io.Reader) bool) {
 | 
			
		||||
		for _, name := range names {
 | 
			
		||||
			f, err := os.Open(name)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			if !yield(f) {
 | 
			
		||||
				f.Close()
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			f.Close()
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type HttpReader struct {
 | 
			
		||||
	ctx     context.Context
 | 
			
		||||
	uri     url.URL
 | 
			
		||||
	tmpfile *os.File
 | 
			
		||||
	pos     int64
 | 
			
		||||
	end     int64
 | 
			
		||||
	bytesRead int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func OpenHttpReader(ctx context.Context, uri string, end int64) (*HttpReader, error) {
 | 
			
		||||
	u, err := url.Parse(uri)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return &HttpReader{ctx: ctx, uri: *u, end: end}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *HttpReader) Read(p []byte) (int, error) {
 | 
			
		||||
	n, err := r.ReadAt(p, r.pos)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return n, err
 | 
			
		||||
	}
 | 
			
		||||
	r.pos += int64(n)
 | 
			
		||||
	r.bytesRead += n
 | 
			
		||||
	return n, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *HttpReader) Seek(offset int64, whence int) (int64, error) {
 | 
			
		||||
	switch whence {
 | 
			
		||||
	case io.SeekStart:
 | 
			
		||||
		r.pos = offset
 | 
			
		||||
	case io.SeekCurrent:
 | 
			
		||||
		r.pos += offset
 | 
			
		||||
	case io.SeekEnd:
 | 
			
		||||
		r.pos = r.end + offset
 | 
			
		||||
	}
 | 
			
		||||
	return r.pos, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *HttpReader) Close() error {
 | 
			
		||||
	r.ctx.Done()
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ReadAt implements io.ReaderAt. It reads data from the internal buffer starting
 | 
			
		||||
// from the specified offset and writes it into the provided data slice. If the
 | 
			
		||||
// offset is negative, it returns an error. If the requested read extends beyond
 | 
			
		||||
// the buffer's length, it returns the data read so far along with an io.EOF error.
 | 
			
		||||
func (r *HttpReader) ReadAt(data []byte, offset int64) (int, error) {
 | 
			
		||||
	if err := r.ctx.Err(); err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if offset < 0 {
 | 
			
		||||
		return 0, errors.New("negative offset")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if r.end > 0 && offset > r.end {
 | 
			
		||||
		return 0, io.EOF
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dlen := len(data) + int(offset)
 | 
			
		||||
 | 
			
		||||
	if r.end > 0 && r.end+int64(dlen) > r.end {
 | 
			
		||||
		dlen = int(r.end)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	end := ""
 | 
			
		||||
	if r.end > 0 {
 | 
			
		||||
		end = fmt.Sprintf("/%d", r.end)
 | 
			
		||||
	}
 | 
			
		||||
	req, err := http.NewRequestWithContext(r.ctx, "GET", r.uri.String(), nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d%s", offset, dlen, end))
 | 
			
		||||
 | 
			
		||||
	fmt.Fprintln(Console.Stderr, req)
 | 
			
		||||
 | 
			
		||||
	resp, err := http.DefaultClient.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	if resp.StatusCode == http.StatusRequestedRangeNotSatisfiable {
 | 
			
		||||
		fmt.Fprintln(Console.Stderr, "requested range not satisfiable")
 | 
			
		||||
		return 0, io.EOF
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if resp.StatusCode == http.StatusOK {
 | 
			
		||||
 | 
			
		||||
		r.tmpfile, err = os.CreateTemp("", "httpReader")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
		defer os.Remove(r.tmpfile.Name())
 | 
			
		||||
		n, err := io.Copy(r.tmpfile, resp.Body)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
		r.bytesRead += int(n)
 | 
			
		||||
 | 
			
		||||
		defer fmt.Fprintln(Console.Stderr, "wrote ", n, " bytes to ", r.tmpfile.Name())
 | 
			
		||||
		resp.Body.Close()
 | 
			
		||||
		r.tmpfile.Seek(offset, 0)
 | 
			
		||||
		return io.ReadFull(r.tmpfile, data)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	n, err := io.ReadFull(resp.Body, data)
 | 
			
		||||
	if n == 0 && err != nil {
 | 
			
		||||
		return n, err
 | 
			
		||||
	}
 | 
			
		||||
	r.bytesRead += n
 | 
			
		||||
	defer fmt.Fprintln(Console.Stderr, "read ", n, " bytes")
 | 
			
		||||
	return n, nil
 | 
			
		||||
}
 | 
			
		||||
@ -1,104 +0,0 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"os"
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestCreate(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name       string
 | 
			
		||||
		args       args
 | 
			
		||||
		wantErr    bool
 | 
			
		||||
		wantOutput string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "no input files",
 | 
			
		||||
			args: args{
 | 
			
		||||
				Create:  true,
 | 
			
		||||
				Archive: "test.txt",
 | 
			
		||||
				Files:   []string{},
 | 
			
		||||
			},
 | 
			
		||||
			wantErr:    false,
 | 
			
		||||
			wantOutput: "creating test.txt from []\nwrote 0 files\n",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "one input file",
 | 
			
		||||
			args: args{
 | 
			
		||||
				Create:  true,
 | 
			
		||||
				Archive: "test.txt",
 | 
			
		||||
				Files:   []string{"test_input.txt"},
 | 
			
		||||
			},
 | 
			
		||||
			wantErr:    false,
 | 
			
		||||
			wantOutput: "creating test.txt from [test_input.txt]\nwrote 1 files\n",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "multiple input files",
 | 
			
		||||
			args: args{
 | 
			
		||||
				Create:  true,
 | 
			
		||||
				Archive: "test.txt",
 | 
			
		||||
				Files:   []string{"test_input1.txt", "test_input2.txt"},
 | 
			
		||||
			},
 | 
			
		||||
			wantErr:    false,
 | 
			
		||||
			wantOutput: "creating test.txt from [test_input1.txt test_input2.txt]\nwrote 2 files\n",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "non-existent input files",
 | 
			
		||||
			args: args{
 | 
			
		||||
				Create:  true,
 | 
			
		||||
				Archive: "test.txt",
 | 
			
		||||
				Files:   []string{"non_existent_file.txt"},
 | 
			
		||||
			}, wantErr: false,
 | 
			
		||||
			wantOutput: "creating test.txt from [non_existent_file.txt]\nwrote 0 files\n",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "invalid command",
 | 
			
		||||
			args: args{
 | 
			
		||||
				Create:  false,
 | 
			
		||||
				Archive: "test.txt",
 | 
			
		||||
				Files:   []string{},
 | 
			
		||||
			},
 | 
			
		||||
			wantErr:    true,
 | 
			
		||||
			wantOutput: "",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range tests {
 | 
			
		||||
		t.Run(tc.name, func(t *testing.T) {
 | 
			
		||||
			// Create a temporary directory for the input files
 | 
			
		||||
			tmpDir, err := os.MkdirTemp("", "lsm2-cli-test")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
			defer os.RemoveAll(tmpDir)
 | 
			
		||||
			os.Chdir(tmpDir)
 | 
			
		||||
 | 
			
		||||
			// Create the input files
 | 
			
		||||
			for _, file := range tc.args.Files {
 | 
			
		||||
				if file == "non_existent_file.txt" {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				if err := os.WriteFile(file, []byte(file), 0o644); err != nil {
 | 
			
		||||
					t.Fatal(err)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Create a buffer to capture the output
 | 
			
		||||
			var output bytes.Buffer
 | 
			
		||||
 | 
			
		||||
			// Call the create function
 | 
			
		||||
			err = run(console{Stdout: &output}, tc.args)
 | 
			
		||||
 | 
			
		||||
			// Check the output
 | 
			
		||||
			if output.String() != tc.wantOutput {
 | 
			
		||||
				t.Errorf("run() output = %q, want %q", output.String(), tc.wantOutput)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Check for errors
 | 
			
		||||
			if tc.wantErr && err == nil {
 | 
			
		||||
				t.Errorf("run() did not return an error")
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										634
									
								
								lsm/sst.go
									
									
									
									
									
								
							
							
						
						
									
										634
									
								
								lsm/sst.go
									
									
									
									
									
								
							@ -1,634 +0,0 @@
 | 
			
		||||
package lsm
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/binary"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"hash/fnv"
 | 
			
		||||
	"io"
 | 
			
		||||
	"iter"
 | 
			
		||||
	"slices"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// [Sour.is|size] [size|hash][data][hash|flag|size]... [prev|count|flag|size]
 | 
			
		||||
 | 
			
		||||
// Commit1: [magic>|<end]{10} ... [<count][<size][<flag]{3..30}
 | 
			
		||||
//                 +---------|--------------------------------> end = seek to end of file
 | 
			
		||||
//                       <---|-------------+                    size = seek to magic header
 | 
			
		||||
//                       <---|-------------+10                  size + 10 = seek to start of file
 | 
			
		||||
//          <-----------------------------T+10----------------> 10 + size + trailer = full file size
 | 
			
		||||
 | 
			
		||||
// Commit2: [magic>|<end]{10} ... [<count][<size][<flag]{3..30} ... [<prev][<count][<size][<flag]{4..40}
 | 
			
		||||
//                           <---|---------+
 | 
			
		||||
//                           <-------------+T----------------->
 | 
			
		||||
//                  +--------|------------------------------------------------------------------------->
 | 
			
		||||
//                           <-------------------------------------|----------------+
 | 
			
		||||
//     prev = seek to last commit                              <---|-+
 | 
			
		||||
//     prev + trailer = size of commit                         <----T+--------------------------------->
 | 
			
		||||
 | 
			
		||||
// Block:  [hash>|<end]{10} ... [<size][<flag]{2..20}
 | 
			
		||||
//               +---------|------------------------>  end = seek to end of block
 | 
			
		||||
//                         <---|-+                     size = seek to end of header
 | 
			
		||||
//         <-------------------|-+10                   size + 10 = seek to start of block
 | 
			
		||||
//         <---------------------T+10--------------->  size + 10 + trailer = full block size
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	TypeUnknown uint64 = iota
 | 
			
		||||
	TypeSegment
 | 
			
		||||
	TypeCommit
 | 
			
		||||
	TypePrevCommit
 | 
			
		||||
 | 
			
		||||
	headerSize = 10
 | 
			
		||||
 | 
			
		||||
	maxCommitSize = 4 * binary.MaxVarintLen64
 | 
			
		||||
	minCommitSize = 3
 | 
			
		||||
 | 
			
		||||
	maxBlockSize = 2 * binary.MaxVarintLen64
 | 
			
		||||
	minBlockSize = 2
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	Magic   = [10]byte([]byte("Sour.is\x00\x00\x00"))
 | 
			
		||||
	Version = uint8(1)
 | 
			
		||||
	hash    = fnv.New64a
 | 
			
		||||
 | 
			
		||||
	ErrDecode = errors.New("decode")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type header struct {
 | 
			
		||||
	end   uint64
 | 
			
		||||
	extra []byte
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
 | 
			
		||||
// It decodes the input binary data into the header struct.
 | 
			
		||||
// The function expects the input data to be of a specific size (headerSize),
 | 
			
		||||
// otherwise it returns an error indicating bad data.
 | 
			
		||||
// It reads the 'end' field from the binary data, updates the 'extra' field,
 | 
			
		||||
// and reverses the byte order of 'extra' in place.
 | 
			
		||||
func (h *header) UnmarshalBinary(data []byte) error {
 | 
			
		||||
	if len(data) != headerSize {
 | 
			
		||||
		return fmt.Errorf("%w: bad data", ErrDecode)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.extra = make([]byte, headerSize)
 | 
			
		||||
	copy(h.extra, data)
 | 
			
		||||
 | 
			
		||||
	var bytesRead int
 | 
			
		||||
	h.end, bytesRead = binary.Uvarint(h.extra)
 | 
			
		||||
	reverse(h.extra)
 | 
			
		||||
	h.extra = h.extra[:min(8,headerSize-bytesRead)]
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Commit struct {
 | 
			
		||||
	flag  uint64 // flag values
 | 
			
		||||
	size  uint64 // size of the trailer
 | 
			
		||||
	count uint64 // number of entries
 | 
			
		||||
	prev  uint64 // previous commit
 | 
			
		||||
 | 
			
		||||
	tsize int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Append marshals the trailer into binary form and appends it to data.
 | 
			
		||||
// It returns the new slice.
 | 
			
		||||
func (h *Commit) AppendTrailer(data []byte) []byte {
 | 
			
		||||
	h.flag |= TypeCommit
 | 
			
		||||
	// if h.prev > 0 {
 | 
			
		||||
	// 	h.flag |= TypePrevCommit
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	size := len(data)
 | 
			
		||||
	data = binary.AppendUvarint(data, h.size)
 | 
			
		||||
	data = binary.AppendUvarint(data, h.flag)
 | 
			
		||||
	data = binary.AppendUvarint(data, h.count)
 | 
			
		||||
	// if h.prev > 0 {
 | 
			
		||||
	// 	data = binary.AppendUvarint(data, h.prev)
 | 
			
		||||
	// }
 | 
			
		||||
	reverse(data[size:])
 | 
			
		||||
 | 
			
		||||
	return data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
 | 
			
		||||
// It reads a trailer from binary data, and sets the fields
 | 
			
		||||
// of the receiver to the values found in the header.
 | 
			
		||||
func (h *Commit) UnmarshalBinary(data []byte) error {
 | 
			
		||||
	if len(data) < minCommitSize {
 | 
			
		||||
		return fmt.Errorf("%w: bad data", ErrDecode)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var n int
 | 
			
		||||
	h.size, n = binary.Uvarint(data)
 | 
			
		||||
	data = data[n:]
 | 
			
		||||
	h.tsize += n
 | 
			
		||||
 | 
			
		||||
	h.flag, n = binary.Uvarint(data)
 | 
			
		||||
	data = data[n:]
 | 
			
		||||
	h.tsize += n
 | 
			
		||||
 | 
			
		||||
	h.count, n = binary.Uvarint(data)
 | 
			
		||||
	data = data[n:]
 | 
			
		||||
	h.tsize += n
 | 
			
		||||
 | 
			
		||||
	// h.prev = h.size
 | 
			
		||||
	if h.flag&TypePrevCommit == TypePrevCommit {
 | 
			
		||||
		h.prev, n = binary.Uvarint(data)
 | 
			
		||||
		h.tsize += n
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Block struct {
 | 
			
		||||
	header
 | 
			
		||||
 | 
			
		||||
	size uint64
 | 
			
		||||
	flag uint64
 | 
			
		||||
 | 
			
		||||
	tsize int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *Block) AppendHeader(data []byte) []byte {
 | 
			
		||||
	size := len(data)
 | 
			
		||||
	data = append(data, make([]byte, 10)...)
 | 
			
		||||
	copy(data, h.extra)
 | 
			
		||||
	if h.size == 0 {
 | 
			
		||||
		return data
 | 
			
		||||
	}
 | 
			
		||||
	hdata := binary.AppendUvarint(make([]byte, 0, 10), h.end)
 | 
			
		||||
	reverse(hdata)
 | 
			
		||||
	copy(data[size+10-len(hdata):], hdata)
 | 
			
		||||
 | 
			
		||||
	return data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AppendTrailer marshals the footer into binary form and appends it to data.
 | 
			
		||||
// It returns the new slice.
 | 
			
		||||
func (h *Block) AppendTrailer(data []byte) []byte {
 | 
			
		||||
	size := len(data)
 | 
			
		||||
 | 
			
		||||
	h.flag |= TypeSegment
 | 
			
		||||
	data = binary.AppendUvarint(data, h.size)
 | 
			
		||||
	data = binary.AppendUvarint(data, h.flag)
 | 
			
		||||
	reverse(data[size:])
 | 
			
		||||
 | 
			
		||||
	return data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
 | 
			
		||||
// It reads a footer from binary data, and sets the fields
 | 
			
		||||
// of the receiver to the values found in the footer.
 | 
			
		||||
func (h *Block) UnmarshalBinary(data []byte) error {
 | 
			
		||||
	if len(data) < minBlockSize {
 | 
			
		||||
		return fmt.Errorf("%w: bad data", ErrDecode)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var n int
 | 
			
		||||
	h.size, n = binary.Uvarint(data)
 | 
			
		||||
	data = data[n:]
 | 
			
		||||
	h.tsize += n
 | 
			
		||||
 | 
			
		||||
	h.flag, n = binary.Uvarint(data)
 | 
			
		||||
	h.tsize += n
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type logFile struct {
 | 
			
		||||
	header
 | 
			
		||||
	Commit
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *logFile) AppendMagic(data []byte) []byte {
 | 
			
		||||
	size := len(data)
 | 
			
		||||
	data = append(data, Magic[:]...)
 | 
			
		||||
	if h.end == 0 {
 | 
			
		||||
		return data
 | 
			
		||||
	}
 | 
			
		||||
	hdata := binary.AppendUvarint(make([]byte, 0, 10), h.end)
 | 
			
		||||
	reverse(hdata)
 | 
			
		||||
	copy(data[size+10-len(hdata):], hdata)
 | 
			
		||||
 | 
			
		||||
	return data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WriteLogFile writes a log file to w, given a list of segments.
 | 
			
		||||
// The caller is responsible for calling WriteAt on the correct offset.
 | 
			
		||||
// The function will return an error if any of the segments fail to write.
 | 
			
		||||
// The offset is the initial offset of the first segment, and will be
 | 
			
		||||
// incremented by the length of the segment on each write.
 | 
			
		||||
//
 | 
			
		||||
// The log file is written with the following format:
 | 
			
		||||
//   - A header with the magic, version, and flag (Dirty)
 | 
			
		||||
//   - A series of segments, each with:
 | 
			
		||||
//   - A footer with the length and hash of the segment
 | 
			
		||||
//   - The contents of the segment
 | 
			
		||||
//   - A header with the magic, version, flag (Clean), and end offset
 | 
			
		||||
func WriteLogFile(w io.WriterAt, segments iter.Seq[io.Reader]) error {
 | 
			
		||||
	_, err := w.WriteAt(Magic[:], 0)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lf := &LogWriter{
 | 
			
		||||
		WriterAt: w,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return lf.writeIter(segments)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type rw interface {
 | 
			
		||||
	io.ReaderAt
 | 
			
		||||
	io.WriterAt
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func AppendLogFile(rw rw, segments iter.Seq[io.Reader]) error {
 | 
			
		||||
	logFile, err := ReadLogFile(rw)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	lf := &LogWriter{
 | 
			
		||||
		WriterAt: rw,
 | 
			
		||||
		logFile:  logFile.logFile,
 | 
			
		||||
	}
 | 
			
		||||
	return lf.writeIter(segments)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (lf *LogWriter) writeIter(segments iter.Seq[io.Reader]) error {
 | 
			
		||||
	lf.size = 0
 | 
			
		||||
	for s := range segments {
 | 
			
		||||
		n, err := lf.writeBlock(s)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		lf.end += n
 | 
			
		||||
		lf.size += n
 | 
			
		||||
		lf.count++
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Write the footer to the log file.
 | 
			
		||||
	// The footer is written at the current end of file position.
 | 
			
		||||
	n, err := lf.WriteAt(lf.AppendTrailer(make([]byte, 0, maxCommitSize)), int64(lf.end)+10)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		// If there is an error, return it.
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	lf.end += uint64(n)
 | 
			
		||||
 | 
			
		||||
	_, err = lf.WriteAt(lf.AppendMagic(make([]byte, 0, 10)), 0)
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type LogWriter struct {
 | 
			
		||||
	logFile
 | 
			
		||||
	io.WriterAt
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// writeBlock writes a segment to the log file at the current end of file position.
 | 
			
		||||
// The segment is written in chunks of 1024 bytes, and the hash of the segment
 | 
			
		||||
func (lf *LogWriter) writeBlock(segment io.Reader) (uint64, error) {
 | 
			
		||||
	h := hash()
 | 
			
		||||
	block := Block{}
 | 
			
		||||
 | 
			
		||||
	start := int64(lf.end) + 10
 | 
			
		||||
	end := start
 | 
			
		||||
 | 
			
		||||
	bytesWritten := 0
 | 
			
		||||
 | 
			
		||||
	// Write the header to the log file.
 | 
			
		||||
	// The footer is written at the current end of file position.
 | 
			
		||||
	n, err := lf.WriteAt(make([]byte, headerSize), start)
 | 
			
		||||
	bytesWritten += n
 | 
			
		||||
	end += int64(n)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		// If there is an error, return it.
 | 
			
		||||
		return uint64(bytesWritten), err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Write the segment to the log file.
 | 
			
		||||
	// The segment is written in chunks of 1024 bytes.
 | 
			
		||||
	for {
 | 
			
		||||
		// Read a chunk of the segment.
 | 
			
		||||
		buf := make([]byte, 1024)
 | 
			
		||||
		n, err := segment.Read(buf)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			// If the segment is empty, break the loop.
 | 
			
		||||
			if err == io.EOF {
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
			// If there is an error, return it.
 | 
			
		||||
			return uint64(bytesWritten), err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Compute the hash of the chunk.
 | 
			
		||||
		h.Write(buf[:n])
 | 
			
		||||
 | 
			
		||||
		// Write the chunk to the log file.
 | 
			
		||||
		// The chunk is written at the current end of file position.
 | 
			
		||||
		n, err = lf.WriteAt(buf[:n], end)
 | 
			
		||||
		bytesWritten += n
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			// If there is an error, return it.
 | 
			
		||||
			return uint64(bytesWritten), err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Update the length of the segment.
 | 
			
		||||
		end += int64(n)
 | 
			
		||||
		block.size += uint64(n)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	block.extra = h.Sum(nil)
 | 
			
		||||
	block.end += block.size
 | 
			
		||||
 | 
			
		||||
	// Write the footer to the log file.
 | 
			
		||||
	// The footer is written at the current end of file position.
 | 
			
		||||
	n, err = lf.WriteAt(block.AppendTrailer(make([]byte, 0, maxBlockSize)), end)
 | 
			
		||||
	bytesWritten += n
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		// If there is an error, return it.
 | 
			
		||||
		return uint64(bytesWritten), err
 | 
			
		||||
	}
 | 
			
		||||
	end += int64(n)
 | 
			
		||||
	block.end += uint64(n)
 | 
			
		||||
 | 
			
		||||
	// Update header to the log file.
 | 
			
		||||
	// The footer is written at the current end of file position.
 | 
			
		||||
	_, err = lf.WriteAt(block.AppendHeader(make([]byte, 0, headerSize)), start)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		// If there is an error, return it.
 | 
			
		||||
		return uint64(bytesWritten), err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return uint64(bytesWritten), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// reverse reverses a slice in-place.
 | 
			
		||||
func reverse[T any](b []T) {
 | 
			
		||||
	l := len(b)
 | 
			
		||||
	for i := 0; i < l/2; i++ {
 | 
			
		||||
		b[i], b[l-i-1] = b[l-i-1], b[i]
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type LogReader struct {
 | 
			
		||||
	logFile
 | 
			
		||||
	io.ReaderAt
 | 
			
		||||
	Err error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ReadLogFile reads a log file from the given io.ReaderAt. It returns a pointer to a LogFile, or an error if the file
 | 
			
		||||
// could not be read.
 | 
			
		||||
func ReadLogFile(reader io.ReaderAt) (*LogReader, error) {
 | 
			
		||||
	header := make([]byte, headerSize)
 | 
			
		||||
	n, err := rsr(reader, 0, 10).ReadAt(header, 0)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	header = header[:n]
 | 
			
		||||
 | 
			
		||||
	logFile := &LogReader{ReaderAt: reader}
 | 
			
		||||
	err = logFile.header.UnmarshalBinary(header)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if logFile.end == 0 {
 | 
			
		||||
		return logFile, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	commit := make([]byte, maxCommitSize)
 | 
			
		||||
	n, err = rsr(reader, 10, int64(logFile.end)).ReadAt(commit, 0)
 | 
			
		||||
	if n == 0 && err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	commit = commit[:n]
 | 
			
		||||
 | 
			
		||||
	err = logFile.Commit.UnmarshalBinary(commit)
 | 
			
		||||
 | 
			
		||||
	return logFile, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Iterate reads the log file and calls the given function for each segment.
 | 
			
		||||
// It passes an io.Reader that reads from the current segment. It will stop
 | 
			
		||||
// calling the function if the function returns false.
 | 
			
		||||
func (lf *LogReader) Iter(begin uint64) iter.Seq2[blockInfo, io.Reader] {
 | 
			
		||||
	var commits []*Commit
 | 
			
		||||
	for commit := range lf.iterCommits() {
 | 
			
		||||
		commits = append(commits, &commit)
 | 
			
		||||
	}
 | 
			
		||||
	if lf.Err != nil {
 | 
			
		||||
		return func(yield func(blockInfo, io.Reader) bool) {}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	reverse(commits)
 | 
			
		||||
 | 
			
		||||
	return func(yield func(blockInfo, io.Reader) bool) {
 | 
			
		||||
		start := int64(10)
 | 
			
		||||
		var adj uint64
 | 
			
		||||
		for _, commit := range commits {
 | 
			
		||||
			size := int64(commit.size)
 | 
			
		||||
			it := iterBlocks(io.NewSectionReader(lf, start, size), size)
 | 
			
		||||
			for bi, block := range it {
 | 
			
		||||
				bi.Commit = *commit
 | 
			
		||||
				bi.Index += adj
 | 
			
		||||
				bi.Start += uint64(start)
 | 
			
		||||
				if begin <= bi.Index {
 | 
			
		||||
					if !yield(bi, block) {
 | 
			
		||||
						return
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			start += size + int64(commit.tsize)
 | 
			
		||||
			adj = commit.count
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type blockInfo struct{
 | 
			
		||||
	Index uint64
 | 
			
		||||
	Commit Commit
 | 
			
		||||
	Start uint64
 | 
			
		||||
	Size uint64
 | 
			
		||||
	Hash []byte
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func iterBlocks(r io.ReaderAt, end int64) iter.Seq2[blockInfo, io.Reader] {
 | 
			
		||||
	var start int64
 | 
			
		||||
	var i uint64
 | 
			
		||||
	var bi blockInfo
 | 
			
		||||
 | 
			
		||||
	return func(yield func(blockInfo, io.Reader) bool) {
 | 
			
		||||
		buf := make([]byte, maxBlockSize)
 | 
			
		||||
		for start < end {
 | 
			
		||||
			block := &Block{}
 | 
			
		||||
			buf = buf[:10]
 | 
			
		||||
			n, err := rsr(r, int64(start), 10).ReadAt(buf, 0)
 | 
			
		||||
			if n == 0 && err != nil {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			start += int64(n)
 | 
			
		||||
 | 
			
		||||
			if err := block.header.UnmarshalBinary(buf); err != nil {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			buf = buf[:maxBlockSize]
 | 
			
		||||
			n, err = rsr(r, int64(start), int64(block.end)).ReadAt(buf, 0)
 | 
			
		||||
			if n == 0 && err != nil {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			buf = buf[:n]
 | 
			
		||||
			err = block.UnmarshalBinary(buf)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			bi.Index = i
 | 
			
		||||
			bi.Start = uint64(start)
 | 
			
		||||
			bi.Size = block.size
 | 
			
		||||
			bi.Hash = block.extra
 | 
			
		||||
 | 
			
		||||
			if !yield(bi, io.NewSectionReader(r, int64(start), int64(block.size))) {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			i++
 | 
			
		||||
			start += int64(block.end)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (lf *LogReader) iterCommits() iter.Seq[Commit] {
 | 
			
		||||
	if lf.end == 0 {
 | 
			
		||||
		return slices.Values([]Commit(nil))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	offset := lf.end - lf.size - uint64(lf.tsize)
 | 
			
		||||
	return func(yield func(Commit) bool) {
 | 
			
		||||
		if !yield(lf.Commit) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		buf := make([]byte, maxCommitSize)
 | 
			
		||||
 | 
			
		||||
		for offset > 10 {
 | 
			
		||||
			commit := Commit{}
 | 
			
		||||
			buf = buf[:10]
 | 
			
		||||
			n, err := rsr(lf, 10, int64(offset)).ReadAt(buf, 0)
 | 
			
		||||
			if n == 0 && err != nil {
 | 
			
		||||
				lf.Err = err
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			buf = buf[:n]
 | 
			
		||||
			err = commit.UnmarshalBinary(buf)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				lf.Err = err
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if !yield(commit) {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			offset -= commit.size + uint64(commit.tsize)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (lf *LogReader) Rev(begin uint64) iter.Seq2[blockInfo, io.Reader] {
 | 
			
		||||
	end := lf.end + 10
 | 
			
		||||
	bi := blockInfo{}
 | 
			
		||||
	bi.Index = lf.count-1
 | 
			
		||||
 | 
			
		||||
	return func(yield func(blockInfo, io.Reader) bool) {
 | 
			
		||||
		buf := make([]byte, maxBlockSize)
 | 
			
		||||
 | 
			
		||||
		for commit := range lf.iterCommits() {
 | 
			
		||||
			end -= uint64(commit.tsize)
 | 
			
		||||
			start := end - commit.size
 | 
			
		||||
 | 
			
		||||
			bi.Commit = commit
 | 
			
		||||
 | 
			
		||||
			for start < end {
 | 
			
		||||
				block := &Block{}
 | 
			
		||||
				buf = buf[:maxBlockSize]
 | 
			
		||||
				n, err := rsr(lf, int64(start), int64(commit.size)).ReadAt(buf, 0)
 | 
			
		||||
				if n == 0 && err != nil {
 | 
			
		||||
					lf.Err = err
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				buf = buf[:n]
 | 
			
		||||
				err = block.UnmarshalBinary(buf)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					lf.Err = err
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				if begin >= bi.Index {
 | 
			
		||||
 | 
			
		||||
					bi.Start = uint64(end-block.size)-uint64(block.tsize)
 | 
			
		||||
					bi.Size = block.size
 | 
			
		||||
 | 
			
		||||
					buf = buf[:10]
 | 
			
		||||
					_, err = rsr(lf, int64(bi.Start)-10, 10).ReadAt(buf, 0)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						lf.Err = err
 | 
			
		||||
						return
 | 
			
		||||
					}
 | 
			
		||||
					err = block.header.UnmarshalBinary(buf)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						lf.Err = err
 | 
			
		||||
						return
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					bi.Hash = block.extra
 | 
			
		||||
 | 
			
		||||
					if !yield(bi, io.NewSectionReader(lf, int64(bi.Start), int64(bi.Size))) {
 | 
			
		||||
						return
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				end -= block.size + 10 + uint64(block.tsize)
 | 
			
		||||
				bi.Index--
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (lf *LogReader) Count() uint64 {
 | 
			
		||||
	return lf.count
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (lf *LogReader) Size() uint64 {
 | 
			
		||||
	return lf.end + 10
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func rsr(r io.ReaderAt, offset, size int64) *revSegmentReader {
 | 
			
		||||
	r = io.NewSectionReader(r, offset, size)
 | 
			
		||||
	return &revSegmentReader{r, size}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type revSegmentReader struct {
 | 
			
		||||
	io.ReaderAt
 | 
			
		||||
	size int64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *revSegmentReader) ReadAt(data []byte, offset int64) (int, error) {
 | 
			
		||||
	if offset < 0 {
 | 
			
		||||
		return 0, errors.New("negative offset")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if offset > int64(r.size) {
 | 
			
		||||
		return 0, io.EOF
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	o := r.size - int64(len(data)) - offset
 | 
			
		||||
	d := int64(len(data))
 | 
			
		||||
	if o < 0 {
 | 
			
		||||
		d = max(0, d+o)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	i, err := r.ReaderAt.ReadAt(data[:d], max(0, o))
 | 
			
		||||
	reverse(data[:i])
 | 
			
		||||
	return i, err
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										300
									
								
								lsm/sst_test.go
									
									
									
									
									
								
							
							
						
						
									
										300
									
								
								lsm/sst_test.go
									
									
									
									
									
								
							@ -1,300 +0,0 @@
 | 
			
		||||
package lsm
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"iter"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/docopt/docopt-go"
 | 
			
		||||
	"github.com/matryer/is"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// TestWriteLogFile tests AppendLogFile and WriteLogFile against a set of test cases.
 | 
			
		||||
//
 | 
			
		||||
// Each test case contains a slice of slices of io.Readers, which are passed to
 | 
			
		||||
// AppendLogFile and WriteLogFile in order. The test case also contains the
 | 
			
		||||
// expected encoded output as a base64 string, as well as the expected output
 | 
			
		||||
// when the file is read back using ReadLogFile.
 | 
			
		||||
//
 | 
			
		||||
// The test case also contains the expected output when the file is read back in
 | 
			
		||||
// reverse order using ReadLogFile.Rev().
 | 
			
		||||
//
 | 
			
		||||
// The test cases are as follows:
 | 
			
		||||
//
 | 
			
		||||
// - nil reader: Passes a nil slice of io.Readers to WriteLogFile.
 | 
			
		||||
// - err reader: Passes a slice of io.Readers to WriteLogFile which returns an
 | 
			
		||||
//   error when read.
 | 
			
		||||
// - single reader: Passes a single io.Reader to WriteLogFile.
 | 
			
		||||
// - multiple readers: Passes a slice of multiple io.Readers to WriteLogFile.
 | 
			
		||||
// - multiple commit: Passes multiple slices of io.Readers to AppendLogFile.
 | 
			
		||||
// - multiple commit 3x: Passes multiple slices of io.Readers to AppendLogFile
 | 
			
		||||
//   three times.
 | 
			
		||||
//
 | 
			
		||||
// The test uses the is package from github.com/matryer/is to check that the
 | 
			
		||||
// output matches the expected output.
 | 
			
		||||
func TestWriteLogFile(t *testing.T) {
 | 
			
		||||
	type test struct {
 | 
			
		||||
		name string
 | 
			
		||||
		in   [][]io.Reader
 | 
			
		||||
		enc  string
 | 
			
		||||
		out  [][]byte
 | 
			
		||||
		rev  [][]byte
 | 
			
		||||
	}
 | 
			
		||||
	tests := []test{
 | 
			
		||||
		{
 | 
			
		||||
			name: "nil reader",
 | 
			
		||||
			in:   nil,
 | 
			
		||||
			enc:  "U291ci5pcwAAAwACAA",
 | 
			
		||||
			out:  [][]byte{},
 | 
			
		||||
			rev:  [][]byte{},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "err reader",
 | 
			
		||||
			in:   nil,
 | 
			
		||||
			enc:  "U291ci5pcwAAAwACAA",
 | 
			
		||||
			out:  [][]byte{},
 | 
			
		||||
			rev:  [][]byte{},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "single reader",
 | 
			
		||||
			in: [][]io.Reader{
 | 
			
		||||
				{
 | 
			
		||||
					bytes.NewBuffer([]byte{1, 2, 3, 4})}},
 | 
			
		||||
			enc: "U291ci5pcwAAE756XndRZXhdAAYBAgMEAQQBAhA",
 | 
			
		||||
			out: [][]byte{{1, 2, 3, 4}},
 | 
			
		||||
			rev: [][]byte{{1, 2, 3, 4}}},
 | 
			
		||||
		{
 | 
			
		||||
			name: "multiple readers",
 | 
			
		||||
			in: [][]io.Reader{
 | 
			
		||||
				{
 | 
			
		||||
					bytes.NewBuffer([]byte{1, 2, 3, 4}),
 | 
			
		||||
					bytes.NewBuffer([]byte{5, 6, 7, 8})}},
 | 
			
		||||
			enc: "U291ci5pcwAAI756XndRZXhdAAYBAgMEAQRhQyZWDDn5BQAGBQYHCAEEAgIg",
 | 
			
		||||
			out: [][]byte{{1, 2, 3, 4}, {5, 6, 7, 8}},
 | 
			
		||||
			rev: [][]byte{{5, 6, 7, 8}, {1, 2, 3, 4}}},
 | 
			
		||||
		{
 | 
			
		||||
			name: "multiple commit",
 | 
			
		||||
			in: [][]io.Reader{
 | 
			
		||||
				{
 | 
			
		||||
					bytes.NewBuffer([]byte{1, 2, 3, 4})},
 | 
			
		||||
				{
 | 
			
		||||
					bytes.NewBuffer([]byte{5, 6, 7, 8})}},
 | 
			
		||||
			enc: "U291ci5pcwAAJr56XndRZXhdAAYBAgMEAQQBAhBhQyZWDDn5BQAGBQYHCAEEAgIQ",
 | 
			
		||||
			out: [][]byte{{1, 2, 3, 4}, {5, 6, 7, 8}},
 | 
			
		||||
			rev: [][]byte{{5, 6, 7, 8}, {1, 2, 3, 4}}},
 | 
			
		||||
		{
 | 
			
		||||
			name: "multiple commit",
 | 
			
		||||
			in: [][]io.Reader{
 | 
			
		||||
				{
 | 
			
		||||
					bytes.NewBuffer([]byte{1, 2, 3, 4}),
 | 
			
		||||
					bytes.NewBuffer([]byte{5, 6, 7, 8})},
 | 
			
		||||
				{
 | 
			
		||||
					bytes.NewBuffer([]byte{9, 10, 11, 12})},
 | 
			
		||||
			},
 | 
			
		||||
			enc: "U291ci5pcwAANr56XndRZXhdAAYBAgMEAQRhQyZWDDn5BQAGBQYHCAEEAgIgA4Buuio8Ro0ABgkKCwwBBAMCEA",
 | 
			
		||||
			out: [][]byte{{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}},
 | 
			
		||||
			rev: [][]byte{{9, 10, 11, 12}, {5, 6, 7, 8}, {1, 2, 3, 4}}},
 | 
			
		||||
		{
 | 
			
		||||
			name: "multiple commit 3x",
 | 
			
		||||
			in: [][]io.Reader{
 | 
			
		||||
				{
 | 
			
		||||
					bytes.NewBuffer([]byte{1, 2, 3}),
 | 
			
		||||
					bytes.NewBuffer([]byte{4, 5, 6}),
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					bytes.NewBuffer([]byte{7, 8, 9}),
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					bytes.NewBuffer([]byte{10, 11, 12}),
 | 
			
		||||
					bytes.NewBuffer([]byte{13, 14, 15}),
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			enc: "U291ci5pcwAAVNCqYhhnLPWrAAUBAgMBA7axWhhYd+HsAAUEBQYBAwICHr9ryhhdbkEZAAUHCAkBAwMCDy/UIhidCwCqAAUKCwwBA/NCwhh6wXgXAAUNDg8BAwUCHg",
 | 
			
		||||
			out: [][]byte{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11, 12}, {13, 14, 15}},
 | 
			
		||||
			rev: [][]byte{{13, 14, 15}, {10, 11, 12}, {7, 8, 9}, {4, 5, 6}, {1, 2, 3}}},
 | 
			
		||||
	}	
 | 
			
		||||
 | 
			
		||||
	for _, test := range tests {
 | 
			
		||||
		t.Run(test.name, func(t *testing.T) {
 | 
			
		||||
			is := is.New(t)
 | 
			
		||||
			buf := &buffer{}
 | 
			
		||||
 | 
			
		||||
			buffers := 0
 | 
			
		||||
 | 
			
		||||
			if len(test.in) == 0 {
 | 
			
		||||
				err := WriteLogFile(buf, slices.Values([]io.Reader{}))
 | 
			
		||||
				is.NoErr(err)
 | 
			
		||||
			}
 | 
			
		||||
			for i, in := range test.in {
 | 
			
		||||
				buffers += len(in)
 | 
			
		||||
 | 
			
		||||
				if i == 0 {
 | 
			
		||||
					err := WriteLogFile(buf, slices.Values(in))
 | 
			
		||||
					is.NoErr(err)
 | 
			
		||||
				} else {
 | 
			
		||||
					err := AppendLogFile(buf, slices.Values(in))
 | 
			
		||||
					is.NoErr(err)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			
 | 
			
		||||
			is.Equal(base64.RawStdEncoding.EncodeToString(buf.Bytes()), test.enc)
 | 
			
		||||
 | 
			
		||||
			files, err := ReadLogFile(bytes.NewReader(buf.Bytes()))
 | 
			
		||||
			is.NoErr(err)
 | 
			
		||||
 | 
			
		||||
			is.Equal(files.Size(), uint64(len(buf.Bytes())))
 | 
			
		||||
 | 
			
		||||
			i := 0
 | 
			
		||||
			for bi, fp := range files.Iter(0) {
 | 
			
		||||
				buf, err := io.ReadAll(fp)
 | 
			
		||||
				is.NoErr(err)
 | 
			
		||||
 | 
			
		||||
				hash := hash()
 | 
			
		||||
				hash.Write(buf)
 | 
			
		||||
				is.Equal(bi.Hash, hash.Sum(nil)[:len(bi.Hash)])
 | 
			
		||||
 | 
			
		||||
				is.True(len(test.out) > int(bi.Index))
 | 
			
		||||
				is.Equal(buf, test.out[bi.Index])
 | 
			
		||||
				i++
 | 
			
		||||
			}
 | 
			
		||||
			is.NoErr(files.Err)
 | 
			
		||||
			is.Equal(i, buffers)
 | 
			
		||||
 | 
			
		||||
			i = 0
 | 
			
		||||
			for bi, fp := range files.Rev(files.Count()) {
 | 
			
		||||
				buf, err := io.ReadAll(fp)
 | 
			
		||||
				is.NoErr(err)
 | 
			
		||||
 | 
			
		||||
				hash := hash()
 | 
			
		||||
				hash.Write(buf)
 | 
			
		||||
				is.Equal(bi.Hash, hash.Sum(nil)[:len(bi.Hash)])
 | 
			
		||||
 | 
			
		||||
				is.Equal(buf, test.rev[i])
 | 
			
		||||
				is.Equal(buf, test.out[bi.Index])
 | 
			
		||||
				i++
 | 
			
		||||
			}
 | 
			
		||||
			is.NoErr(files.Err)
 | 
			
		||||
			is.Equal(i, buffers)
 | 
			
		||||
			is.Equal(files.Count(), uint64(i))
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TestArgs tests that the CLI arguments are correctly parsed.
 | 
			
		||||
func TestArgs(t *testing.T) {
 | 
			
		||||
	is := is.New(t)
 | 
			
		||||
	usage := `Usage: lsm2 create <archive> <files>...`
 | 
			
		||||
 | 
			
		||||
	arguments, err := docopt.ParseArgs(usage, []string{"create", "archive", "file1", "file2"}, "1.0")
 | 
			
		||||
	is.NoErr(err)
 | 
			
		||||
 | 
			
		||||
	var params struct {
 | 
			
		||||
		Create  bool     `docopt:"create"`
 | 
			
		||||
		Archive string   `docopt:"<archive>"`
 | 
			
		||||
		Files   []string `docopt:"<files>"`
 | 
			
		||||
	}
 | 
			
		||||
	err = arguments.Bind(¶ms)
 | 
			
		||||
	is.NoErr(err)
 | 
			
		||||
 | 
			
		||||
	is.Equal(params.Create, true)
 | 
			
		||||
	is.Equal(params.Archive, "archive")
 | 
			
		||||
	is.Equal(params.Files, []string{"file1", "file2"})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func BenchmarkIterate(b *testing.B) {
 | 
			
		||||
	block := make([]byte, 1024)
 | 
			
		||||
	buf := &buffer{}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	b.Run("write", func(b *testing.B) {
 | 
			
		||||
		WriteLogFile(buf, func(yield func(io.Reader) bool) {
 | 
			
		||||
			for range (b.N) {
 | 
			
		||||
				if !yield(bytes.NewBuffer(block)) {
 | 
			
		||||
					break	
 | 
			
		||||
				}	
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	b.Run("read", func(b *testing.B) {
 | 
			
		||||
		lf, _ := ReadLogFile(buf)
 | 
			
		||||
		b.Log(lf.Count())
 | 
			
		||||
		for range (b.N) {
 | 
			
		||||
			for _, fp := range lf.Iter(0) {
 | 
			
		||||
				_, _ = io.Copy(io.Discard, fp)
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	b.Run("rev", func(b *testing.B) {
 | 
			
		||||
		lf, _ := ReadLogFile(buf)
 | 
			
		||||
		b.Log(lf.Count())
 | 
			
		||||
		for range (b.N) {
 | 
			
		||||
			for _, fp := range lf.Rev(lf.Count()) {
 | 
			
		||||
				_, _ = io.Copy(io.Discard, fp)
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type buffer struct {
 | 
			
		||||
	buf []byte
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Bytes returns the underlying byte slice of the bufferWriterAt.
 | 
			
		||||
func (b *buffer) Bytes() []byte {
 | 
			
		||||
	return b.buf
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WriteAt implements io.WriterAt. It appends data to the internal buffer
 | 
			
		||||
// if the offset is beyond the current length of the buffer. It will
 | 
			
		||||
// return an error if the offset is negative.
 | 
			
		||||
func (b *buffer) WriteAt(data []byte, offset int64) (written int, err error) {
 | 
			
		||||
	if offset < 0 {
 | 
			
		||||
		return 0, errors.New("negative offset")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	currentLength := int64(len(b.buf))
 | 
			
		||||
	if currentLength < offset+int64(len(data)) {
 | 
			
		||||
		b.buf = append(b.buf, make([]byte, offset+int64(len(data))-currentLength)...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	written = copy(b.buf[offset:], data)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ReadAt implements io.ReaderAt. It reads data from the internal buffer starting
 | 
			
		||||
// from the specified offset and writes it into the provided data slice. If the
 | 
			
		||||
// offset is negative, it returns an error. If the requested read extends beyond
 | 
			
		||||
// the buffer's length, it returns the data read so far along with an io.EOF error.
 | 
			
		||||
func (b *buffer) ReadAt(data []byte, offset int64) (int, error) {
 | 
			
		||||
	if offset < 0 {
 | 
			
		||||
		return 0, errors.New("negative offset")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if offset > int64(len(b.buf)) || len(b.buf[offset:]) < len(data) {
 | 
			
		||||
		return copy(data, b.buf[offset:]), io.EOF
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return copy(data, b.buf[offset:]), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IterOne takes an iterator that yields values of type T along with a value of
 | 
			
		||||
// type I, and returns an iterator that yields only the values of type T. It
 | 
			
		||||
// discards the values of type I.
 | 
			
		||||
func IterOne[I, T any](it iter.Seq2[I, T]) iter.Seq[T] {
 | 
			
		||||
	return func(yield func(T) bool) {
 | 
			
		||||
		for i, v := range it {
 | 
			
		||||
			_ = i
 | 
			
		||||
			if !yield(v) {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,157 +0,0 @@
 | 
			
		||||
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)
 | 
			
		||||
// 			}
 | 
			
		||||
// 		})
 | 
			
		||||
// 	}
 | 
			
		||||
// }
 | 
			
		||||
@ -1,98 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
@ -1,295 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
@ -1,63 +0,0 @@
 | 
			
		||||
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{} })
 | 
			
		||||
}
 | 
			
		||||
@ -1,727 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
@ -1,27 +0,0 @@
 | 
			
		||||
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
									
									
									
									
									
								
							
							
						
						
									
										127
									
								
								mercury/parse.go
									
									
									
									
									
								
							@ -1,127 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
@ -1,28 +0,0 @@
 | 
			
		||||
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)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 1.1 KiB  | 
@ -1,47 +0,0 @@
 | 
			
		||||
<!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>
 | 
			
		||||
@ -1,218 +0,0 @@
 | 
			
		||||
* {
 | 
			
		||||
    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;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,420 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
@ -1,277 +0,0 @@
 | 
			
		||||
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
									
									
									
									
									
								
							
							
						
						
									
										202
									
								
								mercury/spec.go
									
									
									
									
									
								
							@ -1,202 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
@ -1,109 +0,0 @@
 | 
			
		||||
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"))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,118 +0,0 @@
 | 
			
		||||
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;
 | 
			
		||||
@ -1,120 +0,0 @@
 | 
			
		||||
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;
 | 
			
		||||
@ -1,128 +0,0 @@
 | 
			
		||||
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]
 | 
			
		||||
}
 | 
			
		||||
@ -1,55 +0,0 @@
 | 
			
		||||
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()
 | 
			
		||||
}
 | 
			
		||||
@ -1,44 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
@ -1,99 +0,0 @@
 | 
			
		||||
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()
 | 
			
		||||
}
 | 
			
		||||
@ -1,594 +0,0 @@
 | 
			
		||||
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,7 +8,6 @@ type mux struct {
 | 
			
		||||
	*http.ServeMux
 | 
			
		||||
	api       *http.ServeMux
 | 
			
		||||
	wellknown *http.ServeMux
 | 
			
		||||
	handler   http.Handler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (mux *mux) Add(fns ...interface{ RegisterHTTP(*http.ServeMux) }) {
 | 
			
		||||
@ -25,30 +24,16 @@ 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
									
									
									
									
									
								
							
							
						
						
									
										255
									
								
								rsql/ast.go
									
									
									
									
									
								
							@ -1,255 +0,0 @@
 | 
			
		||||
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()
 | 
			
		||||
}
 | 
			
		||||
@ -1,21 +0,0 @@
 | 
			
		||||
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())
 | 
			
		||||
}
 | 
			
		||||
@ -1,98 +0,0 @@
 | 
			
		||||
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
									
									
									
									
									
								
							
							
						
						
									
										258
									
								
								rsql/lexer.go
									
									
									
									
									
								
							@ -1,258 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
@ -1,105 +0,0 @@
 | 
			
		||||
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
									
									
									
									
									
								
							
							
						
						
									
										285
									
								
								rsql/parser.go
									
									
									
									
									
								
							@ -1,285 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
@ -1,309 +0,0 @@
 | 
			
		||||
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`)
 | 
			
		||||
}
 | 
			
		||||
@ -1,300 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
@ -1,141 +0,0 @@
 | 
			
		||||
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 }
 | 
			
		||||
@ -1,62 +0,0 @@
 | 
			
		||||
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,11 +35,10 @@ 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
 | 
			
		||||
@ -114,6 +113,7 @@ 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,7 +6,6 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"go.sour.is/pkg/math"
 | 
			
		||||
	"golang.org/x/exp/maps"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Set[T comparable] map[T]struct{}
 | 
			
		||||
@ -34,9 +33,6 @@ 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 {
 | 
			
		||||
 | 
			
		||||
@ -160,35 +160,3 @@ func Align[T any](k []T, v []T, less func(T, T) bool) []Pair[*T, *T] {
 | 
			
		||||
	return lis
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Heapify[T any](arr []T, n, i int, less func(T, T) bool) {
 | 
			
		||||
	largest := i
 | 
			
		||||
	l := 2*i + 1
 | 
			
		||||
	r := 2*i + 2
 | 
			
		||||
 | 
			
		||||
	if l < n && less(arr[largest], arr[l]) {
 | 
			
		||||
		largest = l
 | 
			
		||||
	}
 | 
			
		||||
	if r < n && less(arr[largest], arr[r]) {
 | 
			
		||||
		largest = r
 | 
			
		||||
	}
 | 
			
		||||
	if largest != i {
 | 
			
		||||
		arr[i], arr[largest] = arr[largest], arr[i]
 | 
			
		||||
		Heapify(arr, n, largest, less)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func BuildMaxHeap[T any](arr []T, less func(T, T) bool) {
 | 
			
		||||
	n := len(arr)
 | 
			
		||||
	for i := (n / 2) - 1; i >= 0; i-- {
 | 
			
		||||
		Heapify(arr, n, i, less)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func HeapSort[T any](arr []T, less func(T, T) bool) {
 | 
			
		||||
	BuildMaxHeap(arr, less)
 | 
			
		||||
	for i := len(arr) - 1; i > 0; i-- {
 | 
			
		||||
		arr[0], arr[i] = arr[i], arr[0]
 | 
			
		||||
		Heapify(arr, i, 0, less)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -51,21 +51,3 @@ func TestAlign(t *testing.T) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ptr[T any](v T) *T { return &v }
 | 
			
		||||
 | 
			
		||||
func TestHeapSort(t *testing.T) {
 | 
			
		||||
	is := is.New(t)
 | 
			
		||||
 | 
			
		||||
	arr := []int{9, 4, 3, 8, 10, 2, 5}
 | 
			
		||||
	slice.HeapSort(arr, func(l, r int) bool { return l < r })
 | 
			
		||||
 | 
			
		||||
	is.Equal(arr, []int{2, 3, 4, 5, 8, 9, 10})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestBuildHeap(t *testing.T) {
 | 
			
		||||
	is := is.New(t)
 | 
			
		||||
 | 
			
		||||
	arr := []int{1, 3, 5, 4, 6, 13, 10, 9, 8, 15, 17}
 | 
			
		||||
	slice.BuildMaxHeap(arr, func(l, r int) bool { return l < r })
 | 
			
		||||
 | 
			
		||||
	is.Equal(arr, []int{17, 15, 13, 9, 6, 5, 10, 4, 8, 3, 1})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										41
									
								
								xdg/xdg.go
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								xdg/xdg.go
									
									
									
									
									
								
							@ -4,7 +4,6 @@
 | 
			
		||||
package xdg
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
@ -37,20 +36,6 @@ 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 GetRoot(base, suffix string, perm os.FileMode) (*os.Root, error) {
 | 
			
		||||
	fs, err := os.OpenRoot(base)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	err = fs.Mkdir(Get(base, suffix), perm)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return os.OpenRoot(Get(base, suffix))
 | 
			
		||||
}
 | 
			
		||||
func paths(base, suffix string) []string {
 | 
			
		||||
	paths := strings.Split(os.ExpandEnv(base), string(os.PathListSeparator))
 | 
			
		||||
	for i, path := range paths {
 | 
			
		||||
		if strings.HasPrefix(path, "~") {
 | 
			
		||||
@ -58,17 +43,7 @@ func paths(base, suffix string) []string {
 | 
			
		||||
		}
 | 
			
		||||
		paths[i] = os.ExpandEnv(filepath.Join(path, suffix))
 | 
			
		||||
	}
 | 
			
		||||
	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
 | 
			
		||||
	return strings.Join(paths, string(os.PathListSeparator))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getHome() string {
 | 
			
		||||
@ -78,17 +53,3 @@ 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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user