Skip to content

Commit

Permalink
add reload on error
Browse files Browse the repository at this point in the history
  • Loading branch information
dvaumoron committed Nov 6, 2023
1 parent 78de209 commit c8a42ed
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 65 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ Library to load several [go template](https://pkg.go.dev/text/template) decompos

## Getting started

In order to use PartRenderer in your project Cornucopia (with the go langage already installed), you can use the command :
In order to use PartRenderer in your project (with the go langage already installed), you can use the command :

go install github.com/dvaumoron/partrenderer@latest
go get github.com/dvaumoron/partrenderer@latest

Then you can import it :

Expand Down
134 changes: 71 additions & 63 deletions part.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@
package partrenderer

import (
"errors"
"io"
"io/fs"
"path/filepath"
"strings"
"text/template"

Expand All @@ -35,111 +34,120 @@ const (
defaultRootName = "root"
)

type loadOptions struct {
fs afero.Fs
fileExt string
fileExtLen int
funcs template.FuncMap
var ErrViewNotFound = errors.New("view not found")

// a true trigger a reload
type ReloadRule = func(error) bool

func AlwaysReload(err error) bool {
return true
}

func ReloadOnViewNotFound(err error) bool {
return err == ErrViewNotFound
}

func NeverReload(err error) bool {
return false
}

type LoadOption func(loadOptions) loadOptions
type LoadOption func(loadInfos) loadInfos

// option to use an alternate file system
func WithFs(fs afero.Fs) LoadOption {
return func(lo loadOptions) loadOptions {
lo.fs = fs
return lo
return func(li loadInfos) loadInfos {
li.fs = fs
return li
}
}

// option to use an alternate extension to filter loaded file (default is ".html")
func WithFileExt(ext string) LoadOption {
return func(lo loadOptions) loadOptions {
return func(li loadInfos) loadInfos {
if ext != "" && ext[0] != '.' {
ext = "." + ext
}
lo.fileExt = ext
lo.fileExtLen = len(ext)
return lo
li.fileExt = ext
li.fileExtLen = len(ext)
return li
}
}

// allow to load a template.FuncMap before parsing the go templates
func WithFuncs(customFuncs template.FuncMap) LoadOption {
return func(lo loadOptions) loadOptions {
lo.funcs = customFuncs
return lo
return func(li loadInfos) loadInfos {
li.funcs = customFuncs
return li
}
}

// option to change the rule to reload on error (default is ReloadOnViewNotFound)
func WithReloadRule(rule ReloadRule) LoadOption {
return func(li loadInfos) loadInfos {
li.reloadRule = rule
return li
}
}

type PartRenderer struct {
views map[string]*template.Template
Separator string
RootName string
views *viewManager
reloadRule ReloadRule
Separator string
RootName string
}

// The componentsPath argument indicates a directory to walk in order to load all component templates
//
// The viewsPath argument indicates a directory to walk in order to load all view templates (which can see components)
func MakePartRenderer(componentsPath string, viewsPath string, opts ...LoadOption) (PartRenderer, error) {
options := loadOptions{fs: afero.NewOsFs(), fileExt: defaultExt, fileExtLen: defaultExtLen}
infos := loadInfos{
fs: afero.NewOsFs(),
componentsPath: componentsPath,
viewsPath: viewsPath,
fileExt: defaultExt,
fileExtLen: defaultExtLen,
reloadRule: ReloadOnViewNotFound,
}

for _, optionModifier := range opts {
options = optionModifier(options)
infos = optionModifier(infos)
}

components, err := loadComponents(componentsPath, options)
infos, err := infos.init()
if err != nil {
return PartRenderer{}, err
}

views, err := loadViews(viewsPath, components, options)
views, err := infos.loadViews()
if err != nil {
return PartRenderer{}, err
}
return PartRenderer{views: views, Separator: defaultSeparator, RootName: defaultRootName}, nil

vm := newViewManager(views, infos)
return PartRenderer{views: vm, reloadRule: infos.reloadRule, Separator: defaultSeparator, RootName: defaultRootName}, nil
}

// Find a template and render it, global and partial rendering depend on PartRenderer.RootName and PartRenderer.Separator.
// Could try a reload on error depending on the ReloadRule option.
func (r PartRenderer) ExecuteTemplate(w io.Writer, viewName string, data any) error {
partName := r.RootName
if splitted := strings.Split(viewName, r.Separator); len(splitted) > 1 {
viewName, partName = splitted[0], splitted[1]
}
return r.views[viewName].ExecuteTemplate(w, partName, data)
}

func loadComponents(componentsPath string, options loadOptions) (*template.Template, error) {
components := template.New("").Funcs(options.funcs)
err := afero.Walk(options.fs, componentsPath, func(path string, fi fs.FileInfo, err error) error {
if err == nil && !fi.IsDir() && path[len(path)-options.fileExtLen:] == options.fileExt {
err = parseOne(options.fs, path, components)
err := r.innerExecuteTemplate(w, viewName, partName, data)
if err != nil && r.reloadRule(err) {
if err = r.views.reload(); err == nil {
err = r.innerExecuteTemplate(w, viewName, partName, data)
}
return err
})
// not supposed to return data on error, but it's a private function
return components, err
}
return err
}

func loadViews(viewsPath string, components *template.Template, options loadOptions) (map[string]*template.Template, error) {
viewsPath, err := filepath.Abs(viewsPath)
func (r PartRenderer) innerExecuteTemplate(w io.Writer, viewName string, partName string, data any) error {
view, err := r.views.get(viewName)
if err != nil {
return nil, err
}
if last := len(viewsPath) - 1; viewsPath[last] != '/' {
viewsPath += "/"
}

inSize := len(viewsPath)
views := map[string]*template.Template{}
err = afero.Walk(options.fs, viewsPath, func(path string, fi fs.FileInfo, err error) error {
if end := len(path) - options.fileExtLen; err == nil && !fi.IsDir() && path[end:] == options.fileExt {
t, _ := components.Clone() // here error is always nil
err = parseOne(options.fs, path, t)
views[path[inSize:end]] = t
}
return err
})
// not supposed to return data on error, but it's a private function
return views, err
}

func parseOne(fs afero.Fs, path string, tmpl *template.Template) error {
data, err := afero.ReadFile(fs, path)
if err == nil {
_, err = tmpl.New(path).Parse(string(data))
}
return err
return view.ExecuteTemplate(w, partName, data)
}
136 changes: 136 additions & 0 deletions reload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
*
* Copyright 2023 partrenderer authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package partrenderer

import (
"io/fs"
"path/filepath"
"text/template"

"github.com/spf13/afero"
)

type viewManager struct {
views map[string]*template.Template
reloadSender chan<- chan<- error
}

func newViewManager(views map[string]*template.Template, infos loadInfos) *viewManager {
vm := &viewManager{views: views}
reloadChan := make(chan chan<- error)
go manageReload(reloadChan, infos, vm)
vm.reloadSender = reloadChan
return vm
}

func manageReload(reloadReceiver <-chan chan<- error, infos loadInfos, vm *viewManager) {
var waitings []chan<- error
loadingEnded := make(chan error)
for {
select {
case responder := <-reloadReceiver:
if len(waitings) == 0 {
go reloadAndAlert(infos, vm, loadingEnded)
}
waitings = append(waitings, responder)
case err := <-loadingEnded:
for _, responder := range waitings {
responder <- err
}
waitings = waitings[:0]
}
}
}

func reloadAndAlert(infos loadInfos, vm *viewManager, endSender chan<- error) {
views, err := infos.loadViews()
if err == nil {
vm.views = views
}
endSender <- err
}

func (vm *viewManager) get(viewName string) (*template.Template, error) {
view, ok := vm.views[viewName]
if !ok {
return nil, ErrViewNotFound
}
return view, nil
}

func (vm *viewManager) reload() error {
ended := make(chan error)
vm.reloadSender <- ended
return <-ended
}

type loadInfos struct {
fs afero.Fs
componentsPath string
viewsPath string
fileExt string
fileExtLen int
funcs template.FuncMap
reloadRule ReloadRule
}

func (options loadInfos) init() (loadInfos, error) {
var err error
if options.viewsPath, err = filepath.Abs(options.viewsPath); err != nil {
return options, err
}
if last := len(options.viewsPath) - 1; options.viewsPath[last] != '/' {
options.viewsPath += "/"
}
return options, nil
}

func (options loadInfos) loadViews() (map[string]*template.Template, error) {
components := template.New("").Funcs(options.funcs)
err := afero.Walk(options.fs, options.componentsPath, func(path string, fi fs.FileInfo, err error) error {
if err == nil && !fi.IsDir() && path[len(path)-options.fileExtLen:] == options.fileExt {
err = parseOne(options.fs, path, components)
}
return err
})
if err != nil {
return nil, err
}

inSize := len(options.viewsPath)
views := map[string]*template.Template{}
err = afero.Walk(options.fs, options.viewsPath, func(path string, fi fs.FileInfo, err error) error {
if end := len(path) - options.fileExtLen; err == nil && !fi.IsDir() && path[end:] == options.fileExt {
t, _ := components.Clone() // here error is always nil
err = parseOne(options.fs, path, t)
views[path[inSize:end]] = t
}
return err
})
// not supposed to return data on error, but it's a private function
return views, err
}

func parseOne(fs afero.Fs, path string, tmpl *template.Template) error {
data, err := afero.ReadFile(fs, path)
if err == nil {
_, err = tmpl.New(path).Parse(string(data))
}
return err
}

0 comments on commit c8a42ed

Please sign in to comment.