From c8a42ed93958ec1cef18e47c4f704961f9ca5d66 Mon Sep 17 00:00:00 2001 From: dvaumoron Date: Mon, 6 Nov 2023 17:29:01 +0100 Subject: [PATCH] add reload on error --- README.md | 4 +- part.go | 134 ++++++++++++++++++++++++++++------------------------- reload.go | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 209 insertions(+), 65 deletions(-) create mode 100644 reload.go diff --git a/README.md b/README.md index 8211eb5..cd6304e 100644 --- a/README.md +++ b/README.md @@ -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 : diff --git a/part.go b/part.go index d8bfe37..8d094b9 100644 --- a/part.go +++ b/part.go @@ -19,9 +19,8 @@ package partrenderer import ( + "errors" "io" - "io/fs" - "path/filepath" "strings" "text/template" @@ -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) } diff --git a/reload.go b/reload.go new file mode 100644 index 0000000..677d561 --- /dev/null +++ b/reload.go @@ -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 +}