diff --git a/README.md b/README.md index 51ce9b9..0c7c927 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ generator. - [Status](#status) - [Drops](#drops) - [Value Types](#value-types) + - [Template Store](#template-store) - [References](#references) - [Contributing](#contributing) - [Contributors](#contributors) @@ -152,6 +153,25 @@ Any Go value can be used as a variable value. These values have special meaning: - An instance of `yaml.MapSlice` acts as a map. It implements `m.key`, `m[key]`, and `m.size`. +### Template Store + +The template store allows for usage of varying template storage implementations (embedded file system, database, service, etc). In order to use: + +1. Create a struct that implements ITemplateStore + ```go + type ITemplateStore interface { + ReadTemplate(templatename string) ([]byte, error) + } + ``` +1. Register with the engine + ```go + engine.RegisterTemplateStore() + ``` + +`FileTemplateStore` is the default mechanism for backwards compatability. + +Refer to [example](./docs/TemplateStoreExample.md) for an example implementation. + ### References - [Shopify.github.io/liquid](https://shopify.github.io/liquid) diff --git a/docs/TemplateStoreExample.md b/docs/TemplateStoreExample.md new file mode 100644 index 0000000..f34f198 --- /dev/null +++ b/docs/TemplateStoreExample.md @@ -0,0 +1,54 @@ +# Template Store Example + +This document describes the implementation of an `ITemplateStore` that uses an embedded file system as its storage type. + +Add a go file to your project with configuration properties and the ReadTemplate() implementation + +```go +package your_package_name + +import ( + "embed" + "fmt" +) + +type EmbeddedFileSystemTemplateStore struct { + Folder embed.FS + RootDir string +} + +// implementation of ITemplateProvider +func (tl *EmbeddedFileSystemTemplateStore) ReadTemplate(filename string) ([]byte, error) { + + fileName := fmt.Sprintf("%v/%v", tl.RootDir, filename) + templateFile, _ := tl.Folder.ReadFile(fileName) + + return templateFile, nil +} + +``` +initialize your embedded folder. for details on go embedded package see [embed](https://pkg.go.dev/embed) + +```go + +//go:embed all:templates +var folder embed.FS + +``` +create store and register with engine + +```go + // use the embedded file system loader for now. + embedFileSystemTemplateStore := &your_package_name.EmbeddedFileSystemTemplateStore{ + Folder: folder, + RootDir: "templates", + } + + //create engine + engine := liquid.NewEngine() + + //register with the engine + engine.RegisterTemplateStore(embedFileSystemTemplateStore) + + //ready to go +``` \ No newline at end of file diff --git a/engine.go b/engine.go index 814e60e..2e5a094 100644 --- a/engine.go +++ b/engine.go @@ -70,6 +70,10 @@ func (e *Engine) RegisterTag(name string, td Renderer) { }) } +func (e *Engine) RegisterTemplateStore(templateStore render.ITemplateStore) { + e.cfg.TemplateStore = templateStore +} + // StrictVariables causes the renderer to error when the template contains an undefined variable. func (e *Engine) StrictVariables() { e.cfg.StrictVariables = true diff --git a/engine_test.go b/engine_test.go index 9187e86..ddd3984 100644 --- a/engine_test.go +++ b/engine_test.go @@ -3,6 +3,7 @@ package liquid import ( "bytes" "encoding/json" + "fmt" "io" "strconv" "strings" @@ -155,3 +156,24 @@ func TestEngine_ParseTemplateAndCache(t *testing.T) { require.NoError(t, err) require.Equal(t, "Foo, Bar", string(result)) } + +type MockTemplateStore struct{} + +func (tl *MockTemplateStore) ReadTemplate(filename string) ([]byte, error) { + template := []byte(fmt.Sprintf("Message Text: {{ message.Text }} from: %v.", filename)) + return template, nil +} + +func Test_template_store(t *testing.T) { + template := []byte(`{% include "template.liquid" %}`) + mockstore := &MockTemplateStore{} + params := map[string]any{ + "message": testStruct{ + Text: "filename", + }, + } + engine := NewEngine() + engine.RegisterTemplateStore(mockstore) + out, _ := engine.ParseAndRenderString(string(template), params) + require.Equal(t, "Message Text: filename from: template.liquid.", out) +} diff --git a/render/config.go b/render/config.go index afc8f51..e536344 100644 --- a/render/config.go +++ b/render/config.go @@ -10,6 +10,7 @@ type Config struct { grammar Cache map[string][]byte StrictVariables bool + TemplateStore ITemplateStore } type grammar struct { @@ -18,10 +19,16 @@ type grammar struct { } // NewConfig creates a new Settings. +// TemplateStore is initialized to a FileTemplateStore for backwards compatibility func NewConfig() Config { g := grammar{ tags: map[string]TagCompiler{}, blockDefs: map[string]*blockSyntax{}, } - return Config{Config: parser.NewConfig(g), grammar: g, Cache: map[string][]byte{}} + return Config{ + Config: parser.NewConfig(g), + grammar: g, + Cache: map[string][]byte{}, + TemplateStore: &FileTemplateStore{}, + } } diff --git a/render/context.go b/render/context.go index 63d7179..503f826 100644 --- a/render/context.go +++ b/render/context.go @@ -55,6 +55,10 @@ type Context interface { WrapError(err error) Error } +type ITemplateStore interface { + ReadTemplate(templatename string) ([]byte, error) +} + type rendererContext struct { ctx nodeContext node *TagNode @@ -145,7 +149,7 @@ func (c rendererContext) RenderChildren(w io.Writer) Error { } func (c rendererContext) RenderFile(filename string, b map[string]any) (string, error) { - source, err := os.ReadFile(filename) + source, err := c.ctx.config.TemplateStore.ReadTemplate(filename) if err != nil && os.IsNotExist(err) { // Is it cached? if cval, ok := c.ctx.config.Cache[filename]; ok { diff --git a/render/file_template_store.go b/render/file_template_store.go new file mode 100644 index 0000000..0f9aae6 --- /dev/null +++ b/render/file_template_store.go @@ -0,0 +1,12 @@ +package render + +import ( + "os" +) + +type FileTemplateStore struct{} + +func (tl *FileTemplateStore) ReadTemplate(filename string) ([]byte, error) { + source, err := os.ReadFile(filename) + return source, err +} diff --git a/values/drop_test.go b/values/drop_test.go index c29e11a..bd6670d 100644 --- a/values/drop_test.go +++ b/values/drop_test.go @@ -36,20 +36,21 @@ func TestDrop_Resolve_race(t *testing.T) { func BenchmarkDrop_Resolve_1(b *testing.B) { d := ValueOf(testDrop{1}) - for n := 0; n < b.N; n++ { + + for range b.N { _ = d.Int() } } func BenchmarkDrop_Resolve_2(b *testing.B) { - for n := 0; n < b.N; n++ { + for range b.N { d := ValueOf(testDrop{1}) _ = d.Int() } } func BenchmarkDrop_Resolve_3(b *testing.B) { - for n := 0; n < b.N; n++ { + for range b.N { d := ValueOf(testDrop{1}) values := make(chan int, 10) for i := cap(values); i > 0; i-- {