updated templater library

This commit is contained in:
2026-01-19 11:13:16 -07:00
parent 690923efb3
commit af903512be
9 changed files with 69 additions and 221 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ module ruben/inventory2
go 1.24.2 go 1.24.2
replace github.com/angelbeltran/templater v0.1.0 => ../../github.com/angelbeltran/templater replace github.com/angelbeltran/templater v0.2.2 => ../../github.com/angelbeltran/templater
require github.com/jackc/pgx/v5 v5.7.6 require github.com/jackc/pgx/v5 v5.7.6
-2
View File
@@ -9,8 +9,6 @@ github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 h1:ez/4by2iGztzR4
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/angelbeltran/concurry v0.0.0-20260109231127-8e2678d8928c h1:BIa061RHaIVP9TjpIQf0PldVpQmeln/2U1Cf2w8ALNI= github.com/angelbeltran/concurry v0.0.0-20260109231127-8e2678d8928c h1:BIa061RHaIVP9TjpIQf0PldVpQmeln/2U1Cf2w8ALNI=
github.com/angelbeltran/concurry v0.0.0-20260109231127-8e2678d8928c/go.mod h1:Aii6F8lZCJb8Ns0d1FH35bDxNoyiuvwXd5BYJroJnks= github.com/angelbeltran/concurry v0.0.0-20260109231127-8e2678d8928c/go.mod h1:Aii6F8lZCJb8Ns0d1FH35bDxNoyiuvwXd5BYJroJnks=
github.com/angelbeltran/templater v0.2.1 h1:9N2eNRE5/sYUgu7nZrU6VSSrrpKcOTlAw+UeeK3kyhk=
github.com/angelbeltran/templater v0.2.1/go.mod h1:ZA0T/ZmXxPi3gFjgp+RwEkcY9Q+JGyMStGw0c1jvgqA=
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
+55 -136
View File
@@ -1,14 +1,14 @@
package templates package templates
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"path"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"text/template"
"ruben/inventory2/internal/domains/accounts" "ruben/inventory2/internal/domains/accounts"
etsy_platform "ruben/inventory2/internal/domains/platforms/etsy" etsy_platform "ruben/inventory2/internal/domains/platforms/etsy"
@@ -43,23 +43,60 @@ func SetupRoutes(
logger *logging.Logger, logger *logging.Logger,
r gin.IRouter, r gin.IRouter,
contentDir string, contentDir string,
tmpl *templater.Templater,
rawEvents *raw_events.Store, rawEvents *raw_events.Store,
accts *accounts.Store, accts *accounts.Store,
etsy *etsy_platform.Platform, etsy *etsy_platform.Platform,
authMiddleware *middleware.Auth, authMiddleware *middleware.Auth,
) { ) {
s := &webpageRouter{ s := &webpageRouter{
log: logger, log: logger,
contentDir: contentDir, contentDir: contentDir,
templater: tmpl, // TODO: clean up the references to the templates dir throughout as well (should only be referred to in the 'main' package
templater: new(templater.Templater).With(templater.Config{
Funcs: func(name string, props map[string]any) template.FuncMap {
return template.FuncMap{
// parsing
"parseInt": func(s string) (int, error) {
return strconv.Atoi(s)
},
"parseInt64": func(s string) (int64, error) {
return strconv.ParseInt(s, 10, 64)
},
"parsePlatform": func(s string) (accounts.Platform, error) {
return accounts.NewPlatform(s)
},
// arithmetic
"addInt": func(a, b int) int {
return a + b
},
"subInt": func(a, b int) int {
return a - b
},
"multInt": func(a, b int) int {
return a * b
},
// json
"prettyPrintJSON": func(j json.RawMessage) string {
b, err := json.MarshalIndent(j, " ", "")
if err != nil {
return string(j)
}
return string(b)
},
"marshalJSON": json.Marshal,
}
},
}),
rawEvents: rawEvents, rawEvents: rawEvents,
accts: accts, accts: accts,
etsy: etsy, etsy: etsy,
authMiddleware: authMiddleware, authMiddleware: authMiddleware,
} }
fn2 := s.authMiddleware.AuthenticateAndAddIdentity(s.serveTemplates) authAndServeTemplate := s.authMiddleware.AuthenticateAndAddIdentity(s.serveTemplate)
r.GET("/*rest", response.Handler(func(c *gin.Context) (response.Response, error) { r.GET("/*rest", response.Handler(func(c *gin.Context) (response.Response, error) {
c.Request.URL.Path = c.Request.URL.Path[3:] c.Request.URL.Path = c.Request.URL.Path[3:]
@@ -73,48 +110,21 @@ func SetupRoutes(
if err != nil { if err != nil {
return nil, err return nil, err
} }
return s.serveTemplates(c) return s.serveTemplate(c)
} }
return fn2(c)
return authAndServeTemplate(c)
})) }))
} }
// GET / // GET /
// compiles the page template or component template matching the url // compiles the page template or component template matching the url
// func (s *Server) serveTemplates(r *http.Request) (response.Response, error) { func (s *webpageRouter) serveTemplate(c *gin.Context) (response.Response, error) {
func (s *webpageRouter) serveTemplates(c *gin.Context) (response.Response, error) {
r := c.Request r := c.Request
name, args := s.getTemplateNameAndArgs(r, s.contentDir+"/templates/components")
fmt.Println("name:", name)
fmt.Println("args:", args)
// TODO: update
b, err := s.templater.ExecuteComponent(name, args...)
if err == nil {
return response.HTML(b), nil
}
if !isFileNotFoundError(err) {
return s.handleTemplateError(err, args...)
}
name, args = s.getTemplateNameAndArgs(r, s.contentDir+"/templates/pages")
fmt.Println("name (page):", name)
fmt.Println("args (page):", args)
if b, err = s.templater.ExecutePage(name, args...); err == nil {
return response.HTML(b), nil
}
return s.handleTemplateError(err, args...)
}
// func (s *Server) getTemplateNameAndArgs(r *http.Request, templateDir string) (name string, args []any) {
func (s *webpageRouter) getTemplateNameAndArgs(r *http.Request, templateDir string) (name string, args []any) {
ctx := r.Context() ctx := r.Context()
name, pathParams := getTemplateNameForURL(r.URL, templateDir)
return name, []any{ b, err := s.templater.Execute(
strings.Trim(r.URL.Path, "/"),
"Request", "Request",
r, r,
// add services and data here // add services and data here
@@ -122,95 +132,25 @@ func (s *webpageRouter) getTemplateNameAndArgs(r *http.Request, templateDir stri
s.rawEvents.WithContext(ctx), s.rawEvents.WithContext(ctx),
"URLCalc", "URLCalc",
newURLCalculator(r.URL), newURLCalculator(r.URL),
"PathParams",
pathParams,
"Accounts", "Accounts",
s.accts.WithContext(ctx), s.accts.WithContext(ctx),
"Etsy", "Etsy",
s.etsy.WithContext(ctx), s.etsy.WithContext(ctx),
// TODO: apply auth to all templates needed!
// auth tooling // auth tooling
/*
AccessToken string
Claims authentication.AccessTokenClaims
User accounts.OAuthUser
Account *accounts.Account
*/
"Identity", "Identity",
middleware.GetIdentity(r.Context()), middleware.GetIdentity(ctx),
"Auth", "Auth",
newTemplateAuthenticator(r), newTemplateAuthenticator(r),
)
if err != nil {
return s.handleTemplateError(err)
} }
return response.HTML(b), nil
} }
// TODO: clean this up... func (s *webpageRouter) handleTemplateError(err error) (response.Response, error) {
// TODO: somehow tell what the path params are and pass them up.
// - then consider pushing this functionality into the template library.
//
// getComponentTemplateNameForURL eliminate any trailing .html or /, and checks for any
// file with path parameters in the name, eg '{abc}.html.tmpl', prefering exact filename matches.
func getTemplateNameForURL(u *url.URL, templateDir string) (name string, params map[string]string) {
templateDir = path.Clean(templateDir)
fp := strings.TrimPrefix(strings.TrimSuffix(strings.TrimSuffix(u.Path, ".html"), "/"), "/")
if fp == "" {
// "/" maps to "/index"
fp = "index"
}
fpParts := strings.Split(fp, "/")
res := getMatchingGlobPatternsCapturingFilepathIncludingParametrizedFilepaths(fpParts)
for _, combs := range res {
pattern := path.Join(templateDir, path.Join(combs...)) + ".html.tmpl"
matches, _ := filepath.Glob(pattern)
if len(matches) == 0 {
pattern := path.Join(templateDir, path.Join(combs...), "index") + ".html.tmpl"
matches, _ = filepath.Glob(pattern)
}
if len(matches) > 0 {
match := matches[0]
name = strings.TrimPrefix(strings.TrimSuffix(match, ".html.tmpl"), templateDir+"/")
patternParts := strings.Split(name, "/")
params = make(map[string]string)
for i, pp := range patternParts {
if strings.HasPrefix(pp, "{") && strings.HasSuffix(pp, "}") {
params[pp[1:len(pp)-1]] = fpParts[i]
}
}
return name, params
}
}
return fp, nil
}
func getMatchingGlobPatternsCapturingFilepathIncludingParametrizedFilepaths(filepathParts []string) [][]string {
switch len(filepathParts) {
case 0:
return nil
case 1:
return [][]string{
[]string{filepathParts[0]},
[]string{"{*}"},
}
default:
tailCombs := getMatchingGlobPatternsCapturingFilepathIncludingParametrizedFilepaths(filepathParts[1:])
combs := make([][]string, 2*len(tailCombs))
for i, c := range tailCombs {
combs[i*2] = append([]string{filepathParts[0]}, c...)
combs[i*2+1] = append([]string{"{*}"}, c...)
}
return combs
}
}
// func (s *Server) handleTemplateError(err error, templateArgs ...any) (response.Response, error) {
func (s *webpageRouter) handleTemplateError(err error, templateArgs ...any) (response.Response, error) {
if isFileNotFoundError(err) { if isFileNotFoundError(err) {
return nil, response.NotFound(). return nil, response.NotFound().
Wrap(ErrTemplateNotFound{ Wrap(ErrTemplateNotFound{
@@ -223,31 +163,10 @@ func (s *webpageRouter) handleTemplateError(err error, templateArgs ...any) (res
} }
func isFileNotFoundError(err error) bool { func isFileNotFoundError(err error) bool {
/*
var pe *os.PathError
isPathErr := errors.As(err, &pe)
return isPathErr && pe.Err != nil && pe.Err.Error() == "no such file or directory"
*/
var te *templater.ErrNotTemplateFileFound var te *templater.ErrNotTemplateFileFound
return errors.As(err, &te) return errors.As(err, &te)
} }
func getHTTPStatusCode(err error) int {
rerr, ok := response.GetError(err)
if !ok {
return http.StatusInternalServerError
}
code, ok := rerr.GetStatus()
if !ok {
return http.StatusInternalServerError
}
return code
}
// template tooling // template tooling
type URLCalculator struct { type URLCalculator struct {
-55
View File
@@ -2,15 +2,10 @@ package server
import ( import (
"context" "context"
"encoding/json"
"fmt"
"html/template"
"net/http" "net/http"
"path" "path"
"strconv"
"strings" "strings"
"github.com/angelbeltran/templater"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"ruben/inventory2/internal/domains/accounts" "ruben/inventory2/internal/domains/accounts"
@@ -73,56 +68,6 @@ func Router(
logger.WithGroup("templates"), logger.WithGroup("templates"),
r.Group("/ui"), r.Group("/ui"),
contentDir, contentDir,
// TODO: move this constructor call up to main.go
// TODO: clean up the references to the templates dir throughout as well (should only be referred to in the 'main' package
new(templater.Templater).With(templater.Config{
Funcs: func(name string, props map[string]any) template.FuncMap {
return template.FuncMap{
// params
"addPathParam": func(k string, v any, args map[string]any) (map[string]any, error) {
pathParams, ok := args["PathParams"].(map[string]any)
if !ok {
return nil, fmt.Errorf("PathParams not set as args: %v", args)
}
pathParams[k] = fmt.Sprint(v)
return args, nil
},
// parsing
"parseInt": func(s string) (int, error) {
return strconv.Atoi(s)
},
"parseInt64": func(s string) (int64, error) {
return strconv.ParseInt(s, 10, 64)
},
"parsePlatform": func(s string) (accounts.Platform, error) {
return accounts.NewPlatform(s)
},
// arithmetic
"addInt": func(a, b int) int {
return a + b
},
"subInt": func(a, b int) int {
return a - b
},
"multInt": func(a, b int) int {
return a * b
},
// json
"prettyPrintJSON": func(j json.RawMessage) string {
b, err := json.MarshalIndent(j, " ", "")
if err != nil {
return string(j)
}
return string(b)
},
}
},
}),
rawEvents, rawEvents,
accts, accts,
etsy, etsy,
@@ -1,12 +1,8 @@
{{/* TODO: make the save button disabled based on an api call */}} {{/* TODO: make the save button disabled based on an api call */}}
{{/* .Identity.Account.AccountID, .Accounts */}}
{{- $dot := or .dot . -}} {{- $acctID := .PathParams.acctID }}
{{- $orderIndex := .PathParams.orderIndex }}
{{- $acctID := $dot.Identity.Account.AccountID }} {{- $entry := .Accounts.GetSyncGroupListingDraft $acctID $orderIndex }}
{{- $orderIndex := or $dot.OrderIndex (parseInt $dot.PathParams.orderIndex) -}}
{{- $entry := $dot.Accounts.GetSyncGroupListingDraft $acctID $orderIndex }}
{{- $selectedShopPlatform := $entry.Platform }} {{- $selectedShopPlatform := $entry.Platform }}
{{- $selectedShopID := $entry.ShopID }} {{- $selectedShopID := $entry.ShopID }}
{{- $selectedListingID := $entry.ListingID }} {{- $selectedListingID := $entry.ListingID }}
@@ -30,7 +26,7 @@
" "
> >
<td> <td>
{{- $shops := $dot.Accounts.GetShops $acctID -}} {{- $shops := .Accounts.GetShops $acctID -}}
<select <select
class="min-w-fit cursor-pointer" class="min-w-fit cursor-pointer"
hx-put="/api/accounts/{{$acctID}}/inventory/sync-groups/draft/listings/{{$orderIndex}}/shop" hx-put="/api/accounts/{{$acctID}}/inventory/sync-groups/draft/listings/{{$orderIndex}}/shop"
@@ -64,7 +60,7 @@
<td> <td>
{{- $selectedListing := "" }} {{- $selectedListing := "" }}
{{- if $selectedShopID }} {{- if $selectedShopID }}
{{- $listings := $dot.Accounts.GetListingsForShop $acctID $selectedShopPlatform $selectedShopID }} {{- $listings := .Accounts.GetListingsForShop $acctID $selectedShopPlatform $selectedShopID }}
{{- if $listings }} {{- if $listings }}
<select <select
class="min-w-fit cursor-pointer" class="min-w-fit cursor-pointer"
@@ -1,17 +1,8 @@
{{- $dot := or .dot . }}
{{- .Auth.ByMatchingAccountID 2 }} {{- .Auth.ByMatchingAccountID 2 }}
{{- $acctID := .PathParams.acctID }}
{{- $acctID := 0 }} {{- $stores := .Accounts.GetShops $acctID -}}
{{- if $dot.Identity }}
{{- $acctID = $dot.Identity.Account.AccountID }}
{{- else if .PathParams }}
{{- $acctID = $dot.PathParams.acctID | parseInt64 }}
{{- else }}
{{- $acctID = $dot.AccountID }}
{{- end }}
{{- $stores := $dot.Accounts.GetShops $acctID -}}
<table id="accounts-acct-id-inventory-sync-groups-draft-table" class="max-w-full overflow-x-auto"> <table id="accounts-acct-id-inventory-sync-groups-draft-table" class="max-w-full overflow-x-auto">
@@ -58,11 +49,9 @@
send setDisabled(disabled: not allRowsFilledOut or numRows < 2) to #create-sync-group-button send setDisabled(disabled: not allRowsFilledOut or numRows < 2) to #create-sync-group-button
" "
> >
{{- $listings := $dot.Accounts.GetSyncGroupListingDrafts $acctID }} {{- $listings := .Accounts.GetSyncGroupListingDrafts $acctID }}
{{- range $listing := $listings }} {{- range $listing := $listings }}
{{- component "accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}" {{- component (printf "accounts/%d/inventory/sync-groups/draft/listings/%d" $acctID $listing.OrderIndex) }}
"dot" ($dot | addPathParam "orderIndex" $listing.OrderIndex)
}}
{{- end }} {{- end }}
</tbody> </tbody>
<tfoot> <tfoot>
@@ -17,7 +17,8 @@
<h4> <h4>
(Draft 2) (Draft 2)
</h4> </h4>
{{ component "accounts/{acctID}/inventory/sync-groups/draft/table" "dot" . }} {{/* component "accounts/{acctID}/inventory/sync-groups/draft/table" "dot" . */}}
{{ component (printf "accounts/%d/inventory/sync-groups/draft/table" $acctID) "dot" . }}
<div> <div>
{{ component "button" {{ component "button"