Browse Source

init template

master
andrey1s 4 months ago
parent
commit
621afcc59c
  1. 24
      .drone.yml
  2. 42
      .golangci.yml
  3. 38
      engine/encode.go
  4. 27
      engine/encode_test.go
  5. 125
      engine/engine.go
  6. 10
      engine/suport.go
  7. 80
      engine/template.go
  8. 38
      execute.go
  9. 75
      execute_example_test.go
  10. 18
      go.mod
  11. 27
      go.sum
  12. 25
      gohtml/engine/engine.go
  13. 23
      gohtml/engine/engine_test.go
  14. 43
      gohtml/html.go
  15. 25
      gotxt/engine/engine.go
  16. 43
      gotxt/text.go
  17. 13
      json/engine/engine.go
  18. 12
      json/json.go
  19. 29
      loader/chain/loader.go
  20. 20
      loader/empty.go
  21. 34
      loader/loader.go
  22. 43
      parser/name.go
  23. 78
      render/reference.go
  24. 77
      render/render.go

24
.drone.yml

@ -0,0 +1,24 @@
kind: pipeline
name: default
steps:
- name: golangci-lint
image: golangci/golangci-lint:v1.49
volumes:
- name: deps
path: /go/src/mod
commands:
- golangci-lint run --timeout 5m
- name: test
image: golang
volumes:
- name: deps
path: /go/src/mod
commands:
- go test ./...
volumes:
- name: deps
temp: {}

42
.golangci.yml

@ -0,0 +1,42 @@
linters-settings:
dupl:
threshold: 100
funlen:
lines: 100
statements: 50
goconst:
min-len: 2
min-occurrences: 2
gocyclo:
min-complexity: 15
golint:
min-confidence: 0
govet:
check-shadowing: true
lll:
line-length: 140
maligned:
suggest-new: true
misspell:
locale: US
varnamelen:
min-name-length: 2
linters:
enable-all: true
disable:
- maligned
- exhaustivestruct
- golint
- structcheck
- varcheck
- interfacer
- deadcode
- nosnakecase
- scopelint
- ifshort
- gochecknoglobals
- ireturn
- exhaustruct
- gochecknoinits

38
engine/encode.go

@ -0,0 +1,38 @@
package engine
import (
"context"
"fmt"
"io"
"gitoa.ru/go-4devs/mime"
"gitoa.ru/go-4devs/templating/render"
)
func NewEncode(name string, encode func(w io.Writer, data interface{}) error, ext ...mime.Ext) Encode {
return Encode{
name: name,
encode: encode,
formats: ext,
}
}
type Encode struct {
encode func(w io.Writer, data interface{}) error
name string
formats []mime.Ext
}
func (e Encode) Support(_ context.Context, reference render.Reference) bool {
return Support(reference, e.name, e.formats...)
}
func (e Encode) Load(context.Context, render.Reference) (render.Execute, error) {
return func(_ context.Context, wr io.Writer, data interface{}, _ render.Params) error {
if err := e.encode(wr, data); err != nil {
return fmt.Errorf("%s engine:%w", e.name, err)
}
return nil
}, nil
}

27
engine/encode_test.go

@ -0,0 +1,27 @@
package engine_test
import (
"bytes"
"context"
"encoding/json"
"io"
"testing"
"github.com/stretchr/testify/require"
"gitoa.ru/go-4devs/templating/engine"
"gitoa.ru/go-4devs/templating/render"
)
func TestEncodeLoad(t *testing.T) {
t.Parallel()
ctx := context.Background()
buff := bytes.Buffer{}
exec, err := engine.NewEncode("json", func(w io.Writer, v interface{}) error {
return json.NewEncoder(w).Encode(v)
}).Load(ctx, render.NewReference("any"))
require.NoError(t, err)
require.NoError(t, exec(ctx, &buff, map[string]string{"name": "json data"}, nil))
require.Equal(t, "{\"name\":\"json data\"}\n", buff.String())
}

125
engine/engine.go

@ -0,0 +1,125 @@
package engine
import (
"context"
"errors"
"fmt"
"gitoa.ru/go-4devs/mime"
"gitoa.ru/go-4devs/templating/loader"
"gitoa.ru/go-4devs/templating/render"
)
var _ render.Engine = (*Engine)(nil)
var (
ErrNotSupport = errors.New("not support")
ErrDuplicate = errors.New("duplicate")
ErrNotFound = errors.New("not found")
)
type (
Option func(*Engine)
Parse func(loader.Source) (Template, error)
)
type Cache interface {
Set(ctx context.Context, tpl Template) error
Get(ctx context.Context, name string) (Template, error)
List(_ context.Context) []Template
}
func WithTemplates(tpls ...Template) Option {
ctx := context.Background()
return func(l *Engine) {
for _, tpl := range tpls {
_ = l.tpls.Set(ctx, tpl)
}
}
}
func WithLoader(load loader.Loader) Option {
return func(l *Engine) {
l.load = load
}
}
func WithFormats(formats ...mime.Ext) Option {
return func(e *Engine) {
e.formats = formats
}
}
func New(name string, parse Parse, opts ...Option) *Engine {
engine := Engine{
name: name,
parce: parse,
tpls: NewTemplates(),
load: loader.Empty(),
}
for _, opt := range opts {
opt(&engine)
}
return &engine
}
type Engine struct {
name string
formats []mime.Ext
tpls Cache
load loader.Loader
parce Parse
}
func (l Engine) WithLoader(load loader.Loader) *Engine {
return New(l.name, l.parce, WithTemplates(l.tpls.List(context.Background())...), WithLoader(load))
}
func (l Engine) Add(ctx context.Context, tpls ...Template) error {
for idx, tpl := range tpls {
if err := l.tpls.Set(ctx, tpl); err != nil {
return fmt.Errorf("engine add template[%d] with name %s: %w", idx, tpl.Name(), err)
}
}
return nil
}
func (l Engine) Support(ctx context.Context, tpl render.Reference) bool {
return Support(tpl, l.name, l.formats...)
}
func (l Engine) Load(ctx context.Context, reference render.Reference) (render.Execute, error) {
var (
tpl Template
err error
)
tpl, err = l.tpls.Get(ctx, reference.Name())
if err == nil {
return tpl.Execute, nil
}
if !errors.Is(err, ErrNotFound) {
return nil, fmt.Errorf("load get:%w", err)
}
source, err := l.load.Load(ctx, reference)
if err != nil {
return nil, fmt.Errorf("load source:%w", err)
}
tpl, err = l.parce(source)
if err != nil {
return nil, fmt.Errorf("load parce:%w", err)
}
if err := l.tpls.Set(ctx, tpl); err != nil {
return nil, fmt.Errorf("load set:%w", err)
}
return tpl.Execute, nil
}

10
engine/suport.go

@ -0,0 +1,10 @@
package engine
import (
"gitoa.ru/go-4devs/mime"
"gitoa.ru/go-4devs/templating/render"
)
func Support(reference render.Reference, name string, formats ...mime.Ext) bool {
return (reference.Engine != "" && reference.IsEngine(name)) || (len(formats) != 0 && reference.IsFromat(formats...))
}

80
engine/template.go

@ -0,0 +1,80 @@
package engine
import (
"context"
"fmt"
"io"
"sync"
"gitoa.ru/go-4devs/templating/render"
)
type Template interface {
Name() string
Execute(ctx context.Context, w io.Writer, data interface{}, param render.Params) error
}
func NewTemplates() Templates {
return Templates{
list: make(map[string]Template),
mu: &sync.RWMutex{},
}
}
type Templates struct {
list map[string]Template
mu *sync.RWMutex
}
func (t Templates) Get(_ context.Context, name string) (Template, error) {
t.mu.RLock()
defer t.mu.RUnlock()
if tpl, ok := t.list[name]; ok {
return tpl, nil
}
return nil, fmt.Errorf("templates get:%w", ErrNotFound)
}
func (t Templates) Set(_ context.Context, tpl Template) error {
t.mu.Lock()
defer t.mu.Unlock()
if _, ok := t.list[tpl.Name()]; ok {
return fmt.Errorf("templates set:%w", ErrDuplicate)
}
t.list[tpl.Name()] = tpl
return nil
}
func (t Templates) List(_ context.Context) []Template {
list := make([]Template, 0, len(t.list))
for _, tpl := range t.list {
list = append(list, tpl)
}
return list
}
func NewTemplate(name string, execute func(w io.Writer, data interface{}) error) ExecTemplate {
return ExecTemplate{
name: name,
execute: execute,
}
}
type ExecTemplate struct {
name string
execute func(w io.Writer, data interface{}) error
}
func (t ExecTemplate) Name() string {
return t.name
}
func (t ExecTemplate) Execute(_ context.Context, w io.Writer, data interface{}, _ render.Params) error {
return t.execute(w, data)
}

38
execute.go

@ -0,0 +1,38 @@
package templating
import (
"context"
"fmt"
"io"
"sync"
"gitoa.ru/go-4devs/templating/parser"
"gitoa.ru/go-4devs/templating/render"
)
var (
exec = render.New(parser.Name)
mu = sync.Mutex{}
)
func SetParser(parser render.Parser) {
mu.Lock()
defer mu.Unlock()
exec = exec.WithParser(parser)
}
func AddEngine(engine ...render.Engine) {
mu.Lock()
defer mu.Unlock()
exec.Add(engine...)
}
func Execute(ctx context.Context, wr io.Writer, name string, data interface{}, opts ...render.Option) error {
if err := exec.Execute(ctx, wr, name, data, opts...); err != nil {
return fmt.Errorf("templating engine:%w", err)
}
return nil
}

75
execute_example_test.go

@ -0,0 +1,75 @@
package templating_test
import (
"context"
html "html/template"
"log"
"os"
text "text/template"
"gitoa.ru/go-4devs/templating"
"gitoa.ru/go-4devs/templating/gohtml"
"gitoa.ru/go-4devs/templating/gotxt"
"gitoa.ru/go-4devs/templating/render"
)
type Data struct {
Name string
}
func ExampleExecute() {
ctx := context.Background()
data := Data{Name: "andrey"}
gohtml.Must(html.New("example.html").Parse(`Hello {{ .Name }}!`))
err := templating.Execute(ctx, os.Stdout, "example.html.gohtml", data)
if err != nil {
log.Fatalln(err)
}
// Output:
// Hello andrey!
}
func ExampleExecute_withOption() {
ctx := context.Background()
data := Data{Name: "<b>world</b>"}
gohtml.Must(html.New("example_with_option.html").Parse(`Hello <s>{{ .Name }}</s>!`))
err := templating.Execute(ctx, os.Stdout, "example_with_option.html", data, render.WithEngine(gohtml.Name))
if err != nil {
log.Fatalln(err)
}
// Output:
// Hello <s>&lt;b&gt;world&lt;/b&gt;</s>!
}
func ExampleExecute_text() {
ctx := context.Background()
data := Data{Name: "<u>text</u>"}
gotxt.Must(text.New("example.txt").Parse(`Hello {{ .Name }}!`))
err := templating.Execute(ctx, os.Stdout, "example.txt.gotxt", data)
if err != nil {
log.Fatalln(err)
}
// Output:
// Hello <u>text</u>!
}
func ExampleExecute_withoutEngine() {
ctx := context.Background()
data := Data{Name: "engine"}
gotxt.Must(text.New("example_engine.txt").Parse(`Text {{ .Name }}!`))
gohtml.Must(html.New("example_engine.html").Parse(`Html {{ .Name }}!`))
err := templating.Execute(ctx, os.Stdout, "example_engine.html", data)
if err != nil {
log.Fatalln(err)
}
// Output:
// Html engine!
}

18
go.mod

@ -0,0 +1,18 @@
module gitoa.ru/go-4devs/templating
go 1.17
require (
github.com/stretchr/testify v1.8.0
gitoa.ru/go-4devs/encoding/json v0.0.1
gitoa.ru/go-4devs/mime v0.0.1
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gitoa.ru/go-4devs/encoding v0.0.1 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

27
go.sum

@ -0,0 +1,27 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
gitoa.ru/go-4devs/encoding v0.0.1 h1:K93gf7fCwbjkl4yh+eVRDyQ/lNRJPH9Ryd4qWlTKMm4=
gitoa.ru/go-4devs/encoding v0.0.1/go.mod h1:7dsdRnHdwk82P+cH1qSBwT1dAhZVYnMmGHw+zc9nOAI=
gitoa.ru/go-4devs/encoding/json v0.0.1 h1:N6QHhTXaOzNTb3TnPVxGPiAI0vmWt5OPEBRBlU3PDmo=
gitoa.ru/go-4devs/encoding/json v0.0.1/go.mod h1:T+sKiDPkVJnUrmZJjbP8vQvF52lmCfyH92G3G2zNpMQ=
gitoa.ru/go-4devs/mime v0.0.1 h1:WKcEhe1g5l7gMHWuC7xxeir9zIPYmlNKRjmnOj0kciA=
gitoa.ru/go-4devs/mime v0.0.1/go.mod h1:t+UFgjBNqSdLMYzV3xbKoLWX7dZgcqxEzcjAFh4Y8Fs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

25
gohtml/engine/engine.go

@ -0,0 +1,25 @@
package engine
import (
"fmt"
html "html/template"
"gitoa.ru/go-4devs/mime"
"gitoa.ru/go-4devs/templating/engine"
"gitoa.ru/go-4devs/templating/loader"
)
const Name = "gohtml"
func Parser(source loader.Source) (engine.Template, error) {
tpl, err := html.New(source.Name()).Parse(source.Data())
if err != nil {
return nil, fmt.Errorf("parse html:%w", engine.ErrNotSupport)
}
return engine.NewTemplate(tpl.Name(), tpl.Execute), nil
}
func New(opts ...engine.Option) *engine.Engine {
return engine.New(Name, Parser, append(opts, engine.WithFormats(mime.ExtHTML))...)
}

23
gohtml/engine/engine_test.go

@ -0,0 +1,23 @@
package engine_test
import (
"bytes"
"context"
"testing"
"github.com/stretchr/testify/require"
"gitoa.ru/go-4devs/templating/gohtml/engine"
"gitoa.ru/go-4devs/templating/loader"
)
func TestParser(t *testing.T) {
t.Parallel()
ctx := context.Background()
buff := bytes.Buffer{}
tpl, err := engine.Parser(loader.NewSource(t.Name(), `engine:{{.name}}`))
require.NoError(t, err)
require.NoError(t, tpl.Execute(ctx, &buff, map[string]string{"name": "gohtml"}, nil))
require.Equal(t, "engine:gohtml", buff.String())
}

43
gohtml/html.go

@ -0,0 +1,43 @@
package gohtml
import (
"context"
"fmt"
html "html/template"
"gitoa.ru/go-4devs/templating"
"gitoa.ru/go-4devs/templating/engine"
gohtml "gitoa.ru/go-4devs/templating/gohtml/engine"
"gitoa.ru/go-4devs/templating/loader"
)
const Name = gohtml.Name
var htm = gohtml.New()
func init() {
templating.AddEngine(htm)
}
// Loader set new loader. This function is not thread-safe.
func Loader(in loader.Loader) {
htm = htm.WithLoader(in)
}
func Must(template *html.Template, err error) {
MustRegister(html.Must(template, err))
}
func MustRegister(template *html.Template) {
if rerr := Register(template); rerr != nil {
panic(rerr)
}
}
func Register(template *html.Template) error {
if err := htm.Add(context.Background(), engine.NewTemplate(template.Name(), template.Execute)); err != nil {
return fmt.Errorf("gohtml loader:%w", err)
}
return nil
}

25
gotxt/engine/engine.go

@ -0,0 +1,25 @@
package engine
import (
"fmt"
text "text/template"
"gitoa.ru/go-4devs/mime"
"gitoa.ru/go-4devs/templating/engine"
"gitoa.ru/go-4devs/templating/loader"
)
const Name = "gotxt"
func Parser(source loader.Source) (engine.Template, error) {
tpl, err := text.New(source.Name()).Parse(source.Data())
if err != nil {
return nil, fmt.Errorf("parse text:%w", engine.ErrNotSupport)
}
return engine.NewTemplate(tpl.Name(), tpl.Execute), nil
}
func New(opts ...engine.Option) *engine.Engine {
return engine.New(Name, Parser, append(opts, engine.WithFormats(mime.ExtTxt, mime.ExtText))...)
}

43
gotxt/text.go

@ -0,0 +1,43 @@
package gotxt
import (
"context"
"fmt"
text "text/template"
"gitoa.ru/go-4devs/templating"
"gitoa.ru/go-4devs/templating/engine"
gotxt "gitoa.ru/go-4devs/templating/gotxt/engine"
"gitoa.ru/go-4devs/templating/loader"
)
const Name = gotxt.Name
var txt = gotxt.New()
func init() {
templating.AddEngine(txt)
}
// Loader set new loader. This function is not thread-safe.
func Loader(in loader.Loader) {
txt = txt.WithLoader(in)
}
func Must(template *text.Template, err error) {
MustRegister(text.Must(template, err))
}
func MustRegister(template *text.Template) {
if rerr := Register(template); rerr != nil {
panic(rerr)
}
}
func Register(template *text.Template) error {
if err := txt.Add(context.Background(), engine.NewTemplate(template.Name(), template.Execute)); err != nil {
return fmt.Errorf("gotext loader:%w", err)
}
return nil
}

13
json/engine/engine.go

@ -0,0 +1,13 @@
package engine
import (
"gitoa.ru/go-4devs/encoding/json"
"gitoa.ru/go-4devs/mime"
"gitoa.ru/go-4devs/templating/engine"
)
const Name = "json"
func New() engine.Encode {
return engine.NewEncode(Name, json.Encode, mime.ExtJSON)
}

12
json/json.go

@ -0,0 +1,12 @@
package json
import (
"gitoa.ru/go-4devs/templating"
json "gitoa.ru/go-4devs/templating/json/engine"
)
const Name = json.Name
func init() {
templating.AddEngine(json.New())
}

29
loader/chain/loader.go

@ -0,0 +1,29 @@
package chain
import (
"context"
"errors"
"fmt"
"gitoa.ru/go-4devs/templating/loader"
"gitoa.ru/go-4devs/templating/render"
)
var _ loader.Loader = (Loaders)(nil)
type Loaders []loader.Loader
func (l Loaders) Load(ctx context.Context, template render.Reference) (loader.Source, error) {
for idx, load := range l {
sourse, err := load.Load(ctx, template)
if err == nil {
return sourse, nil
}
if !errors.Is(err, loader.ErrNotFound) {
return loader.Source{}, fmt.Errorf("chain[%d]:%w", idx, err)
}
}
return loader.Source{}, fmt.Errorf("chains(%d):%w", len(l), loader.ErrNotFound)
}

20
loader/empty.go

@ -0,0 +1,20 @@
package loader
import (
"context"
"fmt"
"gitoa.ru/go-4devs/templating/render"
)
var _empty = empty{}
type empty struct{}
func (empty) Load(context.Context, render.Reference) (Source, error) {
return Source{}, fmt.Errorf("empty: %w", ErrNotFound)
}
func Empty() Loader {
return _empty
}

34
loader/loader.go

@ -0,0 +1,34 @@
package loader
import (
"context"
"errors"
"gitoa.ru/go-4devs/templating/render"
)
var ErrNotFound = errors.New("not found")
type Loader interface {
Load(ctx context.Context, r render.Reference) (Source, error)
}
func NewSource(name, data string) Source {
return Source{
name: name,
data: data,
}
}
type Source struct {
name string
data string
}
func (s Source) Name() string {
return s.name
}
func (s Source) Data() string {
return s.data
}

43
parser/name.go

@ -0,0 +1,43 @@
package parser
import (
"context"
"path"
"strings"
"gitoa.ru/go-4devs/mime"
"gitoa.ru/go-4devs/templating/render"
)
const (
nameWithExt = 2
nameWithEngine = 3
countOption = 2
)
// Name parse name by <tpl name>.<format>.<engine> or <tpl name>.<format>.
func Name(_ context.Context, name string, opts ...render.Option) (render.Reference, error) {
options := make([]render.Option, 0, countOption)
tplName := strings.ToLower(name)
base := path.Base(tplName)
el := strings.SplitN(base, ".", nameWithEngine)
var ext mime.Ext
switch len(el) {
case nameWithEngine:
ext = mime.ExtFromString(el[1])
options = append(options, render.WithEngine(el[2]))
tplName = strings.TrimRight(tplName, "."+el[1]+"."+el[2])
case nameWithExt:
ext = mime.ExtFromString(el[1])
tplName = strings.TrimRight(tplName, "."+el[1])
}
if !ext.Is(mime.ExtUnrecognized) {
options = append(options, render.WithFormat(ext))
}
return render.NewReference(tplName, append(options, opts...)...), nil
}

78
render/reference.go

@ -0,0 +1,78 @@
package render
import "gitoa.ru/go-4devs/mime"
type Option func(*Reference)
func WithParam(name string, val interface{}) Option {
return func(r *Reference) {
r.Params[name] = val
}
}
func WithEngine(name string) Option {
return func(r *Reference) {
r.Engine = name
}
}
func WithFormat(ext mime.Ext) Option {
return func(r *Reference) {
r.Format = ext
}
}
func NewReference(name string, opts ...Option) Reference {
ref := Reference{
name: name,
}
for _, opt := range opts {
opt(&ref)
}
return ref
}
type Reference struct {
name string
Engine string
Format mime.Ext
Params Params
}
func (r Reference) Name() string {
if !r.Format.Is(mime.ExtUnrecognized) {
return r.name + "." + r.Format.String()
}
return r.name
}
func (r Reference) String() string {
if r.Engine != "" {
return r.name + "." + r.Format.String() + "." + r.Engine
}
return r.name + "." + r.Format.String()
}
func (r Reference) IsEngine(engines ...string) bool {
for _, engine := range engines {
if engine == r.Engine {
return true
}
}
return false
}
func (r Reference) IsFromat(formats ...mime.Ext) bool {
return r.Format.Is(formats...)
}
type Params map[string]interface{}
func (p Params) Get(name string) interface{} {
return p[name]
}

77
render/render.go

@ -0,0 +1,77 @@
package render
import (
"context"
"errors"
"fmt"
"io"
)
var ErrNotFound = errors.New("not found")
func New(parser Parser, engines ...Engine) *Render {
return &Render{
parser: parser,
engines: engines,
}
}
type Render struct {
engines []Engine
parser Parser
}
type Execute func(ctx context.Context, wr io.Writer, data interface{}, params Params) error
type Engine interface {
Support(context.Context, Reference) bool
Load(ctx context.Context, reference Reference) (Execute, error)
}
type Parser func(ctx context.Context, name string, opts ...Option) (Reference, error)
// Add add engine. This function is not thread-safe.
func (e *Render) Add(engine ...Engine) {
e.engines = append(e.engines, engine...)
}
func (e *Render) WithParser(parser Parser) *Render {
return New(parser, e.engines...)
}
func (e *Render) Load(ctx context.Context, reference Reference) (Execute, error) {
for _, engine := range e.engines {
if engine.Support(ctx, reference) {
exec, err := engine.Load(ctx, reference)
if err != nil {
return nil, fmt.Errorf("render load:%w", err)
}
return exec, nil
}
}
return nil, fmt.Errorf("loader:%w", ErrNotFound)
}
func (e *Render) Parse(ctx context.Context, name string, opts ...Option) (Reference, error) {
return e.parser(ctx, name, opts...)
}
func (e *Render) Execute(ctx context.Context, wr io.Writer, name string, data interface{}, opts ...Option) error {
reference, rerr := e.parser(ctx, name, opts...)
if rerr != nil {
return fmt.Errorf("parse: %w", rerr)
}
execute, err := e.Load(ctx, reference)
if err != nil {
return fmt.Errorf("engine load:%w", err)
}
if err := execute(ctx, wr, data, reference.Params); err != nil {
return fmt.Errorf("render execute:%w", err)
}
return nil
}
Loading…
Cancel
Save