From 726cb1b43f30528835518de6a8ffb61c43cc139e Mon Sep 17 00:00:00 2001 From: chrisatbd <168666836+chrisatbd@users.noreply.github.com> Date: Thu, 13 Mar 2025 17:11:48 -0700 Subject: [PATCH 1/2] Implement template loader Implement template loader --- README.md | 20 +++++++++++++ docs/TemplateStoreExample.md | 54 +++++++++++++++++++++++++++++++++++ engine.go | 4 +++ engine_test.go | 23 +++++++++++++++ render/config.go | 9 +++++- render/context.go | 6 +++- render/file_template_store.go | 12 ++++++++ 7 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 docs/TemplateStoreExample.md create mode 100644 render/file_template_store.go 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..a9e0541 100644 --- a/engine_test.go +++ b/engine_test.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" "testing" + "fmt" "github.com/stretchr/testify/require" ) @@ -155,3 +156,25 @@ 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) +} \ No newline at end of file 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 +} From 2a00b80501fed8806f3d6780e0483fd6fbbbe548 Mon Sep 17 00:00:00 2001 From: chrisatbd Date: Mon, 17 Mar 2025 13:11:12 -0700 Subject: [PATCH 2/2] linter updates --- engine_test.go | 9 ++++----- values/drop_test.go | 7 ++++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/engine_test.go b/engine_test.go index a9e0541..ddd3984 100644 --- a/engine_test.go +++ b/engine_test.go @@ -3,11 +3,11 @@ package liquid import ( "bytes" "encoding/json" + "fmt" "io" "strconv" "strings" "testing" - "fmt" "github.com/stretchr/testify/require" ) @@ -157,15 +157,14 @@ func TestEngine_ParseTemplateAndCache(t *testing.T) { require.Equal(t, "Foo, Bar", string(result)) } -type MockTemplateStore struct {} +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){ +func Test_template_store(t *testing.T) { template := []byte(`{% include "template.liquid" %}`) mockstore := &MockTemplateStore{} params := map[string]any{ @@ -177,4 +176,4 @@ func Test_template_store(t *testing.T){ engine.RegisterTemplateStore(mockstore) out, _ := engine.ParseAndRenderString(string(template), params) require.Equal(t, "Message Text: filename from: template.liquid.", out) -} \ No newline at end of file +} 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-- {