diff --git a/api/endpoints.go b/api/endpoints.go index abf6eb2338101b76970ca9c71c498630283b866a..f80b20c2610836526edd1e1a660b112134510c40 100644 --- a/api/endpoints.go +++ b/api/endpoints.go @@ -25,7 +25,7 @@ import ( type ServerInterface interface { // Returns main page // (GET /) - Index(w http.ResponseWriter, r *http.Request) + GetIndex(w http.ResponseWriter, r *http.Request) } // ServerInterfaceWrapper converts contexts to parameters. @@ -37,12 +37,12 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc func(http.Handler) http.Handler -// Index operation middleware -func (siw *ServerInterfaceWrapper) Index(w http.ResponseWriter, r *http.Request) { +// GetIndex operation middleware +func (siw *ServerInterfaceWrapper) GetIndex(w http.ResponseWriter, r *http.Request) { ctx := r.Context() handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.Index(w, r) + siw.Handler.GetIndex(w, r) })) for _, middleware := range siw.HandlerMiddlewares { @@ -166,24 +166,24 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H ErrorHandlerFunc: options.ErrorHandlerFunc, } - m.HandleFunc("GET "+options.BaseURL+"/", wrapper.Index) + m.HandleFunc("GET "+options.BaseURL+"/", wrapper.GetIndex) return m } -type IndexRequestObject struct { +type GetIndexRequestObject struct { } -type IndexResponseObject interface { - VisitIndexResponse(w http.ResponseWriter) error +type GetIndexResponseObject interface { + VisitGetIndexResponse(w http.ResponseWriter) error } -type Index200TexthtmlResponse struct { +type GetIndex200TexthtmlResponse struct { Body io.Reader ContentLength int64 } -func (response Index200TexthtmlResponse) VisitIndexResponse(w http.ResponseWriter) error { +func (response GetIndex200TexthtmlResponse) VisitGetIndexResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "text/html") if response.ContentLength != 0 { w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) @@ -201,7 +201,7 @@ func (response Index200TexthtmlResponse) VisitIndexResponse(w http.ResponseWrite type StrictServerInterface interface { // Returns main page // (GET /) - Index(ctx context.Context, request IndexRequestObject) (IndexResponseObject, error) + GetIndex(ctx context.Context, request GetIndexRequestObject) (GetIndexResponseObject, error) } type StrictHandlerFunc = strictnethttp.StrictHTTPHandlerFunc @@ -233,23 +233,23 @@ type strictHandler struct { options StrictHTTPServerOptions } -// Index operation middleware -func (sh *strictHandler) Index(w http.ResponseWriter, r *http.Request) { - var request IndexRequestObject +// GetIndex operation middleware +func (sh *strictHandler) GetIndex(w http.ResponseWriter, r *http.Request) { + var request GetIndexRequestObject handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { - return sh.ssi.Index(ctx, request.(IndexRequestObject)) + return sh.ssi.GetIndex(ctx, request.(GetIndexRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "Index") + handler = middleware(handler, "GetIndex") } response, err := handler(r.Context(), w, r, request) if err != nil { sh.options.ResponseErrorHandlerFunc(w, r, err) - } else if validResponse, ok := response.(IndexResponseObject); ok { - if err := validResponse.VisitIndexResponse(w); err != nil { + } else if validResponse, ok := response.(GetIndexResponseObject); ok { + if err := validResponse.VisitGetIndexResponse(w); err != nil { sh.options.ResponseErrorHandlerFunc(w, r, err) } } else if response != nil { @@ -260,12 +260,12 @@ func (sh *strictHandler) Index(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/2RRwY7UMAz9leidq3aAW05wHAECsbsn4JBtPW1E41SOO2w16r8jh1k0Yi5VYz+/9/x8", - "QeRThr+gz6yhV/ulFOIMj190jvx+3obQRkIDDong8dHK7tM2BDRYxZCT6lJ8171i9wYDlV7iojEzPB4n", - "citHLe43PbunY4sGc+yJC5nilfnz8fGOMi/EJa/SU5tl7K5DXYpqKkqSypfTA8k59nQzNkZtr266ukdX", - "5dFAo86GfKp2+sxnEiVBgzNJ+Wv3TXtoD8Zv6mGJ8HhXSw2WoFMxz519RqqJ3S/7HAq5JYzkTlmcTuQe", - "vn5ofzAqpwSDHgd4HHmgFzQQKkvmQpX77eHwehPiqqD0ot2kabZH6SdKoZa3xXYpKpFH7Ptd8ClErj5g", - "vbKmFGSDxzfSVbi4274BSCwF+O+X/+5Q4/sXKfaf+58AAAD//yaFJNI8AgAA", + "H4sIAAAAAAAC/2RRTW/cIBD9K+idkdm2N07tqVq1VasmufVC8KyNagYEYzdW5P9eQTbSSntBMPPmfQyv", + "CHxJsK/wicV5aVeKLiyw+Etb4M/LProhEDTYRYLFt1ZW3/fRQWMtDTmL5GqNecceGiNVX0KWkBgWjzOp", + "lYNU9Y+e1dN5gMYSPHGlpnhl/nF+vKNMmbimtXgaUpnMdcjEIE1FqMT68/JAZQuebsamIMPVjek5TJeH", + "hgRZGvKp2/GJNypCBRoblfpm98NwGk6Nv6m7HGDxqZc0spO5Ns+mHRP1jd2HfXaVVHYTqUsqSmZSD7++", + "DH8YnbO4Bj2PsPhKcuaRXqBRqObElTr9x9Pp/VuIu4jQi5hZ4tIe1c8UXS/vucWpUgJPOI673UcXuFtB", + "69U1Rld2WPwmWQtXdds/jv8BAAD//xuhfPkSAgAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/implementation.go b/api/implementation.go new file mode 100644 index 0000000000000000000000000000000000000000..fa849e16eadc47135a55ac0748e189f95cefddfb --- /dev/null +++ b/api/implementation.go @@ -0,0 +1,28 @@ +// Package api implements the units API. +package api + +import ( + "context" + "strings" +) + +// UnitsAPI implements the units web ui. +type UnitsAPI struct { +} + +// GetIndex implements the main page. +func (ua UnitsAPI) GetIndex(_ context.Context, _ GetIndexRequestObject) (GetIndexResponseObject, error) { + body := `<html> +<head> + <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> +</head> +<body> + <h1 x-data="{ message: 'I ❤️ Alpine' }" x-text="message"></h1> +</body> +</html>` + response := GetIndex200TexthtmlResponse{ + Body: strings.NewReader(body), + ContentLength: int64(len(body)), + } + return response, nil +} diff --git a/api/server.gen.go b/api/server.gen.go deleted file mode 100644 index e2bd3230a9c375942075cf06f79b880fb7b50f12..0000000000000000000000000000000000000000 --- a/api/server.gen.go +++ /dev/null @@ -1,135 +0,0 @@ -// Package api provides primitives to interact with the openapi HTTP API. -// -// Code generated by github.com/deepmap/oapi-codegen version v1.16.3 DO NOT EDIT. -package api - -import ( - "context" - "fmt" - "io" - "net/http" - - "github.com/labstack/echo/v4" - strictecho "github.com/oapi-codegen/runtime/strictmiddleware/echo" -) - -// ServerInterface represents all server handlers. -type ServerInterface interface { - // Returns main page - // (GET /) - Index(ctx echo.Context) error -} - -// ServerInterfaceWrapper converts echo contexts to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface -} - -// Index converts echo context to params. -func (w *ServerInterfaceWrapper) Index(ctx echo.Context) error { - var err error - - // Invoke the callback with all the unmarshaled arguments - err = w.Handler.Index(ctx) - return err -} - -// This is a simple interface which specifies echo.Route addition functions which -// are present on both echo.Echo and echo.Group, since we want to allow using -// either of them for path registration -type EchoRouter interface { - CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route - DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route - GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route - HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route - OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route - PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route - POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route - PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route - TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route -} - -// RegisterHandlers adds each server route to the EchoRouter. -func RegisterHandlers(router EchoRouter, si ServerInterface) { - RegisterHandlersWithBaseURL(router, si, "") -} - -// Registers handlers, and prepends BaseURL to the paths, so that the paths -// can be served under a prefix. -func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) { - - wrapper := ServerInterfaceWrapper{ - Handler: si, - } - - router.GET(baseURL+"/", wrapper.Index) - -} - -type IndexRequestObject struct { -} - -type IndexResponseObject interface { - VisitIndexResponse(w http.ResponseWriter) error -} - -type Index200TexthtmlResponse struct { - Body io.Reader - ContentLength int64 -} - -func (response Index200TexthtmlResponse) VisitIndexResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "text/html") - if response.ContentLength != 0 { - w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) - } - w.WriteHeader(200) - - if closer, ok := response.Body.(io.ReadCloser); ok { - defer closer.Close() - } - _, err := io.Copy(w, response.Body) - return err -} - -// StrictServerInterface represents all server handlers. -type StrictServerInterface interface { - // Returns main page - // (GET /) - Index(ctx context.Context, request IndexRequestObject) (IndexResponseObject, error) -} - -type StrictHandlerFunc = strictecho.StrictEchoHandlerFunc -type StrictMiddlewareFunc = strictecho.StrictEchoMiddlewareFunc - -func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { - return &strictHandler{ssi: ssi, middlewares: middlewares} -} - -type strictHandler struct { - ssi StrictServerInterface - middlewares []StrictMiddlewareFunc -} - -// Index operation middleware -func (sh *strictHandler) Index(ctx echo.Context) error { - var request IndexRequestObject - - handler := func(ctx echo.Context, request interface{}) (interface{}, error) { - return sh.ssi.Index(ctx.Request().Context(), request.(IndexRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "Index") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(IndexResponseObject); ok { - return validResponse.VisitIndexResponse(ctx.Response()) - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} diff --git a/api/units.yaml b/api/units.yaml index 39d98d401946922806acf9570e1b3e271ad9e490..1b0f6a99c3cbd22e07569297f0fa3df1126dcc1e 100644 --- a/api/units.yaml +++ b/api/units.yaml @@ -12,15 +12,17 @@ info: license: name: MIT url: https://opensource.org/license/mit -servers: - - url: https://units.lyda.ie/ + +# servers: +# - url: https://units.lyda.ie/ + paths: /: get: summary: Returns main page description: | The base page for the SPA. - operationId: index + operationId: GetIndex responses: '200': description: main page diff --git a/cmd/root.go b/cmd/root.go index f44849997588309f4f7ec2df3f4814160b2e688a..15628a8bd88b1647b61245a42bc741480560de23 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -29,7 +29,9 @@ TODO: example 3`, } func run(_ *cobra.Command, _ []string) { - server := &web.Config{} + fmt.Printf("Server listening on '%s'\n", viper.GetString("listen")) + server, err := web.New(viper.GetString("listen")) + cobra.CheckErr(err) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() @@ -48,16 +50,8 @@ func Execute() { func init() { cobra.OnInitialize(initConfig) - - // Here you will define your flags and configuration settings. - // Cobra supports persistent flags, which, if defined here, - // will be global for your application. - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.units.yaml)") - - // Cobra also supports local flags, which will only run - // when this action is called directly. - rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + rootCmd.Flags().StringP("listen", "l", "0.0.0.0:8989", "host:port to listen on") } // initConfig reads in config file and ENV variables if set. @@ -77,6 +71,7 @@ func initConfig() { } viper.AutomaticEnv() // read in environment variables that match + viper.BindPFlags(rootCmd.Flags()) // If a config file is found, read it in. if err := viper.ReadInConfig(); err == nil { diff --git a/go.mod b/go.mod index 2d82a386cdbb2231ce0d0c100cf046bf440a7f19..7e8ed8568d035aa87c845e8a19697ecd53938101 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/getkin/kin-openapi v0.124.0 // indirect github.com/go-openapi/jsonpointer v0.20.2 // indirect github.com/go-openapi/swag v0.22.8 // indirect + github.com/gorilla/mux v1.8.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/yaml v0.2.0 // indirect @@ -24,6 +25,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/oapi-codegen/nethttp-middleware v1.0.2 // indirect github.com/oapi-codegen/runtime v1.1.1 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect diff --git a/go.sum b/go.sum index d79654a55679d7eaf00cb929e7a79fc4a899ccdc..a71f3670ff3ce7f3e06552b2eba82070cfdc50ce 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,8 @@ github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicb github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI= 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/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -44,6 +46,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/oapi-codegen/nethttp-middleware v1.0.2 h1:A5tfAcKJhWIbIPnlQH+l/DtfVE1i5TFgPlQAiW+l1vQ= +github.com/oapi-codegen/nethttp-middleware v1.0.2/go.mod h1:DfDalonSO+eRQ3RTb8kYoWZByCCPFRxm9WKq1UbY0E4= github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= diff --git a/web/web.go b/web/web.go index 924dd6d73208e56ed25fe8a41014840db3d3032c..37e30ad13ee9fc44fd6f406ddc67566fcf372113 100644 --- a/web/web.go +++ b/web/web.go @@ -1,16 +1,69 @@ // Package web implements the web interface. package web -import "context" +import ( + "context" + "fmt" + "net/http" + "time" -// Config contains the web config. -type Config struct { + "github.com/getkin/kin-openapi/openapi3" + om "github.com/oapi-codegen/nethttp-middleware" + + "git.lyda.ie/kevin/units/api" +) + +// Server contains the web config. +type Server struct { + swagger *openapi3.T + listen string +} + +// New creates a new server. +func New(listen string) (*Server, error) { + swagger, err := api.GetSwagger() + if err != nil { + return nil, err + } + + svr := &Server{ + swagger: swagger, + listen: listen, + } + + return svr, nil } // Serve serves the web interface -func (c *Config) Serve(ctx context.Context) { +func (svr *Server) Serve(ctx context.Context) { + oapiValidator := om.OapiRequestValidator(svr.swagger) + unitsAPI := &api.UnitsAPI{} + server := api.NewStrictHandler(unitsAPI, []api.StrictMiddlewareFunc{}) + mux := http.NewServeMux() + + // get an `http.Handler` that we can use + h := oapiValidator(api.HandlerFromMux(server, mux)) + + s := &http.Server{ + Handler: h, + Addr: svr.listen, + } + + exit := make(chan struct{}) + go func() { + err := s.ListenAndServe() + if err != nil { + fmt.Println(err) + } + close(exit) + }() + select { + case <-exit: + fmt.Println("Server exited unexpectedly") case <-ctx.Done(): - // exit now. + shutCtx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Second*30)) + defer cancel() + s.Shutdown(shutCtx) } }