diff --git a/cmd/parka/cmds/serve.go b/cmd/parka/cmds/serve.go index 0d768eb..8b929db 100644 --- a/cmd/parka/cmds/serve.go +++ b/cmd/parka/cmds/serve.go @@ -45,7 +45,7 @@ var ServeCmd = &cobra.Command{ Str("templateDir", "pkg/web/src/templates"). Msg("Using assets from disk") serverOptions = append(serverOptions, - server.WithStaticPaths(fs.NewStaticPath(http.FS(os.DirFS("pkg/web/dist")), "/dist")), + server.WithStaticPaths(fs.NewStaticPath(os.DirFS("pkg/web/dist"), "/dist")), ) defaultLookups = append(defaultLookups, render.NewLookupTemplateFromDirectory("pkg/web/src/templates")) } else { diff --git a/go.mod b/go.mod index 5fbaf1f..fef8ef0 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,9 @@ require ( github.com/aws/aws-sdk-go-v2 v1.21.0 github.com/aws/aws-sdk-go-v2/config v1.18.37 github.com/aws/aws-sdk-go-v2/service/ssm v1.37.5 - github.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2 - github.com/gin-gonic/gin v1.9.1 - github.com/go-go-golems/clay v0.1.10 - github.com/go-go-golems/glazed v0.5.12 + github.com/go-go-golems/clay v0.1.12 + github.com/go-go-golems/glazed v0.5.13 + github.com/labstack/echo/v4 v4.12.0 github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.30.0 github.com/spf13/cobra v1.8.0 @@ -28,7 +27,6 @@ require ( github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/sprig v2.22.0+incompatible // indirect github.com/adrg/frontmatter v0.2.0 // indirect - github.com/alecthomas/assert/v2 v2.2.2 // indirect github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.13.35 // indirect @@ -46,21 +44,13 @@ require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect - github.com/bytedance/sonic v1.9.1 // indirect github.com/charmbracelet/glamour v0.6.1-0.20230531150759-6d5b52861a9d // indirect - github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dlclark/regexp2 v1.10.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-openapi/errors v0.20.3 // indirect github.com/go-openapi/strfmt v0.21.7 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.14.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/google/uuid v1.5.0 // indirect github.com/gorilla/css v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -71,22 +61,18 @@ require ( github.com/itchyny/timefmt-go v0.1.5 // indirect github.com/jedib0t/go-pretty v4.3.0+incompatible // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/kopoli/go-terminal-size v0.0.0-20170219200355-5c97524c8b54 // indirect - github.com/leodido/go-urn v1.2.4 // indirect + github.com/labstack/gommon v0.4.2 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/microcosm-cc/bluemonday v1.0.25 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect @@ -103,21 +89,20 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/tj/go-naturaldate v1.3.0 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 // indirect github.com/xuri/excelize/v2 v2.7.1 // indirect github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 // indirect github.com/yuin/goldmark-emoji v1.0.2 // indirect go.mongodb.org/mongo-driver v1.11.3 // indirect - golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.16.0 // indirect - golang.org/x/image v0.14.0 // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/sys v0.18.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect - google.golang.org/protobuf v1.31.0 // indirect + golang.org/x/time v0.5.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 754d267..cb49e0a 100644 --- a/go.sum +++ b/go.sum @@ -49,7 +49,6 @@ github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuN github.com/adrg/frontmatter v0.2.0 h1:/DgnNe82o03riBd1S+ZDjd43wAmC6W35q67NHeLkPd4= github.com/adrg/frontmatter v0.2.0/go.mod h1:93rQCj3z3ZlwyxxpQioRKC1wDLto4aXHrbqIsnH9wmE= github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= -github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= github.com/alecthomas/chroma/v2 v2.7.0 h1:hm1rY6c/Ob4eGclpQ7X/A3yhqBOZNUTk9q+yhyLIViI= github.com/alecthomas/chroma/v2 v2.7.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= @@ -95,15 +94,9 @@ github.com/bmatcuk/doublestar/v4 v4.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvz github.com/bmatcuk/doublestar/v4 v4.6.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= -github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/charmbracelet/glamour v0.6.1-0.20230531150759-6d5b52861a9d h1:S4Ejl/M2VrryIgDrDbiuvkwMUDa67/t/H3Wz3i2/vUw= github.com/charmbracelet/glamour v0.6.1-0.20230531150759-6d5b52861a9d/go.mod h1:swCB3CXFsh22H1ESDYdY1tirLiNqCziulDyJ1B6Nt7Q= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -130,35 +123,20 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2 h1:dyuNlYlG1faymw39NdJddnzJICy6587tiGSVioWhYoE= -github.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2/go.mod h1:iqneQ2Df3omzIVTkIfn7c1acsVnMGiSLn4XF5Blh3Yg= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-go-golems/clay v0.1.10 h1:68ws6W7qohMpKhsLZumXDxFk7AuFjNoyHo8bv7FSVHs= -github.com/go-go-golems/clay v0.1.10/go.mod h1:ovEpMRRJQ3ndnoc9qKh9T+TTPAtWYvyHjupwo191GpU= -github.com/go-go-golems/glazed v0.5.12 h1:RChhR4Mbl+zOGUZwVGAPne0nFpsKudISTlVVNDzJQzs= -github.com/go-go-golems/glazed v0.5.12/go.mod h1:K1600pUk7xB/LKmvIafRWyfAdxE1sboruqQ9Jia8V9M= +github.com/go-go-golems/clay v0.1.12 h1:V/bRQOYjCzg5BKnX7MkBazmTWnVhvH/ir25M8X7YJG4= +github.com/go-go-golems/clay v0.1.12/go.mod h1:SBmD8dSWOMuGEbbpw77xOAJWGU7NB10W4KbxtzzfhvA= +github.com/go-go-golems/glazed v0.5.13 h1:Mk2NQDAuXRDTvtHqiSKeMnncMa7w3CKOTuxACBM4fuE= +github.com/go-go-golems/glazed v0.5.13/go.mod h1:SvPA8GVBwAbyNBZVrK2ZH9cIHLMKxc/bf+rB7ewuonE= github.com/go-openapi/errors v0.20.3 h1:rz6kiC84sqNQoqrtulzaL/VERgkoCyB6WdEkc2ujzUc= github.com/go-openapi/errors v0.20.3/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= github.com/go-openapi/strfmt v0.21.7 h1:rspiXgNWgeUzhjo1YU01do6qsahtJNByjLVbPLNHb8k= github.com/go-openapi/strfmt v0.21.7/go.mod h1:adeGTkxE44sPyLk0JV235VQAO/ZXUr8KAzYjclFs3ew= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= -github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -184,7 +162,6 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -197,11 +174,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -248,15 +222,10 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/kopoli/go-terminal-size v0.0.0-20170219200355-5c97524c8b54 h1:0SMHxjkLKNawqUjjnMlCtEdj6uWZjv0+qDZ3F6GOADI= github.com/kopoli/go-terminal-size v0.0.0-20170219200355-5c97524c8b54/go.mod h1:bm7MVZZvHQBfqHG5X59jrRE/3ak6HvK+/Zb6aZhLR2s= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= @@ -266,17 +235,21 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= +github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 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-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= @@ -292,11 +265,6 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= @@ -349,15 +317,12 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -369,10 +334,12 @@ github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160 h1:NSWpaDaurcAJY7PkL8Xt0 github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= github.com/tj/go-naturaldate v1.3.0 h1:OgJIPkR/Jk4bFMBLbxZ8w+QUxwjqSvzd9x+yXocY4RI= github.com/tj/go-naturaldate v1.3.0/go.mod h1:rpUbjivDKiS1BlfMGc2qUKNZ/yxgthOfmytQs8d8hKk= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= @@ -407,9 +374,6 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= -golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -420,8 +384,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -436,7 +400,6 @@ golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMx golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= -golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -495,8 +458,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -559,14 +522,14 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.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/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -586,6 +549,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -727,9 +692,6 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -753,6 +715,5 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/pkg/NOTES.md b/pkg/NOTES.md index 544d9e7..8c86c0a 100644 --- a/pkg/NOTES.md +++ b/pkg/NOTES.md @@ -40,7 +40,7 @@ and call Run on the underlying glaze.Command. #### CommandHandlerFunc - parsing incoming requests into a command's parameters and layers This is done through the `CommandHandlerFunc` type, which is a function that -takes a `*gin.Context` and a `*CommandContext` and is allowed to modify both as +takes a `echo.Context` and a `*CommandContext` and is allowed to modify both as it does its thing. A `CommandContext` is a struct that has a reference to the `GlazeCommand` and keeps @@ -53,7 +53,7 @@ necessary to run the glaze command. #### CreateProcessorFunc - creating the output formatter We also have something called a `CreateProcessorFunc`, which is a function that -takes a `*gin.Context` and a `*CommandContext` and returns a `glaze.Processor`. +takes a `echo.Context` and a `*CommandContext` and returns a `glaze.Processor`. This allows us to override what output formatter is created, depending on the request. The default handler will process the parsed parameters for the `glazed` layer, just as it would on the command line. If nothing is set, it would create a JSON output formatter. diff --git a/pkg/glazed/handlers/datatables/datatables.go b/pkg/glazed/handlers/datatables/datatables.go index da4aec3..3bf3130 100644 --- a/pkg/glazed/handlers/datatables/datatables.go +++ b/pkg/glazed/handlers/datatables/datatables.go @@ -4,7 +4,6 @@ import ( "context" "embed" "fmt" - "github.com/gin-gonic/gin" "github.com/go-go-golems/glazed/pkg/cmds" "github.com/go-go-golems/glazed/pkg/cmds/layers" "github.com/go-go-golems/glazed/pkg/cmds/middlewares" @@ -19,6 +18,7 @@ import ( parka_middlewares "github.com/go-go-golems/parka/pkg/glazed/middlewares" "github.com/go-go-golems/parka/pkg/render" "github.com/go-go-golems/parka/pkg/render/layout" + "github.com/labstack/echo/v4" "github.com/pkg/errors" "github.com/rs/zerolog/log" "golang.org/x/sync/errgroup" @@ -166,35 +166,13 @@ func WithStreamRows(streamRows bool) QueryHandlerOption { } var _ handlers.Handler = &QueryHandler{} +var _ echo.HandlerFunc = (&QueryHandler{}).Handle -func (qh *QueryHandler) Handle(c *gin.Context, w io.Writer) error { +func (qh *QueryHandler) Handle(c echo.Context) error { description := qh.cmd.Description() parsedLayers := layers.NewParsedLayers() - err := middlewares.ExecuteMiddlewares(description.Layers, parsedLayers, - append( - qh.middlewares, - parka_middlewares.UpdateFromQueryParameters(c, parameters.WithParseStepSource("query")), - middlewares.SetFromDefaults(), - )..., - ) - dt_ := qh.dt.Clone() - if cm_, ok := qh.cmd.(cmds.CommandWithMetadata); ok { - dt_.CommandMetadata, err = cm_.Metadata(c, parsedLayers) - } - - var of formatters.RowOutputFormatter - // buffered so that we don't hang on it when exiting - dt_.ErrorStream = make(chan string, 1) - if dt_.JSRendering { - of = json.NewOutputFormatter(json.WithOutputIndividualRows(true)) - dt_.JSStream = make(chan template.JS, 100) - } else { - of = table_formatter.NewOutputFormatter("html") - dt_.HTMLStream = make(chan template.HTML, 100) - } - // rowC is the channel where the rows are sent to. They will need to get converted // to template.JS or template.HTML before being sent to either rowC := make(chan string, 100) @@ -205,6 +183,14 @@ func (qh *QueryHandler) Handle(c *gin.Context, w io.Writer) error { // since we have a context there, and there is no need to block the middleware processing. columnsC := make(chan []types.FieldName, 10) + err := middlewares.ExecuteMiddlewares(description.Layers, parsedLayers, + append( + qh.middlewares, + parka_middlewares.UpdateFromQueryParameters(c, parameters.WithParseStepSource("query")), + middlewares.SetFromDefaults(), + )..., + ) + if err != nil { if dt_.JSStream != nil { close(dt_.JSStream) @@ -217,13 +203,32 @@ func (qh *QueryHandler) Handle(c *gin.Context, w io.Writer) error { columnsC <- []types.FieldName{} close(columnsC) close(rowC) - err_ := qh.renderTemplate(parsedLayers, w, dt_, columnsC) + err_ := qh.renderTemplate(parsedLayers, c.Response(), dt_, columnsC) if err_ != nil { return err_ } return err_ } + // This needs to run after parsing the layers + if cm_, ok := qh.cmd.(cmds.CommandWithMetadata); ok { + dt_.CommandMetadata, err = cm_.Metadata(c.Request().Context(), parsedLayers) + if err != nil { + return err + } + } + + var of formatters.RowOutputFormatter + // buffered so that we don't hang on it when exiting + dt_.ErrorStream = make(chan string, 1) + if dt_.JSRendering { + of = json.NewOutputFormatter(json.WithOutputIndividualRows(true)) + dt_.JSStream = make(chan template.JS, 100) + } else { + of = table_formatter.NewOutputFormatter("html") + dt_.HTMLStream = make(chan template.HTML, 100) + } + // manually create a streaming output TableProcessor gp, err := handlers.CreateTableProcessorWithOutput(parsedLayers, "table", "ascii") if err != nil { @@ -239,7 +244,7 @@ func (qh *QueryHandler) Handle(c *gin.Context, w io.Writer) error { gp.AddTableMiddleware(table.NewOutputChannelMiddleware(of, rowC)) } - ctx := c.Request.Context() + ctx := c.Request().Context() ctx2, cancel := context.WithCancel(ctx) eg, ctx3 := errgroup.WithContext(ctx2) @@ -298,7 +303,7 @@ func (qh *QueryHandler) Handle(c *gin.Context, w io.Writer) error { eg.Go(func() error { // if qh.Cmd implements cmds.CommandWithMetadata, get Metadata - err := qh.renderTemplate(parsedLayers, w, dt_, columnsC) + err := qh.renderTemplate(parsedLayers, c.Response(), dt_, columnsC) if err != nil { return err } @@ -343,10 +348,7 @@ func (qh *QueryHandler) renderTemplate( // Wait for the column names to be sent to the channel. This will only // take the first row into account. - columns, ok := <-columnsC - if !ok { - return fmt.Errorf("no columns received") - } + columns := <-columnsC dt_.Columns = columns // start copying from rowC to HTML or JS stream @@ -361,41 +363,41 @@ func (qh *QueryHandler) renderTemplate( func CreateDataTablesHandler( cmd cmds.GlazeCommand, - path string, - commandPath string, + basePath string, + downloadPath string, options ...QueryHandlerOption, -) gin.HandlerFunc { - return func(c *gin.Context) { +) echo.HandlerFunc { + return func(c echo.Context) error { name := cmd.Description().Name dateTime := time.Now().Format("2006-01-02--15-04-05") links := []layout.Link{ { - Href: fmt.Sprintf("%s/download/%s/%s-%s.csv", path, commandPath, dateTime, name), + Href: fmt.Sprintf("%s/%s-%s.csv", downloadPath, dateTime, name), Text: "Download CSV", Class: "download", }, { - Href: fmt.Sprintf("%s/download/%s/%s-%s.json", path, commandPath, dateTime, name), + Href: fmt.Sprintf("%s/%s-%s.json", downloadPath, dateTime, name), Text: "Download JSON", Class: "download", }, { - Href: fmt.Sprintf("%s/download/%s/%s-%s.xlsx", path, commandPath, dateTime, name), + Href: fmt.Sprintf("%s/%s-%s.xlsx", downloadPath, dateTime, name), Text: "Download Excel", Class: "download", }, { - Href: fmt.Sprintf("%s/download/%s/%s-%s.md", path, commandPath, dateTime, name), + Href: fmt.Sprintf("%s/%s-%s.md", downloadPath, dateTime, name), Text: "Download Markdown", Class: "download", }, { - Href: fmt.Sprintf("%s/download/%s/%s-%s.html", path, commandPath, dateTime, name), + Href: fmt.Sprintf("%s/%s-%s.html", downloadPath, dateTime, name), Text: "Download HTML", Class: "download", }, { - Href: fmt.Sprintf("%s/download/%s/%s-%s.txt", path, commandPath, dateTime, name), + Href: fmt.Sprintf("%s/%s-%s.txt", downloadPath, dateTime, name), Text: "Download Text", Class: "download", }, @@ -404,7 +406,7 @@ func CreateDataTablesHandler( dt := NewDataTables() dt.Command = cmd.Description() dt.Links = links - dt.BasePath = path + dt.BasePath = basePath dt.JSRendering = true dt.UseDataTables = false @@ -415,9 +417,11 @@ func CreateDataTablesHandler( handler := NewQueryHandler(cmd, options_...) - err := handler.Handle(c, c.Writer) + err := handler.Handle(c) if err != nil && !errors.Is(err, context.Canceled) { log.Error().Err(err).Msg("error handling query") } + + return nil } } diff --git a/pkg/glazed/handlers/glazed/glazed.go b/pkg/glazed/handlers/glazed/glazed.go index c9bd16d..5040cf5 100644 --- a/pkg/glazed/handlers/glazed/glazed.go +++ b/pkg/glazed/handlers/glazed/glazed.go @@ -1,7 +1,6 @@ package glazed import ( - "github.com/gin-gonic/gin" "github.com/go-go-golems/glazed/pkg/cmds" "github.com/go-go-golems/glazed/pkg/cmds/layers" "github.com/go-go-golems/glazed/pkg/cmds/middlewares" @@ -9,8 +8,9 @@ import ( "github.com/go-go-golems/glazed/pkg/settings" "github.com/go-go-golems/parka/pkg/glazed/handlers" middlewares2 "github.com/go-go-golems/parka/pkg/glazed/middlewares" + "github.com/labstack/echo/v4" "github.com/pkg/errors" - "io" + "net/http" ) type QueryHandler struct { @@ -40,7 +40,7 @@ func NewQueryHandler(cmd cmds.GlazeCommand, options ...QueryHandlerOption) *Quer var _ handlers.Handler = (*QueryHandler)(nil) -func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { +func (h *QueryHandler) Handle(c echo.Context) error { description := h.cmd.Description() parsedLayers := layers.NewParsedLayers() @@ -63,14 +63,15 @@ func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { return err } - of, err := settings.SetupProcessorOutput(gp, glazedLayer, writer) + of, err := settings.SetupProcessorOutput(gp, glazedLayer, c.Response()) if err != nil { return err } - c.Header("Content-Type", of.ContentType()) + c.Response().Header().Set("Content-Type", of.ContentType()) + c.Response().WriteHeader(http.StatusOK) - ctx := c.Request.Context() + ctx := c.Request().Context() err = h.cmd.RunIntoGlazeProcessor(ctx, parsedLayers, gp) if err != nil { return err diff --git a/pkg/glazed/handlers/handler.go b/pkg/glazed/handlers/handler.go index 9229b66..89d684b 100644 --- a/pkg/glazed/handlers/handler.go +++ b/pkg/glazed/handlers/handler.go @@ -1,13 +1,16 @@ package handlers import ( - "github.com/gin-gonic/gin" "github.com/go-go-golems/glazed/pkg/cmds" - "io" + "github.com/labstack/echo/v4" ) +// Handler wraps the normal handle method to allow rendering into a different writer, +// so that we can provide file downloads. +// +// TODO(manuel, 2024-05-07) I don't think we actually need this type Handler interface { - Handle(c *gin.Context, w io.Writer) error + Handle(c echo.Context) error } type UnsupportedCommandError struct { diff --git a/pkg/glazed/handlers/json/json.go b/pkg/glazed/handlers/json/json.go index d83755a..d12282c 100644 --- a/pkg/glazed/handlers/json/json.go +++ b/pkg/glazed/handlers/json/json.go @@ -3,7 +3,6 @@ package json import ( "bytes" "encoding/json" - "github.com/gin-gonic/gin" "github.com/go-go-golems/glazed/pkg/cmds" "github.com/go-go-golems/glazed/pkg/cmds/layers" "github.com/go-go-golems/glazed/pkg/cmds/middlewares" @@ -12,8 +11,7 @@ import ( "github.com/go-go-golems/glazed/pkg/middlewares/row" "github.com/go-go-golems/parka/pkg/glazed/handlers" middlewares2 "github.com/go-go-golems/parka/pkg/glazed/middlewares" - "github.com/rs/zerolog/log" - "io" + "github.com/labstack/echo/v4" "net/http" ) @@ -44,7 +42,7 @@ func WithMiddlewares(middlewares ...middlewares.Middleware) QueryHandlerOption { var _ handlers.Handler = (*QueryHandler)(nil) -func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { +func (h *QueryHandler) Handle(c echo.Context) error { description := h.cmd.Description() parsedLayers := layers.NewParsedLayers() @@ -58,9 +56,10 @@ func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { return err } - c.Header("Content-Type", "application/json") + c.Response().Header().Set("Content-Type", "application/json") + c.Response().WriteHeader(http.StatusOK) - ctx := c.Request.Context() + ctx := c.Request().Context() switch cmd := h.cmd.(type) { case cmds.WriterCommand: buf := bytes.Buffer{} @@ -74,7 +73,7 @@ func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { }{ Data: buf.String(), } - encoder := json.NewEncoder(writer) + encoder := json.NewEncoder(c.Response()) encoder.SetIndent("", " ") err = encoder.Encode(foo) if err != nil { @@ -89,7 +88,7 @@ func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { // remove table middlewares because we are a streaming handler gp.ReplaceTableMiddleware() - gp.AddRowMiddleware(row.NewOutputMiddleware(json2.NewOutputFormatter(), writer)) + gp.AddRowMiddleware(row.NewOutputMiddleware(json2.NewOutputFormatter(), c.Response())) if err != nil { return err @@ -121,15 +120,7 @@ func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { func CreateJSONQueryHandler( cmd cmds.Command, options ...QueryHandlerOption, -) gin.HandlerFunc { +) echo.HandlerFunc { handler := NewQueryHandler(cmd, options...) - return func(c *gin.Context) { - err := handler.Handle(c, c.Writer) - if err != nil { - log.Error().Err(err).Msg("failed to handle query") - c.JSON(http.StatusInternalServerError, gin.H{ - "error": err.Error(), - }) - } - } + return handler.Handle } diff --git a/pkg/glazed/handlers/output-file/output-file.go b/pkg/glazed/handlers/output-file/output-file.go index 7922574..e655451 100644 --- a/pkg/glazed/handlers/output-file/output-file.go +++ b/pkg/glazed/handlers/output-file/output-file.go @@ -1,64 +1,24 @@ package output_file import ( - "bytes" "fmt" - "github.com/gin-gonic/gin" "github.com/go-go-golems/glazed/pkg/cmds" "github.com/go-go-golems/glazed/pkg/cmds/middlewares" "github.com/go-go-golems/glazed/pkg/cmds/parameters" "github.com/go-go-golems/glazed/pkg/helpers/list" "github.com/go-go-golems/glazed/pkg/settings" - "github.com/go-go-golems/parka/pkg/glazed/handlers" "github.com/go-go-golems/parka/pkg/glazed/handlers/glazed" parka_middlewares "github.com/go-go-golems/parka/pkg/glazed/middlewares" + "github.com/labstack/echo/v4" + "github.com/pkg/errors" "io" + "net/http" + "net/http/httptest" "os" "path/filepath" "strings" ) -type OutputFileHandler struct { - handler handlers.Handler - outputFileName string -} - -func NewOutputFileHandler(handler handlers.Handler, outputFileName string) *OutputFileHandler { - h := &OutputFileHandler{ - handler: handler, - outputFileName: outputFileName, - } - - return h -} - -var _ handlers.Handler = (*OutputFileHandler)(nil) - -func (h *OutputFileHandler) Handle(c *gin.Context, w io.Writer) error { - buf := &bytes.Buffer{} - err := h.handler.Handle(c, buf) - if err != nil { - return err - } - - c.Status(200) - - f, err := os.Open(h.outputFileName) - if err != nil { - return err - } - defer func(f *os.File) { - _ = f.Close() - }(f) - - _, err = io.Copy(c.Writer, f) - if err != nil { - return err - } - - return nil -} - // CreateGlazedFileHandler creates a handler that will run a glazed command and write the output // with a Content-Disposition header to the response writer. // @@ -68,8 +28,8 @@ func CreateGlazedFileHandler( cmd cmds.GlazeCommand, fileName string, middlewares_ ...middlewares.Middleware, -) gin.HandlerFunc { - return func(c *gin.Context) { +) echo.HandlerFunc { + return func(c echo.Context) error { glazedOverrides := map[string]interface{}{} needsRealFileOutput := false @@ -97,28 +57,12 @@ func CreateGlazedFileHandler( glazedOverrides["output"] = "table" glazedOverrides["table-format"] = "ascii" } else { - c.JSON(500, gin.H{"error": "could not determine output format"}) - return + return errors.New("unsupported file format") } var tmpFile *os.File var err error - // excel output needs a real output file, otherwise we can go stream to the HTTP response - if needsRealFileOutput { - tmpFile, err = os.CreateTemp("/tmp", fmt.Sprintf("glazed-output-*.%s", fileName)) - if err != nil { - c.JSON(500, gin.H{"error": "could not create temporary file"}) - return - } - defer func(name string) { - _ = os.Remove(name) - }(tmpFile.Name()) - - // now check file suffix for content-type - glazedOverrides["output-file"] = tmpFile.Name() - } - glazedOverride := middlewares.UpdateFromMap( map[string]map[string]interface{}{ settings.GlazedSlug: glazedOverrides, @@ -134,25 +78,55 @@ func CreateGlazedFileHandler( )) baseName := filepath.Base(fileName) - c.Writer.Header().Set("Content-Disposition", "attachment; filename="+baseName) + c.Response().Header().Set("Content-Disposition", "attachment; filename="+baseName) + // excel output needs a real output file, otherwise we can go stream to the HTTP response if needsRealFileOutput { - outputFileHandler := NewOutputFileHandler(handler, tmpFile.Name()) + tmpFile, err = os.CreateTemp("/tmp", fmt.Sprintf("glazed-output-*.%s", fileName)) + if err != nil { + return errors.Wrap(err, "could not create temporary file") + } + defer func(name string) { + _ = os.Remove(name) + }(tmpFile.Name()) - err = outputFileHandler.Handle(c, c.Writer) + // now check file suffix for content-type + glazedOverrides["output-file"] = tmpFile.Name() + + // here we have the output of the handler go to a request that we discard, and + // we instead copy the temporary file to the response writer + res := httptest.NewRecorder() + req := c.Request() + newCtx := c.Echo().NewContext(req, res) + + err = handler.Handle(newCtx) + if err != nil { + return err + } + + // copy tmpFile to output + f, err := os.Open(tmpFile.Name()) + if err != nil { + return errors.Wrap(err, "could not open temporary file") + } + defer func(f *os.File) { + _ = f.Close() + }(f) + + c.Response().Header().Set("Content-Type", "application/octet-stream") + c.Response().WriteHeader(http.StatusOK) + + _, err = io.Copy(c.Response().Writer, f) if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return + return err } } else { - err = handler.Handle(c, c.Writer) + err = handler.Handle(c) if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return + return err } } - // override parameter layers at the end - + return nil } } diff --git a/pkg/glazed/handlers/sse/sse.go b/pkg/glazed/handlers/sse/sse.go index a7082b2..3bb58ea 100644 --- a/pkg/glazed/handlers/sse/sse.go +++ b/pkg/glazed/handlers/sse/sse.go @@ -4,7 +4,6 @@ package sse import ( "fmt" - "github.com/gin-gonic/gin" "github.com/go-go-golems/glazed/pkg/cmds" "github.com/go-go-golems/glazed/pkg/cmds/layers" "github.com/go-go-golems/glazed/pkg/cmds/middlewares" @@ -13,7 +12,7 @@ import ( "github.com/go-go-golems/glazed/pkg/middlewares/row" "github.com/go-go-golems/parka/pkg/glazed/handlers" middlewares2 "github.com/go-go-golems/parka/pkg/glazed/middlewares" - "github.com/rs/zerolog/log" + "github.com/labstack/echo/v4" "golang.org/x/sync/errgroup" "net/http" ) @@ -43,7 +42,7 @@ func WithMiddlewares(middlewares ...middlewares.Middleware) QueryHandlerOption { } } -func (h *QueryHandler) Handle(c *gin.Context, writer gin.ResponseWriter) error { +func (h *QueryHandler) Handle(c echo.Context) error { description := h.cmd.Description() parsedLayers := layers.NewParsedLayers() @@ -54,9 +53,12 @@ func (h *QueryHandler) Handle(c *gin.Context, writer gin.ResponseWriter) error { if err != nil { return err } - c.Header("Content-Type", "text/event-stream") + c.Response().Header().Set("Content-Type", "text/event-stream") + c.Response().Header().Set("Cache-Control", "no-cache") + c.Response().Header().Set("Connection", "keep-alive") + c.Response().WriteHeader(http.StatusOK) - ctx := c.Request.Context() + ctx := c.Request().Context() switch cmd := h.cmd.(type) { case cmds.WriterCommand: // Create a writer that on every read amount of bytes sends an sse message @@ -83,12 +85,11 @@ func (h *QueryHandler) Handle(c *gin.Context, writer gin.ResponseWriter) error { return nil } // write SSE event to writer - s := fmt.Sprintf("data: %s\n\n", msg) - _, err := writer.Write([]byte(s)) + _, err := fmt.Fprintf(c.Response(), "data: %s\n\n", msg) if err != nil { return err } - writer.Flush() + c.Response().Flush() } } }) @@ -102,8 +103,8 @@ func (h *QueryHandler) Handle(c *gin.Context, writer gin.ResponseWriter) error { } gp.ReplaceTableMiddleware() - c := make(chan string, 100) - r := row.NewOutputChannelMiddleware(json2.NewOutputFormatter(), c) + eventChan := make(chan string, 100) + r := row.NewOutputChannelMiddleware(json2.NewOutputFormatter(), eventChan) gp.AddRowMiddleware(r) eg := errgroup.Group{} @@ -124,8 +125,8 @@ func (h *QueryHandler) Handle(c *gin.Context, writer gin.ResponseWriter) error { select { case <-ctx.Done(): return ctx.Err() - case row := <-c: - _, err := writer.Write([]byte(row)) + case row := <-eventChan: + _, err := c.Response().Write([]byte(row)) if err != nil { return err } @@ -153,17 +154,9 @@ func (h *QueryHandler) Handle(c *gin.Context, writer gin.ResponseWriter) error { func CreateQueryHandler( cmd cmds.Command, middlewares_ ...middlewares.Middleware, -) gin.HandlerFunc { +) echo.HandlerFunc { handler := NewQueryHandler(cmd, WithMiddlewares(middlewares_...), ) - return func(c *gin.Context) { - err := handler.Handle(c, c.Writer) - if err != nil { - log.Error().Err(err).Msg("failed to handle query") - c.JSON(http.StatusInternalServerError, gin.H{ - "error": err.Error(), - }) - } - } + return handler.Handle } diff --git a/pkg/glazed/handlers/text/text-glazed-cmd_test.go b/pkg/glazed/handlers/text/text-glazed-cmd_test.go index 4449b34..96ba147 100644 --- a/pkg/glazed/handlers/text/text-glazed-cmd_test.go +++ b/pkg/glazed/handlers/text/text-glazed-cmd_test.go @@ -3,11 +3,11 @@ package text import ( _ "embed" "encoding/json" - "github.com/gin-gonic/gin" "github.com/go-go-golems/glazed/pkg/cmds" "github.com/go-go-golems/glazed/pkg/cmds/helpers" "github.com/go-go-golems/glazed/pkg/helpers/yaml" "github.com/go-go-golems/parka/pkg/utils" + "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "net/http" @@ -37,19 +37,17 @@ func TestTextHandlerGlazeCommand(t *testing.T) { for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { - gin.SetMode(gin.TestMode) - c, _ := utils.MockGinContextWithQueryParameters(tt.QueryParameters) + req := utils.NewRequestWithQueryParameters(tt.QueryParameters) // Create ParameterLayers and ParsedLayers from test definitions layers_ := helpers.NewTestParameterLayers(tt.ParameterLayers) cmd, err := utils.NewTestGlazedCommand(cmds.WithLayers(layers_)) require.NoError(t, err) - router := gin.Default() - router.GET("/", CreateQueryHandler(cmd)) - resp := httptest.NewRecorder() - router.ServeHTTP(resp, c.Request) + e := echo.New() + e.GET("/", CreateQueryHandler(cmd)) + e.ServeHTTP(resp, req) // Check for expected error if tt.ExpectedError { diff --git a/pkg/glazed/handlers/text/text-writer-cmd_test.go b/pkg/glazed/handlers/text/text-writer-cmd_test.go index 3c2d2f3..c00f8e6 100644 --- a/pkg/glazed/handlers/text/text-writer-cmd_test.go +++ b/pkg/glazed/handlers/text/text-writer-cmd_test.go @@ -3,11 +3,12 @@ package text import ( _ "embed" "encoding/json" - "github.com/gin-gonic/gin" "github.com/go-go-golems/glazed/pkg/cmds" "github.com/go-go-golems/glazed/pkg/cmds/helpers" "github.com/go-go-golems/glazed/pkg/helpers/yaml" + "github.com/go-go-golems/parka/pkg/server" "github.com/go-go-golems/parka/pkg/utils" + "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "net/http" @@ -38,8 +39,7 @@ func TestTextHandler(t *testing.T) { for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { - gin.SetMode(gin.TestMode) - c, _ := utils.MockGinContextWithQueryParameters(tt.QueryParameters) + req := utils.NewRequestWithQueryParameters(tt.QueryParameters) // Create ParameterLayers and ParsedLayers from test definitions layers_ := helpers.NewTestParameterLayers(tt.ParameterLayers) @@ -47,25 +47,25 @@ func TestTextHandler(t *testing.T) { // TODO(manuel, 2024-01-02) We also need to test with glazed commands cmd := cmds.NewTemplateCommand(tt.Name, tt.Template, cmds.WithLayers(layers_)) - router := gin.Default() - router.GET("/", CreateQueryHandler(cmd)) - - resp := httptest.NewRecorder() - router.ServeHTTP(resp, c.Request) + rec := httptest.NewRecorder() + e := echo.New() + e.HTTPErrorHandler = server.CustomHTTPErrorHandler + e.GET("/", CreateQueryHandler(cmd)) + e.ServeHTTP(rec, req) // Check for expected error if tt.ExpectedError { - assert.Equal(t, http.StatusInternalServerError, resp.Code) + assert.Equal(t, http.StatusInternalServerError, rec.Code) var json_ map[string]interface{} - err := json.Unmarshal(resp.Body.Bytes(), &json_) + err := json.Unmarshal(rec.Body.Bytes(), &json_) require.NoError(t, err) if tt.ErrorString != "" { assert.Equal(t, tt.ErrorString, json_["error"]) } } else { - assert.Equal(t, http.StatusOK, resp.Code) - assert.Equal(t, tt.ExpectedOutput, resp.Body.String()) - assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type")) + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, tt.ExpectedOutput, rec.Body.String()) + assert.Equal(t, "text/plain; charset=utf-8", rec.Header().Get("Content-Type")) } }) } diff --git a/pkg/glazed/handlers/text/text.go b/pkg/glazed/handlers/text/text.go index 62f3449..2478063 100644 --- a/pkg/glazed/handlers/text/text.go +++ b/pkg/glazed/handlers/text/text.go @@ -1,7 +1,6 @@ package text import ( - "github.com/gin-gonic/gin" "github.com/go-go-golems/glazed/pkg/cmds" "github.com/go-go-golems/glazed/pkg/cmds/layers" "github.com/go-go-golems/glazed/pkg/cmds/middlewares" @@ -10,10 +9,8 @@ import ( "github.com/go-go-golems/glazed/pkg/settings" "github.com/go-go-golems/parka/pkg/glazed/handlers" parka_middlewares "github.com/go-go-golems/parka/pkg/glazed/middlewares" + "github.com/labstack/echo/v4" "github.com/pkg/errors" - "github.com/rs/zerolog/log" - "io" - "net/http" ) type QueryHandler struct { @@ -43,7 +40,7 @@ func WithMiddlewares(middlewares ...middlewares.Middleware) QueryHandlerOption { var _ handlers.Handler = (*QueryHandler)(nil) -func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { +func (h *QueryHandler) Handle(c echo.Context) error { description := h.cmd.Description() parsedLayers := layers.NewParsedLayers() @@ -57,12 +54,12 @@ func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { if err != nil { return err } - c.Header("Content-Type", "text/plain; charset=utf-8") + c.Response().Header().Set("Content-Type", "text/plain; charset=utf-8") - ctx := c.Request.Context() + ctx := c.Request().Context() switch cmd := h.cmd.(type) { case cmds.WriterCommand: - err := cmd.RunIntoWriter(ctx, parsedLayers, writer) + err := cmd.RunIntoWriter(ctx, parsedLayers, c.Response()) if err != nil { return err } @@ -87,7 +84,7 @@ func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { return err } - gp.AddTableMiddleware(table.NewOutputMiddleware(of, writer)) + gp.AddTableMiddleware(table.NewOutputMiddleware(of, c.Response())) err = cmd.RunIntoGlazeProcessor(ctx, parsedLayers, gp) if err != nil { @@ -114,17 +111,9 @@ func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { func CreateQueryHandler( cmd cmds.Command, middlewares_ ...middlewares.Middleware, -) gin.HandlerFunc { +) echo.HandlerFunc { handler := NewQueryHandler(cmd, WithMiddlewares(middlewares_...), ) - return func(c *gin.Context) { - err := handler.Handle(c, c.Writer) - if err != nil { - log.Error().Err(err).Msg("failed to handle query") - c.JSON(http.StatusInternalServerError, gin.H{ - "error": err.Error(), - }) - } - } + return handler.Handle } diff --git a/pkg/glazed/middlewares/form.go b/pkg/glazed/middlewares/form.go index 161ab8e..2197ab2 100644 --- a/pkg/glazed/middlewares/form.go +++ b/pkg/glazed/middlewares/form.go @@ -2,18 +2,22 @@ package middlewares import ( "fmt" - "github.com/gin-gonic/gin" "github.com/go-go-golems/glazed/pkg/cmds/layers" "github.com/go-go-golems/glazed/pkg/cmds/middlewares" "github.com/go-go-golems/glazed/pkg/cmds/parameters" "github.com/go-go-golems/glazed/pkg/helpers/cast" + "github.com/labstack/echo/v4" "github.com/pkg/errors" ) -func getListParameterFromForm(c *gin.Context, p *parameters.ParameterDefinition, options ...parameters.ParseStepOption) (*parameters.ParsedParameter, error) { +func getListParameterFromForm(c echo.Context, p *parameters.ParameterDefinition, options ...parameters.ParseStepOption) (*parameters.ParsedParameter, error) { if p.Type.IsList() { // check p.Name[] parameter - values, ok := c.GetPostFormArray(fmt.Sprintf("%s[]", p.Name)) + values_, err := c.FormParams() + if err != nil { + return nil, err + } + values, ok := values_[fmt.Sprintf("%s[]", p.Name)] if ok { pValue, err := p.ParseParameter(values, options...) if err != nil { @@ -28,7 +32,7 @@ func getListParameterFromForm(c *gin.Context, p *parameters.ParameterDefinition, } } -func getFileParameterFromForm(c *gin.Context, p *parameters.ParameterDefinition) (interface{}, error) { +func getFileParameterFromForm(c echo.Context, p *parameters.ParameterDefinition) (interface{}, error) { form, err := c.MultipartForm() if err != nil { return nil, err @@ -105,7 +109,7 @@ func getFileParameterFromForm(c *gin.Context, p *parameters.ParameterDefinition) return v, nil } -func UpdateFromFormQuery(c *gin.Context, options ...parameters.ParseStepOption) middlewares.Middleware { +func UpdateFromFormQuery(c echo.Context, options ...parameters.ParseStepOption) middlewares.Middleware { return func(next middlewares.HandlerFunc) middlewares.HandlerFunc { return func(layers_ *layers.ParameterLayers, parsedLayers *layers.ParsedLayers) error { err := layers_.ForEachE(func(_ string, l layers.ParameterLayer) error { @@ -141,9 +145,9 @@ func UpdateFromFormQuery(c *gin.Context, options ...parameters.ParseStepOption) return nil } - value, ok := c.GetPostForm(p.Name) + value := c.FormValue(p.Name) // TODO(manuel, 2023-02-28) is this enough to check if a file is missing? - if !ok { + if value == "" { if p.Required { return fmt.Errorf("required parameter '%s' is missing", p.Name) } diff --git a/pkg/glazed/middlewares/form_test.go b/pkg/glazed/middlewares/form_test.go index 02e541f..d02cbfc 100644 --- a/pkg/glazed/middlewares/form_test.go +++ b/pkg/glazed/middlewares/form_test.go @@ -3,13 +3,14 @@ package middlewares import ( _ "embed" "github.com/go-go-golems/parka/pkg/utils" + "github.com/labstack/echo/v4" + "net/http/httptest" "testing" "github.com/go-go-golems/glazed/pkg/cmds/helpers" "github.com/go-go-golems/glazed/pkg/cmds/layers" "github.com/go-go-golems/glazed/pkg/helpers/yaml" - "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -35,17 +36,21 @@ func TestUpdateFromFormQuery(t *testing.T) { for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { - // Create a mock gin.Context with the multipart form data - gin.SetMode(gin.TestMode) - c, _ := utils.MockGinContextWithMultipartForm(tt.Form) + req, err := utils.NewRequestWithMultipartForm(tt.Form) + require.NoError(t, err) // Create ParameterLayers and ParsedLayers from test definitions layers_ := helpers.NewTestParameterLayers(tt.ParameterLayers) parsedLayers := helpers.NewTestParsedLayers(layers_, tt.ParsedLayers...) + resp := httptest.NewRecorder() + e := echo.New() + + c := e.NewContext(req, resp) + // Create the middleware and execute it middleware := UpdateFromFormQuery(c) - err := middleware(func(layers_ *layers.ParameterLayers, parsedLayers *layers.ParsedLayers) error { + err = middleware(func(layers_ *layers.ParameterLayers, parsedLayers *layers.ParsedLayers) error { return nil })(layers_, parsedLayers) diff --git a/pkg/glazed/middlewares/query.go b/pkg/glazed/middlewares/query.go index ba47ebb..b2c1622 100644 --- a/pkg/glazed/middlewares/query.go +++ b/pkg/glazed/middlewares/query.go @@ -2,14 +2,14 @@ package middlewares import ( "fmt" - "github.com/gin-gonic/gin" "github.com/go-go-golems/glazed/pkg/cmds/layers" "github.com/go-go-golems/glazed/pkg/cmds/middlewares" "github.com/go-go-golems/glazed/pkg/cmds/parameters" + "github.com/labstack/echo/v4" "strings" ) -func UpdateFromQueryParameters(c *gin.Context, options ...parameters.ParseStepOption) middlewares.Middleware { +func UpdateFromQueryParameters(c echo.Context, options ...parameters.ParseStepOption) middlewares.Middleware { return func(next middlewares.HandlerFunc) middlewares.HandlerFunc { return func(layers_ *layers.ParameterLayers, parsedLayers *layers.ParsedLayers) error { err := next(layers_, parsedLayers) @@ -28,7 +28,7 @@ func UpdateFromQueryParameters(c *gin.Context, options ...parameters.ParseStepOp if p.Type.IsList() { // check p.Name[] parameter - values, ok := c.GetQueryArray(fmt.Sprintf("%s[]", p.Name)) + values, ok := c.QueryParams()[fmt.Sprintf("%s[]", p.Name)] if ok { // TODO(manuel, 2023-12-25) Need to pass in options to ParseParameter pp, err := p.ParseParameter(values, options...) @@ -39,8 +39,8 @@ func UpdateFromQueryParameters(c *gin.Context, options ...parameters.ParseStepOp return nil } } - value, ok := c.GetQuery(p.Name) - if !ok || value == "" { + value := c.QueryParam(p.Name) + if value == "" { if p.Required { return fmt.Errorf("required parameter '%s' is missing", p.Name) } diff --git a/pkg/glazed/middlewares/query_test.go b/pkg/glazed/middlewares/query_test.go index 9b6ba28..4fbedd8 100644 --- a/pkg/glazed/middlewares/query_test.go +++ b/pkg/glazed/middlewares/query_test.go @@ -6,9 +6,10 @@ import ( "github.com/go-go-golems/glazed/pkg/cmds/layers" "github.com/go-go-golems/glazed/pkg/helpers/yaml" "github.com/go-go-golems/parka/pkg/utils" + "github.com/labstack/echo/v4" + "net/http/httptest" "testing" - "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -34,14 +35,17 @@ func TestUpdateFromQueryParameters(t *testing.T) { for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { - // Create a mock gin.Context with the multipart form data - gin.SetMode(gin.TestMode) - c, _ := utils.MockGinContextWithQueryParameters(tt.QueryParameters) + req := utils.NewRequestWithQueryParameters(tt.QueryParameters) // Create ParameterLayers and ParsedLayers from test definitions layers_ := helpers.NewTestParameterLayers(tt.ParameterLayers) parsedLayers := helpers.NewTestParsedLayers(layers_, tt.ParsedLayers...) + resp := httptest.NewRecorder() + e := echo.New() + e.ServeHTTP(resp, req) + c := e.NewContext(req, resp) + // Create the middleware and execute it middleware := UpdateFromQueryParameters(c) err := middleware(func(layers_ *layers.ParameterLayers, parsedLayers *layers.ParsedLayers) error { diff --git a/pkg/handlers/command-dir/command-dir.go b/pkg/handlers/command-dir/command-dir.go index db77a6a..0451311 100644 --- a/pkg/handlers/command-dir/command-dir.go +++ b/pkg/handlers/command-dir/command-dir.go @@ -33,142 +33,60 @@ package command_dir // returns an error. import ( + "context" "fmt" - "github.com/gin-gonic/gin" "github.com/go-go-golems/clay/pkg/repositories" - "github.com/go-go-golems/glazed/pkg/cmds" "github.com/go-go-golems/parka/pkg/glazed/handlers/datatables" - "github.com/go-go-golems/parka/pkg/glazed/handlers/json" - output_file "github.com/go-go-golems/parka/pkg/glazed/handlers/output-file" - "github.com/go-go-golems/parka/pkg/glazed/handlers/sse" - "github.com/go-go-golems/parka/pkg/glazed/handlers/text" "github.com/go-go-golems/parka/pkg/handlers/config" + "github.com/go-go-golems/parka/pkg/handlers/generic-command" "github.com/go-go-golems/parka/pkg/render" parka "github.com/go-go-golems/parka/pkg/server" "github.com/pkg/errors" "os" - "path/filepath" - "strings" ) type CommandDirHandler struct { - DevMode bool + generic_command.GenericCommandHandler - // TemplateName is the name of the template that is lookup up through the given TemplateLookup - // used to render the glazed command. - TemplateName string - // IndexTemplateName is the name of the template that is looked up through TemplateLookup to render - // command indexes. Leave empty to not render index pages at all. - IndexTemplateName string - // TemplateLookup is used to look up both TemplateName and IndexTemplateName - TemplateLookup render.TemplateLookup + DevMode bool // Repository is the command repository that is exposed over HTTP through this handler. Repository *repositories.Repository - - // AdditionalData is passed to the template being rendered. - AdditionalData map[string]interface{} - - ParameterFilter *config.ParameterFilter - - // If true, all glazed outputs will try to use a row output if possible. - // This means that "ragged" objects (where columns might not all be present) - // will have missing columns, only the fields of the first object will be used - // as rows. - // - // This is true per default, and needs to be explicitly set to false to use - // a normal TableMiddleware oriented output. - Stream bool } type CommandDirHandlerOption func(handler *CommandDirHandler) -func WithTemplateName(name string) CommandDirHandlerOption { - return func(handler *CommandDirHandler) { - handler.TemplateName = name - } -} - -func WithParameterFilter(overridesAndDefaults *config.ParameterFilter) CommandDirHandlerOption { - return func(handler *CommandDirHandler) { - handler.ParameterFilter = overridesAndDefaults - } -} - -func WithParameterFilterOptions(opts ...config.ParameterFilterOption) CommandDirHandlerOption { - return func(handler *CommandDirHandler) { - for _, opt := range opts { - opt(handler.ParameterFilter) - } - } -} - -func WithDefaultTemplateName(name string) CommandDirHandlerOption { - return func(handler *CommandDirHandler) { - if handler.TemplateName == "" { - handler.TemplateName = name - } - } -} - -func WithIndexTemplateName(name string) CommandDirHandlerOption { +func WithDevMode(devMode bool) CommandDirHandlerOption { return func(handler *CommandDirHandler) { - handler.IndexTemplateName = name + handler.DevMode = devMode } } -func WithDefaultIndexTemplateName(name string) CommandDirHandlerOption { +func WithRepository(r *repositories.Repository) CommandDirHandlerOption { return func(handler *CommandDirHandler) { - if handler.IndexTemplateName == "" { - handler.IndexTemplateName = name - } + handler.Repository = r } } -// WithMergeAdditionalData merges the passed in map with the handler's AdditionalData map. -// If a value is already set in the AdditionalData map and override is true, it will get overwritten. -func WithMergeAdditionalData(data map[string]interface{}, override bool) CommandDirHandlerOption { +func WithGenericCommandHandlerOptions(options ...generic_command.GenericCommandHandlerOption) CommandDirHandlerOption { return func(handler *CommandDirHandler) { - if handler.AdditionalData == nil { - handler.AdditionalData = data - } else { - for k, v := range data { - if _, ok := handler.AdditionalData[k]; !ok || override { - handler.AdditionalData[k] = v - } - } + for _, option := range options { + option(&handler.GenericCommandHandler) } } } -func WithTemplateLookup(lookup render.TemplateLookup) CommandDirHandlerOption { - return func(handler *CommandDirHandler) { - handler.TemplateLookup = lookup - } -} - -// handling all the ways to configure overrides -func WithDevMode(devMode bool) CommandDirHandlerOption { - return func(handler *CommandDirHandler) { - handler.DevMode = devMode - } -} - -func WithRepository(r *repositories.Repository) CommandDirHandlerOption { - return func(handler *CommandDirHandler) { - handler.Repository = r - } -} - func NewCommandDirHandlerFromConfig( config_ *config.CommandDir, options ...CommandDirHandlerOption, ) (*CommandDirHandler, error) { + genericOptions := []generic_command.GenericCommandHandlerOption{ + generic_command.WithTemplateName(config_.TemplateName), + generic_command.WithIndexTemplateName(config_.IndexTemplateName), + generic_command.WithMergeAdditionalData(config_.AdditionalData, true), + } cd := &CommandDirHandler{ - TemplateName: config_.TemplateName, - IndexTemplateName: config_.IndexTemplateName, - AdditionalData: config_.AdditionalData, - ParameterFilter: &config.ParameterFilter{}, + GenericCommandHandler: *generic_command.NewGenericCommandHandler(genericOptions...), } cd.ParameterFilter.Overrides = config_.Overrides @@ -216,191 +134,14 @@ func NewCommandDirHandlerFromConfig( return cd, nil } -func (cd *CommandDirHandler) Serve(server *parka.Server, path string) error { - if cd.Repository == nil { - return fmt.Errorf("no repository configured") - } - - path = strings.TrimSuffix(path, "/") - - middlewares_ := cd.ParameterFilter.ComputeMiddlewares(cd.Stream) - server.Router.GET(path+"/data/*path", func(c *gin.Context) { - commandPath := c.Param("path") - commandPath = strings.TrimPrefix(commandPath, "/") - command, ok := getRepositoryCommand(c, cd.Repository, commandPath) - if !ok { - c.JSON(404, gin.H{"error": fmt.Sprintf("command %s not found", commandPath)}) - return - } - - switch v := command.(type) { - case cmds.GlazeCommand: - json.CreateJSONQueryHandler(v, json.WithMiddlewares(middlewares_...))(c) - default: - text.CreateQueryHandler(v)(c) - } - }) - - server.Router.GET(path+"/text/*path", func(c *gin.Context) { - commandPath := c.Param("path") - commandPath = strings.TrimPrefix(commandPath, "/") - command, ok := getRepositoryCommand(c, cd.Repository, commandPath) - if !ok { - c.JSON(404, gin.H{"error": fmt.Sprintf("command %s not found", commandPath)}) - return - } - - text.CreateQueryHandler(command, middlewares_...)(c) - }) - - server.Router.GET(path+"/streaming/*path", func(c *gin.Context) { - commandPath := c.Param("path") - commandPath = strings.TrimPrefix(commandPath, "/") - command, ok := getRepositoryCommand(c, cd.Repository, commandPath) - if !ok { - c.JSON(404, gin.H{"error": fmt.Sprintf("command %s not found", commandPath)}) - return - } - - sse.CreateQueryHandler(command, middlewares_...)(c) - }) - - server.Router.GET(path+"/datatables/*path", - func(c *gin.Context) { - commandPath := c.Param("path") - commandPath = strings.TrimPrefix(commandPath, "/") - - // Get repository command - command, ok := getRepositoryCommand(c, cd.Repository, commandPath) - if !ok { - c.JSON(404, gin.H{"error": fmt.Sprintf("command %s not found", commandPath)}) - return - } - switch v := command.(type) { - case cmds.GlazeCommand: - options := []datatables.QueryHandlerOption{ - datatables.WithMiddlewares(middlewares_...), - datatables.WithTemplateLookup(cd.TemplateLookup), - datatables.WithTemplateName(cd.TemplateName), - datatables.WithAdditionalData(cd.AdditionalData), - datatables.WithStreamRows(cd.Stream), - } - - datatables.CreateDataTablesHandler(v, path, commandPath, options...)(c) - default: - c.JSON(500, gin.H{"error": fmt.Sprintf("command %s is not a glazed command", commandPath)}) - } - }) - - server.Router.GET(path+"/download/*path", func(c *gin.Context) { - path_ := c.Param("path") - path_ = strings.TrimPrefix(path_, "/") - index := strings.LastIndex(path_, "/") - if index == -1 { - c.JSON(500, gin.H{"error": "could not find file name"}) - return - } - if index >= len(path_)-1 { - c.JSON(500, gin.H{"error": "could not find file name"}) - return - } - fileName := path_[index+1:] - - commandPath := strings.TrimPrefix(path_[:index], "/") - command, ok := getRepositoryCommand(c, cd.Repository, commandPath) - if !ok { - // JSON output and error code already handled by getRepositoryCommand - return - } - - switch v := command.(type) { - case cmds.GlazeCommand: - output_file.CreateGlazedFileHandler( - v, - fileName, - middlewares_..., - )(c) - - case cmds.WriterCommand: - handler := text.NewQueryHandler(command) - - baseName := filepath.Base(fileName) - c.Writer.Header().Set("Content-Disposition", "attachment; filename="+baseName) - - err := handler.Handle(c, c.Writer) - if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return - } - - default: - c.JSON(500, gin.H{"error": fmt.Sprintf("command %s is not a glazed/writer command", commandPath)}) - } - }) - - server.Router.GET(path+"/commands/*path", func(c *gin.Context) { - path_ := c.Param("path") - path_ = strings.TrimPrefix(path_, "/") - path_ = strings.TrimSuffix(path_, "/") - splitPath := strings.Split(path_, "/") - if path_ == "" { - splitPath = []string{} - } - renderNode, ok := cd.Repository.GetRenderNode(splitPath) - if !ok { - c.JSON(404, gin.H{"error": fmt.Sprintf("command %s not found", path_)}) - return - } - templateName := cd.IndexTemplateName - if cd.IndexTemplateName == "" { - templateName = "commands.tmpl.html" - } - templ, err := cd.TemplateLookup.Lookup(templateName) - if err != nil { - c.JSON(500, gin.H{"error": errors.Wrapf(err, "could not load index template").Error()}) - return - } - - var nodes []*repositories.RenderNode - - if renderNode.Command != nil { - nodes = append(nodes, renderNode) - } else { - nodes = append(nodes, renderNode.Children...) - } - err = templ.Execute(c.Writer, gin.H{ - "nodes": nodes, - "path": path, - }) - if err != nil { - c.JSON(500, gin.H{"error": errors.Wrapf(err, "could not execute index template").Error()}) - return - } - }) - - return nil +func (cd *CommandDirHandler) Watch(ctx context.Context) error { + return cd.Repository.Watch(ctx) } -// getRepositoryCommand lookups a command in the given repository and return success as bool and the given command, -// or sends an error code over HTTP using the gin.Context. -func getRepositoryCommand(c *gin.Context, r *repositories.Repository, commandPath string) ( - cmds.Command, - bool, -) { - path := strings.Split(commandPath, "/") - commands := r.CollectCommands(path, false) - if len(commands) == 0 { - c.JSON(404, gin.H{"error": fmt.Sprintf("command %s not found", commandPath)}) - return nil, false - } - - if len(commands) > 1 { - c.JSON(404, gin.H{"error": fmt.Sprintf("ambiguous command %s", commandPath)}) - return nil, false +func (cd *CommandDirHandler) Serve(server *parka.Server, basePath string) error { + if cd.Repository == nil { + return fmt.Errorf("no repository configured") } - // NOTE(manuel, 2023-05-15) Check if this is actually an alias, and populate the defaults from the alias flags - // This could potentially be moved to the repository code itself - - return commands[0], true + return cd.GenericCommandHandler.ServeRepository(server, basePath, cd.Repository) } diff --git a/pkg/handlers/command/command.go b/pkg/handlers/command/command.go index f4fd0d8..8e8b4bd 100644 --- a/pkg/handlers/command/command.go +++ b/pkg/handlers/command/command.go @@ -1,14 +1,12 @@ package command import ( - "github.com/gin-gonic/gin" "github.com/go-go-golems/glazed/pkg/cmds" "github.com/go-go-golems/glazed/pkg/cmds/alias" "github.com/go-go-golems/glazed/pkg/cmds/loaders" "github.com/go-go-golems/parka/pkg/glazed/handlers/datatables" - "github.com/go-go-golems/parka/pkg/glazed/handlers/json" - output_file "github.com/go-go-golems/parka/pkg/glazed/handlers/output-file" "github.com/go-go-golems/parka/pkg/handlers/config" + generic_command "github.com/go-go-golems/parka/pkg/handlers/generic-command" "github.com/go-go-golems/parka/pkg/render" parka "github.com/go-go-golems/parka/pkg/server" "github.com/pkg/errors" @@ -17,30 +15,11 @@ import ( ) type CommandHandler struct { + generic_command.GenericCommandHandler DevMode bool - // TemplateName is the name of the template that is lookup up through the given TemplateLookup - // used to render the glazed command. - TemplateName string - // TemplateLookup is used to look up both TemplateName and IndexTemplateName - TemplateLookup render.TemplateLookup - // can be any of BareCommand, WriterCommand or GlazeCommand - Command cmds.GlazeCommand - - // AdditionalData is passed to the template being rendered. - AdditionalData map[string]interface{} - - ParameterFilter *config.ParameterFilter - - // If true, all glazed outputs will try to use a row output if possible. - // This means that "ragged" objects (where columns might not all be present) - // will have missing columns, only the fields of the first object will be used - // as rows. - // - // This is true per default, and needs to be explicitly set to false to use - // a normal TableMiddleware oriented output. - Stream bool + Command cmds.Command } type CommandHandlerOption func(*CommandHandler) @@ -51,65 +30,21 @@ func WithDevMode(devMode bool) CommandHandlerOption { } } -func WithTemplateName(templateName string) CommandHandlerOption { - return func(handler *CommandHandler) { - handler.TemplateName = templateName - } -} - -func WithDefaultTemplateName(defaultTemplateName string) CommandHandlerOption { +func WithGenericCommandHandlerOptions(options ...generic_command.GenericCommandHandlerOption) CommandHandlerOption { return func(handler *CommandHandler) { - if handler.TemplateName == "" { - handler.TemplateName = defaultTemplateName - } - } -} - -func WithTemplateLookup(templateLookup render.TemplateLookup) CommandHandlerOption { - return func(handler *CommandHandler) { - handler.TemplateLookup = templateLookup - } -} - -// WithMergeAdditionalData merges the passed in map with the handler's AdditionalData map. -// If a value is already set in the AdditionalData map and override is true, it will get overwritten. -func WithMergeAdditionalData(data map[string]interface{}, override bool) CommandHandlerOption { - return func(handler *CommandHandler) { - if handler.AdditionalData == nil { - handler.AdditionalData = data - } else { - for k, v := range data { - if _, ok := handler.AdditionalData[k]; !ok || override { - handler.AdditionalData[k] = v - } - } - } - } -} - -func WithParameterFilter(parameterFilter *config.ParameterFilter) CommandHandlerOption { - return func(handler *CommandHandler) { - handler.ParameterFilter = parameterFilter - } -} - -func WithParameterFilterOptions(opts ...config.ParameterFilterOption) CommandHandlerOption { - return func(handler *CommandHandler) { - for _, opt := range opts { - opt(handler.ParameterFilter) + for _, option := range options { + option(&handler.GenericCommandHandler) } } } func NewCommandHandler( - command cmds.GlazeCommand, + command cmds.Command, options ...CommandHandlerOption, ) *CommandHandler { c := &CommandHandler{ - Command: command, - TemplateName: "", - AdditionalData: map[string]interface{}{}, - ParameterFilter: &config.ParameterFilter{}, + GenericCommandHandler: *generic_command.NewGenericCommandHandler(), + Command: command, } for _, opt := range options { @@ -119,24 +54,14 @@ func NewCommandHandler( return c } -func NewCommandHandlerFromConfig( - config_ *config.Command, - loader loaders.CommandLoader, - options ...CommandHandlerOption, -) (*CommandHandler, error) { - c := &CommandHandler{ - TemplateName: config_.TemplateName, - AdditionalData: config_.AdditionalData, - ParameterFilter: &config.ParameterFilter{}, - } - - fs_, filePath, err := loaders.FileNameToFsFilePath(config_.File) +func LoadCommandFromFile(path string, loader loaders.CommandLoader) (cmds.Command, error) { + fs_, filePath, err := loaders.FileNameToFsFilePath(path) if err != nil { return nil, errors.Wrap(err, "failed to get absolute path") } cmds_, err := loaders.LoadCommandsFromFS( - fs_, filePath, config_.File, + fs_, filePath, path, loader, []cmds.CommandDescriptionOption{}, []alias.Option{}) if err != nil { return nil, errors.Wrap(err, "failed to load commands from file") @@ -171,7 +96,27 @@ func NewCommandHandlerFromConfig( ) } - c.Command = allCmds[0] + return allCmds[0], nil +} + +func NewCommandHandlerFromConfig( + config_ *config.Command, + loader loaders.CommandLoader, + options ...CommandHandlerOption, +) (*CommandHandler, error) { + genericOptions := []generic_command.GenericCommandHandlerOption{ + generic_command.WithTemplateName(config_.TemplateName), + generic_command.WithMergeAdditionalData(config_.AdditionalData, true), + } + // TODO(manuel, 2024-05-09) To make this reloadable on dev mode, we would actually need to thunk this and pass the thunk to the GenericCommandHandler + cmd, err := LoadCommandFromFile(config_.File, loader) + if err != nil { + return nil, err + } + + c := NewCommandHandler(cmd, WithGenericCommandHandlerOptions(genericOptions...)) + // TODO(manuel, 2024-05-09) Handle devmode + c.Command = cmd // TODO(manuel, 2023-08-06) I think we hav eto find the proper command here @@ -193,6 +138,9 @@ func NewCommandHandlerFromConfig( option(c) } + // TODO(manuel, 2024-05-09) The TemplateLookup initialization probably makes sense to be extracted as a reusable component + // to load templates dynamically in dev mode, since it's shared across quite a few handlers (see command-dir). + // we run this after the options in order to get the DevMode value if c.TemplateLookup == nil { if config_.TemplateLookup != nil { @@ -226,46 +174,5 @@ func NewCommandHandlerFromConfig( func (ch *CommandHandler) Serve(server *parka.Server, path string) error { path = strings.TrimSuffix(path, "/") - middlewares_ := ch.ParameterFilter.ComputeMiddlewares(ch.Stream) - - server.Router.GET(path+"/data", func(c *gin.Context) { - options := []json.QueryHandlerOption{ - json.WithMiddlewares(middlewares_...), - } - json.CreateJSONQueryHandler(ch.Command, options...)(c) - }) - // TODO(manuel, 2024-01-17) This doesn't seem to match what is in command-dir - server.Router.GET(path+"/glazed", func(c *gin.Context) { - options := []datatables.QueryHandlerOption{ - datatables.WithMiddlewares(middlewares_...), - datatables.WithTemplateLookup(ch.TemplateLookup), - datatables.WithTemplateName(ch.TemplateName), - datatables.WithAdditionalData(ch.AdditionalData), - datatables.WithStreamRows(ch.Stream), - } - - datatables.CreateDataTablesHandler(ch.Command, path, "", options...)(c) - }) - server.Router.GET(path+"/download/*path", func(c *gin.Context) { - path_ := c.Param("path") - path_ = strings.TrimPrefix(path_, "/") - index := strings.LastIndex(path_, "/") - if index == -1 { - c.JSON(500, gin.H{"error": "could not find file name"}) - return - } - if index >= len(path_)-1 { - c.JSON(500, gin.H{"error": "could not find file name"}) - return - } - fileName := path_[index+1:] - - output_file.CreateGlazedFileHandler( - ch.Command, - fileName, - middlewares_..., - )(c) - }) - - return nil + return ch.GenericCommandHandler.ServeSingleCommand(server, path, ch.Command) } diff --git a/pkg/handlers/config-file.go b/pkg/handlers/config-file.go index 726187b..36aa56c 100644 --- a/pkg/handlers/config-file.go +++ b/pkg/handlers/config-file.go @@ -8,6 +8,7 @@ import ( "github.com/go-go-golems/glazed/pkg/cmds/loaders" "github.com/go-go-golems/glazed/pkg/help" "github.com/go-go-golems/glazed/pkg/helpers/strings" + "github.com/go-go-golems/parka/pkg/handlers/command" "github.com/go-go-golems/parka/pkg/handlers/command-dir" "github.com/go-go-golems/parka/pkg/handlers/config" "github.com/go-go-golems/parka/pkg/handlers/static-dir" @@ -45,6 +46,7 @@ func NewRepositoryFactoryFromReaderLoaders( FS: os.DirFS(dir), RootDirectory: ".", RootDocDirectory: "doc", + WatchDirectory: dir, Name: dir, SourcePrefix: "file", }) @@ -93,6 +95,7 @@ type ConfigFileHandler struct { CommandDirectoryOptions []command_dir.CommandDirHandlerOption TemplateDirectoryOptions []template_dir.TemplateDirHandlerOption TemplateOptions []template.TemplateHandlerOption + CommandOptions []command.CommandHandlerOption // ConfigFileLocation is an optional path to the config file on disk in case it needs to be reloaded ConfigFileLocation string @@ -123,6 +126,12 @@ func WithAppendTemplateDirHandlerOptions(options ...template_dir.TemplateDirHand } } +func WithAppendCommandHandlerOptions(options ...command.CommandHandlerOption) ConfigFileHandlerOption { + return func(handler *ConfigFileHandler) { + handler.CommandOptions = append(handler.CommandOptions, options...) + } +} + func WithAppendTemplateHandlerOptions(options ...template.TemplateHandlerOption) ConfigFileHandlerOption { return func(handler *ConfigFileHandler) { handler.TemplateOptions = append(handler.TemplateOptions, options...) @@ -300,11 +309,7 @@ func (cfh *ConfigFileHandler) Serve(server_ *server.Server) error { } if route.Template != nil { - th, err := template.NewTemplateHandlerFromConfig( - route.Path, - route.Template, - cfh.TemplateOptions..., - ) + th, err := template.NewTemplateHandlerFromConfig(route.Template, cfh.TemplateOptions...) if err != nil { return err } @@ -369,12 +374,9 @@ func (cfh *ConfigFileHandler) Serve(server_ *server.Server) error { func (cfh *ConfigFileHandler) Watch(ctx context.Context) error { errGroup, ctx2 := errgroup.WithContext(ctx) for _, cdh := range cfh.commandDirectoryHandlers { - cdh2 := cdh - if cdh.Repository == nil { - continue - } + cdh_ := cdh errGroup.Go(func() error { - return cdh2.Repository.Watch(ctx2) + return cdh_.Watch(ctx2) }) } diff --git a/pkg/handlers/generic-command/generic.go b/pkg/handlers/generic-command/generic.go new file mode 100644 index 0000000..a903d6c --- /dev/null +++ b/pkg/handlers/generic-command/generic.go @@ -0,0 +1,346 @@ +package generic_command + +import ( + "fmt" + "github.com/go-go-golems/clay/pkg/repositories" + "github.com/go-go-golems/glazed/pkg/cmds" + "github.com/go-go-golems/glazed/pkg/cmds/middlewares" + "github.com/go-go-golems/parka/pkg/glazed/handlers/datatables" + "github.com/go-go-golems/parka/pkg/glazed/handlers/json" + output_file "github.com/go-go-golems/parka/pkg/glazed/handlers/output-file" + "github.com/go-go-golems/parka/pkg/glazed/handlers/sse" + "github.com/go-go-golems/parka/pkg/glazed/handlers/text" + "github.com/go-go-golems/parka/pkg/handlers/config" + "github.com/go-go-golems/parka/pkg/render" + parka "github.com/go-go-golems/parka/pkg/server" + "github.com/go-go-golems/parka/pkg/utils" + "github.com/labstack/echo/v4" + "net/http" + "path/filepath" + "strings" +) + +type GenericCommandHandler struct { + // If true, all glazed outputs will try to use a row output if possible. + // This means that "ragged" objects (where columns might not all be present) + // will have missing columns, only the fields of the first object will be used + // as rows. + // + // This is true per default, and needs to be explicitly set to false to use + // a normal TableMiddleware oriented output. + Stream bool + + // AdditionalData is passed to the template being rendered. + AdditionalData map[string]interface{} + + ParameterFilter *config.ParameterFilter + + // TemplateName is the name of the template that is lookup up through the given TemplateLookup + // used to render the glazed command. + TemplateName string + // IndexTemplateName is the name of the template that is looked up through TemplateLookup to render + // command indexes. Leave empty to not render index pages at all. + IndexTemplateName string + // TemplateLookup is used to look up both TemplateName and IndexTemplateName + TemplateLookup render.TemplateLookup + + // path under which the command handler is served + BasePath string + + middlewares []middlewares.Middleware +} + +func NewGenericCommandHandler(options ...GenericCommandHandlerOption) *GenericCommandHandler { + handler := &GenericCommandHandler{ + AdditionalData: map[string]interface{}{}, + ParameterFilter: &config.ParameterFilter{}, + } + + for _, opt := range options { + opt(handler) + } + + return handler +} + +type GenericCommandHandlerOption func(handler *GenericCommandHandler) + +func WithTemplateName(name string) GenericCommandHandlerOption { + return func(handler *GenericCommandHandler) { + handler.TemplateName = name + } +} + +func WithParameterFilter(overridesAndDefaults *config.ParameterFilter) GenericCommandHandlerOption { + return func(handler *GenericCommandHandler) { + handler.ParameterFilter = overridesAndDefaults + } +} + +func WithParameterFilterOptions(opts ...config.ParameterFilterOption) GenericCommandHandlerOption { + return func(handler *GenericCommandHandler) { + for _, opt := range opts { + opt(handler.ParameterFilter) + } + } +} + +func WithDefaultTemplateName(name string) GenericCommandHandlerOption { + return func(handler *GenericCommandHandler) { + if handler.TemplateName == "" { + handler.TemplateName = name + } + } +} + +func WithIndexTemplateName(name string) GenericCommandHandlerOption { + return func(handler *GenericCommandHandler) { + handler.IndexTemplateName = name + } +} + +func WithDefaultIndexTemplateName(name string) GenericCommandHandlerOption { + return func(handler *GenericCommandHandler) { + if handler.IndexTemplateName == "" { + handler.IndexTemplateName = name + } + } +} + +// WithMergeAdditionalData merges the passed in map with the handler's AdditionalData map. +// If a value is already set in the AdditionalData map and override is true, it will get overwritten. +func WithMergeAdditionalData(data map[string]interface{}, override bool) GenericCommandHandlerOption { + return func(handler *GenericCommandHandler) { + if handler.AdditionalData == nil { + handler.AdditionalData = data + } else { + for k, v := range data { + if _, ok := handler.AdditionalData[k]; !ok || override { + handler.AdditionalData[k] = v + } + } + } + } +} + +func WithTemplateLookup(lookup render.TemplateLookup) GenericCommandHandlerOption { + return func(handler *GenericCommandHandler) { + handler.TemplateLookup = lookup + } +} + +func (gch *GenericCommandHandler) ServeSingleCommand(server *parka.Server, basePath string, command cmds.Command) error { + gch.BasePath = basePath + + gch.middlewares = gch.ParameterFilter.ComputeMiddlewares(gch.Stream) + server.Router.GET(basePath+"/data", func(c echo.Context) error { + return gch.ServeData(c, command) + }) + server.Router.GET(basePath+"/text", func(c echo.Context) error { + return gch.ServeText(c, command) + }) + server.Router.GET(basePath+"/stream", func(c echo.Context) error { + return gch.ServeStreaming(c, command) + }) + server.Router.GET(basePath+"/download/*", func(c echo.Context) error { + return gch.ServeDownload(c, command) + }) + // don't use a specific datatables path here + server.Router.GET(basePath, func(c echo.Context) error { + return gch.ServeDataTables(c, command, basePath+"/download") + }) + + return nil +} + +func (gch *GenericCommandHandler) ServeRepository(server *parka.Server, basePath string, repository *repositories.Repository) error { + basePath = strings.TrimSuffix(basePath, "/") + gch.BasePath = basePath + + gch.middlewares = gch.ParameterFilter.ComputeMiddlewares(gch.Stream) + + server.Router.GET(basePath+"/data/*", func(c echo.Context) error { + commandPath := c.Param("*") + commandPath = strings.TrimPrefix(commandPath, "/") + command, err := getRepositoryCommand(repository, commandPath) + if err != nil { + return err + } + + return gch.ServeData(c, command) + }) + + server.Router.GET(basePath+"/text/*", func(c echo.Context) error { + commandPath := c.Param("*") + commandPath = strings.TrimPrefix(commandPath, "/") + command, err := getRepositoryCommand(repository, commandPath) + if err != nil { + return err + } + + return gch.ServeText(c, command) + }) + + server.Router.GET(basePath+"/streaming/*", func(c echo.Context) error { + commandPath := c.Param("*") + commandPath = strings.TrimPrefix(commandPath, "/") + command, err := getRepositoryCommand(repository, commandPath) + if err != nil { + return err + } + + return gch.ServeStreaming(c, command) + }) + + server.Router.GET(basePath+"/datatables/*", func(c echo.Context) error { + commandPath := c.Param("*") + commandPath = strings.TrimPrefix(commandPath, "/") + + // Get repository command + command, err := getRepositoryCommand(repository, commandPath) + if err != nil { + return err + } + + return gch.ServeDataTables(c, command, basePath+"/download/"+commandPath) + }) + + server.Router.GET(basePath+"/download/*", func(c echo.Context) error { + commandPath := c.Param("*") + commandPath = strings.TrimPrefix(commandPath, "/") + // strip file name from path + index := strings.LastIndex(commandPath, "/") + if index == -1 { + return c.JSON(http.StatusInternalServerError, utils.H{"error": "could not find file name"}) + } + if index >= len(commandPath)-1 { + return c.JSON(http.StatusInternalServerError, utils.H{"error": "could not find file name"}) + } + commandPath = commandPath[:index] + + command, err := getRepositoryCommand(repository, commandPath) + if err != nil { + return err + } + + return gch.ServeDownload(c, command) + }) + + return nil +} + +func (gch *GenericCommandHandler) ServeData(c echo.Context, command cmds.Command) error { + switch v := command.(type) { + case cmds.GlazeCommand: + return json.CreateJSONQueryHandler(v, json.WithMiddlewares(gch.middlewares...))(c) + default: + return text.CreateQueryHandler(v)(c) + } +} + +func (gch *GenericCommandHandler) ServeText(c echo.Context, command cmds.Command) error { + return text.CreateQueryHandler(command, gch.middlewares...)(c) +} + +func (gch *GenericCommandHandler) ServeStreaming(c echo.Context, command cmds.Command) error { + return sse.CreateQueryHandler(command, gch.middlewares...)(c) +} + +func (gch *GenericCommandHandler) ServeDataTables(c echo.Context, command cmds.Command, downloadPath string) error { + switch v := command.(type) { + case cmds.GlazeCommand: + options := []datatables.QueryHandlerOption{ + datatables.WithMiddlewares(gch.middlewares...), + datatables.WithTemplateLookup(gch.TemplateLookup), + datatables.WithTemplateName(gch.TemplateName), + datatables.WithAdditionalData(gch.AdditionalData), + datatables.WithStreamRows(gch.Stream), + } + + return datatables.CreateDataTablesHandler(v, gch.BasePath, downloadPath, options...)(c) + default: + return c.JSON(http.StatusInternalServerError, utils.H{"error": "command is not a glazed command"}) + } +} + +func (gch *GenericCommandHandler) ServeDownload(c echo.Context, command cmds.Command) error { + path_ := c.Request().URL.Path + index := strings.LastIndex(path_, "/") + if index == -1 { + return c.JSON(http.StatusInternalServerError, utils.H{"error": "could not find file name"}) + } + if index >= len(path_)-1 { + return c.JSON(http.StatusInternalServerError, utils.H{"error": "could not find file name"}) + } + fileName := path_[index+1:] + + switch v := command.(type) { + case cmds.GlazeCommand: + return output_file.CreateGlazedFileHandler( + v, + fileName, + gch.middlewares..., + )(c) + + case cmds.WriterCommand: + handler := text.NewQueryHandler(command) + + baseName := filepath.Base(fileName) + c.Response().Header().Set("Content-Disposition", "attachment; filename="+baseName) + + err := handler.Handle(c) + if err != nil { + return err + } + + return nil + + default: + return c.JSON(http.StatusInternalServerError, utils.H{"error": "command is not a glazed/writer command"}) + } + +} + +// getRepositoryCommand lookups a command in the given repository and return success as bool and the given command, +// or sends an error code over HTTP using the gin.Context. +func getRepositoryCommand(r *repositories.Repository, commandPath string) (cmds.Command, error) { + path := strings.Split(commandPath, "/") + commands := r.CollectCommands(path, false) + if len(commands) == 0 { + return nil, CommandNotFound{CommandPath: commandPath} + } + + if len(commands) > 1 { + err := &AmbiguousCommand{ + CommandPath: commandPath, + } + for _, command := range commands { + description := command.Description() + err.PotentialCommands = append(err.PotentialCommands, strings.Join(description.Parents, " ")+" "+description.Name) + } + return nil, err + } + + // NOTE(manuel, 2023-05-15) Check if this is actually an alias, and populate the defaults from the alias flags + // This could potentially be moved to the repository code itself + + return commands[0], nil +} + +type CommandNotFound struct { + CommandPath string +} + +func (e CommandNotFound) Error() string { + return fmt.Sprintf("command %s not found", e.CommandPath) +} + +type AmbiguousCommand struct { + CommandPath string + PotentialCommands []string +} + +func (e AmbiguousCommand) Error() string { + return fmt.Sprintf("command %s is ambiguous, could be one of: %s", e.CommandPath, strings.Join(e.PotentialCommands, ", ")) + +} diff --git a/pkg/handlers/static-dir/static.go b/pkg/handlers/static-dir/static.go index de9b2d5..75be66d 100644 --- a/pkg/handlers/static-dir/static.go +++ b/pkg/handlers/static-dir/static.go @@ -5,7 +5,6 @@ import ( "github.com/go-go-golems/parka/pkg/server" fs2 "github.com/go-go-golems/parka/pkg/utils/fs" "io/fs" - "net/http" "os" "strings" ) @@ -63,6 +62,6 @@ func (s *StaticDirHandler) Serve(server *server.Server, path string) error { if s.localPath != "" { fs_ = fs2.NewAddPrefixPathFS(s.fs, s.localPath) } - server.Router.StaticFS(path, http.FS(fs_)) + server.Router.StaticFS(path, fs_) return nil } diff --git a/pkg/handlers/static-file/static-file.go b/pkg/handlers/static-file/static-file.go index d5c0c24..882190b 100644 --- a/pkg/handlers/static-file/static-file.go +++ b/pkg/handlers/static-file/static-file.go @@ -3,8 +3,8 @@ package static_file import ( "github.com/go-go-golems/parka/pkg/handlers/config" "github.com/go-go-golems/parka/pkg/server" + "github.com/labstack/echo/v4" "io/fs" - "net/http" "os" "strings" ) @@ -56,10 +56,6 @@ func NewStaticFileHandlerFromConfig(shf *config.StaticFile, options ...StaticFil } func (s *StaticFileHandler) Serve(server *server.Server, path string) error { - server.Router.StaticFileFS( - path, - s.localPath, - http.FS(s.fs), - ) + server.Router.StaticFS(path, echo.MustSubFS(s.fs, s.localPath)) return nil } diff --git a/pkg/handlers/template-dir/template-dir.go b/pkg/handlers/template-dir/template-dir.go index efb8db0..da0e430 100644 --- a/pkg/handlers/template-dir/template-dir.go +++ b/pkg/handlers/template-dir/template-dir.go @@ -8,6 +8,7 @@ import ( "github.com/go-go-golems/parka/pkg/server" "io/fs" "path/filepath" + "strings" ) // TODO(manuel, 2023-05-28) Add a proper Handler interface that also @@ -128,11 +129,7 @@ func NewTemplateDirHandlerFromConfig(td *config.TemplateDir, options ...Template } func (td *TemplateDirHandler) Serve(server *server.Server, path string) error { - // TODO(manuel, 2023-05-26) This is a hack because we currently mix and match content with commands. - // The use of a middleware to handle something that could be handled by the routing framework itself - // is because gin (which really should get replaced because we actually go against its grain heavily) - // does not allow routes to overlap. - server.Router.Use(td.renderer.HandleWithTrimPrefix(path, nil)) - + path = strings.TrimSuffix(path, "/") + server.Router.GET(path+"/*", td.renderer.WithTemplateDirHandler(nil)) return nil } diff --git a/pkg/handlers/template/template.go b/pkg/handlers/template/template.go index ad66d40..08e8e38 100644 --- a/pkg/handlers/template/template.go +++ b/pkg/handlers/template/template.go @@ -5,7 +5,6 @@ import ( "github.com/go-go-golems/parka/pkg/render" "github.com/go-go-golems/parka/pkg/server" "io/fs" - "strings" ) type TemplateHandler struct { @@ -49,11 +48,7 @@ func NewTemplateHandler(templateFile string, options ...TemplateHandlerOption) * return handler } -func NewTemplateHandlerFromConfig( - path string, - t *config.Template, - options ...TemplateHandlerOption, -) (*TemplateHandler, error) { +func NewTemplateHandlerFromConfig(t *config.Template, options ...TemplateHandlerOption) (*TemplateHandler, error) { handler := &TemplateHandler{ TemplateFile: t.TemplateFile, } @@ -61,12 +56,10 @@ func NewTemplateHandlerFromConfig( option(handler) } - path = strings.TrimPrefix(path, "/") - if path == "" || strings.HasSuffix(path, "/") { - path += "index.tmpl.md" - } - - templateLookup := render.NewLookupTemplateFromFile(handler.TemplateFile, path) + // the template name used to lookup the template is the template file path. We do need to specify + // a template name because we are also using the lookup to get the base template to render markdown files. + templateLookup := render.NewLookupTemplateFromFile(handler.TemplateFile, handler.TemplateFile) + // TODO(manuel, 2024-05-09) In dev mode, we want to watch the template file and reload when it changes err := templateLookup.Reload() if err != nil { return nil, err @@ -86,7 +79,7 @@ func NewTemplateHandlerFromConfig( } func (t *TemplateHandler) Serve(server_ *server.Server, path string) error { - server_.Router.Handle("GET", path, t.renderer.HandleWithTrimPrefix("", nil)) + server_.Router.GET(path, t.renderer.WithTemplateHandler(t.TemplateFile, nil)) return nil } diff --git a/pkg/render/renderer.go b/pkg/render/renderer.go index 5061658..80811a5 100644 --- a/pkg/render/renderer.go +++ b/pkg/render/renderer.go @@ -1,7 +1,8 @@ package render import ( - "github.com/gin-gonic/gin" + "github.com/go-go-golems/parka/pkg/utils" + "github.com/labstack/echo/v4" "github.com/pkg/errors" "github.com/rs/zerolog/log" "html/template" @@ -167,8 +168,8 @@ func (r *Renderer) LookupTemplate(name ...string) (*template.Template, error) { // This could lead to partial writes with an error code of 200 if there is an error rendering the template, // not sure if that's exactly what we want. func (r *Renderer) Render( - c *gin.Context, - page string, + c echo.Context, + templateName string, data map[string]interface{}, ) error { // first, merge the data we want to pass to the templates, with the data passed in overridding @@ -183,7 +184,7 @@ func (r *Renderer) Render( // TODO(manuel, 2023-05-26) Don't render plain files as templates // See https://github.com/go-go-golems/parka/issues/47 - t, err := r.LookupTemplate(page+".tmpl.md", page+".md", page) + t, err := r.LookupTemplate(templateName+".tmpl.md", templateName+".md", templateName) if err != nil { return errors.Wrap(err, "error looking up template") } @@ -195,10 +196,10 @@ func (r *Renderer) Render( if baseTemplate == nil { // no base template to render the markdown to HTML, so just return the markdown - c.Header("Content-Type", "text/plain") - c.Status(http.StatusOK) + c.Response().Header().Set("Content-Type", "text/plain") + c.Response().WriteHeader(http.StatusOK) - err := t.Execute(c.Writer, data) + err := t.Execute(c.Response(), data) if err != nil { return errors.Wrap(err, "error executing template") } @@ -212,9 +213,9 @@ func (r *Renderer) Render( return errors.Wrap(err, "error rendering markdown") } - c.Status(http.StatusOK) + c.Response().WriteHeader(http.StatusOK) err = baseTemplate.Execute( - c.Writer, + c.Response(), map[string]interface{}{ "markdown": template.HTML(markdown), }) @@ -222,17 +223,17 @@ func (r *Renderer) Render( return errors.Wrap(err, "error executing base template") } } else { - t, err = r.LookupTemplate(page+".tmpl.html", page+".html") + t, err = r.LookupTemplate(templateName+".tmpl.html", templateName+".html") if err != nil { return errors.Wrap(err, "error looking up template") } if t == nil { - return &NoPageFoundError{Page: page} + return &utils.NoPageFoundError{Page: templateName} } - c.Status(http.StatusOK) + c.Response().WriteHeader(http.StatusOK) - err := t.Execute(c.Writer, data) + err := t.Execute(c.Response(), data) if err != nil { return errors.Wrap(err, "error executing template") } @@ -240,57 +241,24 @@ func (r *Renderer) Render( return nil } -func (r *Renderer) HandleWithTemplate( - templateName string, - data map[string]interface{}, -) gin.HandlerFunc { - return func(c *gin.Context) { - if c.Writer.Written() { - c.Next() - return - } - err := r.Render(c, templateName, data) - if err != nil { - if _, ok := err.(*NoPageFoundError); ok { - c.Next() - return - } - _ = c.AbortWithError(http.StatusInternalServerError, err) - return - } - c.Status(http.StatusOK) +func (r *Renderer) WithTemplateHandler(templateName string, data map[string]interface{}) echo.HandlerFunc { + return func(c echo.Context) error { + return r.Render(c, templateName, data) } } -func (r *Renderer) Handle(data map[string]interface{}) gin.HandlerFunc { - return r.HandleWithTrimPrefix("", data) -} - -func (r *Renderer) HandleWithTrimPrefix(prefix string, data map[string]interface{}) gin.HandlerFunc { - prefix = strings.TrimPrefix(prefix, "/") - return func(c *gin.Context) { - if c.Writer.Written() { - c.Next() - return +func (r *Renderer) WithTemplateDirHandler(data map[string]interface{}) echo.HandlerFunc { + return func(c echo.Context) error { + path := c.Param("*") + if path == "" || strings.HasSuffix(path, "/") { + path += "index" } - // check if context is already finished - rawPath := c.Request.URL.Path - if len(rawPath) > 0 && rawPath[0] == '/' { - trimmedPath := rawPath[1:] - trimmedPath = strings.TrimPrefix(trimmedPath, prefix) - if trimmedPath == "" || strings.HasSuffix(trimmedPath, "/") { - trimmedPath += "index" - } - - err := r.Render(c, trimmedPath, data) - if err != nil { - if _, ok := err.(*NoPageFoundError); ok { - c.Next() - return - } - _ = c.AbortWithError(http.StatusInternalServerError, err) - return - } + + err := r.Render(c, path, data) + if err != nil { + return err } + + return nil } } diff --git a/pkg/render/template-lookup.go b/pkg/render/template-lookup.go index 6875030..e8d67af 100644 --- a/pkg/render/template-lookup.go +++ b/pkg/render/template-lookup.go @@ -38,10 +38,10 @@ type LookupTemplateFromFile struct { TemplateName string } -func NewLookupTemplateFromFile(file string, path string) *LookupTemplateFromFile { +func NewLookupTemplateFromFile(file string, templateName string) *LookupTemplateFromFile { return &LookupTemplateFromFile{ File: file, - TemplateName: path, + TemplateName: templateName, } } diff --git a/pkg/server/server.go b/pkg/server/server.go index ca48918..912cfbd 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -4,10 +4,10 @@ import ( "context" "embed" "fmt" - "github.com/gin-gonic/contrib/gzip" - "github.com/gin-gonic/gin" "github.com/go-go-golems/parka/pkg/render" utils_fs "github.com/go-go-golems/parka/pkg/utils/fs" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" "golang.org/x/sync/errgroup" "io/fs" "net/http" @@ -29,7 +29,7 @@ var distFS embed.FS // Router is the gin.Engine that is used to serve the content, and it is exposed so that you // can use it as just a gin.Engine if you want to. type Server struct { - Router *gin.Engine + Router *echo.Echo // TODO(manuel, 2023-06-05) This should become a standard Static handler to be added to the Routes StaticPaths []utils_fs.StaticPath @@ -143,7 +143,7 @@ func WithDefaultParkaRenderer(options ...render.RendererOption) ServerOption { return WithDefaultRenderer(renderer) } -func GetParkaStaticHttpFS() http.FileSystem { +func GetParkaStaticHttpFS() fs.FS { return utils_fs.NewEmbedFileSystem(distFS, "web/dist") } @@ -159,7 +159,7 @@ func WithDefaultParkaStaticPaths() ServerOption { func WithGzip() ServerOption { return func(s *Server) error { - s.Router.Use(gzip.Gzip(gzip.DefaultCompression)) + s.Router.Use(middleware.Gzip()) return nil } } @@ -169,7 +169,7 @@ func WithGzip() ServerOption { // These files provide tailwind support for Markdown rendering and a standard index and base page template. // NOTE(manuel, 2023-04-16) This is definitely ripe to be removed. func NewServer(options ...ServerOption) (*Server, error) { - router := gin.Default() + router := echo.New() s := &Server{ Router: router, @@ -201,9 +201,7 @@ func (s *Server) RegisterDebugRoutes() { for route, handler := range handlers_ { route_ := route handler_ := handler - s.Router.GET(route_, func(c *gin.Context) { - handler_(c.Writer, c.Request) - }) + s.Router.GET(route_, echo.WrapHandler(handler_)) } } @@ -215,8 +213,9 @@ func (s *Server) Run(ctx context.Context) error { // match all remaining paths to the templates if s.DefaultRenderer != nil { - s.Router.GET("/", s.DefaultRenderer.HandleWithTemplate("index", nil)) - s.Router.Use(s.DefaultRenderer.Handle(nil)) + // TODO(manuel, 2024-05-08) I don't think we even need the explicit index mapping + //s.Router.GET("/", s.DefaultRenderer.WithTemplateHandler("index", nil)) + s.Router.GET("/*", s.DefaultRenderer.WithTemplateDirHandler(nil)) } addr := fmt.Sprintf("%s:%d", s.Address, s.Port) @@ -239,3 +238,22 @@ func (s *Server) Run(ctx context.Context) error { return eg.Wait() } + +func CustomHTTPErrorHandler(err error, c echo.Context) { + code := http.StatusInternalServerError + if he, ok := err.(*echo.HTTPError); ok { + code = he.Code + } + + // Create a custom error response + errorResponse := map[string]interface{}{ + "error": err.Error(), + } + + // Send the custom error response + if !c.Response().Committed { + _ = c.JSON(code, errorResponse) + } + + c.Logger().Error(err) +} diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index 0c51d6f..fbb924f 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -2,7 +2,6 @@ package server_test import ( "encoding/json" - "github.com/gin-gonic/gin" json2 "github.com/go-go-golems/parka/pkg/glazed/handlers/json" "github.com/go-go-golems/parka/pkg/server" "github.com/go-go-golems/parka/pkg/utils" @@ -23,8 +22,6 @@ func TestRunGlazedCommand(t *testing.T) { handler := json2.CreateJSONQueryHandler(tc) - gin.SetMode(gin.TestMode) - s.Router.GET("/test", handler) server := httptest.NewServer(s.Router) diff --git a/pkg/render/errors.go b/pkg/utils/errors.go similarity index 95% rename from pkg/render/errors.go rename to pkg/utils/errors.go index 0b5a0cd..b752981 100644 --- a/pkg/render/errors.go +++ b/pkg/utils/errors.go @@ -1,4 +1,4 @@ -package render +package utils import "fmt" diff --git a/pkg/utils/fs/fs.go b/pkg/utils/fs/fs.go index 9c48ea7..2a72b32 100644 --- a/pkg/utils/fs/fs.go +++ b/pkg/utils/fs/fs.go @@ -2,7 +2,6 @@ package fs import ( "io/fs" - "net/http" "path/filepath" "strings" ) @@ -10,10 +9,12 @@ import ( // EmbedFileSystem is a helper to make an embed FS work as a http.FS, // which allows us to serve embed.FS using gin's `Static` middleware. type EmbedFileSystem struct { - f http.FileSystem + f fs.FS stripPrefix string } +var _ fs.FS = &EmbedFileSystem{} + // NewEmbedFileSystem will create a new EmbedFileSystem that will serve the given embed.FS // under the given URL path. stripPrefix will be added to the beginning of all paths when // looking up files in the embed.FS. @@ -22,14 +23,14 @@ func NewEmbedFileSystem(f fs.FS, stripPrefix string) *EmbedFileSystem { stripPrefix += "/" } return &EmbedFileSystem{ - f: http.FS(f), + f: f, stripPrefix: stripPrefix, } } // Open will open the file with the given name from the embed.FS. The name will be prefixed // with the stripPrefix that was given when creating the EmbedFileSystem. -func (e *EmbedFileSystem) Open(name string) (http.File, error) { +func (e *EmbedFileSystem) Open(name string) (fs.File, error) { name = strings.TrimPrefix(name, "/") return e.f.Open(e.stripPrefix + name) } @@ -49,7 +50,7 @@ func (e *EmbedFileSystem) Exists(prefix string, path string) bool { if err != nil { return false } - defer func(f http.File) { + defer func(f fs.File) { _ = f.Close() }(f) return true @@ -57,12 +58,12 @@ func (e *EmbedFileSystem) Exists(prefix string, path string) bool { // StaticPath allows you to serve static files from a http.FileSystem under a given URL path UrlPath. type StaticPath struct { - FS http.FileSystem + FS fs.FS UrlPath string } // NewStaticPath creates a new StaticPath that will serve files from the given http.FileSystem. -func NewStaticPath(fs http.FileSystem, urlPath string) StaticPath { +func NewStaticPath(fs fs.FS, urlPath string) StaticPath { return StaticPath{ FS: fs, UrlPath: urlPath, diff --git a/pkg/utils/http.go b/pkg/utils/http.go new file mode 100644 index 0000000..cb054de --- /dev/null +++ b/pkg/utils/http.go @@ -0,0 +1,3 @@ +package utils + +type H map[string]interface{} diff --git a/pkg/utils/test-helpers.go b/pkg/utils/test-helpers.go index d4e01b7..3c3ff14 100644 --- a/pkg/utils/test-helpers.go +++ b/pkg/utils/test-helpers.go @@ -6,8 +6,6 @@ import ( "net/http" "net/http/httptest" "net/url" - - "github.com/gin-gonic/gin" ) // QueryParameter holds a key-value pair for a query parameter. @@ -34,11 +32,11 @@ type File struct { Content string `yaml:"content"` } -// MockGinContextWithQueryParameters creates a mock *gin.Context with the +// NewRequestWithQueryParameters creates a mock echo.Context with the // provided query parameters set. It is intended for use in tests. // // It returns a GET request for the path "/" -func MockGinContextWithQueryParameters(parameters []QueryParameter) (*gin.Context, error) { +func NewRequestWithQueryParameters(parameters []QueryParameter) *http.Request { // Create a new HTTP request req := httptest.NewRequest(http.MethodGet, "/", nil) @@ -53,20 +51,16 @@ func MockGinContextWithQueryParameters(parameters []QueryParameter) (*gin.Contex // Set the RawQuery field of the request URL req.URL.RawQuery = values.Encode() - // Create a new gin context with the request - c, _ := gin.CreateTestContext(httptest.NewRecorder()) - c.Request = req - - return c, nil + return req } -// MockGinContextWithMultipartForm creates a mock gin.Context populated +// NewRequestWithMultipartForm creates a mock gin.Context populated // with the provided multipart form data. It constructs the multipart // form, attaches it to a mock request, and creates a gin.Context using // that request. Returns the created context and any error. // // The request is a POST request to "/" with the provided form data. -func MockGinContextWithMultipartForm(form MultipartForm) (*gin.Context, error) { +func NewRequestWithMultipartForm(form MultipartForm) (*http.Request, error) { body := &bytes.Buffer{} writer := multipart.NewWriter(body) @@ -99,8 +93,5 @@ func MockGinContextWithMultipartForm(form MultipartForm) (*gin.Context, error) { req := httptest.NewRequest("POST", "/", body) req.Header.Set("Content-Type", writer.FormDataContentType()) - - c, _ := gin.CreateTestContext(httptest.NewRecorder()) - c.Request = req - return c, nil + return req, nil }