htmgo/framework/h/app.go
2024-10-09 10:31:34 -05:00

246 lines
5.7 KiB
Go

package h
import (
"context"
"fmt"
"github.com/go-chi/chi/v5"
"github.com/maddalax/htmgo/framework/hx"
"github.com/maddalax/htmgo/framework/service"
"log/slog"
"net/http"
"os"
"os/exec"
"runtime"
"strings"
"time"
)
type RequestContext struct {
Request *http.Request
Response http.ResponseWriter
locator *service.Locator
isBoosted bool
currentBrowserUrl string
hxPromptResponse string
isHxRequest bool
hxTargetId string
hxTriggerName string
hxTriggerId string
kv map[string]interface{}
}
func GetRequestContext(r *http.Request) *RequestContext {
return r.Context().Value(RequestContextKey).(*RequestContext)
}
func (c *RequestContext) FormValue(key string) string {
return c.Request.FormValue(key)
}
func (c *RequestContext) Header(key string) string {
return c.Request.Header.Get(key)
}
func (c *RequestContext) UrlParam(key string) string {
return chi.URLParam(c.Request, key)
}
func (c *RequestContext) QueryParam(key string) string {
return c.Request.URL.Query().Get(key)
}
func (c *RequestContext) IsBoosted() bool {
return c.isBoosted
}
func (c *RequestContext) IsHxRequest() bool {
return c.isHxRequest
}
func (c *RequestContext) HxPromptResponse() string {
return c.hxPromptResponse
}
func (c *RequestContext) HxTargetId() string {
return c.hxTargetId
}
func (c *RequestContext) HxTriggerName() string {
return c.hxTriggerName
}
func (c *RequestContext) HxTriggerId() string {
return c.hxTriggerId
}
func (c *RequestContext) HxCurrentBrowserUrl() string {
return c.currentBrowserUrl
}
func (c *RequestContext) Set(key string, value interface{}) {
if c.kv == nil {
c.kv = make(map[string]interface{})
}
c.kv[key] = value
}
func (c *RequestContext) Get(key string) interface{} {
if c.kv == nil {
return nil
}
return c.kv[key]
}
func (c *RequestContext) ServiceLocator() *service.Locator {
return c.locator
}
type AppOpts struct {
LiveReload bool
ServiceLocator *service.Locator
Register func(app *App)
}
type App struct {
Opts AppOpts
Router *chi.Mux
}
func Start(opts AppOpts) {
router := chi.NewRouter()
instance := App{
Opts: opts,
Router: router,
}
instance.start()
}
const RequestContextKey = "htmgo.request.context"
func populateHxFields(cc *RequestContext) {
cc.isBoosted = cc.Request.Header.Get(hx.BoostedHeader) == "true"
cc.currentBrowserUrl = cc.Request.Header.Get(hx.CurrentUrlHeader)
cc.hxPromptResponse = cc.Request.Header.Get(hx.PromptResponseHeader)
cc.isHxRequest = cc.Request.Header.Get(hx.RequestHeader) == "true"
cc.hxTargetId = cc.Request.Header.Get(hx.TargetIdHeader)
cc.hxTriggerName = cc.Request.Header.Get(hx.TriggerNameHeader)
cc.hxTriggerId = cc.Request.Header.Get(hx.TriggerIdHeader)
}
func (app *App) UseWithContext(h func(w http.ResponseWriter, r *http.Request, context map[string]any)) {
app.Router.Use(func(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cc := r.Context().Value(RequestContextKey).(*RequestContext)
h(w, r, cc.kv)
handler.ServeHTTP(w, r)
})
})
}
func GetLogLevel() slog.Level {
// Get the log level from the environment variable
logLevel := os.Getenv("LOG_LEVEL")
switch strings.ToUpper(logLevel) {
case "DEBUG":
return slog.LevelDebug
case "INFO":
return slog.LevelInfo
case "WARN":
return slog.LevelWarn
case "ERROR":
return slog.LevelError
default:
// Default to INFO if no valid log level is set
return slog.LevelInfo
}
}
func (app *App) start() {
slog.SetLogLoggerLevel(GetLogLevel())
app.Router.Use(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cc := &RequestContext{
locator: app.Opts.ServiceLocator,
Request: r,
Response: w,
kv: make(map[string]interface{}),
}
populateHxFields(cc)
ctx := context.WithValue(r.Context(), RequestContextKey, cc)
h.ServeHTTP(w, r.WithContext(ctx))
})
})
if app.Opts.Register != nil {
app.Opts.Register(app)
}
if app.Opts.LiveReload && IsDevelopment() {
app.AddLiveReloadHandler("/dev/livereload")
}
port := ":3000"
slog.Info(fmt.Sprintf("Server started on port %s", port))
err := http.ListenAndServe(port, app.Router)
if err != nil {
// If we are in watch mode, just try to kill any processes holding that port
// and try again
if IsDevelopment() && IsWatchMode() {
slog.Info("Port already in use, trying to kill the process and start again")
if runtime.GOOS == "windows" {
cmd := exec.Command("cmd", "/C", fmt.Sprintf(`for /F "tokens=5" %%i in ('netstat -aon ^| findstr :%s') do taskkill /F /PID %%i`, port))
cmd.Run()
} else {
cmd := exec.Command("bash", "-c", fmt.Sprintf("kill -9 $(lsof -ti%s)", port))
cmd.Run()
}
time.Sleep(time.Millisecond * 50)
err = http.ListenAndServe(":3000", app.Router)
if err != nil {
panic(err)
}
} else {
panic(err)
}
panic(err)
}
}
func writeHtml(w http.ResponseWriter, element Ren) error {
w.Header().Set("Content-Type", "text/html")
_, err := fmt.Fprint(w, Render(element))
return err
}
func HtmlView(w http.ResponseWriter, page *Page) error {
return writeHtml(w, page.Root)
}
func PartialViewWithHeaders(w http.ResponseWriter, headers *Headers, partial *Partial) error {
if partial.Headers != nil {
for s, a := range *partial.Headers {
w.Header().Set(s, a)
}
}
if headers != nil {
for s, a := range *headers {
w.Header().Set(s, a)
}
}
return writeHtml(w, partial.Root)
}
func PartialView(w http.ResponseWriter, partial *Partial) error {
if partial.Headers != nil {
for s, a := range *partial.Headers {
w.Header().Set(s, a)
}
}
return writeHtml(w, partial.Root)
}