diff --git a/go.mod b/go.mod index 877c202..4a12103 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/go-go-golems/parka -go 1.21 +go 1.23 toolchain go1.23.3 @@ -10,8 +10,8 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.29.1 github.com/aws/aws-sdk-go-v2/service/ssm v1.56.7 github.com/aws/aws-sdk-go-v2/service/sts v1.33.9 - github.com/go-go-golems/clay v0.1.15 - github.com/go-go-golems/glazed v0.5.17 + github.com/go-go-golems/clay v0.1.20 + github.com/go-go-golems/glazed v0.5.26 github.com/kucherenkovova/safegroup v1.0.2 github.com/labstack/echo/v4 v4.12.0 github.com/pkg/errors v0.9.1 @@ -22,7 +22,7 @@ require ( github.com/yuin/goldmark v1.7.8 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87 github.com/ziflex/lecho/v3 v3.6.0 - golang.org/x/sync v0.9.0 + golang.org/x/sync v0.10.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -51,7 +51,7 @@ require ( github.com/buger/jsonparser v1.1.1 // indirect github.com/charmbracelet/glamour v0.7.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/dlclark/regexp2 v1.11.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-openapi/errors v0.22.0 // indirect github.com/go-openapi/strfmt v0.23.0 // indirect @@ -59,7 +59,7 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/huandu/xstrings v1.4.0 // indirect + github.com/huandu/xstrings v1.5.0 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/itchyny/gojq v0.12.12 // indirect @@ -92,7 +92,7 @@ require ( github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/cast v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tj/go-naturaldate v1.3.0 // indirect @@ -106,11 +106,11 @@ require ( github.com/yuin/goldmark-emoji v1.0.3 // indirect go.mongodb.org/mongo-driver v1.14.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.29.0 // indirect + golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/net v0.31.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/text v0.20.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.5.0 // indirect gopkg.in/errgo.v2 v2.1.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index db37ea8..8beb29f 100644 --- a/go.sum +++ b/go.sum @@ -69,16 +69,16 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/go-go-golems/clay v0.1.15 h1:rnvuwnKBJeZ86weFXvaPEek3JaieidLKuQozTKAIsZI= -github.com/go-go-golems/clay v0.1.15/go.mod h1:KLkwWVFcSuctQvh9cSjMCUPU6McnRSF13aJQrT2s3YE= -github.com/go-go-golems/glazed v0.5.17 h1:rqkSPGClE2i0wgq/Bo4+dWK0polGITioCs494sWp3+Y= -github.com/go-go-golems/glazed v0.5.17/go.mod h1:C1zWpbRfs3+xmtAZoW6RFgFvTLeC2xq+gkc1S5Luvz4= +github.com/go-go-golems/clay v0.1.20 h1:KUTbDBA/Q7vgG22B9uBnwDpacwG2+bMavQS8SDwolks= +github.com/go-go-golems/clay v0.1.20/go.mod h1:hyQirWoEICmaSTcAiPRy7If1n5JEncPi4WVM6tivjoY= +github.com/go-go-golems/glazed v0.5.26 h1:/Y+Sq6An0IyRVRG1shjV+FZmcOplJ6NvzbQ1edYw3QU= +github.com/go-go-golems/glazed v0.5.26/go.mod h1:/ZgeDXELDOcAkD505fijARmbF6x5Ev7oewNV4V6Andk= github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= @@ -86,8 +86,8 @@ github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S 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/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/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= @@ -96,8 +96,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= -github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -194,8 +194,8 @@ github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9yS github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -253,8 +253,8 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= @@ -267,13 +267,13 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v 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.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -284,8 +284,8 @@ 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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.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= @@ -295,8 +295,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 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= diff --git a/pkg/doc/topics/01-parka-server.md b/pkg/doc/topics/01-parka-server.md new file mode 100644 index 0000000..7901588 --- /dev/null +++ b/pkg/doc/topics/01-parka-server.md @@ -0,0 +1,170 @@ +# Parka Server Documentation + +The Parka server is a flexible HTTP server built on top of the Echo web framework that provides both static file serving and dynamic template rendering capabilities. This document explains how the server works and how to extend it. + +## Core Concepts + +The Parka server is built around these main concepts: + +1. Static File Serving +2. Template Rendering +3. Custom Route Handlers +4. Middleware Support + +## Server Configuration + +### Creating a New Server + +To create a new Parka server, use the `NewServer` function with desired options: + +```go +server, err := server.NewServer( + server.WithPort(8080), + server.WithAddress("localhost"), + server.WithGzip(), + server.WithDefaultParkaRenderer(), + server.WithDefaultParkaStaticPaths(), +) +``` + +### Server Options + +The server can be configured using various options: + +- `WithPort(port uint16)` - Sets the listening port +- `WithAddress(address string)` - Sets the listening address +- `WithGzip()` - Enables Gzip compression +- `WithDefaultParkaRenderer()` - Sets up the default template renderer +- `WithDefaultParkaStaticPaths()` - Configures default static file paths +- `WithStaticPaths(paths ...utils_fs.StaticPath)` - Adds custom static file paths +- `WithDefaultRenderer(r *render.Renderer)` - Sets a custom renderer + +## Static File Serving + +Static files can be served using the `StaticPaths` configuration. Each static path consists of: + +1. A filesystem implementation (can be embed.FS or os.FS) +2. A URL path where the files will be served + +Example: + +```go +staticPath := utils_fs.NewStaticPath(myFS, "/static") +server, err := server.NewServer( + server.WithStaticPaths(staticPath), +) +``` + +## Template Rendering + +Parka uses a flexible template rendering system that supports: + +1. Multiple template lookups +2. Markdown rendering with Tailwind CSS support +3. Custom base templates +4. Template directory handling + +The default renderer can be configured using: + +```go +options, err := server.GetDefaultParkaRendererOptions() +renderer, err := render.NewRenderer(options...) +server, err := server.NewServer( + server.WithDefaultRenderer(renderer), +) +``` + +## Adding Custom Routes + +Since Parka is built on Echo, you can add custom routes using the standard Echo routing system: + +```go +s.Router.GET("/api/hello", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]string{ + "message": "Hello, World!", + }) +}) +``` + +### Route Groups + +You can organize routes using Echo's group feature: + +```go +api := s.Router.Group("/api") +api.GET("/users", handleUsers) +api.POST("/users", createUser) +``` + +## Middleware + +Parka comes with some default middleware: + +1. Recovery middleware +2. Request logging using zerolog +3. Optional Gzip compression + +Adding custom middleware: + +```go +s.Router.Use(myCustomMiddleware) +``` + +## Running the Server + +To start the server: + +```go +ctx := context.Background() +err := server.Run(ctx) +``` + +The server supports graceful shutdown through context cancellation. + +## Error Handling + +Parka uses Echo's error handling system. You can customize error handling by implementing custom error handlers: + +```go +s.Router.HTTPErrorHandler = func(err error, c echo.Context) { + // Custom error handling logic +} +``` + + +## Examples + +Here's a complete example of setting up a Parka server with custom routes and middleware: + +```go +server, err := server.NewServer( + server.WithPort(8080), + server.WithAddress("localhost"), + server.WithGzip(), + server.WithDefaultParkaRenderer(), + server.WithDefaultParkaStaticPaths(), +) +if err != nil { + log.Fatal(err) +} + +// Add custom routes +server.Router.GET("/api/status", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]string{ + "status": "healthy", + }) +}) + +// Add custom middleware +server.Router.Use(middleware.CORS()) + +// Start the server +ctx := context.Background() +if err := server.Run(ctx); err != nil { + log.Fatal(err) +} +``` + +## Further Reading + +- [Echo Framework Documentation](https://echo.labstack.com/) \ No newline at end of file diff --git a/pkg/doc/topics/02-handlers.md b/pkg/doc/topics/02-handlers.md new file mode 100644 index 0000000..b6dae9d --- /dev/null +++ b/pkg/doc/topics/02-handlers.md @@ -0,0 +1,836 @@ +# Parka Static Handlers Documentation + +Parka provides two specialized handlers for serving static content: `StaticDirHandler` and `StaticFileHandler`. These handlers are designed to serve static files from either the filesystem or embedded files, with different strategies for path handling and file organization. + +## StaticDirHandler + +The `StaticDirHandler` is designed to serve an entire directory of static files, maintaining the directory structure when serving the files over HTTP. + +### Structure + +```go +type StaticDirHandler struct { + fs fs.FS + localPath string +} +``` + +- `fs`: The filesystem interface that provides access to the static files +- `localPath`: The base path within the filesystem where the static files are located + +### Configuration Options + +The handler can be configured using functional options: + +1. `WithDefaultFS(fs fs.FS, localPath string)`: Sets a default filesystem and local path + ```go + handler := NewStaticDirHandler( + WithDefaultFS(embeddedFS, "static"), + ) + ``` + +2. `WithLocalPath(localPath string)`: Sets up the handler to serve files from a local directory + ```go + handler := NewStaticDirHandler( + WithLocalPath("/path/to/static/files"), + ) + ``` + +### Creation Methods + +1. Basic creation with options: + ```go + handler := NewStaticDirHandler(options...) + ``` + +2. Creation from configuration: + ```go + handler := NewStaticDirHandlerFromConfig(staticConfig, options...) + ``` + +### Usage + +The handler is registered with a Parka server using the `Serve` method: + +```go +// Create a new Parka server +server, err := server.NewServer( + server.WithPort(8080), + server.WithAddress("localhost"), +) +if err != nil { + return err +} + +// Create and configure the handler +handler := NewStaticDirHandler( + WithLocalPath("./static"), +) + +// Register the handler with a base path +err = handler.Serve(server, "/static") +if err != nil { + return err +} +``` + +This will serve all files from the configured directory under the `/static` URL path. + +### Path Handling + +- If serving from a local path, the handler automatically creates a directory filesystem +- Trailing slashes are automatically handled +- The local path is prefixed to filesystem paths when necessary + +## StaticFileHandler + +The `StaticFileHandler` is designed to serve individual files or specific subdirectories, with more precise control over the served paths. + +### Structure + +```go +type StaticFileHandler struct { + fs fs.FS + localPath string +} +``` + +- `fs`: The filesystem interface that provides access to the static files +- `localPath`: The specific path to the file or subdirectory to serve + +### Configuration Options + +1. `WithDefaultFS(fs fs.FS, localPath string)`: Sets a default filesystem and local path + ```go + handler := NewStaticFileHandler( + WithDefaultFS(embeddedFS, "assets/file.css"), + ) + ``` + +2. `WithLocalPath(localPath string)`: Sets up the handler to serve from a local path + ```go + handler := NewStaticFileHandler( + WithLocalPath("/path/to/specific/file.js"), + ) + ``` + +### Creation Methods + +1. Basic creation with options: + ```go + handler := NewStaticFileHandler(options...) + ``` + +2. Creation from configuration: + ```go + handler := NewStaticFileHandlerFromConfig(staticFileConfig, options...) + ``` + +### Usage + +The handler is registered with a Parka server using the `Serve` method: + +```go +// Create a new Parka server +server, err := server.NewServer( + server.WithPort(8080), + server.WithAddress("localhost"), +) +if err != nil { + return err +} + +// Create and configure the handler +handler := NewStaticFileHandler( + WithLocalPath("/path/to/specific/file.js"), +) + +// Register the handler with a specific URL path +err = handler.Serve(server, "/assets/js/script.js") +if err != nil { + return err +} +``` + +### Path Handling + +- Leading slashes in local paths are automatically handled +- Uses Echo's `MustSubFS` for safe subpath handling +- Maintains exact path mapping between filesystem and URL paths + +## Differences Between Handlers + +1. **Scope**: + - `StaticDirHandler`: Serves entire directories with their structure + - `StaticFileHandler`: Serves specific files or subdirectories with precise path control + +2. **Path Handling**: + - `StaticDirHandler`: Automatically handles directory structure and trailing slashes + - `StaticFileHandler`: Provides exact path mapping and uses Echo's subfilesystem functionality + +3. **Use Cases**: + - `StaticDirHandler`: Best for serving static assets like images, CSS, and JavaScript files in their directory structure + - `StaticFileHandler`: Best for serving individual files or when precise control over URL paths is needed + +## Best Practices + +1. **Directory Structure**: + - Keep static files organized in a clear directory structure + - Use `StaticDirHandler` for serving multiple related files + - Use `StaticFileHandler` for specific files that need custom URL paths + +2. **Security**: + - Always validate and sanitize paths + - Be careful with directory traversal attacks + - Use embedded filesystems when possible for better security + +3. **Performance**: + - Consider using a CDN for large static assets + - Enable compression middleware for text-based files + - Use caching headers appropriately + +## Examples + +### Serving an Embedded Directory + +```go +//go:embed static/* +var staticFS embed.FS + +handler := NewStaticDirHandler( + WithDefaultFS(staticFS, "static"), +) +err = handler.Serve(server, "/assets") +if err != nil { + return err +} +``` + +### Serving a Local Directory + +```go +handler := NewStaticDirHandler( + WithLocalPath("./static"), +) +err = handler.Serve(server, "/static") +if err != nil { + return err +} +``` + +### Serving a Specific File + +```go +handler := NewStaticFileHandler( + WithLocalPath("./assets/main.js"), +) +err = handler.Serve(server, "/js/main.js") +if err != nil { + return err +} +``` + +### Configuration-based Setup + +```go +config := &config.Static{ + LocalPath: "./static", +} +handler := NewStaticDirHandlerFromConfig(config) +err = handler.Serve(server, "/assets") +if err != nil { + return err +} +``` + +## Error Handling + +Both handlers handle errors gracefully: +- Invalid paths return appropriate HTTP errors +- Missing files return 404 Not Found +- Permission issues return 403 Forbidden + +## Integration with Echo + +Both handlers integrate seamlessly with Echo's static file serving capabilities: +- Use Echo's `StaticFS` method internally +- Support Echo's middleware stack +- Compatible with Echo's error handling + +## Further Reading + +- [Echo Static File Serving](https://echo.labstack.com/guide/static-files) +- [Go Filesystem Interface](https://golang.org/pkg/io/fs/) +- [Embedding Static Files](https://golang.org/pkg/embed/) + +# Template Handlers + +Parka provides two specialized handlers for serving templated content: `TemplateHandler` and `TemplateDirHandler`. These handlers enable dynamic content rendering using Go templates, with support for both HTML and Markdown files. + +## TemplateHandler + +The `TemplateHandler` is designed to serve a single template file, rendering it with optional data and supporting both HTML and Markdown content. + +### Structure + +```go +type TemplateHandler struct { + fs fs.FS + TemplateFile string + rendererOptions []render.RendererOption + renderer *render.Renderer + alwaysReload bool +} +``` + +- `fs`: The filesystem interface that provides access to the template files +- `TemplateFile`: The path to the template file to be rendered +- `rendererOptions`: Additional options for configuring the template renderer +- `renderer`: The renderer instance used to process templates +- `alwaysReload`: Whether to reload templates on every request (useful for development) + +### Configuration Options + +The handler can be configured using functional options: + +1. `WithDefaultFS(fs fs.FS)`: Sets a default filesystem for template loading + ```go + handler := NewTemplateHandler("index.tmpl.html", + WithDefaultFS(embeddedFS), + ) + ``` + +2. `WithAlwaysReload(alwaysReload bool)`: Enables template reloading for development + ```go + handler := NewTemplateHandler("index.tmpl.html", + WithAlwaysReload(true), + ) + ``` + +### Usage + +The handler is registered with a Parka server using the `Serve` method: + +```go +// Create a new Parka server +server, err := server.NewServer( + server.WithPort(8080), + server.WithAddress("localhost"), +) +if err != nil { + return err +} + +// Create and configure the handler +handler := NewTemplateHandler("index.tmpl.html", + WithDefaultFS(embeddedFS), + WithAlwaysReload(true), +) + +// Register the handler with a URL path +err = handler.Serve(server, "/") +if err != nil { + return err +} +``` + +## TemplateDirHandler + +The `TemplateDirHandler` is designed to serve an entire directory of templates, supporting both HTML and Markdown files with automatic routing based on file paths. + +### Structure + +```go +type TemplateDirHandler struct { + fs fs.FS + LocalDirectory string + IndexTemplateName string + MarkdownBaseTemplateName string + rendererOptions []render.RendererOption + renderer *render.Renderer + alwaysReload bool +} +``` + +- `fs`: The filesystem interface that provides access to the template files +- `LocalDirectory`: The base directory containing templates +- `IndexTemplateName`: The template to use for directory index pages +- `MarkdownBaseTemplateName`: The base template for rendering Markdown files +- `rendererOptions`: Additional options for configuring the template renderer +- `renderer`: The renderer instance used to process templates +- `alwaysReload`: Whether to reload templates on every request + +### Configuration Options + +1. `WithDefaultFS(fs fs.FS, localPath string)`: Sets a default filesystem and local path + ```go + handler, err := NewTemplateDirHandler( + WithDefaultFS(embeddedFS, "templates"), + ) + ``` + +2. `WithLocalDirectory(localPath string)`: Sets up the handler to serve from a local directory + ```go + handler, err := NewTemplateDirHandler( + WithLocalDirectory("./templates"), + ) + ``` + +### Usage + +The handler is registered with a Parka server using the `Serve` method: + +```go +// Create a new Parka server +server, err := server.NewServer( + server.WithPort(8080), + server.WithAddress("localhost"), +) +if err != nil { + return err +} + +// Create and configure the handler +handler, err := NewTemplateDirHandler( + WithLocalDirectory("./templates"), + WithAlwaysReload(true), +) +if err != nil { + return err +} + +// Register the handler with a base path +err = handler.Serve(server, "/") +if err != nil { + return err +} +``` + +### Template Discovery + +The TemplateDirHandler automatically discovers and serves: +- `*.tmpl.md` - Markdown templates +- `*.md` - Plain Markdown files +- `*.tmpl.html` - HTML templates +- `*.html` - Plain HTML files + +## Differences Between Handlers + +1. **Scope**: + - `TemplateHandler`: Serves a single template file + - `TemplateDirHandler`: Serves an entire directory of templates with automatic routing + +2. **File Support**: + - `TemplateHandler`: Focused on single template rendering + - `TemplateDirHandler`: Supports multiple template types and automatic discovery + +3. **Use Cases**: + - `TemplateHandler`: Best for single pages or specific templates + - `TemplateDirHandler`: Best for documentation sites, multi-page applications, or content-heavy sites + +## Best Practices + +1. **Template Organization**: + - Use clear naming conventions for templates + - Separate content from layout templates + - Use base templates for consistent styling + - Keep templates modular and reusable + +2. **Development Workflow**: + - Use `WithAlwaysReload(true)` during development + - Create a base template for consistent layouts + - Use partials for reusable components + - Implement proper error handling in templates + +3. **Performance**: + - Disable template reloading in production + - Use caching headers appropriately + - Minimize template complexity + - Consider precompiling templates + +## Examples + +### Serving a Single Template + +```go +handler := NewTemplateHandler("index.tmpl.html", + WithDefaultFS(embeddedFS), + WithAlwaysReload(true), +) +server.AddHandler(handler, "/") +``` + +### Serving a Documentation Site + +```go +handler, err := NewTemplateDirHandler( + WithLocalDirectory("./docs"), + WithAlwaysReload(true), +) +server.AddHandler(handler, "/docs") +``` + +### Configuration-based Setup + +```go +config := &config.TemplateDir{ + LocalDirectory: "./templates", + IndexTemplateName: "index.tmpl.html", +} +handler, err := NewTemplateDirHandlerFromConfig(config) +server.AddHandler(handler, "/content") +``` + +## Error Handling + +All handlers handle errors gracefully and return appropriate error types that should be checked: +- Invalid configuration returns initialization errors +- Registration errors are returned by the Serve method +- Runtime errors are handled through Echo's error handling system + +## Integration with Echo + +The handlers integrate with Echo's routing system: +- Use Echo's context for request handling +- Support middleware for authentication and logging +- Compatible with Echo's error handling +- Support streaming responses + +## Further Reading + +- [Go Template Documentation](https://golang.org/pkg/text/template/) +- [Echo Template Guide](https://echo.labstack.com/guide/templates) +- [Markdown Processing](https://github.com/gomarkdown/markdown) + +# Command Handlers + +Parka provides three specialized handlers for serving commands: `CommandHandler`, `CommandDirHandler`, and `GenericCommandHandler`. These handlers enable exposing commands as HTTP endpoints with various output formats and interactive UIs. + +## GenericCommandHandler + +The `GenericCommandHandler` is the base handler that provides core functionality for serving commands over HTTP. It's used internally by both `CommandHandler` and `CommandDirHandler`. + +### Structure + +```go +type GenericCommandHandler struct { + Stream bool + AdditionalData map[string]interface{} + ParameterFilter *config.ParameterFilter + TemplateName string + IndexTemplateName string + TemplateLookup render.TemplateLookup + BasePath string + preMiddlewares []middlewares.Middleware + postMiddlewares []middlewares.Middleware + middlewares []middlewares.Middleware +} +``` + +- `Stream`: Whether to use row-based streaming output (true by default) +- `AdditionalData`: Extra data passed to templates +- `ParameterFilter`: Configuration for parameter filtering, defaults, and overrides +- `TemplateName`: Template for rendering command output +- `IndexTemplateName`: Template for rendering command indexes +- `TemplateLookup`: Interface for finding templates +- `BasePath`: Base URL path for the handler +- `preMiddlewares`: Middleware chain to run before parameter filter middlewares +- `postMiddlewares`: Middleware chain to run after parameter filter middlewares + +### Endpoints + +The handler provides several endpoints for different output formats: + +1. `/data/*`: Returns command output in JSON format +2. `/text/*`: Returns command output as plain text +3. `/streaming/*`: Streams command output using Server-Sent Events (SSE) +4. `/datatables/*`: Displays command output in an interactive DataTables UI +5. `/download/*`: Allows downloading command output in various formats + +### Configuration Options + +1. `WithTemplateName(name string)`: Sets the template for command output +2. `WithParameterFilter(filter *config.ParameterFilter)`: Configures parameter handling +3. `WithMergeAdditionalData(data map[string]interface{}, override bool)`: Adds template data +4. `WithPreMiddlewares(middlewares ...middlewares.Middleware)`: Add middlewares to run before parameter filter middlewares +5. `WithPostMiddlewares(middlewares ...middlewares.Middleware)`: Add middlewares to run after parameter filter middlewares + +Example: + +```go +handler := NewGenericCommandHandler( + WithTemplateName("command.tmpl.html"), + WithParameterFilter(filter), + WithPreMiddlewares(myPreMiddleware1, myPreMiddleware2), + WithPostMiddlewares(myPostMiddleware1, myPostMiddleware2), +) +``` + +The middlewares will be executed in this order: +1. Pre-middlewares (in the order they were added) +2. Parameter filter middlewares +3. Post-middlewares (in the order they were added) + +## CommandHandler + +The `CommandHandler` is designed to serve a single command with multiple output formats. + +### Structure + +```go +type CommandHandler struct { + GenericCommandHandler + DevMode bool + Command cmds.Command +} +``` + +- Inherits all functionality from `GenericCommandHandler` +- `DevMode`: Enables development features like template reloading +- `Command`: The command to be served + +### Configuration Options + +1. `WithDevMode(devMode bool)`: Enables development mode + ```go + handler := NewCommandHandler(cmd, + WithDevMode(true), + ) + ``` + +2. `WithGenericCommandHandlerOptions(options ...GenericCommandHandlerOption)`: Adds generic options + ```go + handler := NewCommandHandler(cmd, + WithGenericCommandHandlerOptions( + WithTemplateName("command.tmpl.html"), + WithParameterFilter(filter), + ), + ) + ``` + +### Creation Methods + +1. Basic creation with options: + ```go + handler := NewCommandHandler(myCommand, options...) + ``` + +2. Creation from configuration: + ```go + handler, err := NewCommandHandlerFromConfig(config, loader, options...) + ``` + +### Usage + +```go +// Create a new Parka server +server, err := server.NewServer( + server.WithPort(8080), + server.WithAddress("localhost"), +) +if err != nil { + return err +} + +// Create your command +cmd := &MyCommand{} + +// Create and configure the handler +handler := NewCommandHandler(cmd, + WithDevMode(true), + WithGenericCommandHandlerOptions( + WithTemplateName("command.tmpl.html"), + WithParameterFilter(filter), + ), +) + +// Register the handler with a URL path +err = handler.Serve(server, "/my-command") +if err != nil { + return err +} +``` + +## CommandDirHandler + +The `CommandDirHandler` serves multiple commands from a repository, providing automatic routing and discovery. + +### Structure + +```go +type CommandDirHandler struct { + GenericCommandHandler + DevMode bool + Repository *repositories.Repository +} +``` + +- Inherits all functionality from `GenericCommandHandler` +- `DevMode`: Enables development features +- `Repository`: The command repository to serve + +### Configuration Options + +1. `WithDevMode(devMode bool)`: Enables development mode + ```go + handler := NewCommandDirHandler( + WithDevMode(true), + ) + ``` + +2. `WithRepository(r *repositories.Repository)`: Sets the command repository + ```go + handler := NewCommandDirHandler( + WithRepository(myRepo), + ) + ``` + +### Configuration File Example + +```yaml +routes: + - path: /commands + commandDirectory: + includeDefaultRepositories: true + repositories: + - ~/code/my-commands + templateLookup: + directories: + - ~/templates + indexTemplateName: index.tmpl.html + defaults: + flags: + limit: 100 + overrides: + layers: + glazed: + filter: + - id + - name + additionalData: + title: "My Commands" +``` + +### Usage + +```go +// Create a new Parka server +server, err := server.NewServer( + server.WithPort(8080), + server.WithAddress("localhost"), +) +if err != nil { + return err +} + +// Create and configure your repository +repo := repositories.NewRepository() +repo.AddCommand(commands.NewHelloCommand()) +// Add more commands... + +// Create and configure the handler +handler, err := NewCommandDirHandler( + WithRepository(repo), + WithDevMode(true), + WithGenericCommandHandlerOptions( + WithIndexTemplateName("commands/index.tmpl.html"), + WithTemplateName("commands/view.tmpl.html"), + ), +) +if err != nil { + return err +} + +// Register the handler with a base path +err = handler.Serve(server, "/commands") +if err != nil { + return err +} +``` + +## Best Practices + +1. **Command Organization**: + - Group related commands in repositories + - Use clear naming conventions + - Provide comprehensive command documentation + - Use appropriate output formats for different use cases + +2. **Security**: + - Validate command parameters + - Use parameter filters to restrict access + - Consider authentication for sensitive commands + - Implement proper error handling + +3. **Performance**: + - Use streaming for large outputs + - Enable caching when appropriate + - Consider rate limiting for heavy commands + - Monitor command execution times + +## Examples + +### Serving a Single Command + +```go +cmd := &MyCommand{} +handler := NewCommandHandler(cmd, + WithDevMode(true), + WithGenericCommandHandlerOptions( + WithTemplateName("command.tmpl.html"), + WithParameterFilter(filter), + ), +) +server.AddHandler(handler, "/my-command") +``` + +### Serving a Command Repository + +```go +repo := repositories.NewRepository() +repo.AddCommandFromFile("./commands/my-command.yaml") + +handler, err := NewCommandDirHandler( + WithRepository(repo), + WithDevMode(true), + WithGenericCommandHandlerOptions( + WithIndexTemplateName("index.tmpl.html"), + WithTemplateName("command.tmpl.html"), + ), +) +server.AddHandler(handler, "/commands") +``` + +### Configuration-based Setup + +```go +config := &config.CommandDir{ + IncludeDefaultRepositories: true, + Repositories: []string{"./commands"}, + IndexTemplateName: "index.tmpl.html", +} +handler, err := NewCommandDirHandlerFromConfig(config) +server.AddHandler(handler, "/api") +``` + +## Error Handling + +All handlers handle errors gracefully and return appropriate error types that should be checked: +- Invalid configuration returns initialization errors +- Registration errors are returned by the Serve method +- Runtime errors are handled through Echo's error handling system + +## Integration with Echo + +The handlers integrate with Echo's routing system: +- Use Echo's context for request handling +- Support middleware for authentication and logging +- Compatible with Echo's error handling +- Support streaming responses + +## Further Reading + +- [Command Repository Documentation](./command-repository.md) +- [Parameter Filtering](./parameter-filtering.md) +- [DataTables Integration](./datatables.md) +- [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) \ No newline at end of file diff --git a/pkg/doc/topics/03-parka-tutorial.md b/pkg/doc/topics/03-parka-tutorial.md new file mode 100644 index 0000000..c4a1431 --- /dev/null +++ b/pkg/doc/topics/03-parka-tutorial.md @@ -0,0 +1,733 @@ +# Building a Parka Server: A Comprehensive Tutorial + +This tutorial will guide you through building a Parka server from scratch, covering everything from basic setup to advanced features. We'll create a complete application that demonstrates the various capabilities of Parka. + +## The Big Picture + +Before diving into the implementation, let's understand how Parka works and how its components fit together. + +### Core Concepts + +Parka is built around several key concepts: + +1. **Server Core**: The central server built on Echo, providing HTTP routing and middleware support +2. **Handlers**: Specialized components that serve different types of content: + - Static handlers for files and directories + - Template handlers for dynamic HTML content + - Command handlers for exposing CLI tools as web services +3. **Commands**: CLI tools that can be exposed via HTTP endpoints with various output formats +4. **Repository**: A collection of commands that can be served together +5. **Middleware**: Components that process requests/responses (logging, security, etc.) + +### Architecture Overview + +``` +┌─────────────────────────────────────────────────┐ +│ Parka Server │ +├─────────────┬─────────────────┬────────────────┤ +│ Static │ Template │ Command │ +│ Handlers │ Handlers │ Handlers │ +├─────────────┴─────────────────┴────────────────┤ +│ Echo Framework │ +├──────────────────────────────────────────────┬─┤ +│ Command Repository │M││ +├──────────────────────────────────────────────┤i││ +│ Commands │d││ +├──────────────────────────────────────────────┤d││ +│ Glazed Framework │w││ +└──────────────────────────────────────────────┴─┘ +``` + +### How It All Works Together + +1. **Request Flow**: + - HTTP request comes in + - Echo routes it to the appropriate handler + - Handler processes the request using its specific logic + - Response is formatted and returned + +2. **Handler Types**: + - Static handlers serve files directly + - Template handlers render dynamic content + - Command handlers execute CLI tools and format their output + +3. **Command Integration**: + - Commands are defined using the Glazed framework + - They can be exposed individually or through a repository + - Output can be formatted as JSON, HTML tables, or downloadable files + +4. **Configuration**: + - Server settings control basic HTTP behavior + - Handler configurations define content serving + - Command settings control CLI tool behavior + +Now that we understand the big picture, let's build our application step by step. + +## Prerequisites + +- Go 1.18 or later +- Basic understanding of Go and web development +- Familiarity with command-line applications + +## Project Setup + +First, let's create a new Go project. This structure will help us organize our code according to Parka's architecture: + +```bash +mkdir my-parka-app +cd my-parka-app +go mod init my-parka-app +``` + +Add the required dependencies: + +```bash +go get github.com/go-go-golems/parka # Core Parka framework +go get github.com/labstack/echo/v4 # Web framework +go get github.com/spf13/cobra # CLI framework +``` + +### Why These Dependencies? + +- **Parka**: The main framework that ties everything together +- **Echo**: A high-performance web framework that provides routing and middleware +- **Cobra**: For building CLI applications that can be exposed via HTTP + +## Basic Server Structure + +Let's start with a basic server structure. This forms the foundation of our application: + +```go +package main + +import ( + "context" + "github.com/go-go-golems/parka/pkg/server" + "github.com/spf13/cobra" + "os" + "os/signal" +) + +var rootCmd = &cobra.Command{ + Use: "my-server", + Short: "A Parka-based web server", + Run: func(cmd *cobra.Command, args []string) { + port, _ := cmd.Flags().GetUint16("port") + host, _ := cmd.Flags().GetString("host") + + s, err := server.NewServer( + server.WithPort(port), + server.WithAddress(host), + server.WithGzip(), + ) + cobra.CheckErr(err) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + ctx, stop := signal.NotifyContext(ctx, os.Interrupt) + defer stop() + + err = s.Run(ctx) + cobra.CheckErr(err) + }, +} + +func init() { + rootCmd.Flags().Uint16("port", 8080, "Port to listen on") + rootCmd.Flags().String("host", "localhost", "Host to listen on") +} + +func main() { + _ = rootCmd.Execute() +} +``` + +### Understanding the Server Structure + +1. **Command-Line Interface**: + - Uses Cobra for CLI functionality + - Provides flags for configuration + - Handles graceful shutdown + +2. **Server Configuration**: + - Port and host settings + - Gzip compression for better performance + - Context handling for clean shutdown + +3. **Signal Handling**: + - Captures interrupt signals + - Ensures graceful shutdown + - Prevents resource leaks + +## Static File Serving + +```bash +mkdir -p static/css static/js +``` + +Static file serving is essential for web applications. Here's how Parka handles it: + +### Why Static File Serving? + +- Serves CSS, JavaScript, images, and other assets +- Improves performance with proper caching +- Separates static content from dynamic rendering + +### How It Works + +1. **File System Abstraction**: + - Uses Go's `fs.FS` interface + - Supports both local and embedded files + - Handles path resolution securely + +2. **Handler Configuration**: + - Maps URL paths to filesystem locations + - Handles directory listings (optional) + - Manages content types automatically + +Let's add static file serving for CSS, JavaScript, and other assets: + +```go +// In your Run function +staticHandler := static_dir.NewStaticDirHandler( + static_dir.WithLocalPath("./static"), +) +err = staticHandler.Serve(s, "/assets") +if err != nil { + return err +} +``` + +## Template Support + +Templates are crucial for generating dynamic HTML content. Parka's template system provides: + +Create a templates directory for your HTML templates: + +```bash +mkdir -p templates/layouts templates/pages +``` + +Create a base layout (`templates/layouts/base.tmpl.html`): + +```html + + + + {{ .Title }} + + + +
+

{{ .Title }}

+
+
+ {{ template "content" . }} +
+ + + +``` + +### Why Templates? +- Separates HTML structure from logic +- Enables dynamic content generation +- Supports reusable layouts and components + +### Template System Features + +1. **Layout System**: + - Base templates for consistent structure + - Content blocks for page-specific content + - Partial templates for reusable components + +2. **Development Support**: + - Hot reloading in development mode + - Clear error messages + - Template debugging tools + +```go +templateHandler := template_dir.NewTemplateDirHandler( + template_dir.WithLocalDirectory("./templates"), + template_dir.WithAlwaysReload(true), // For development +) +err = templateHandler.Serve(s, "/") +if err != nil { + return err +} +``` + +## Command System + +Commands are the core feature of Parka, allowing you to expose CLI tools as web services. + +### Why Commands? + +- Convert CLI tools to web services +- Provide multiple output formats +- Enable interactive web interfaces +- Support parameter validation and processing + +### Command Architecture + +1. **Command Definition**: + - Parameters and flags + - Input validation + - Output formatting + - Documentation + +2. **Integration Points**: + - HTTP endpoints + - Web UI + - File downloads + - Streaming output + +Let's create a simple command that we can expose via the web interface. Create `pkg/commands/hello.go`: + +```go +package commands + +import ( + "context" + "github.com/go-go-golems/glazed/pkg/cmds" + "github.com/go-go-golems/glazed/pkg/cmds/parameters" + "github.com/go-go-golems/glazed/pkg/types" +) + +type HelloCommand struct { + cmds.BaseCommand + name string +} + +func NewHelloCommand() *HelloCommand { + return &HelloCommand{ + BaseCommand: cmds.BaseCommand{ + Name: "hello", + Short: "A friendly greeting", + Parameters: parameters.ParameterDefinitions{ + { + Name: "name", + Type: types.String, + Help: "Name to greet", + Default: "World", + Required: false, + }, + }, + }, + } +} + +func (c *HelloCommand) Run(ctx context.Context, gp cmds.GlazeProcessor) error { + return gp.AddRow(ctx, types.NewRowFromMap(map[string]interface{}{ + "message": fmt.Sprintf("Hello, %s!", c.name), + })) +} +``` + +## Exposing Commands via HTTP + +Now let's expose our command through various endpoints: + +```go +// In your Run function +helloCmd := commands.NewHelloCommand() + +// JSON API endpoint +s.Router.GET("/api/hello", json.CreateJSONQueryHandler(helloCmd)) + +// Interactive DataTables UI +s.Router.GET("/hello", datatables.CreateDataTablesHandler( + helloCmd, + "hello.tmpl.html", + "hello", +)) + +// File download endpoint +s.Router.GET("/download/hello.csv", output_file.CreateGlazedFileHandler( + helloCmd, + "hello.csv", +)) +``` + +## Command Directory Handler + +The Command Directory Handler manages multiple commands in a structured way. + +### Why Use Command Directory? + +- Organize multiple commands +- Automatic routing and discovery +- Consistent interface across commands +- Centralized configuration + +### Key Features + +1. **Repository Management**: + - Command discovery + - Version control + - Documentation generation + - Parameter handling + +2. **Output Formats**: + - JSON API endpoints + - Interactive DataTables + - File downloads + - Streaming data + +```go +repo := repositories.NewRepository() +repo.AddCommand(commands.NewHelloCommand()) +// Add more commands... + +cmdHandler, err := command_dir.NewCommandDirHandler( + command_dir.WithRepository(repo), + command_dir.WithDevMode(true), + command_dir.WithGenericCommandHandlerOptions( + generic_command.WithIndexTemplateName("commands/index.tmpl.html"), + generic_command.WithTemplateName("commands/view.tmpl.html"), + ), +) +cobra.CheckErr(err) + +err = cmdHandler.Serve(s, "/commands") +if err != nil { + return err +} +``` + +## Configuration System + +A flexible configuration system is essential for managing complex applications. + +### Why Configuration Files? + +- Separate configuration from code +- Environment-specific settings +- Easy deployment management +- Runtime configuration changes + +### Configuration Features + +1. **Server Settings**: + - Network configuration + - Handler setup + - Middleware configuration + - Development options + +2. **Route Configuration**: + - Path mapping + - Handler selection + - Command settings + - Template configuration + +Create a configuration file structure (`config/config.yaml`): + +```yaml +server: + port: 8080 + host: localhost + +routes: + - path: /static + staticDir: + localPath: ./static + + - path: / + templateDir: + localDirectory: ./templates + indexTemplateName: index.tmpl.html + + - path: /commands + commandDirectory: + includeDefaultRepositories: true + repositories: + - ./pkg/commands + templateLookup: + directories: + - ./templates/commands + indexTemplateName: commands/index.tmpl.html + defaults: + flags: + limit: 100 +``` + +Add configuration loading to your server: + +```go +type Config struct { + Server struct { + Port uint16 `yaml:"port"` + Host string `yaml:"host"` + } `yaml:"server"` + Routes []config.Route `yaml:"routes"` +} + +func loadConfig(file string) (*Config, error) { + data, err := os.ReadFile(file) + if err != nil { + return nil, err + } + + var cfg Config + err = yaml.Unmarshal(data, &cfg) + return &cfg, err +} + +// In your Run function +cfg, err := loadConfig("config/config.yaml") +cobra.CheckErr(err) + +s, err := server.NewServer( + server.WithPort(cfg.Server.Port), + server.WithAddress(cfg.Server.Host), +) +cobra.CheckErr(err) + +for _, route := range cfg.Routes { + handler, err := route.CreateHandler() + cobra.CheckErr(err) + err = handler.Serve(s, route.Path) + cobra.CheckErr(err) +} +``` + +## Development Mode + +Development mode enhances the development experience with useful features. + +### Why Development Mode? + +- Faster development cycle +- Better debugging information +- Hot reloading support +- Detailed error messages + +### Development Features + +1. **Hot Reloading**: + - Template changes + - Static files + - Configuration updates + +2. **Debugging**: + - Detailed logging + - Stack traces + - Performance metrics + - Request inspection + +```go +func init() { + rootCmd.Flags().Bool("dev", false, "Enable development mode") +} + +// In your Run function +dev, _ := cmd.Flags().GetBool("dev") + +if dev { + // Enable template reloading + templateOptions = append(templateOptions, + render.WithAlwaysReload(true), + ) + + // Use local assets + serverOptions = append(serverOptions, + server.WithStaticPaths(fs.NewStaticPath(os.DirFS("static"), "/assets")), + ) + + // Enable detailed logging + log.Logger = log.Logger.Level(zerolog.DebugLevel) +} +``` + +## Error Handling and Logging + +Proper error handling and logging are crucial for maintaining and debugging applications. + +```go +// Create a custom error handler +s.Router.HTTPErrorHandler = func(err error, c echo.Context) { + code := http.StatusInternalServerError + if he, ok := err.(*echo.HTTPError); ok { + code = he.Code + } + + log.Error(). + Err(err). + Str("path", c.Path()). + Int("status", code). + Msg("Request error") + + if dev { + // Show detailed error in development + err = c.JSON(code, map[string]interface{}{ + "error": err.Error(), + "stack": fmt.Sprintf("%+v", err), + }) + } else { + // Show generic error in production + err = c.JSON(code, map[string]interface{}{ + "error": http.StatusText(code), + }) + } + + if err != nil { + log.Error().Err(err).Msg("Error sending error response") + } +} +``` + +## Security Considerations + +Security is a critical aspect of any web application. + +### Why Security Middleware? + +- Protect against common attacks +- Control access to resources +- Monitor application usage +- Ensure data integrity + +### Security Features + +1. **Protection Layers**: + - CORS configuration + - Rate limiting + - Request validation + - Error sanitization + +2. **Monitoring**: + - Request tracking + - Error logging + - Access patterns + - Security events + +```go +// Add security middleware +s.Router.Use(middleware.Secure()) +s.Router.Use(middleware.CORS()) +s.Router.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20))) + +// Add request ID tracking +s.Router.Use(middleware.RequestID()) + +// Add recovery middleware +s.Router.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ + StackSize: 1 << 10, // 1 KB + LogLevel: log.ERROR, + LogErrorFunc: func(c echo.Context, err error, stack []byte) error { + log.Error(). + Err(err). + Str("stack", string(stack)). + Msg("Panic recovered") + return nil + }, +})) +``` + +## Complete Project Structure + +Your final project structure should look like this: + +``` +my-parka-app/ +├── cmd/ +│ └── server/ +│ └── main.go +├── config/ +│ └── config.yaml +├── pkg/ +│ ├── commands/ +│ │ └── hello.go +│ └── handlers/ +│ └── custom_handlers.go +├── static/ +│ ├── css/ +│ │ └── style.css +│ └── js/ +│ └── main.js +├── templates/ +│ ├── layouts/ +│ │ └── base.tmpl.html +│ ├── pages/ +│ │ └── index.tmpl.html +│ └── commands/ +│ ├── index.tmpl.html +│ └── view.tmpl.html +├── go.mod +└── go.sum +``` + +## Running the Server + +Start the server in development mode: + +```bash +go run cmd/server/main.go --dev +``` + +For production: + +```bash +go build -o my-server cmd/server/main.go +./my-server +``` + +## Further Enhancements + +1. Add authentication and authorization +2. Implement WebSocket support for real-time updates +3. Add database integration +4. Create a custom UI theme +5. Add monitoring and metrics +6. Implement caching +7. Add API documentation using Swagger/OpenAPI + +## Troubleshooting + +Common issues and solutions: + +1. Template not found + - Check template paths + - Verify template lookup configuration + - Enable development mode for detailed errors + +2. Static files not serving + - Verify file permissions + - Check static path configuration + - Ensure files exist in the correct location + +3. Command not found + - Verify command registration + - Check repository configuration + - Enable debug logging + +## Further Reading + +- [Parka Server Documentation](./01-parka-server.md) +- [Handlers Documentation](./02-handlers.md) +- [Echo Framework Documentation](https://echo.labstack.com/) +- [Glazed Command Documentation](https://github.com/go-go-golems/glazed) + +## Putting It All Together + +Now that we understand each component, here's how they work together in a typical request: + +1. **HTTP Request Arrives**: + ``` + GET /commands/hello?name=World + ``` + +2. **Request Processing**: + - Echo routes to correct handler + - Command handler parses parameters + - Command executes with parameters + - Output is formatted according to endpoint + +3. **Response Generation**: + - Handler formats command output + - Middleware processes response + - Content is sent to client + +This flow combines all the components we've built into a cohesive system. \ No newline at end of file diff --git a/pkg/doc/topics/04-config-file.md b/pkg/doc/topics/04-config-file.md new file mode 100644 index 0000000..0316303 --- /dev/null +++ b/pkg/doc/topics/04-config-file.md @@ -0,0 +1,289 @@ +# Configuring Parka Servers with Config Files + +Parka servers can be configured using YAML configuration files that define routes, handlers, and their settings. This document explains how to use config files to set up your Parka server, with a focus on integrating Glazed commands and other handlers. + +## Overview + +A Parka config file allows you to: +- Define multiple routes with different handlers +- Configure static file serving +- Set up template rendering +- Register Glazed commands and command directories +- Configure parameter filters and defaults +- Set up development mode options + +## Basic Structure + +The configuration file has this basic structure: + +```yaml +defaults: + useParkaStaticFiles: true + renderer: + useDefaultParkaRenderer: true + templateDirectory: "./templates" + markdownBaseTemplateName: "base.tmpl.html" + +routes: + - path: "/api" + commandDirectory: + repositories: + - "./commands" + includeDefaultRepositories: true + templateLookup: + directories: + - "./templates" + indexTemplateName: "commands/index.tmpl.html" + defaults: + flags: + limit: 100 +``` + +## Configuration Sections + +### Global Defaults + +The `defaults` section configures global settings for the server: + +```yaml +defaults: + # Whether to use Parka's built-in static files (CSS, JS, etc.) + useParkaStaticFiles: true + + # Renderer configuration for templates + renderer: + # Use Parka's default renderer (includes markdown support) + useDefaultParkaRenderer: true + # Directory containing templates + templateDirectory: "./templates" + # Base template for markdown rendering + markdownBaseTemplateName: "base.tmpl.html" +``` + +### Routes + +Routes define the URL paths and their corresponding handlers. Each route can use one of several handler types: + +#### 1. Command Directory Handler + +The Command Directory handler serves multiple Glazed commands from a repository: + +```yaml +routes: + - path: "/commands" + commandDirectory: + # List of directories containing command definitions + repositories: + - "./commands" + - "./more-commands" + + # Include repositories from environment variables + includeDefaultRepositories: true + + # Template configuration + templateLookup: + directories: + - "./templates/commands" + indexTemplateName: "index.tmpl.html" + + # Default parameter values + defaults: + flags: + limit: 100 + format: "table" + + # Parameter overrides + overrides: + layers: + glazed: + filter: + - id + - name + + # Additional data passed to templates + additionalData: + title: "Command Repository" +``` + +#### 2. Single Command Handler + +For serving individual Glazed commands: + +```yaml +routes: + - path: "/hello" + command: + name: "hello" + templateName: "command.tmpl.html" + defaults: + flags: + greeting: "Hello" +``` + +#### 3. Template Directory Handler + +Serves a directory of templates with support for both HTML and Markdown: + +```yaml +routes: + - path: "/docs" + templateDirectory: + localDirectory: "./templates" + indexTemplateName: "index.tmpl.html" + markdownBaseTemplateName: "base.tmpl.html" + alwaysReload: true +``` + +#### 4. Single Template Handler + +For serving a single template: + +```yaml +routes: + - path: "/" + template: + templateFile: "index.tmpl.html" + alwaysReload: true +``` + +#### 5. Static Directory Handler + +Serves static files from a directory: + +```yaml +routes: + - path: "/static" + static: + localPath: "./static" +``` + +#### 6. Static File Handler + +Serves a single static file: + +```yaml +routes: + - path: "/favicon.ico" + staticFile: + localPath: "./static/favicon.ico" +``` + +## Integration with Glazed Commands + +When integrating Glazed commands, you can configure various aspects of their behavior through the config file: + +### Parameter Filtering + +Control which parameters are exposed and their default values: + +```yaml +commandDirectory: + defaults: + flags: + limit: 100 + format: "json" + layers: + sql-connection: + host: "localhost" + port: 5432 + + overrides: + layers: + glazed: + filter: + - id + - name +``` + +### Template Configuration + +Configure how commands are rendered in the web interface: + +```yaml +commandDirectory: + templateLookup: + directories: + - "./templates/commands" + indexTemplateName: "commands/index.tmpl.html" + defaultTemplateName: "commands/view.tmpl.html" +``` + +## Development Mode + +Development mode can be enabled through the configuration file or programmatically. It affects various aspects of the server: + +- Template reloading +- Static file serving from local directories +- Detailed error messages +- Debug endpoints + +Example configuration with development settings: + +```yaml +defaults: + renderer: + alwaysReload: true + +routes: + - path: "/api" + commandDirectory: + devMode: true + alwaysReload: true +``` + +## Example Implementation + +Here's an example of how to use a config file in your Parka server: + +```go +func main() { + // Read config file + configData, err := os.ReadFile("config.yaml") + if err != nil { + log.Fatal(err) + } + + // Parse config + configFile, err := config.ParseConfig(configData) + if err != nil { + log.Fatal(err) + } + + // Create server + server, err := server.NewServer( + server.WithPort(8080), + server.WithAddress("localhost"), + ) + if err != nil { + log.Fatal(err) + } + + // Create config file handler + cfh := handlers.NewConfigFileHandler( + configFile, + handlers.WithDevMode(true), + handlers.WithRepositoryFactory(myRepositoryFactory), + handlers.WithAppendCommandDirHandlerOptions( + command_dir.WithDevMode(true), + ), + ) + + // Serve + if err := cfh.Serve(server); err != nil { + log.Fatal(err) + } + + // Run server with config file watching + ctx := context.Background() + if err := runConfigFileHandler(ctx, server, cfh); err != nil { + log.Fatal(err) + } +} +``` + + +## Further Reading + +- [Parka Server Documentation](./01-parka-server.md) +- [Handlers Documentation](./02-handlers.md) +- [Glazed Command Tutorial](../../../glazed/prompto/glazed/create-command-tutorial.md) \ No newline at end of file diff --git a/pkg/doc/topics/05-templates-and-rendering.md b/pkg/doc/topics/05-templates-and-rendering.md new file mode 100644 index 0000000..972839f --- /dev/null +++ b/pkg/doc/topics/05-templates-and-rendering.md @@ -0,0 +1,400 @@ +# Template Rendering in Parka + +Parka provides a flexible and powerful template rendering system that supports both HTML and Markdown templates, with features like template lookup, reloading, and directory-based serving. This document explains how the template system works and how to use it effectively. + +## Overview + +The template rendering system in Parka is built around several key concepts: + +1. **Template Lookups**: Interfaces that define how templates are found and loaded +2. **Renderers**: Components that handle the actual template rendering process +3. **Handlers**: Web handlers that serve templates and template directories +4. **Template Types**: Support for both HTML and Markdown templates + +## Template Lookup System + +The template lookup system is the foundation of Parka's template rendering. It's designed to provide a flexible way to load templates from different sources while supporting development and production needs. + +### The TemplateLookup Interface + +```go +type TemplateLookup interface { + // Lookup returns a template by name. If there are multiple names given, + // implementations may choose how to handle them. + Lookup(name ...string) (*template.Template, error) + + // Reload reloads all or partial templates. This is useful for development + // where templates might change without server restart. + Reload(name ...string) error +} +``` + +### Lookup Algorithm + +The Renderer's lookup process follows these steps: + +1. **Template Name Resolution**: + ```go + // From renderer.go + t, err := r.LookupTemplate(templateName+".tmpl.md", templateName+".md", templateName) + if err != nil { + return errors.Wrap(err, "error looking up template") + } + ``` + - First tries `.tmpl.md` for markdown templates + - Then tries `.md` for static markdown + - Then tries `` directly + - If none found, tries `.tmpl.html` and `.html` + +2. **Base Template Resolution** (for markdown): + ```go + baseTemplate, err := r.LookupTemplate(r.MarkdownBaseTemplateName) + if err != nil { + return errors.Wrap(err, "error looking up base template") + } + ``` + +3. **Template Chain Search**: + - Iterates through all configured template lookups + - Returns the first successful match + - Logs debug information for failed lookups + +### Template Lookup Implementations + +#### 1. File-based Lookup (`LookupTemplateFromFile`) + +Loads a single template file and optionally restricts it to a specific template name. + +```go +// Basic usage +lookup := render.NewLookupTemplateFromFile("templates/index.tmpl.html", "") +tmpl, err := lookup.Lookup("index.tmpl.html") + +// With specific template name +lookup := render.NewLookupTemplateFromFile("templates/base.tmpl.html", "base") +tmpl, err := lookup.Lookup("base") // Only responds to "base" +``` + +Example from tests: +```go +// Create a file-based lookup that always returns the same file +lookup := NewLookupTemplateFromFile("templates/tests/test.txt", "") +tmpl, err := lookup.Lookup("any-name.txt") // Will return test.txt content +if err != nil { + log.Fatal(err) +} +``` + +#### 2. Directory-based Lookup (`LookupTemplateFromDirectory`) + +Loads templates from a directory, reloading on every request. + +```go +// Basic usage +lookup := render.NewLookupTemplateFromDirectory("./templates") +tmpl, err := lookup.Lookup("pages/index.html") + +// With trailing slash handling +lookup := render.NewLookupTemplateFromDirectory("templates/") +tmpl, err := lookup.Lookup("index.html") +``` + +Example from tests: +```go +// Create a directory-based lookup +lookup := NewLookupTemplateFromDirectory("templates/tests") +tmpl, err := lookup.Lookup("test.txt") +if err != nil { + log.Fatal(err) +} + +// Execute the template +buf := new(bytes.Buffer) +err = tmpl.Execute(buf, nil) +fmt.Println(buf.String()) // Output: template content +``` + +#### 3. Filesystem-based Lookup (`LookupTemplateFromFS`) + +Most flexible implementation, supporting embedded files and pattern matching. + +```go +// Basic usage with default patterns (*.html) +lookup := render.NewLookupTemplateFromFS( + render.WithFS(os.DirFS("./templates")), +) + +// With custom patterns and base directory +lookup := render.NewLookupTemplateFromFS( + render.WithFS(os.DirFS("./content")), + render.WithBaseDir("pages"), + render.WithPatterns("*.md", "*.html"), + render.WithAlwaysReload(true), +) +``` + +Example from tests: +```go +// Create a filesystem-based lookup with specific patterns +lookup := NewLookupTemplateFromFS( + WithFS(os.DirFS("templates/tests")), + WithPatterns("*.html"), +) +tmpl, err := lookup.Lookup("test.html") +if err != nil { + log.Fatal(err) +} + +// With base directory +lookup := NewLookupTemplateFromFS( + WithFS(os.DirFS("templates")), + WithBaseDir("tests"), + WithPatterns("*.html"), +) +``` + +### Template Reloading + +Each implementation handles reloading differently: + +1. **File-based**: Always reloads on lookup + ```go + func (l *LookupTemplateFromFile) Reload(name ...string) error { + return nil // No need to reload, happens on every lookup + } + ``` + +2. **Directory-based**: Always reloads on lookup + ```go + func (l *LookupTemplateFromDirectory) Reload(_ ...string) error { + return nil // No need to reload, happens on every lookup + } + ``` + +3. **Filesystem-based**: Configurable reloading + ```go + // Configure reloading + lookup := NewLookupTemplateFromFS( + WithAlwaysReload(true), // Reload on every lookup + ) + + // Manual reload + err := lookup.Reload() + if err != nil { + log.Fatal(err) + } + ``` + +Example of dynamic template reloading from tests: +```go +func TestLookupTemplateFromFS_Reload(t *testing.T) { + l := NewLookupTemplateFromFS() + + // Copy a template file + err := copyFile("templates/tests/test.html", "templates/tests/test_tmpl.html") + require.NoError(t, err) + + // Reload to pick up the new file + err = l.Reload() + require.NoError(t, err) + + // Lookup should now find the new template + tmpl, err := l.Lookup("templates/tests/test_tmpl.html") + require.NoError(t, err) + + // Clean up + os.Remove("templates/tests/test_tmpl.html") +} +``` + +## Template Rendering Process + +The rendering process in Parka follows these steps: + +1. **Template Resolution**: + - Looks for templates in the following order: + 1. `.tmpl.md` + 2. `.md` + 3. `.tmpl.html` + 4. `.html` + +2. **Template Processing**: + - For Markdown templates: + 1. Renders the markdown template with data + 2. Converts markdown to HTML + 3. Wraps the result in a base template (if configured) + - For HTML templates: + 1. Renders directly with the provided data + +3. **Data Injection**: + - Merges global renderer data with request-specific data + - Makes data available to templates during rendering + +## Template Handlers + +Parka provides two main types of template handlers for serving templates over HTTP: + +1. **Single Template Handler** (`TemplateHandler`): For serving individual template files +2. **Template Directory Handler** (`TemplateDirHandler`): For serving multiple templates from a directory + +For detailed information about these handlers, including their structure, configuration options, and usage examples, see the [Template Handlers Documentation](./02-handlers.md#template-handlers). + +## Development Mode Features + +When developing with Parka templates, you can enable several features to make development easier: + +1. **Always Reload**: Templates are reloaded on every request +2. **Local Directory Override**: Use local files instead of embedded ones +3. **Base Template Override**: Customize the base template for markdown rendering + +## Best Practices + +1. **Template Organization**: + - Use `.tmpl.html` for HTML templates that need processing + - Use `.html` for static HTML files + - Use `.tmpl.md` for markdown templates that need processing + - Use `.md` for static markdown files + +2. **Template Lookup Configuration**: + - Use `LookupTemplateFromFS` for production + - Use `LookupTemplateFromDirectory` for development + - Use `LookupTemplateFromFile` for single-file cases + +3. **Performance Optimization**: + - Disable `alwaysReload` in production + - Use pattern matching to limit template scanning + - Consider using embedded filesystems in production + +4. **Development Workflow**: + - Enable `alwaysReload` during development + - Use local directories for easy template editing + - Utilize the markdown base template for consistent styling + +## Configuration File Examples + +This section shows how to configure template handlers using Parka's configuration file system. For detailed information about the handlers themselves, refer to the [Template Handlers Documentation](./02-handlers.md#template-handlers). + +### Single Template Configuration + +```yaml +routes: + - path: "/about" + template: + templateFile: "about.tmpl.html" + alwaysReload: true +``` + +### Template Directory Configuration + +```yaml +routes: + - path: "/docs" + templateDirectory: + localDirectory: "./docs" + indexTemplateName: "index.tmpl.html" + markdownBaseTemplateName: "base.tmpl.html" + alwaysReload: true +``` + +### Complete Configuration Example + +Here's a complete example showing various template configurations: + +```yaml +defaults: + renderer: + useDefaultParkaRenderer: true + templateDirectory: "./templates" + markdownBaseTemplateName: "base.tmpl.html" + +routes: + # Serve a single template + - path: "/" + template: + templateFile: "index.tmpl.html" + alwaysReload: true + + # Serve a documentation directory + - path: "/docs" + templateDirectory: + localDirectory: "./docs" + indexTemplateName: "index.tmpl.html" + markdownBaseTemplateName: "docs-base.tmpl.html" + alwaysReload: true + + # Serve API documentation + - path: "/api" + template: + templateFile: "api.tmpl.html" + data: + title: "API Documentation" + version: "1.0" +``` + +And here's the equivalent programmatic setup: + +```go +package main + +import ( + "github.com/go-go-golems/parka/pkg/render" + "github.com/go-go-golems/parka/pkg/handlers/template" + "github.com/go-go-golems/parka/pkg/handlers/template-dir" + "github.com/go-go-golems/parka/pkg/server" +) + +func main() { + // Create a new server + server, err := server.NewServer() + if err != nil { + panic(err) + } + + // Set up default renderer + defaultRenderer, err := render.NewRenderer( + render.WithMarkdownBaseTemplateName("base.tmpl.html"), + ) + if err != nil { + panic(err) + } + + // Set up index page + indexHandler := template.NewTemplateHandler( + "index.tmpl.html", + template.WithAlwaysReload(true), + ) + indexHandler.Serve(server, "/") + + // Set up documentation pages + docsHandler := template_dir.NewTemplateDirHandler( + template_dir.WithLocalDirectory("./docs"), + template_dir.WithAlwaysReload(true), + template_dir.WithAppendRendererOptions( + render.WithMarkdownBaseTemplateName("docs-base.tmpl.html"), + render.WithIndexTemplateName("index.tmpl.html"), + ), + ) + docsHandler.Serve(server, "/docs") + + // Set up API documentation + apiHandler := template.NewTemplateHandler( + "api.tmpl.html", + template.WithAppendRendererOptions( + render.WithMergeData(map[string]interface{}{ + "title": "API Documentation", + "version": "1.0", + }), + ), + ) + apiHandler.Serve(server, "/api") + + // Start the server + if err := server.Start(":8080"); err != nil { + panic(err) + } +} +``` + +## Conclusion + +Parka's template rendering system provides a flexible and powerful way to serve both HTML and Markdown content. By understanding the different components and their interactions, you can effectively use templates in your Parka applications while maintaining good development practices and performance considerations. \ No newline at end of file diff --git a/pkg/doc/topics/06-command-handlers.md b/pkg/doc/topics/06-command-handlers.md new file mode 100644 index 0000000..36f5298 --- /dev/null +++ b/pkg/doc/topics/06-command-handlers.md @@ -0,0 +1,1161 @@ +# Command Handler Parameter Filtering + +This document describes the parameter filtering options available for command handlers in Parka. These options allow you to control how parameters are handled, including setting defaults, overrides, and filtering which parameters are exposed. + +## Overview + +Parameter filtering can be configured at three levels: +1. Generic Command Handler +2. Command Handler +3. Command Directory Handler + +Each handler type supports the same parameter filtering options through the `ParameterFilter` configuration. + +## Programmatic Configuration + +### Required Imports + +```go +import ( + "context" + "os" + + "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/parameters" + "github.com/go-go-golems/glazed/pkg/middlewares" + "github.com/go-go-golems/parka/pkg/handlers" + "github.com/go-go-golems/parka/pkg/handlers/command" + "github.com/go-go-golems/parka/pkg/handlers/generic-command" + "github.com/go-go-golems/parka/pkg/handlers/config" + "github.com/go-go-golems/parka/pkg/repositories" +) +``` + +### Package Structure + +The main packages you'll interact with are: + +- `handlers/config` - Contains the parameter filtering configuration types and options +- `handlers/command` - Command handler implementation +- `handlers/generic-command` - Generic command handler base implementation +- `handlers/command-dir` - Command directory handler implementation +- `repositories` - Command repository management + +Common import aliases: +```go +import ( + config "github.com/go-go-golems/parka/pkg/handlers/config" + command "github.com/go-go-golems/parka/pkg/handlers/command" + generic_command "github.com/go-go-golems/parka/pkg/handlers/generic-command" +) +``` + +### Basic Structure + +The parameter filter is configured using the `ParameterFilter` struct and its associated types: + +```go +type ParameterFilter struct { + Overrides *LayerParameters + Defaults *LayerParameters + Whitelist *ParameterFilterList + Blacklist *ParameterFilterList +} + +type LayerParameters struct { + Parameters map[string]string + Layers map[string]map[string]interface{} +} +``` + +### Creating Parameter Filters + +```go +// Basic filter creation using options +filter := config.NewParameterFilter( + config.WithOverrideParameter("limit", "100"), + config.WithMergeOverrideLayer("glazed", map[string]interface{}{ + "filter": []string{"id", "name"}, + }), + config.WithDefaultParameter("format", "table"), + config.WithMergeDefaultLayer("sql-connection", map[string]interface{}{ + "host": "localhost", + "port": 5432, + }), +) + +// Using replace options +filter := config.NewParameterFilter( + config.WithReplaceOverrides(&config.LayerParameters{ + Parameters: map[string]string{ + "limit": "100", + }, + Layers: map[string]map[string]interface{}{ + "glazed": { + "filter": []string{"id", "name"}, + }, + }, + }), +) + +// Merging configurations +filter := config.NewParameterFilter( + config.WithMergeOverrides(&config.LayerParameters{ + Parameters: map[string]string{ + "limit": "100", + }, + }), + config.WithMergeDefaults(&config.LayerParameters{ + Layers: map[string]map[string]interface{}{ + "sql-connection": { + "host": "localhost", + }, + }, + }), +) +``` + +### Available Configuration Options + +```go +// Override options +config.WithReplaceOverrides(*LayerParameters) // Replace all overrides +config.WithMergeOverrides(*LayerParameters) // Merge with existing overrides +config.WithOverrideParameter(name string, value interface{}) // Set single parameter override +config.WithOverrideParameters(map[string]interface{}) // Set multiple parameter overrides +config.WithMergeOverrideLayer(name string, layer map[string]interface{}) // Merge layer overrides +config.WithReplaceOverrideLayer(name string, layer map[string]interface{}) // Replace layer overrides +config.WithOverrideLayers(map[string]map[string]interface{}) // Set multiple layer overrides + +// Default options +config.WithReplaceDefaults(*LayerParameters) // Replace all defaults +config.WithMergeDefaults(*LayerParameters) // Merge with existing defaults +config.WithDefaultParameter(name string, value interface{}) // Set single parameter default +config.WithDefaultParameters(map[string]interface{}) // Set multiple parameter defaults +config.WithMergeDefaultLayer(name string, layer map[string]interface{}) // Merge layer defaults +config.WithReplaceDefaultLayer(name string, layer map[string]interface{}) // Replace layer defaults +config.WithDefaultLayers(map[string]map[string]interface{}) // Set multiple layer defaults + +// Layer defaults helper +config.WithLayerDefaults(name string, layer map[string]interface{}) // Set defaults for layer, skip if exists + +// Whitelist options +config.WithWhitelist(*ParameterFilterList) // Replace entire whitelist +config.WithWhitelistParameters(...string) // Add parameters to whitelist +config.WithWhitelistLayers(...string) // Add layers to whitelist +config.WithWhitelistLayerParameters(layer string, ...string) // Add parameters for specific layer to whitelist + +// Blacklist options +config.WithBlacklist(*ParameterFilterList) // Replace entire blacklist +config.WithBlacklistParameters(...string) // Add parameters to blacklist +config.WithBlacklistLayers(...string) // Add layers to blacklist +config.WithBlacklistLayerParameters(layer string, ...string) // Add parameters for specific layer to blacklist +``` + +### Parameter Filter List Structure + +The `ParameterFilterList` is used for both whitelists and blacklists: + +```go +type ParameterFilterList struct { + Layers []string // Layers to filter + LayerParameters map[string][]string // Parameters to filter per layer + Parameters []string // Parameters to filter in default layer +} +``` + +### Whitelist/Blacklist Examples + +```go +// Creating a whitelist +filter := config.NewParameterFilter( + // Whitelist specific parameters + config.WithWhitelistParameters("limit", "offset", "format"), + + // Whitelist entire layers + config.WithWhitelistLayers("sql-connection", "glazed"), + + // Whitelist specific parameters in a layer + config.WithWhitelistLayerParameters("glazed", "filter", "output"), +) + +// Creating a blacklist +filter := config.NewParameterFilter( + // Blacklist debug parameters + config.WithBlacklistParameters("debug", "verbose", "trace"), + + // Blacklist sensitive layers + config.WithBlacklistLayers("secrets", "internal"), + + // Blacklist sensitive parameters in a layer + config.WithBlacklistLayerParameters("sql-connection", "password", "api_key"), +) + +// Combining whitelist and blacklist +filter := config.NewParameterFilter( + // Whitelist allowed parameters + config.WithWhitelistParameters("limit", "offset"), + config.WithWhitelistLayerParameters("glazed", "filter"), + + // Blacklist sensitive information + config.WithBlacklistParameters("debug"), + config.WithBlacklistLayerParameters("sql-connection", "password"), +) + +// Using complete filter lists +filter := config.NewParameterFilter( + config.WithWhitelist(&config.ParameterFilterList{ + Layers: []string{"glazed", "sql-connection"}, + LayerParameters: map[string][]string{ + "glazed": {"filter", "output"}, + "sql-connection": {"host", "port", "database"}, + }, + Parameters: []string{"limit", "offset", "format"}, + }), + config.WithBlacklist(&config.ParameterFilterList{ + Parameters: []string{"debug", "verbose"}, + LayerParameters: map[string][]string{ + "sql-connection": {"password", "api_key"}, + }, + }), +) +``` + +### Using Filters with Command Handlers + +```go +handler := handlers.NewCommandHandler(cmd, + handlers.WithParameterFilter( + config.NewParameterFilter( + // Allow only specific parameters + config.WithWhitelistParameters("limit", "offset", "format"), + config.WithWhitelistLayerParameters("glazed", "filter", "output"), + + // Block sensitive parameters + config.WithBlacklistLayerParameters("sql-connection", "password"), + + // Set defaults for allowed parameters + config.WithDefaultParameter("limit", "100"), + config.WithMergeDefaultLayer("glazed", map[string]interface{}{ + "filter": []string{"id", "name"}, + }), + ), + ), +) +``` + +### Configuring Command Handlers + +#### Generic Command Handler + +```go +handler := handlers.NewGenericCommandHandler( + handlers.WithParameterFilter( + config.NewParameterFilter( + config.WithMergeDefaultLayer("glazed", map[string]interface{}{ + "filter": []string{"id", "name"}, + }), + config.WithMergeOverrideLayer("sql-connection", map[string]interface{}{ + "host": os.Getenv("DB_HOST"), + "port": os.Getenv("DB_PORT"), + }), + ), + ), +) +``` + +#### Command Handler + +```go +cmd := &MyCommand{} +handler := handlers.NewCommandHandler(cmd, + handlers.WithParameterFilter(filter), + handlers.WithLayerDefaults("sql-connection", map[string]interface{}{ + "host": "localhost", + "port": 5432, + }), + handlers.WithLayerOverrides("glazed", map[string]interface{}{ + "filter": []string{"quantity_sold", "sales_usd"}, + }), +) +``` + +#### Command Directory Handler + +```go +repo := repositories.NewRepository() +handler := handlers.NewCommandDirHandler( + handlers.WithRepository(repo), + handlers.WithParameterFilter(filter), + handlers.WithLayerConfiguration("sql-connection", &SQLConnectionConfig{ + Host: os.Getenv("DB_HOST"), + Port: os.Getenv("DB_PORT"), + User: "ttc_analytics", + Password: os.Getenv("DB_PASSWORD"), + }), +) +``` + +### Layer-Specific Configuration + +```go +// Creating layer-specific defaults +layerDefaults := layers.NewParameterLayerDefaults(). + WithLayer("glazed", map[string]interface{}{ + "filter": []string{"id", "name"}, + }). + WithLayer("sql-connection", map[string]interface{}{ + "host": "localhost", + "port": 5432, + }) + +// Creating layer-specific overrides +layerOverrides := layers.NewParameterLayerOverrides(). + WithLayer("dbt", map[string]interface{}{ + "dbt-profile": "ttc.analytics", + }). + WithLayer("glazed", map[string]interface{}{ + "filter": []string{"quantity_sold", "sales_usd"}, + }) + +// Applying to a handler +handler := handlers.NewCommandHandler(cmd, + handlers.WithLayerDefaults(layerDefaults), + handlers.WithLayerOverrides(layerOverrides), +) +``` + +### Environment and External Values + +```go +// Environment variables +envConfig := config.NewEnvironmentConfig(). + WithVariable("DB_HOST", "localhost"). + WithVariable("DB_PORT", "5432") + +// AWS SSM parameters +ssmConfig := config.NewAWSSSMConfig(). + WithParameter("DB_PASSWORD", "/prod/db/password") + +// Applying to a handler +handler := handlers.NewCommandHandler(cmd, + handlers.WithEnvironmentConfig(envConfig), + handlers.WithAWSSSMConfig(ssmConfig), + handlers.WithLayerOverrides("sql-connection", map[string]interface{}{ + "host": config.EnvVar("DB_HOST"), + "port": config.EnvVar("DB_PORT"), + "password": config.SSMParam("DB_PASSWORD"), + }), +) +``` + +### Dynamic Configuration + +```go +// Creating a dynamic parameter filter +filter := config.NewDynamicParameterFilter( + config.WithFilterFunc(func(ctx context.Context, name string, value interface{}) (interface{}, error) { + // Custom filtering logic + return value, nil + }), + config.WithValidationFunc(func(ctx context.Context, name string, value interface{}) error { + // Custom validation logic + return nil + }), +) + +// Adding middleware for parameter processing +handler := handlers.NewCommandHandler(cmd, + handlers.WithParameterFilter(filter), + handlers.WithMiddleware( + middlewares.NewParameterProcessingMiddleware(). + WithPreProcessor(func(ctx context.Context, params map[string]interface{}) error { + // Pre-processing logic + return nil + }). + WithPostProcessor(func(ctx context.Context, params map[string]interface{}) error { + // Post-processing logic + return nil + }), + ), +) +``` + +### Complete Example + +Here's a comprehensive example showing various programmatic configuration options: + +```go +func NewAnalyticsHandler(ctx context.Context) (*handlers.CommandHandler, error) { + // Create base parameter filter + filter := config.NewParameterFilter(). + WithDefaults(map[string]interface{}{ + "flags": map[string]interface{}{ + "limit": 100, + "format": "table", + }, + }). + WithWhitelist("limit", "offset", "format"). + WithBlacklist("debug", "verbose") + + // Configure layers + layerConfig := layers.NewParameterLayers( + layers.WithDefaults(map[string]interface{}{ + "glazed": map[string]interface{}{ + "filter": []string{"id", "timestamp"}, + }, + }), + layers.WithOverrides(map[string]interface{}{ + "sql-connection": map[string]interface{}{ + "host": config.EnvVar("SQLETON_HOST"), + "port": config.EnvVar("SQLETON_PORT"), + "user": "ttc_analytics", + "password": config.SSMParam("DB_PASSWORD_SSM_KEY"), + "schema": "ttc_analytics", + "database": "ttc_analytics", + "db-type": "mysql", + }, + "dbt": map[string]interface{}{ + "dbt-profile": "ttc.analytics", + }, + "glazed": map[string]interface{}{ + "filter": []string{"quantity_sold", "sales_usd"}, + }, + }), + ) + + // Create command + cmd := &AnalyticsCommand{} + + // Create and configure handler + handler := handlers.NewCommandHandler(cmd, + handlers.WithParameterFilter(filter), + handlers.WithLayers(layerConfig), + handlers.WithMiddleware( + middlewares.NewLoggingMiddleware(), + middlewares.NewValidationMiddleware(), + middlewares.NewParameterProcessingMiddleware(), + ), + handlers.WithErrorHandler(func(ctx context.Context, err error) error { + log.Error().Err(err).Msg("Command execution failed") + return err + }), + ) + + return handler, nil +} +``` + +### Best Practices for Programmatic Configuration + +1. **Type Safety** + - Use strongly typed configuration structs where possible + - Implement validation for parameter values + - Use constants for parameter names + +2. **Error Handling** + - Always check errors from configuration methods + - Provide meaningful error messages + - Implement proper cleanup in error cases + +3. **Testing** + - Create test configurations + - Mock external dependencies + - Verify parameter filtering behavior + +4. **Middleware** + - Use middleware for cross-cutting concerns + - Implement proper middleware ordering + - Keep middleware focused and composable + +## Parameter Filter Configuration + +The parameter filter configuration can include: + +- `defaults`: Default values for parameters +- `overrides`: Values that override any user input +- `whitelist`: List of parameters that are allowed +- `blacklist`: List of parameters that are blocked + +### Structure + +```yaml +commandDirectory: + defaults: + flags: + paramName: value + layers: + layerName: + paramName: value + overrides: + layers: + layerName: + paramName: value + whitelist: + - param1 + - param2 + blacklist: + - param3 + - param4 +``` + +## Default Values + +Default values are applied when a parameter is not provided by the user. They can be specified for both flags and layer parameters. + +### Flag Defaults + +```yaml +defaults: + flags: + limit: 1337 + offset: 0 + format: "table" +``` + +### Layer Defaults + +```yaml +defaults: + layers: + glazed: + filter: + - id + - name + sql-connection: + host: "localhost" + port: 5432 +``` + +## Parameter Overrides + +Overrides force specific parameter values regardless of user input. This is useful for enforcing security policies or ensuring consistent configuration. + +```yaml +overrides: + layers: + dbt: + dbt-profile: "ttc.analytics" + glazed: + filter: + - quantity_sold + - sales_usd + sql-connection: + schema: "ttc_analytics" + database: "ttc_analytics" + user: "ttc_analytics" +``` + +## Environment Variables and External Values + +Parameters can reference environment variables and external sources: + +```yaml +overrides: + layers: + sql-connection: + host: + _env: SQLETON_HOST + port: + _env: SQLETON_PORT + password: + _aws_ssm: + _env: SSM_KEY_DB_PASSWORD +``` + +## Layer Inheritance + +You can use YAML anchors and aliases to share configuration between routes: + +```yaml +overrides: + layers: + sql-connection: &sql-connection + host: + _env: SQLETON_HOST + port: + _env: SQLETON_PORT + user: "ttc_analytics" + +# Later in the configuration +- path: /reports/ + commandDirectory: + overrides: + layers: + sql-connection: + <<: *sql-connection + schema: "ttc_prod" + database: "ttc_prod" +``` + +## Complete Example + +Here's a comprehensive example showing various parameter filtering options: + +```yaml +routes: + - path: /analytics/ + commandDirectory: + includeDefaultRepositories: false + repositories: + - /queries/ttc + + # Default values for parameters + defaults: + flags: + limit: 100 + format: "table" + layers: + glazed: + filter: + - id + - timestamp + + # Override specific parameters + overrides: + layers: + sql-connection: + host: + _env: SQLETON_HOST + port: + _env: SQLETON_PORT + user: "ttc_analytics" + password: + _aws_ssm: + _env: SSM_KEY_DB_PASSWORD + schema: "ttc_analytics" + database: "ttc_analytics" + db-type: "mysql" + + dbt: + dbt-profile: "ttc.analytics" + + glazed: + filter: + - quantity_sold + - sales_usd + + # Optional whitelist/blacklist + whitelist: + - limit + - offset + - format + blacklist: + - debug + - verbose +``` + +## Best Practices + +1. **Security** + - Use overrides for sensitive configuration like database credentials + - Blacklist debug or verbose flags in production + - Use environment variables for configuration that changes between environments + +2. **Defaults** + - Set reasonable defaults for pagination (limit, offset) + - Configure default output formats + - Provide sensible filter columns + +3. **Layer Configuration** + - Group related parameters in layers + - Use YAML anchors for shared configurations + - Override only necessary parameters in derived configurations + +4. **Parameter Filtering** + - Whitelist parameters that should be exposed to users + - Blacklist sensitive or dangerous parameters + - Document which parameters are available/blocked + +## Common Use Cases + +### Database Connection Configuration + +```yaml +overrides: + layers: + sql-connection: + host: + _env: DB_HOST + port: + _env: DB_PORT + user: + _env: DB_USER + password: + _aws_ssm: + _env: DB_PASSWORD_SSM_KEY +``` + +### Output Formatting + +```yaml +defaults: + flags: + format: "table" + limit: 1000 + layers: + glazed: + filter: + - id + - name + - timestamp +``` + +### Environment-Specific Settings + +```yaml +- path: /prod/ + commandDirectory: + overrides: + layers: + dbt: + dbt-profile: "prod" + sql-connection: + schema: "prod" + database: "prod" +``` + +## Troubleshooting + +1. **Parameter Not Available** + - Check if parameter is blacklisted + - Verify parameter isn't overridden + - Ensure parameter is whitelisted if whitelist is used + +2. **Default Not Applied** + - Confirm default is in correct section (flags vs layers) + - Check for overrides that might supersede default + - Verify parameter name matches exactly + +3. **Override Not Working** + - Verify override is in correct layer + - Check environment variables are set if used + - Confirm YAML syntax for anchors and aliases + +## Parameter Filter Middleware Implementation + +The parameter filter system in Parka is implemented through a series of middlewares that modify the parameter layers before and after command execution. Here's a detailed look at how these middlewares work and how they can be used effectively. + +### Middleware Chain Execution + +The middleware system follows these key principles: + +1. Middlewares are executed in reverse order of their definition +2. Each middleware can modify both the parameter layers and parsed layers +3. The execution order matters for different types of operations: + - For modifying parsed layers (e.g., setting values): Call `next` first + - For modifying parameter layers (e.g., filtering): Call `next` last +4. Middlewares can be added before or after the parameter filter middlewares using `WithPreMiddlewares` and `WithPostMiddlewares` + +Example of middleware execution order: + +```go +// This chain with pre and post middlewares: +handler := NewGenericCommandHandler( + WithPreMiddlewares( + LoggingMiddleware(), // Run first + ValidationMiddleware(), // Run second + ), + WithParameterFilter(filter), // Parameter filter middlewares run third + WithPostMiddlewares( + MetricsMiddleware(), // Run fourth + AuditMiddleware(), // Run last + ), +) + +// Executes as: +LoggingMiddleware( + ValidationMiddleware( + ParameterFilterMiddlewares( + MetricsMiddleware( + AuditMiddleware( + Identity + ) + ) + ) + ) +) +``` + +### Core Middleware Types + +#### 1. Whitelist Middlewares + +Whitelist middlewares restrict which layers and parameters are available: + +```go +// Whitelist entire layers +filter := config.NewParameterFilter( + config.WithWhitelistLayers("sql", "http"), +) + +// Whitelist specific parameters in layers +filter := config.NewParameterFilter( + config.WithWhitelistLayerParameters("sql", "host", "port", "database"), + config.WithWhitelistLayerParameters("http", "timeout", "retries"), +) +``` + +Implementation details: +- `WhitelistLayers`: Removes any layers not in the whitelist +- `WhitelistLayerParameters`: Removes parameters not in the whitelist for each layer +- Both can be applied before or after other middlewares using `First` variants + +#### 2. Blacklist Middlewares + +Blacklist middlewares exclude specific layers and parameters: + +```go +// Blacklist sensitive layers +filter := config.NewParameterFilter( + config.WithBlacklistLayers("secrets", "internal"), +) + +// Blacklist sensitive parameters +filter := config.NewParameterFilter( + config.WithBlacklistLayerParameters("database", "password", "api_key"), + config.WithBlacklistLayerParameters("auth", "token", "secret"), +) +``` + +Implementation details: +- `BlacklistLayers`: Removes specified layers +- `BlacklistLayerParameters`: Removes specified parameters from layers +- Both support `First` variants for execution order control + +#### 3. Parameter Update Middlewares + +Update middlewares modify parameter values: + +```go +// Set overrides +filter := config.NewParameterFilter( + config.WithOverrideParameter("debug", "false"), + config.WithMergeOverrideLayer("database", map[string]interface{}{ + "max_connections": 100, + "timeout": "30s", + }), +) + +// Set defaults +filter := config.NewParameterFilter( + config.WithDefaultParameter("page_size", "50"), + config.WithMergeDefaultLayer("http", map[string]interface{}{ + "timeout": "5s", + "retries": 3, + }), +) +``` + +### Advanced Middleware Patterns + +#### 1. Conditional Parameter Filtering + +Apply different filters based on conditions: + +```go +// Production environment filter +if env == "production" { + filter := config.NewParameterFilter( + // Strict security in production + config.WithBlacklistParameters("debug", "trace"), + config.WithWhitelistLayerParameters("database", + "host", "port", "name", "pool_size"), + config.WithOverrideLayer("logging", map[string]interface{}{ + "level": "error", + "format": "json", + }), + ) +} else { + // Development environment - more permissive + filter := config.NewParameterFilter( + config.WithDefaultParameter("debug", "true"), + config.WithMergeDefaultLayer("database", map[string]interface{}{ + "host": "localhost", + "port": 5432, + }), + ) +} +``` + +#### 2. Layer-Specific Middleware Chains + +Apply different middleware chains to different layers: + +```go +handler := handlers.NewCommandHandler(cmd, + handlers.WithParameterFilter( + config.NewParameterFilter( + // Database layer gets special treatment + config.WithWrapWithWhitelistedLayers( + []string{"database"}, + config.WithOverrideParameter("ssl", "true"), + config.WithBlacklistParameters("password"), + ), + // HTTP layer gets different treatment + config.WithWrapWithWhitelistedLayers( + []string{"http"}, + config.WithDefaultParameter("timeout", "10s"), + config.WithWhitelistParameters("method", "url", "headers"), + ), + ), + ), +) +``` + +#### 3. Pre and Post Middleware Configuration + +Configure different middleware chains for different stages: + +```go +// Create middleware chains +preMiddlewares := []middlewares.Middleware{ + // Run before parameter filtering + middlewares.NewLoggingMiddleware(), + middlewares.NewValidationMiddleware(), +} + +postMiddlewares := []middlewares.Middleware{ + // Run after parameter filtering + middlewares.NewMetricsMiddleware(), + middlewares.NewAuditMiddleware(), +} + +// Apply to handler +handler := handlers.NewCommandHandler(cmd, + handlers.WithParameterFilter(filter), + handlers.WithPreMiddlewares(preMiddlewares...), + handlers.WithPostMiddlewares(postMiddlewares...), +) +``` + +#### 4. Comprehensive Middleware Configuration + +```go +func NewSecureHandlerWithMiddlewares() *handlers.CommandHandler { + // Create pre-middlewares for validation and logging + preMiddlewares := []middlewares.Middleware{ + middlewares.NewValidationMiddleware( + middlewares.WithRequiredParameters("user_id", "action"), + middlewares.WithParameterValidation(func(name string, value interface{}) error { + // Custom validation logic + return nil + }), + ), + middlewares.NewLoggingMiddleware( + middlewares.WithLogLevel("debug"), + middlewares.WithLogFormat("json"), + ), + } + + // Create post-middlewares for metrics and auditing + postMiddlewares := []middlewares.Middleware{ + middlewares.NewMetricsMiddleware( + middlewares.WithMetricsPrefix("api"), + middlewares.WithLabels(map[string]string{ + "service": "command-handler", + }), + ), + middlewares.NewAuditMiddleware( + middlewares.WithAuditLogger(auditLogger), + middlewares.WithAuditLevel("info"), + ), + } + + // Create parameter filter + filter := config.NewParameterFilter( + config.WithWhitelistParameters( + "user_id", "resource_id", "action", + ), + config.WithBlacklistParameters( + "password", "token", "api_key", + ), + ) + + // Create handler with all middleware chains + return handlers.NewCommandHandler(cmd, + handlers.WithParameterFilter(filter), + handlers.WithPreMiddlewares(preMiddlewares...), + handlers.WithPostMiddlewares(postMiddlewares...), + ) +} +``` + +### Best Practices for Parameter Filtering + +1. **Security First** + - Always blacklist sensitive parameters + - Use whitelists in production environments + - Override security-critical settings + +2. **Configuration Layering** + - Use defaults for development-friendly values + - Apply environment-specific overrides + - Keep security parameters separate from functional ones + +3. **Middleware Ordering** + - Apply blacklists before whitelists + - Set defaults before overrides + - Consider using `First` variants for critical filters + +4. **Error Handling** + - Validate parameter values after filtering + - Provide clear error messages for filtered parameters + - Log attempted access to restricted parameters + +5. **Documentation** + - Document which parameters are available/restricted + - Explain the rationale for parameter filtering + - Provide examples for common use cases + +### Parameter Configuration Examples + +```go +// Setting individual parameters +filter := config.NewParameterFilter( + // Override single parameter + config.WithOverrideParameter("debug", false), + + // Override multiple parameters + config.WithOverrideParameters(map[string]interface{}{ + "limit": 100, + "format": "json", + }), + + // Default single parameter + config.WithDefaultParameter("timeout", "30s"), + + // Default multiple parameters + config.WithDefaultParameters(map[string]interface{}{ + "page_size": 50, + "sort_order": "desc", + }), +) + +// Configuring layers +filter := config.NewParameterFilter( + // Merge layer overrides + config.WithMergeOverrideLayer("sql-connection", map[string]interface{}{ + "host": "localhost", + "port": 5432, + }), + + // Replace layer overrides + config.WithReplaceOverrideLayer("http", map[string]interface{}{ + "timeout": "5s", + "retries": 3, + }), + + // Set multiple layer overrides + config.WithOverrideLayers(map[string]map[string]interface{}{ + "sql-connection": { + "host": "localhost", + "port": 5432, + }, + "http": { + "timeout": "5s", + "retries": 3, + }, + }), +) + +// Combining defaults and overrides +filter := config.NewParameterFilter( + // Set defaults + config.WithDefaultParameters(map[string]interface{}{ + "limit": 50, + "format": "table", + }), + config.WithDefaultLayers(map[string]map[string]interface{}{ + "sql-connection": { + "host": "localhost", + "port": 5432, + }, + }), + + // Override specific values + config.WithOverrideParameters(map[string]interface{}{ + "format": "json", + }), + config.WithMergeOverrideLayer("sql-connection", map[string]interface{}{ + "ssl": true, + }), +) + +// Complete example with all options +filter := config.NewParameterFilter( + // Defaults + config.WithDefaultParameters(map[string]interface{}{ + "limit": 50, + "format": "table", + }), + config.WithDefaultLayers(map[string]map[string]interface{}{ + "sql-connection": { + "host": "localhost", + "port": 5432, + }, + "http": { + "timeout": "5s", + }, + }), + + // Overrides + config.WithOverrideParameters(map[string]interface{}{ + "debug": false, + }), + config.WithOverrideLayers(map[string]map[string]interface{}{ + "sql-connection": { + "ssl": true, + "max_connections": 100, + }, + }), + + // Whitelist + config.WithWhitelistParameters("limit", "format"), + config.WithWhitelistLayerParameters("sql-connection", "host", "port", "ssl"), + + // Blacklist + config.WithBlacklistParameters("verbose"), + config.WithBlacklistLayerParameters("sql-connection", "password"), +) +``` + +### Using with Command Handlers + +```go +handler := handlers.NewCommandHandler(cmd, + handlers.WithParameterFilter( + config.NewParameterFilter( + // Set defaults + config.WithDefaultParameters(map[string]interface{}{ + "limit": 50, + "format": "table", + }), + config.WithDefaultLayers(map[string]map[string]interface{}{ + "sql-connection": { + "host": "localhost", + "port": 5432, + }, + }), + + // Override production settings + config.WithOverrideLayers(map[string]map[string]interface{}{ + "sql-connection": { + "host": os.Getenv("DB_HOST"), + "port": os.Getenv("DB_PORT"), + "ssl": true, + }, + "logging": { + "level": "error", + "format": "json", + }, + }), + + // Security filters + config.WithWhitelistParameters("limit", "format"), + config.WithBlacklistLayerParameters("sql-connection", "password"), + ), + ), +) +``` \ No newline at end of file diff --git a/pkg/glazed/handlers/datatables/datatables.go b/pkg/glazed/handlers/datatables/datatables.go index b876850..3810e07 100644 --- a/pkg/glazed/handlers/datatables/datatables.go +++ b/pkg/glazed/handlers/datatables/datatables.go @@ -4,6 +4,10 @@ import ( "context" "embed" "fmt" + "html/template" + "io" + "time" + "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" @@ -22,9 +26,6 @@ import ( "github.com/labstack/echo/v4" "github.com/pkg/errors" "github.com/rs/zerolog/log" - "html/template" - "io" - "time" ) // DataTables describes the data passed to the template displaying the results of a glazed command. @@ -149,7 +150,11 @@ func WithTemplateLookup(lookup render.TemplateLookup) QueryHandlerOption { func WithTemplateName(templateName string) QueryHandlerOption { return func(h *QueryHandler) { - h.templateName = templateName + if templateName == "" { + h.templateName = "data-tables.tmpl.html" + } else { + h.templateName = templateName + } } } @@ -186,14 +191,13 @@ func (qh *QueryHandler) Handle(c echo.Context) 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(), - )..., - ) - + middlewares_ := []middlewares.Middleware{ + parka_middlewares.UpdateFromQueryParameters(c, + parameters.WithParseStepSource("query")), + } + middlewares_ = append(middlewares_, qh.middlewares...) + middlewares_ = append(middlewares_, middlewares.SetFromDefaults()) + err := middlewares.ExecuteMiddlewares(description.Layers.Clone(), parsedLayers, middlewares_...) if err != nil { log.Debug().Err(err).Msg("error executing middlewares") g := &safegroup.Group{} @@ -309,15 +313,18 @@ func (qh *QueryHandler) Handle(c echo.Context) error { g := &safegroup.Group{} err_ := err g.Go(func() error { - log.Debug().Err(err_).Msg("error running command") - // make sure to render the ErrorStream at the bottom, because we would otherwise get into a deadlock with the streaming channels - // NOTE(manuel, 2024-05-14) I'm not sure if with the addition of goroutines this is actually still necessary - // - log.Debug().Msg("sending error to error stream") + defer close(dt_.ErrorStream) + if err_ != nil { - dt_.ErrorStream <- err_.Error() + log.Debug().Err(err_).Msg("error running command") + // make sure to render the ErrorStream at the bottom, because we would otherwise get into a deadlock with the streaming channels + // NOTE(manuel, 2024-05-14) I'm not sure if with the addition of goroutines this is actually still necessary + // + log.Debug().Msg("sending error to error stream") + if err_ != nil { + dt_.ErrorStream <- err_.Error() + } } - close(dt_.ErrorStream) return nil }) @@ -372,6 +379,9 @@ func (qh *QueryHandler) renderTemplate( // our own output formatter that renders into an HTML template. var err error + if qh.lookup == nil { + qh.lookup = NewDataTablesLookupTemplate() + } t, err := qh.lookup.Lookup(qh.templateName) if err != nil { return err diff --git a/pkg/handlers/command-dir/command-dir.go b/pkg/handlers/command-dir/command-dir.go index a9f176b..ecbbd2d 100644 --- a/pkg/handlers/command-dir/command-dir.go +++ b/pkg/handlers/command-dir/command-dir.go @@ -84,8 +84,12 @@ func NewCommandDirHandlerFromConfig( generic_command.WithIndexTemplateName(config_.IndexTemplateName), generic_command.WithMergeAdditionalData(config_.AdditionalData, true), } + genericHandler, err := generic_command.NewGenericCommandHandler(genericOptions...) + if err != nil { + return nil, err + } cd := &CommandDirHandler{ - GenericCommandHandler: *generic_command.NewGenericCommandHandler(genericOptions...), + GenericCommandHandler: *genericHandler, } cd.ParameterFilter.Overrides = config_.Overrides @@ -125,7 +129,7 @@ func NewCommandDirHandlerFromConfig( } } - err := cd.TemplateLookup.Reload() + err = cd.TemplateLookup.Reload() if err != nil { return nil, err } diff --git a/pkg/handlers/command/command.go b/pkg/handlers/command/command.go index 8e8b4bd..e371e81 100644 --- a/pkg/handlers/command/command.go +++ b/pkg/handlers/command/command.go @@ -1,6 +1,9 @@ package command import ( + "os" + "strings" + "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" @@ -10,8 +13,6 @@ import ( "github.com/go-go-golems/parka/pkg/render" parka "github.com/go-go-golems/parka/pkg/server" "github.com/pkg/errors" - "os" - "strings" ) type CommandHandler struct { @@ -41,9 +42,13 @@ func WithGenericCommandHandlerOptions(options ...generic_command.GenericCommandH func NewCommandHandler( command cmds.Command, options ...CommandHandlerOption, -) *CommandHandler { +) (*CommandHandler, error) { + genericHandler, err := generic_command.NewGenericCommandHandler() + if err != nil { + return nil, err + } c := &CommandHandler{ - GenericCommandHandler: *generic_command.NewGenericCommandHandler(), + GenericCommandHandler: *genericHandler, Command: command, } @@ -51,7 +56,7 @@ func NewCommandHandler( opt(c) } - return c + return c, nil } func LoadCommandFromFile(path string, loader loaders.CommandLoader) (cmds.Command, error) { @@ -114,7 +119,10 @@ func NewCommandHandlerFromConfig( return nil, err } - c := NewCommandHandler(cmd, WithGenericCommandHandlerOptions(genericOptions...)) + c, err := NewCommandHandler(cmd, WithGenericCommandHandlerOptions(genericOptions...)) + if err != nil { + return nil, err + } // TODO(manuel, 2024-05-09) Handle devmode c.Command = cmd diff --git a/pkg/handlers/config/config.go b/pkg/handlers/config/config.go index cb3fdf8..ecf87e9 100644 --- a/pkg/handlers/config/config.go +++ b/pkg/handlers/config/config.go @@ -1,10 +1,10 @@ package config import ( - "github.com/go-go-golems/glazed/pkg/cmds/layers" + "os" + "github.com/rs/zerolog/log" "gopkg.in/yaml.v3" - "os" ) func expandPath(path string) string { @@ -48,76 +48,6 @@ type TemplateLookupConfig struct { Patterns []string `yaml:"patterns,omitempty"` } -// ParameterFilterList are used to configure whitelists and blacklists. -// Entire layers as well as individual flags and arguments can be whitelisted or blacklisted. -// Params is used for the default layer. -type ParameterFilterList struct { - Layers []string `yaml:"layers,omitempty"` - LayerParameters map[string][]string `yaml:"layerParameters,omitempty"` - Parameters []string `yaml:"parameters,omitempty"` -} - -func (p *ParameterFilterList) GetAllLayerParameters() map[string][]string { - ret := map[string][]string{} - for layer, params := range p.LayerParameters { - ret[layer] = params - } - if _, ok := ret[layers.DefaultSlug]; !ok { - ret[layers.DefaultSlug] = []string{} - } - ret[layers.DefaultSlug] = append(ret[layers.DefaultSlug], p.Parameters...) - return ret -} - -type LayerParameters struct { - Layers map[string]map[string]interface{} `yaml:"layers,omitempty"` - Parameters map[string]interface{} `yaml:"parameters,omitempty"` -} - -func NewLayerParameters() *LayerParameters { - return &LayerParameters{ - Layers: map[string]map[string]interface{}{}, - Parameters: map[string]interface{}{}, - } -} - -// Merge merges the two LayerParameters, with the overrides taking precedence. -// It merges all the layers, flags, and arguments. For each layer, the layer flags are merged as well, -// overrides taking precedence. -func (p *LayerParameters) Merge(overrides *LayerParameters) { - for k, v := range overrides.Layers { - if _, ok := p.Layers[k]; !ok { - p.Layers[k] = map[string]interface{}{} - } - for k2, v2 := range v { - p.Layers[k][k2] = v2 - } - } - - for k, v := range overrides.Parameters { - p.Parameters[k] = v - } -} - -func (p *LayerParameters) Clone() *LayerParameters { - ret := NewLayerParameters() - ret.Merge(p) - return ret -} - -func (p *LayerParameters) GetParameterMap() map[string]map[string]interface{} { - r := p.Clone() - ret := r.Layers - if _, ok := ret[layers.DefaultSlug]; !ok { - ret[layers.DefaultSlug] = map[string]interface{}{} - } - for k, v := range r.Parameters { - ret[layers.DefaultSlug][k] = v - } - - return ret -} - // Defaults controls the default renderer and which embedded static files to serve. type Defaults struct { Renderer *DefaultRendererOptions `yaml:"renderer,omitempty"` diff --git a/pkg/handlers/config/parameters.go b/pkg/handlers/config/parameters.go index 4b10d54..67614c5 100644 --- a/pkg/handlers/config/parameters.go +++ b/pkg/handlers/config/parameters.go @@ -1,10 +1,81 @@ package config import ( + "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" ) +// ParameterFilterList are used to configure whitelists and blacklists. +// Entire layers as well as individual flags and arguments can be whitelisted or blacklisted. +// Params is used for the default layer. +type ParameterFilterList struct { + Layers []string `yaml:"layers,omitempty"` + LayerParameters map[string][]string `yaml:"layerParameters,omitempty"` + Parameters []string `yaml:"parameters,omitempty"` +} + +func (p *ParameterFilterList) GetAllLayerParameters() map[string][]string { + ret := map[string][]string{} + for layer, params := range p.LayerParameters { + ret[layer] = params + } + if _, ok := ret[layers.DefaultSlug]; !ok { + ret[layers.DefaultSlug] = []string{} + } + ret[layers.DefaultSlug] = append(ret[layers.DefaultSlug], p.Parameters...) + return ret +} + +type LayerParameters struct { + Layers map[string]map[string]interface{} `yaml:"layers,omitempty"` + Parameters map[string]interface{} `yaml:"parameters,omitempty"` +} + +func NewLayerParameters() *LayerParameters { + return &LayerParameters{ + Layers: map[string]map[string]interface{}{}, + Parameters: map[string]interface{}{}, + } +} + +// Merge merges the two LayerParameters, with the overrides taking precedence. +// It merges all the layers, flags, and arguments. For each layer, the layer flags are merged as well, +// overrides taking precedence. +func (p *LayerParameters) Merge(overrides *LayerParameters) { + for k, v := range overrides.Layers { + if _, ok := p.Layers[k]; !ok { + p.Layers[k] = map[string]interface{}{} + } + for k2, v2 := range v { + p.Layers[k][k2] = v2 + } + } + + for k, v := range overrides.Parameters { + p.Parameters[k] = v + } +} + +func (p *LayerParameters) Clone() *LayerParameters { + ret := NewLayerParameters() + ret.Merge(p) + return ret +} + +func (p *LayerParameters) GetParameterMap() map[string]map[string]interface{} { + r := p.Clone() + ret := r.Layers + if _, ok := ret[layers.DefaultSlug]; !ok { + ret[layers.DefaultSlug] = map[string]interface{}{} + } + for k, v := range r.Parameters { + ret[layers.DefaultSlug][k] = v + } + + return ret +} + type ParameterFilter struct { Overrides *LayerParameters Defaults *LayerParameters @@ -14,6 +85,7 @@ type ParameterFilter struct { type ParameterFilterOption func(*ParameterFilter) +// Override options func WithReplaceOverrides(overrides *LayerParameters) ParameterFilterOption { return func(handler *ParameterFilter) { handler.Overrides = overrides @@ -30,7 +102,7 @@ func WithMergeOverrides(overrides *LayerParameters) ParameterFilterOption { } } -func WithOverrideParameter(name string, value string) ParameterFilterOption { +func WithOverrideParameter(name string, value interface{}) ParameterFilterOption { return func(handler *ParameterFilter) { if handler.Overrides == nil { handler.Overrides = NewLayerParameters() @@ -39,33 +111,27 @@ func WithOverrideParameter(name string, value string) ParameterFilterOption { } } -func WithMergeOverrideLayer(name string, layer map[string]interface{}) ParameterFilterOption { +func WithOverrideParameters(params map[string]interface{}) ParameterFilterOption { return func(handler *ParameterFilter) { if handler.Overrides == nil { handler.Overrides = NewLayerParameters() } - for k, v := range layer { - if _, ok := handler.Overrides.Layers[name]; !ok { - handler.Overrides.Layers[name] = map[string]interface{}{} - } - handler.Overrides.Layers[name][k] = v + for k, v := range params { + handler.Overrides.Parameters[k] = v } } } -// WithLayerDefaults populates the defaults for the given layer. If a value is already set, the value is skipped. -func WithLayerDefaults(name string, layer map[string]interface{}) ParameterFilterOption { +func WithMergeOverrideLayer(name string, layer map[string]interface{}) ParameterFilterOption { return func(handler *ParameterFilter) { if handler.Overrides == nil { handler.Overrides = NewLayerParameters() } + if _, ok := handler.Overrides.Layers[name]; !ok { + handler.Overrides.Layers[name] = map[string]interface{}{} + } for k, v := range layer { - if _, ok := handler.Overrides.Layers[name]; !ok { - handler.Overrides.Layers[name] = map[string]interface{}{} - } - if _, ok := handler.Overrides.Layers[name][k]; !ok { - handler.Overrides.Layers[name][k] = v - } + handler.Overrides.Layers[name][k] = v } } } @@ -79,12 +145,18 @@ func WithReplaceOverrideLayer(name string, layer map[string]interface{}) Paramet } } -// TODO(manuel, 2023-05-25) We can't currently override defaults, since they are parsed up front. -// For that we would need https://github.com/go-go-golems/glazed/issues/239 -// So for now, we only deal with overrides. -// -// Handling all the way to configure defaults. +func WithOverrideLayers(layers map[string]map[string]interface{}) ParameterFilterOption { + return func(handler *ParameterFilter) { + if handler.Overrides == nil { + handler.Overrides = NewLayerParameters() + } + for name, layer := range layers { + handler.Overrides.Layers[name] = layer + } + } +} +// Default options func WithReplaceDefaults(defaults *LayerParameters) ParameterFilterOption { return func(handler *ParameterFilter) { handler.Defaults = defaults @@ -101,7 +173,7 @@ func WithMergeDefaults(defaults *LayerParameters) ParameterFilterOption { } } -func WithDefaultParameter(name string, value string) ParameterFilterOption { +func WithDefaultParameter(name string, value interface{}) ParameterFilterOption { return func(handler *ParameterFilter) { if handler.Defaults == nil { handler.Defaults = NewLayerParameters() @@ -110,15 +182,26 @@ func WithDefaultParameter(name string, value string) ParameterFilterOption { } } +func WithDefaultParameters(params map[string]interface{}) ParameterFilterOption { + return func(handler *ParameterFilter) { + if handler.Defaults == nil { + handler.Defaults = NewLayerParameters() + } + for k, v := range params { + handler.Defaults.Parameters[k] = v + } + } +} + func WithMergeDefaultLayer(name string, layer map[string]interface{}) ParameterFilterOption { return func(handler *ParameterFilter) { if handler.Defaults == nil { handler.Defaults = NewLayerParameters() } + if _, ok := handler.Defaults.Layers[name]; !ok { + handler.Defaults.Layers[name] = map[string]interface{}{} + } for k, v := range layer { - if _, ok := handler.Defaults.Layers[name]; !ok { - handler.Defaults.Layers[name] = map[string]interface{}{} - } handler.Defaults.Layers[name][k] = v } } @@ -133,6 +216,91 @@ func WithReplaceDefaultLayer(name string, layer map[string]interface{}) Paramete } } +func WithDefaultLayers(layers map[string]map[string]interface{}) ParameterFilterOption { + return func(handler *ParameterFilter) { + if handler.Defaults == nil { + handler.Defaults = NewLayerParameters() + } + for name, layer := range layers { + handler.Defaults.Layers[name] = layer + } + } +} + +// Whitelist options +func WithWhitelist(whitelist *ParameterFilterList) ParameterFilterOption { + return func(handler *ParameterFilter) { + handler.Whitelist = whitelist + } +} + +func WithWhitelistParameters(params ...string) ParameterFilterOption { + return func(handler *ParameterFilter) { + if handler.Whitelist == nil { + handler.Whitelist = &ParameterFilterList{} + } + handler.Whitelist.Parameters = append(handler.Whitelist.Parameters, params...) + } +} + +func WithWhitelistLayers(layers ...string) ParameterFilterOption { + return func(handler *ParameterFilter) { + if handler.Whitelist == nil { + handler.Whitelist = &ParameterFilterList{} + } + handler.Whitelist.Layers = append(handler.Whitelist.Layers, layers...) + } +} + +func WithWhitelistLayerParameters(layer string, params ...string) ParameterFilterOption { + return func(handler *ParameterFilter) { + if handler.Whitelist == nil { + handler.Whitelist = &ParameterFilterList{} + } + if handler.Whitelist.LayerParameters == nil { + handler.Whitelist.LayerParameters = map[string][]string{} + } + handler.Whitelist.LayerParameters[layer] = append(handler.Whitelist.LayerParameters[layer], params...) + } +} + +// Blacklist options +func WithBlacklist(blacklist *ParameterFilterList) ParameterFilterOption { + return func(handler *ParameterFilter) { + handler.Blacklist = blacklist + } +} + +func WithBlacklistParameters(params ...string) ParameterFilterOption { + return func(handler *ParameterFilter) { + if handler.Blacklist == nil { + handler.Blacklist = &ParameterFilterList{} + } + handler.Blacklist.Parameters = append(handler.Blacklist.Parameters, params...) + } +} + +func WithBlacklistLayers(layers ...string) ParameterFilterOption { + return func(handler *ParameterFilter) { + if handler.Blacklist == nil { + handler.Blacklist = &ParameterFilterList{} + } + handler.Blacklist.Layers = append(handler.Blacklist.Layers, layers...) + } +} + +func WithBlacklistLayerParameters(layer string, params ...string) ParameterFilterOption { + return func(handler *ParameterFilter) { + if handler.Blacklist == nil { + handler.Blacklist = &ParameterFilterList{} + } + if handler.Blacklist.LayerParameters == nil { + handler.Blacklist.LayerParameters = map[string][]string{} + } + handler.Blacklist.LayerParameters[layer] = append(handler.Blacklist.LayerParameters[layer], params...) + } +} + func NewParameterFilter(options ...ParameterFilterOption) *ParameterFilter { ret := &ParameterFilter{} for _, opt := range options { @@ -144,9 +312,17 @@ func NewParameterFilter(options ...ParameterFilterOption) *ParameterFilter { func (od *ParameterFilter) ComputeMiddlewares(stream bool) []middlewares.Middleware { ret := []middlewares.Middleware{} - if od.Defaults != nil { - // this needs to override the defaults set by the underlying handler... - ret = append(ret, middlewares.UpdateFromMapAsDefaultFirst(od.Defaults.GetParameterMap(), parameters.WithParseStepSource("defaults"))) + // in reverse order of applications. This means that ultimately, the defaults are run first, + // then overrides, then whitelist, then blacklist, and then finally the query handlers. + + if od.Blacklist != nil { + ret = append(ret, middlewares.BlacklistLayers(od.Blacklist.Layers)) + ret = append(ret, middlewares.BlacklistLayerParameters(od.Blacklist.GetAllLayerParameters())) + } + + if od.Whitelist != nil { + ret = append(ret, middlewares.WhitelistLayers(od.Whitelist.Layers)) + ret = append(ret, middlewares.WhitelistLayerParameters(od.Whitelist.GetAllLayerParameters())) } if od.Overrides != nil { @@ -158,14 +334,9 @@ func (od *ParameterFilter) ComputeMiddlewares(stream bool) []middlewares.Middlew ) } - if od.Whitelist != nil { - ret = append(ret, middlewares.WhitelistLayers(od.Whitelist.Layers)) - ret = append(ret, middlewares.WhitelistLayerParameters(od.Whitelist.GetAllLayerParameters())) - } - - if od.Blacklist != nil { - ret = append(ret, middlewares.BlacklistLayers(od.Blacklist.Layers)) - ret = append(ret, middlewares.BlacklistLayerParameters(od.Blacklist.GetAllLayerParameters())) + if od.Defaults != nil { + // this needs to override the defaults set by the underlying handler... + ret = append(ret, middlewares.UpdateFromMapAsDefaultFirst(od.Defaults.GetParameterMap(), parameters.WithParseStepSource("defaults"))) } return ret diff --git a/pkg/handlers/generic-command/generic.go b/pkg/handlers/generic-command/generic.go index 90eab6b..97ad1af 100644 --- a/pkg/handlers/generic-command/generic.go +++ b/pkg/handlers/generic-command/generic.go @@ -2,6 +2,10 @@ package generic_command import ( "fmt" + "net/http" + "path/filepath" + "strings" + "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" @@ -17,9 +21,6 @@ import ( "github.com/labstack/echo/v4" "github.com/pkg/errors" "github.com/rs/zerolog/log" - "net/http" - "path/filepath" - "strings" ) type GenericCommandHandler struct { @@ -49,20 +50,42 @@ type GenericCommandHandler struct { // path under which the command handler is served BasePath string + // preMiddlewares are run before the parameter filter middlewares + preMiddlewares []middlewares.Middleware + // postMiddlewares are run after the parameter filter middlewares + postMiddlewares []middlewares.Middleware + // middlewares contains all middlewares in order: pre + parameter filter + post middlewares []middlewares.Middleware } -func NewGenericCommandHandler(options ...GenericCommandHandlerOption) *GenericCommandHandler { +func NewGenericCommandHandler(options ...GenericCommandHandlerOption) (*GenericCommandHandler, error) { handler := &GenericCommandHandler{ AdditionalData: map[string]interface{}{}, + TemplateLookup: datatables.NewDataTablesLookupTemplate(), ParameterFilter: &config.ParameterFilter{}, + preMiddlewares: []middlewares.Middleware{}, + postMiddlewares: []middlewares.Middleware{}, } for _, opt := range options { opt(handler) } - return handler + // Compute the final middleware chain + parameterMiddlewares := handler.ParameterFilter.ComputeMiddlewares(handler.Stream) + handler.middlewares = append(handler.preMiddlewares, parameterMiddlewares...) + handler.middlewares = append(handler.middlewares, handler.postMiddlewares...) + + if handler.TemplateLookup == nil { + handler.TemplateLookup = datatables.NewDataTablesLookupTemplate() + } + + err := handler.TemplateLookup.Reload() + if err != nil { + return nil, err + } + + return handler, nil } type GenericCommandHandlerOption func(handler *GenericCommandHandler) @@ -131,10 +154,21 @@ func WithTemplateLookup(lookup render.TemplateLookup) GenericCommandHandlerOptio } } +func WithPreMiddlewares(middlewares ...middlewares.Middleware) GenericCommandHandlerOption { + return func(handler *GenericCommandHandler) { + handler.preMiddlewares = append(handler.preMiddlewares, middlewares...) + } +} + +func WithPostMiddlewares(middlewares ...middlewares.Middleware) GenericCommandHandlerOption { + return func(handler *GenericCommandHandler) { + handler.postMiddlewares = append(handler.postMiddlewares, middlewares...) + } +} + 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) }) @@ -159,8 +193,6 @@ func (gch *GenericCommandHandler) ServeRepository(server *parka.Server, basePath 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, "/") @@ -311,7 +343,9 @@ func (gch *GenericCommandHandler) ServeDataTables(c echo.Context, command cmds.C switch v := command.(type) { case cmds.GlazeCommand: options := []datatables.QueryHandlerOption{ + datatables.WithMiddlewares(gch.preMiddlewares...), datatables.WithMiddlewares(gch.middlewares...), + datatables.WithMiddlewares(gch.postMiddlewares...), datatables.WithTemplateLookup(gch.TemplateLookup), datatables.WithTemplateName(gch.TemplateName), datatables.WithAdditionalData(gch.AdditionalData),