]> git.earlybird.gay Git - today/commitdiff
Move app stuff to root dir
authorearly <me@earlybird.gay>
Sat, 2 Nov 2024 18:23:44 +0000 (12:23 -0600)
committerearly <me@earlybird.gay>
Sat, 2 Nov 2024 18:23:44 +0000 (12:23 -0600)
app/app.go [new file with mode: 0644]
app/app/app.go [deleted file]
app/app/env.go [deleted file]
app/app/process.go [deleted file]
app/env.go [new file with mode: 0644]
app/process.go [new file with mode: 0644]

diff --git a/app/app.go b/app/app.go
new file mode 100644 (file)
index 0000000..ccf1b75
--- /dev/null
@@ -0,0 +1,338 @@
+// Copyright (C) 2024 early (MIT)
+package app
+
+import (
+       "context"
+       "errors"
+       "fmt"
+       "io"
+       "log/slog"
+       "net"
+       "net/http"
+       "os"
+       "path"
+       "path/filepath"
+       "runtime"
+       "slices"
+       "strings"
+       "sync"
+       "sync/atomic"
+
+       "git.earlybird.gay/today-engine/include"
+       "git.earlybird.gay/today-engine/page"
+)
+
+func init() {
+       include.SetNotEligible("git.earlybird.gay/today-app/app")
+}
+
+type App struct {
+       http.Server
+       http.ServeMux
+
+       Logger *slog.Logger
+
+       static         map[string]http.Handler
+       nonStaticFiles []string
+
+       running        atomic.Bool
+       wg             sync.WaitGroup
+       shutdown, stop chan struct{}
+
+       ready    atomic.Bool
+       initLock sync.Mutex
+}
+
+func New() *App {
+       app := new(App)
+       app.shutdown = make(chan struct{})
+       app.stop = make(chan struct{})
+       return app
+}
+
+func (app *App) setDefaults() {
+       if app.Server.Handler == nil {
+               app.Server.Handler = app
+       }
+       if app.Addr == "" {
+               app.Addr = "0.0.0.0:3000"
+       }
+       if app.Logger == nil {
+               app.Logger = slog.New(slog.NewTextHandler(os.Stdout, nil))
+       }
+}
+
+// Subprocess will run process and ask the app to wait on it to close.
+// Process must accept a channel that indicates when it should stop, and it
+// should respect that command when possible.
+func (app *App) Subprocess(process func(done <-chan struct{}) error) {
+       done := make(chan error)
+       go func() {
+               done <- process(app.shutdown)
+       }()
+       app.wg.Add(1)
+       go func() {
+               select {
+               case err := <-done:
+                       if err != nil {
+                               app.Logger.Warn("subprocess exited with error", "detail", err)
+                       }
+               case <-app.stop:
+               }
+               app.wg.Done()
+       }()
+}
+
+// List of static content types for app.Static.
+// http *should* handle this, but sometimes it doesn't. Use this, then let the
+// stdlib try to sort it out otherwise.
+//
+// Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
+var staticContentTypes = map[string]string{
+       ".aac":    "audio/aac",
+       ".abw":    "application/x-abiword",
+       ".apng":   "image/apng",
+       ".arc":    "application/x-freearc",
+       ".avif":   "image/avif",
+       ".avi":    "video/x-msvideo",
+       ".azw":    "application/vnd.amazon.ebook",
+       ".bin":    "application/octet-stream",
+       ".bmp":    "image/bmp",
+       ".bz":     "application/x-bzip",
+       ".bz2":    "application/x-bzip2",
+       ".cda":    "application/x-cdf",
+       ".csh":    "application/x-csh",
+       ".css":    "text/css",
+       ".csv":    "text/csv",
+       ".doc":    "application/msword",
+       ".docx":   "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+       ".eot":    "application/vnd.ms-fontobject",
+       ".epub":   "application/epub+zip",
+       ".gz":     "application/gzip",
+       ".gif":    "image/gif",
+       ".htm":    "text/html",
+       ".html":   "text/html",
+       ".ico":    "image/vnd.microsoft.icon",
+       ".ics":    "text/calendar",
+       ".jar":    "application/java-archive",
+       ".jpeg":   "image/jpeg",
+       ".jpg":    "image/jpeg",
+       ".js":     "text/javascript",
+       ".json":   "application/json",
+       ".jsonld": "application/ld+json",
+       ".mid":    "audio/midi",
+       ".midi":   "audio/midi",
+       ".mjs":    "text/javascript",
+       ".mp3":    "audio/mpeg",
+       ".mp4":    "video/mp4",
+       ".mpeg":   "video/mpeg",
+       ".mpkg":   "application/vnd.apple.installer+xml",
+       ".odp":    "application/vnd.oasis.opendocument.presentation",
+       ".ods":    "application/vnd.oasis.opendocument.spreadsheet",
+       ".odt":    "application/vnd.oasis.opendocument.text",
+       ".oga":    "audio/ogg",
+       ".ogv":    "video/ogg",
+       ".ogx":    "application/ogg",
+       ".opus":   "audio/ogg",
+       ".otf":    "font/otf",
+       ".png":    "image/png",
+       ".pdf":    "application/pdf",
+       ".php":    "application/x-httpd-php",
+       ".ppt":    "application/vnd.ms-powerpoint",
+       ".pptx":   "application/vnd.openxmlformats-officedocument.presentationml.presentation",
+       ".rar":    "application/vnd.rar",
+       ".rtf":    "application/rtf",
+       ".sh":     "application/x-sh",
+       ".svg":    "image/svg+xml",
+       ".tar":    "application/x-tar",
+       ".tif":    "image/tiff",
+       ".tiff":   "image/tiff",
+       ".ts":     "video/mp2t",
+       ".ttf":    "font/ttf",
+       ".txt":    "text/plain",
+       ".vsd":    "application/vnd.visio",
+       ".wav":    "audio/wav",
+       ".weba":   "audio/webm",
+       ".webm":   "video/webm",
+       ".webp":   "image/webp",
+       ".woff":   "font/woff",
+       ".woff2":  "font/woff2",
+       ".xhtml":  "application/xhtml+xml",
+       ".xls":    "application/vnd.ms-excel",
+       ".xlsx":   "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+       ".xml":    "application/xml",
+       ".xul":    "application/vnd.mozilla.xul+xml",
+       ".zip":    "application/zip",
+       ".7z":     "application/x-7z-compressed",
+}
+
+// Static serves the files in rootDir at rootPath.
+//
+//   - .html files have their extensions stripped, and are served as their
+//     filenames, following a directory structure. If there is an index.html
+//     in a directory, it is served under the directory name, with a trailing
+//     slash, so /foo/bar/index.html is served at /foo/bar/.
+//   - Other non-excluded files are served with their extensions intact.
+//     Currently, .go files and anything in a hidden directory are excluded.
+//     MIME types are chosen by Go's standard HTTP library behavior until that
+//     becomes a pain.
+//   - If using today-engine, page.New and page.Static will *override* the
+//     default behavior of Static. *This is only possible when using
+//     today-engine.*
+//   - If static is called twice for the same rootPath, the second call will
+//     override the first.
+func (app *App) Static(rootPath, rootDir string) error {
+       if app.static == nil {
+               app.static = make(map[string]http.Handler)
+       }
+       _, relativeTo, _, _ := runtime.Caller(1)
+       if !path.IsAbs(rootDir) {
+               rootDir = path.Join(path.Dir(relativeTo), rootDir)
+       }
+       var f func(fpath string) error
+       f = func(fpath string) error {
+               stat, err := os.Stat(fpath)
+               if err != nil {
+                       return err
+               }
+               if stat.IsDir() {
+                       // Recurse to subdirectories
+                       entries, err := os.ReadDir(fpath)
+                       if err != nil {
+                               return err
+                       }
+                       for _, entry := range entries {
+                               err = errors.Join(err, f(filepath.Join(fpath, entry.Name())))
+                       }
+                       return err
+               } else {
+                       // Serve file
+                       fileName := filepath.Base(fpath)
+                       fileExt := filepath.Ext(fileName)
+                       servePath := filepath.Join(rootPath, strings.TrimPrefix(fpath, rootDir))
+                       var handler http.Handler
+                       switch fileExt {
+                       case ".go":
+                               // explicit ignore .go
+                               handler = nil
+                       case ".html":
+                               if fileName == "index.html" {
+                                       servePath = path.Clean(path.Dir(servePath) + "/{$}")
+                               } else {
+                                       servePath = strings.TrimSuffix(servePath, ".html")
+                               }
+                               handler = page.Static(fpath)
+                       default:
+                               handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+                                       f, err := os.Open(fpath)
+                                       if err != nil {
+                                               w.WriteHeader(http.StatusInternalServerError)
+                                               fmt.Fprintln(w, "failed to open file")
+                                               return
+                                       }
+                                       if contentType, ok := staticContentTypes[fileExt]; ok {
+                                               w.Header().Set("Content-Type", contentType)
+                                       }
+                                       w.WriteHeader(http.StatusOK)
+                                       io.Copy(w, f)
+                               })
+                       }
+
+                       // If not ignoring the file, handle it.
+                       if handler != nil {
+                               serveExpr := "GET " + servePath
+                               app.static[serveExpr] = handler
+                       }
+                       return nil
+               }
+       }
+       return f(rootDir)
+}
+
+func (app *App) Handle(expr string, handler http.Handler) {
+       // Check page handlers against static
+       if todayPage, ok := handler.(*page.Page); ok {
+               app.nonStaticFiles = append(app.nonStaticFiles, todayPage.FileDependencies()...)
+       }
+       app.ServeMux.Handle(expr, handler)
+}
+
+func (app *App) registerStatic() {
+registerStaticHandlers:
+       for expr, staticHandler := range app.static {
+               // If the staticHandler is a *page.Page,
+               if staticPage, ok := staticHandler.(*page.Page); ok {
+                       // get the static file dependencies (should just be the static file)
+                       staticFile := staticPage.FileDependencies()[0]
+                       // Make sure it isn't overridden by an explicitly handled *page.Page.
+                       if slices.Contains(app.nonStaticFiles, staticFile) {
+                               continue registerStaticHandlers
+                       }
+               }
+               app.Handle(expr, staticHandler)
+       }
+}
+
+func (app *App) initOnce() {
+       // Only let one through
+       // can't use CAS because we need non-init processes to wait
+       if app.ready.Load() {
+               return
+       }
+       app.initLock.Lock()
+       if app.ready.Load() {
+               app.initLock.Unlock()
+               return
+       }
+       app.setDefaults()
+       app.registerStatic()
+
+       app.ready.Store(true)
+       app.running.Store(true)
+       app.initLock.Unlock()
+}
+
+func (app *App) ListenAndServe() error {
+       app.initOnce()
+       app.Logger.Info("application starting", "host", app.Addr)
+
+       var listener net.Listener
+       var err error
+       if i := strings.Index(app.Addr, "://"); i != -1 {
+               scheme := app.Addr[:i]
+               host := app.Addr[i+3:]
+               switch scheme {
+               case "unix":
+                       listener, err = net.Listen("unix", host)
+               default:
+                       listener, err = net.Listen("tcp", host)
+               }
+       } else {
+               listener, err = net.Listen("tcp", app.Addr)
+       }
+       if err != nil {
+               return err
+       }
+
+       err = app.Server.Serve(listener)
+
+       app.Logger.Info("application stopped", "reason", err)
+       return nil
+}
+
+func (app *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+       app.initOnce()
+       app.Logger.Debug("serving request", "host", r.Host, "path", r.URL.Path)
+       app.ServeMux.ServeHTTP(w, r)
+}
+
+func (app *App) Shutdown(ctx context.Context) error {
+       if app.running.CompareAndSwap(true, false) {
+               app.Logger.Info("application shutting down")
+               app.running.Store(false)
+               close(app.shutdown)
+               app.wg.Wait()
+               return app.Server.Shutdown(ctx)
+       }
+       return errors.New("cannot Shutdown an app that isn't running")
+}
diff --git a/app/app/app.go b/app/app/app.go
deleted file mode 100644 (file)
index ccf1b75..0000000
+++ /dev/null
@@ -1,338 +0,0 @@
-// Copyright (C) 2024 early (MIT)
-package app
-
-import (
-       "context"
-       "errors"
-       "fmt"
-       "io"
-       "log/slog"
-       "net"
-       "net/http"
-       "os"
-       "path"
-       "path/filepath"
-       "runtime"
-       "slices"
-       "strings"
-       "sync"
-       "sync/atomic"
-
-       "git.earlybird.gay/today-engine/include"
-       "git.earlybird.gay/today-engine/page"
-)
-
-func init() {
-       include.SetNotEligible("git.earlybird.gay/today-app/app")
-}
-
-type App struct {
-       http.Server
-       http.ServeMux
-
-       Logger *slog.Logger
-
-       static         map[string]http.Handler
-       nonStaticFiles []string
-
-       running        atomic.Bool
-       wg             sync.WaitGroup
-       shutdown, stop chan struct{}
-
-       ready    atomic.Bool
-       initLock sync.Mutex
-}
-
-func New() *App {
-       app := new(App)
-       app.shutdown = make(chan struct{})
-       app.stop = make(chan struct{})
-       return app
-}
-
-func (app *App) setDefaults() {
-       if app.Server.Handler == nil {
-               app.Server.Handler = app
-       }
-       if app.Addr == "" {
-               app.Addr = "0.0.0.0:3000"
-       }
-       if app.Logger == nil {
-               app.Logger = slog.New(slog.NewTextHandler(os.Stdout, nil))
-       }
-}
-
-// Subprocess will run process and ask the app to wait on it to close.
-// Process must accept a channel that indicates when it should stop, and it
-// should respect that command when possible.
-func (app *App) Subprocess(process func(done <-chan struct{}) error) {
-       done := make(chan error)
-       go func() {
-               done <- process(app.shutdown)
-       }()
-       app.wg.Add(1)
-       go func() {
-               select {
-               case err := <-done:
-                       if err != nil {
-                               app.Logger.Warn("subprocess exited with error", "detail", err)
-                       }
-               case <-app.stop:
-               }
-               app.wg.Done()
-       }()
-}
-
-// List of static content types for app.Static.
-// http *should* handle this, but sometimes it doesn't. Use this, then let the
-// stdlib try to sort it out otherwise.
-//
-// Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
-var staticContentTypes = map[string]string{
-       ".aac":    "audio/aac",
-       ".abw":    "application/x-abiword",
-       ".apng":   "image/apng",
-       ".arc":    "application/x-freearc",
-       ".avif":   "image/avif",
-       ".avi":    "video/x-msvideo",
-       ".azw":    "application/vnd.amazon.ebook",
-       ".bin":    "application/octet-stream",
-       ".bmp":    "image/bmp",
-       ".bz":     "application/x-bzip",
-       ".bz2":    "application/x-bzip2",
-       ".cda":    "application/x-cdf",
-       ".csh":    "application/x-csh",
-       ".css":    "text/css",
-       ".csv":    "text/csv",
-       ".doc":    "application/msword",
-       ".docx":   "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
-       ".eot":    "application/vnd.ms-fontobject",
-       ".epub":   "application/epub+zip",
-       ".gz":     "application/gzip",
-       ".gif":    "image/gif",
-       ".htm":    "text/html",
-       ".html":   "text/html",
-       ".ico":    "image/vnd.microsoft.icon",
-       ".ics":    "text/calendar",
-       ".jar":    "application/java-archive",
-       ".jpeg":   "image/jpeg",
-       ".jpg":    "image/jpeg",
-       ".js":     "text/javascript",
-       ".json":   "application/json",
-       ".jsonld": "application/ld+json",
-       ".mid":    "audio/midi",
-       ".midi":   "audio/midi",
-       ".mjs":    "text/javascript",
-       ".mp3":    "audio/mpeg",
-       ".mp4":    "video/mp4",
-       ".mpeg":   "video/mpeg",
-       ".mpkg":   "application/vnd.apple.installer+xml",
-       ".odp":    "application/vnd.oasis.opendocument.presentation",
-       ".ods":    "application/vnd.oasis.opendocument.spreadsheet",
-       ".odt":    "application/vnd.oasis.opendocument.text",
-       ".oga":    "audio/ogg",
-       ".ogv":    "video/ogg",
-       ".ogx":    "application/ogg",
-       ".opus":   "audio/ogg",
-       ".otf":    "font/otf",
-       ".png":    "image/png",
-       ".pdf":    "application/pdf",
-       ".php":    "application/x-httpd-php",
-       ".ppt":    "application/vnd.ms-powerpoint",
-       ".pptx":   "application/vnd.openxmlformats-officedocument.presentationml.presentation",
-       ".rar":    "application/vnd.rar",
-       ".rtf":    "application/rtf",
-       ".sh":     "application/x-sh",
-       ".svg":    "image/svg+xml",
-       ".tar":    "application/x-tar",
-       ".tif":    "image/tiff",
-       ".tiff":   "image/tiff",
-       ".ts":     "video/mp2t",
-       ".ttf":    "font/ttf",
-       ".txt":    "text/plain",
-       ".vsd":    "application/vnd.visio",
-       ".wav":    "audio/wav",
-       ".weba":   "audio/webm",
-       ".webm":   "video/webm",
-       ".webp":   "image/webp",
-       ".woff":   "font/woff",
-       ".woff2":  "font/woff2",
-       ".xhtml":  "application/xhtml+xml",
-       ".xls":    "application/vnd.ms-excel",
-       ".xlsx":   "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
-       ".xml":    "application/xml",
-       ".xul":    "application/vnd.mozilla.xul+xml",
-       ".zip":    "application/zip",
-       ".7z":     "application/x-7z-compressed",
-}
-
-// Static serves the files in rootDir at rootPath.
-//
-//   - .html files have their extensions stripped, and are served as their
-//     filenames, following a directory structure. If there is an index.html
-//     in a directory, it is served under the directory name, with a trailing
-//     slash, so /foo/bar/index.html is served at /foo/bar/.
-//   - Other non-excluded files are served with their extensions intact.
-//     Currently, .go files and anything in a hidden directory are excluded.
-//     MIME types are chosen by Go's standard HTTP library behavior until that
-//     becomes a pain.
-//   - If using today-engine, page.New and page.Static will *override* the
-//     default behavior of Static. *This is only possible when using
-//     today-engine.*
-//   - If static is called twice for the same rootPath, the second call will
-//     override the first.
-func (app *App) Static(rootPath, rootDir string) error {
-       if app.static == nil {
-               app.static = make(map[string]http.Handler)
-       }
-       _, relativeTo, _, _ := runtime.Caller(1)
-       if !path.IsAbs(rootDir) {
-               rootDir = path.Join(path.Dir(relativeTo), rootDir)
-       }
-       var f func(fpath string) error
-       f = func(fpath string) error {
-               stat, err := os.Stat(fpath)
-               if err != nil {
-                       return err
-               }
-               if stat.IsDir() {
-                       // Recurse to subdirectories
-                       entries, err := os.ReadDir(fpath)
-                       if err != nil {
-                               return err
-                       }
-                       for _, entry := range entries {
-                               err = errors.Join(err, f(filepath.Join(fpath, entry.Name())))
-                       }
-                       return err
-               } else {
-                       // Serve file
-                       fileName := filepath.Base(fpath)
-                       fileExt := filepath.Ext(fileName)
-                       servePath := filepath.Join(rootPath, strings.TrimPrefix(fpath, rootDir))
-                       var handler http.Handler
-                       switch fileExt {
-                       case ".go":
-                               // explicit ignore .go
-                               handler = nil
-                       case ".html":
-                               if fileName == "index.html" {
-                                       servePath = path.Clean(path.Dir(servePath) + "/{$}")
-                               } else {
-                                       servePath = strings.TrimSuffix(servePath, ".html")
-                               }
-                               handler = page.Static(fpath)
-                       default:
-                               handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-                                       f, err := os.Open(fpath)
-                                       if err != nil {
-                                               w.WriteHeader(http.StatusInternalServerError)
-                                               fmt.Fprintln(w, "failed to open file")
-                                               return
-                                       }
-                                       if contentType, ok := staticContentTypes[fileExt]; ok {
-                                               w.Header().Set("Content-Type", contentType)
-                                       }
-                                       w.WriteHeader(http.StatusOK)
-                                       io.Copy(w, f)
-                               })
-                       }
-
-                       // If not ignoring the file, handle it.
-                       if handler != nil {
-                               serveExpr := "GET " + servePath
-                               app.static[serveExpr] = handler
-                       }
-                       return nil
-               }
-       }
-       return f(rootDir)
-}
-
-func (app *App) Handle(expr string, handler http.Handler) {
-       // Check page handlers against static
-       if todayPage, ok := handler.(*page.Page); ok {
-               app.nonStaticFiles = append(app.nonStaticFiles, todayPage.FileDependencies()...)
-       }
-       app.ServeMux.Handle(expr, handler)
-}
-
-func (app *App) registerStatic() {
-registerStaticHandlers:
-       for expr, staticHandler := range app.static {
-               // If the staticHandler is a *page.Page,
-               if staticPage, ok := staticHandler.(*page.Page); ok {
-                       // get the static file dependencies (should just be the static file)
-                       staticFile := staticPage.FileDependencies()[0]
-                       // Make sure it isn't overridden by an explicitly handled *page.Page.
-                       if slices.Contains(app.nonStaticFiles, staticFile) {
-                               continue registerStaticHandlers
-                       }
-               }
-               app.Handle(expr, staticHandler)
-       }
-}
-
-func (app *App) initOnce() {
-       // Only let one through
-       // can't use CAS because we need non-init processes to wait
-       if app.ready.Load() {
-               return
-       }
-       app.initLock.Lock()
-       if app.ready.Load() {
-               app.initLock.Unlock()
-               return
-       }
-       app.setDefaults()
-       app.registerStatic()
-
-       app.ready.Store(true)
-       app.running.Store(true)
-       app.initLock.Unlock()
-}
-
-func (app *App) ListenAndServe() error {
-       app.initOnce()
-       app.Logger.Info("application starting", "host", app.Addr)
-
-       var listener net.Listener
-       var err error
-       if i := strings.Index(app.Addr, "://"); i != -1 {
-               scheme := app.Addr[:i]
-               host := app.Addr[i+3:]
-               switch scheme {
-               case "unix":
-                       listener, err = net.Listen("unix", host)
-               default:
-                       listener, err = net.Listen("tcp", host)
-               }
-       } else {
-               listener, err = net.Listen("tcp", app.Addr)
-       }
-       if err != nil {
-               return err
-       }
-
-       err = app.Server.Serve(listener)
-
-       app.Logger.Info("application stopped", "reason", err)
-       return nil
-}
-
-func (app *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-       app.initOnce()
-       app.Logger.Debug("serving request", "host", r.Host, "path", r.URL.Path)
-       app.ServeMux.ServeHTTP(w, r)
-}
-
-func (app *App) Shutdown(ctx context.Context) error {
-       if app.running.CompareAndSwap(true, false) {
-               app.Logger.Info("application shutting down")
-               app.running.Store(false)
-               close(app.shutdown)
-               app.wg.Wait()
-               return app.Server.Shutdown(ctx)
-       }
-       return errors.New("cannot Shutdown an app that isn't running")
-}
diff --git a/app/app/env.go b/app/app/env.go
deleted file mode 100644 (file)
index c593f4d..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright (C) 2024 early (MIT)
-package app
-
-import (
-       "fmt"
-       "log/slog"
-       "os"
-       "strings"
-)
-
-const (
-       ENV_APP_NAME    = "APP_NAME"
-       ENV_LISTEN_HTTP = "LISTEN_HTTP"
-       ENV_LOG_LEVEL   = "LOG_LEVEL"
-)
-
-var (
-       logLevels = map[string]slog.Level{
-               "debug":   slog.LevelDebug,
-               "info":    slog.LevelInfo,
-               "warn":    slog.LevelWarn,
-               "warning": slog.LevelWarn,
-               "error":   slog.LevelError,
-               "err":     slog.LevelError,
-       }
-)
-
-type Env struct {
-       AppName    string
-       ListenHTTP string
-       LogLevel   slog.Level
-}
-
-type AcceptsEnv interface {
-       Apply(env Env)
-}
-
-// GetEnv gets a set of values from environment variables that are common when
-// running web applications.
-// You don't have to use GetEnv for this
-func GetEnv() Env {
-       var env Env
-       env.AppName = os.Getenv(ENV_APP_NAME)
-       env.ListenHTTP = os.Getenv(ENV_LISTEN_HTTP)
-       logLevelStr := strings.ToLower(os.Getenv(ENV_LOG_LEVEL))
-       if logLevel, ok := logLevels[logLevelStr]; ok {
-               env.LogLevel = logLevel
-       }
-       return env
-}
-
-func (env Env) Apply(app *App) {
-       app.Addr = env.ListenHTTP
-       if app.Logger == nil {
-               app.Logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
-                       Level: env.LogLevel,
-               }))
-               if env.AppName != "" {
-                       app.Logger = app.Logger.With("app", env.AppName)
-               }
-       }
-}
-
-// AppendEnv appends the env as key=value pairs to a list of vars.
-// This is intended for use with os.Environ() to pass environments to
-// subprocesses:
-//
-//     cmd := exec.Command(...)
-//     osEnv := os.Environ()
-//     subprocessEnv := app.Env{...}
-//     osEnv = app.AppendEnv(osEnv, subprocessEnv)
-//     cmd.Exec()
-func AppendEnv(vars []string, env Env) []string {
-       if env.AppName != "" {
-               vars = append(vars, fmt.Sprintf("%s=%s", ENV_APP_NAME, env.AppName))
-       }
-       if env.ListenHTTP != "" {
-               vars = append(vars, fmt.Sprintf("%s=%s", ENV_LISTEN_HTTP, env.ListenHTTP))
-       }
-       vars = append(vars, fmt.Sprintf("%s=%s", ENV_LOG_LEVEL, env.LogLevel.String()))
-       return vars
-}
diff --git a/app/app/process.go b/app/app/process.go
deleted file mode 100644 (file)
index 6bc5bd9..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2024 early (MIT)
-package app
-
-import (
-       "context"
-       "os"
-       "os/signal"
-       "syscall"
-)
-
-// ShutdownOnSignal will start the process of an app shutting down if an OS
-// signal is received.
-func (app *App) ShutdownOnSignal(sigs ...os.Signal) {
-       sigc := make(chan os.Signal, 1)
-       signal.Notify(sigc, syscall.SIGTERM, syscall.SIGINT)
-       app.Subprocess(func(done <-chan struct{}) error {
-               select {
-               case sig := <-sigc:
-                       app.Logger.Info("received signal", "type", sig)
-                       go app.Shutdown(context.Background())
-               case <-done:
-               }
-               return nil
-       })
-}
diff --git a/app/env.go b/app/env.go
new file mode 100644 (file)
index 0000000..c593f4d
--- /dev/null
@@ -0,0 +1,82 @@
+// Copyright (C) 2024 early (MIT)
+package app
+
+import (
+       "fmt"
+       "log/slog"
+       "os"
+       "strings"
+)
+
+const (
+       ENV_APP_NAME    = "APP_NAME"
+       ENV_LISTEN_HTTP = "LISTEN_HTTP"
+       ENV_LOG_LEVEL   = "LOG_LEVEL"
+)
+
+var (
+       logLevels = map[string]slog.Level{
+               "debug":   slog.LevelDebug,
+               "info":    slog.LevelInfo,
+               "warn":    slog.LevelWarn,
+               "warning": slog.LevelWarn,
+               "error":   slog.LevelError,
+               "err":     slog.LevelError,
+       }
+)
+
+type Env struct {
+       AppName    string
+       ListenHTTP string
+       LogLevel   slog.Level
+}
+
+type AcceptsEnv interface {
+       Apply(env Env)
+}
+
+// GetEnv gets a set of values from environment variables that are common when
+// running web applications.
+// You don't have to use GetEnv for this
+func GetEnv() Env {
+       var env Env
+       env.AppName = os.Getenv(ENV_APP_NAME)
+       env.ListenHTTP = os.Getenv(ENV_LISTEN_HTTP)
+       logLevelStr := strings.ToLower(os.Getenv(ENV_LOG_LEVEL))
+       if logLevel, ok := logLevels[logLevelStr]; ok {
+               env.LogLevel = logLevel
+       }
+       return env
+}
+
+func (env Env) Apply(app *App) {
+       app.Addr = env.ListenHTTP
+       if app.Logger == nil {
+               app.Logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
+                       Level: env.LogLevel,
+               }))
+               if env.AppName != "" {
+                       app.Logger = app.Logger.With("app", env.AppName)
+               }
+       }
+}
+
+// AppendEnv appends the env as key=value pairs to a list of vars.
+// This is intended for use with os.Environ() to pass environments to
+// subprocesses:
+//
+//     cmd := exec.Command(...)
+//     osEnv := os.Environ()
+//     subprocessEnv := app.Env{...}
+//     osEnv = app.AppendEnv(osEnv, subprocessEnv)
+//     cmd.Exec()
+func AppendEnv(vars []string, env Env) []string {
+       if env.AppName != "" {
+               vars = append(vars, fmt.Sprintf("%s=%s", ENV_APP_NAME, env.AppName))
+       }
+       if env.ListenHTTP != "" {
+               vars = append(vars, fmt.Sprintf("%s=%s", ENV_LISTEN_HTTP, env.ListenHTTP))
+       }
+       vars = append(vars, fmt.Sprintf("%s=%s", ENV_LOG_LEVEL, env.LogLevel.String()))
+       return vars
+}
diff --git a/app/process.go b/app/process.go
new file mode 100644 (file)
index 0000000..6bc5bd9
--- /dev/null
@@ -0,0 +1,25 @@
+// Copyright (C) 2024 early (MIT)
+package app
+
+import (
+       "context"
+       "os"
+       "os/signal"
+       "syscall"
+)
+
+// ShutdownOnSignal will start the process of an app shutting down if an OS
+// signal is received.
+func (app *App) ShutdownOnSignal(sigs ...os.Signal) {
+       sigc := make(chan os.Signal, 1)
+       signal.Notify(sigc, syscall.SIGTERM, syscall.SIGINT)
+       app.Subprocess(func(done <-chan struct{}) error {
+               select {
+               case sig := <-sigc:
+                       app.Logger.Info("received signal", "type", sig)
+                       go app.Shutdown(context.Background())
+               case <-done:
+               }
+               return nil
+       })
+}