From 621afcc59c7b270805b52bac734fa85582b78e88 Mon Sep 17 00:00:00 2001 From: andrey1s Date: Sun, 2 Oct 2022 23:31:19 +0300 Subject: [PATCH] init template --- .drone.yml | 24 +++++++ .golangci.yml | 42 ++++++++++++ engine/encode.go | 38 +++++++++++ engine/encode_test.go | 27 ++++++++ engine/engine.go | 125 +++++++++++++++++++++++++++++++++++ engine/suport.go | 10 +++ engine/template.go | 80 ++++++++++++++++++++++ execute.go | 38 +++++++++++ execute_example_test.go | 75 +++++++++++++++++++++ go.mod | 18 +++++ go.sum | 27 ++++++++ gohtml/engine/engine.go | 25 +++++++ gohtml/engine/engine_test.go | 23 +++++++ gohtml/html.go | 43 ++++++++++++ gotxt/engine/engine.go | 25 +++++++ gotxt/text.go | 43 ++++++++++++ json/engine/engine.go | 13 ++++ json/json.go | 12 ++++ loader/chain/loader.go | 29 ++++++++ loader/empty.go | 20 ++++++ loader/loader.go | 34 ++++++++++ parser/name.go | 43 ++++++++++++ render/reference.go | 78 ++++++++++++++++++++++ render/render.go | 77 +++++++++++++++++++++ 24 files changed, 969 insertions(+) create mode 100644 .drone.yml create mode 100644 .golangci.yml create mode 100644 engine/encode.go create mode 100644 engine/encode_test.go create mode 100644 engine/engine.go create mode 100644 engine/suport.go create mode 100644 engine/template.go create mode 100644 execute.go create mode 100644 execute_example_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 gohtml/engine/engine.go create mode 100644 gohtml/engine/engine_test.go create mode 100644 gohtml/html.go create mode 100644 gotxt/engine/engine.go create mode 100644 gotxt/text.go create mode 100644 json/engine/engine.go create mode 100644 json/json.go create mode 100644 loader/chain/loader.go create mode 100644 loader/empty.go create mode 100644 loader/loader.go create mode 100644 parser/name.go create mode 100644 render/reference.go create mode 100644 render/render.go diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..97b9960 --- /dev/null +++ b/.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: {} + diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..4c318db --- /dev/null +++ b/.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 \ No newline at end of file diff --git a/engine/encode.go b/engine/encode.go new file mode 100644 index 0000000..669b5bb --- /dev/null +++ b/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 +} diff --git a/engine/encode_test.go b/engine/encode_test.go new file mode 100644 index 0000000..c12e2d7 --- /dev/null +++ b/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()) +} diff --git a/engine/engine.go b/engine/engine.go new file mode 100644 index 0000000..99580bb --- /dev/null +++ b/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 +} diff --git a/engine/suport.go b/engine/suport.go new file mode 100644 index 0000000..2aad50d --- /dev/null +++ b/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...)) +} diff --git a/engine/template.go b/engine/template.go new file mode 100644 index 0000000..ef9c57c --- /dev/null +++ b/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) +} diff --git a/execute.go b/execute.go new file mode 100644 index 0000000..683a979 --- /dev/null +++ b/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 +} diff --git a/execute_example_test.go b/execute_example_test.go new file mode 100644 index 0000000..cb4c780 --- /dev/null +++ b/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: "world"} + + gohtml.Must(html.New("example_with_option.html").Parse(`Hello {{ .Name }}!`)) + + err := templating.Execute(ctx, os.Stdout, "example_with_option.html", data, render.WithEngine(gohtml.Name)) + if err != nil { + log.Fatalln(err) + } + // Output: + // Hello <b>world</b>! +} + +func ExampleExecute_text() { + ctx := context.Background() + data := Data{Name: "text"} + + 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 text! +} + +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! +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..94f76f4 --- /dev/null +++ b/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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9280ccf --- /dev/null +++ b/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= diff --git a/gohtml/engine/engine.go b/gohtml/engine/engine.go new file mode 100644 index 0000000..2675524 --- /dev/null +++ b/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))...) +} diff --git a/gohtml/engine/engine_test.go b/gohtml/engine/engine_test.go new file mode 100644 index 0000000..7514259 --- /dev/null +++ b/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()) +} diff --git a/gohtml/html.go b/gohtml/html.go new file mode 100644 index 0000000..22898bf --- /dev/null +++ b/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 +} diff --git a/gotxt/engine/engine.go b/gotxt/engine/engine.go new file mode 100644 index 0000000..811f1aa --- /dev/null +++ b/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))...) +} diff --git a/gotxt/text.go b/gotxt/text.go new file mode 100644 index 0000000..a7825bb --- /dev/null +++ b/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 +} diff --git a/json/engine/engine.go b/json/engine/engine.go new file mode 100644 index 0000000..c565533 --- /dev/null +++ b/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) +} diff --git a/json/json.go b/json/json.go new file mode 100644 index 0000000..9c91baf --- /dev/null +++ b/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()) +} diff --git a/loader/chain/loader.go b/loader/chain/loader.go new file mode 100644 index 0000000..6eb8a37 --- /dev/null +++ b/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) +} diff --git a/loader/empty.go b/loader/empty.go new file mode 100644 index 0000000..9a7d58e --- /dev/null +++ b/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 +} diff --git a/loader/loader.go b/loader/loader.go new file mode 100644 index 0000000..f72f1e5 --- /dev/null +++ b/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 +} diff --git a/parser/name.go b/parser/name.go new file mode 100644 index 0000000..0bb3736 --- /dev/null +++ b/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 .. or .. +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 +} diff --git a/render/reference.go b/render/reference.go new file mode 100644 index 0000000..aa1e873 --- /dev/null +++ b/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] +} diff --git a/render/render.go b/render/render.go new file mode 100644 index 0000000..9bdac6e --- /dev/null +++ b/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 +}