From 82e86e269aa0f4aa920b922bdfd030edb170d6ff Mon Sep 17 00:00:00 2001 From: "Gustavo L de Mello (Guz)" Date: Mon, 13 Jan 2025 09:31:50 -0300 Subject: [PATCH] feat(blogo,renderer): built-in multirenderer support via plugin --- blogo/blogo.go | 68 ++++++++++++--------- blogo/plugins.go | 3 - blogo/renderer.go | 153 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+), 33 deletions(-) create mode 100644 blogo/renderer.go diff --git a/blogo/blogo.go b/blogo/blogo.go index ebea7b6..9c04f62 100644 --- a/blogo/blogo.go +++ b/blogo/blogo.go @@ -142,11 +142,18 @@ func (b *Blogo) Init() error { b.Use(&defaultSourcer{}) } - renderer := &defaultRenderer{} - log.Debug(fmt.Sprintf("Adding %q as fallback renderer", renderer.Name())) - b.Use(&defaultRenderer{}) + if len(b.renderers) == 0 { + renderer := NewPlainTextRenderer() + log.Debug( + fmt.Sprintf( + "No RendererPlugin plugin found, adding %q as fallback renderer", + renderer.Name(), + ), + ) + b.Use(renderer) + } - fs, err := b.sources[0].Source() // TOOD: Support for multiple sources (via another plugin or built-in, with prefixes or not) + fs, err := b.sources[0].Source() // TODO: Support for multiple sources (via another plugin or built-in, with prefixes or not) if err != nil { return errors.Join(errors.New("failed to source files"), err) } @@ -156,33 +163,34 @@ func (b *Blogo) Init() error { } func (b *Blogo) render(src fs.File, w io.Writer) error { - for _, r := range b.renderers { - log := b.log.With(slog.String("step", "RENDERING"), slog.String("plugin", r.Name())) + log := b.log.With(slog.String("step", "RENDERING")) - log.Debug("Using renderer") - - // FIX?: io.Reader can only be read once, but the plugin may need to read - // from it to know if it can even render at all, which can break the next - // plugin render method. Maybe io.ReadSeeker or io.TeeReader could solve this? - // but it would change the API away from the fs.FS API. Also, a combination of - // io.TeeReader and io.MultiReader (example: https://abdus.dev/posts/sniffing-io-reader-in-golang/#solution-io.teereader-and-io.multireader) - // could solve without changing the API, but it would use more memory for each file. - // We could also just put multi-renderer and multi-sourcer support in optional plugins. - err := r.Render(src, w) - if errors.Is(err, ErrRendererNotSupportedFile) { - log.Debug("File not supported, skipping") - - continue - } else if err != nil { - log.Error("Renderer failed") - - return errors.Join(fmt.Errorf("failed to render with plugin %q", r.Name()), err) - } else { - log.Debug("Successfully rendered file!") - - break - } + if len(b.renderers) == 1 { + log.Debug( + "Just one renderer found, using it directly", + slog.String("plugin", b.renderers[0].Name()), + ) + return b.renderers[0].Render(src, w) } - return nil + log.Debug("Multiple renderers found, initializing built-in multi-renderer plugin") + + f, t := false, true + multi := NewMultiRenderer(MultiRendererOpts{ + SkipOnError: &f, + PanicOnInit: &t, + Logger: log, + }) + + for _, r := range b.renderers { + log.Debug("Adding plugin to multi-renderer", slog.String("plugin", r.Name())) + multi.Use(r) + } + + log.Debug("Overriding renderers slice") + + b.renderers = make([]RendererPlugin, 1) + b.renderers[0] = multi + + return b.render(src, w) } diff --git a/blogo/plugins.go b/blogo/plugins.go index 9f01801..63d71ed 100644 --- a/blogo/plugins.go +++ b/blogo/plugins.go @@ -16,7 +16,6 @@ package blogo import ( - "errors" "io" "io/fs" ) @@ -25,8 +24,6 @@ type Plugin interface { Name() string } -var ErrRendererNotSupportedFile = errors.New("this file is not supported by renderer") - type RendererPlugin interface { Plugin Render(src fs.File, out io.Writer) error diff --git a/blogo/renderer.go b/blogo/renderer.go new file mode 100644 index 0000000..14026ac --- /dev/null +++ b/blogo/renderer.go @@ -0,0 +1,153 @@ +package blogo + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/fs" + "log/slog" +) + +var ErrRendererNotSupportedFile = errors.New("this file is not supported by renderer") + +const multiRendererPluginName = "blogo-multirenderer" + +type MultiRenderer interface { + RendererPlugin + Use(Plugin) +} + +type multiRenderer struct { + renderers []RendererPlugin + + skipOnError bool + panicOnInit bool + + log *slog.Logger +} + +type MultiRendererOpts struct { + SkipOnError *bool + PanicOnInit *bool + Logger *slog.Logger +} + +func NewMultiRenderer(opts ...MultiRendererOpts) MultiRenderer { + opt := MultiRendererOpts{} + if len(opts) > 0 { + opt = opts[0] + } + + if opt.SkipOnError == nil { + d := true + opt.SkipOnError = &d + } + + if opt.PanicOnInit == nil { + d := true + opt.PanicOnInit = &d + } + + if opt.Logger == nil { + opt.Logger = slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) + } + opt.Logger = opt.Logger.WithGroup(multiRendererPluginName) + + return &multiRenderer{ + renderers: []RendererPlugin{}, + + skipOnError: *opt.SkipOnError, + panicOnInit: *opt.PanicOnInit, + + log: opt.Logger, + } +} + +func (p *multiRenderer) Name() string { + return multiRendererPluginName +} + +func (p *multiRenderer) Use(plugin Plugin) { + log := p.log.With(slog.String("plugin", plugin.Name())) + + if plg, ok := plugin.(RendererPlugin); ok { + log.Debug("Added renderer plugin") + p.renderers = append(p.renderers, plg) + } else { + m := fmt.Sprintf("failed to add plugin %q, since it doesn't implement RendererPlugin", plugin.Name()) + log.Error(m) + if p.panicOnInit { + panic(fmt.Sprintf("%s: %s", multiRendererPluginName, m)) + } + } +} + +func (p *multiRenderer) Render(f fs.File, w io.Writer) error { + mf := newMultiRendererFile(f) + for _, r := range p.renderers { + log := p.log.With(slog.String("plugin", r.Name())) + + log.Debug("Trying to render with plugin") + err := r.Render(f, w) + + if err == nil { + break + } + + if !p.skipOnError && !errors.Is(err, ErrRendererNotSupportedFile) { + log.Error("Failed to render using plugin", slog.String("error", err.Error())) + return errors.Join(fmt.Errorf("failed to render using plugin %q", p.Name()), err) + } + + log.Debug("Unable to render using plugin", slog.String("error", err.Error())) + log.Debug("Resetting file for next read") + + if err := mf.Reset(); err != nil { + log.Error("Failed to reset file read offset", slog.String("error", err.Error())) + return errors.Join(fmt.Errorf("failed to reset file read offset"), err) + } + } + + return nil +} + +type multiRendererFile struct { + fs.File + buf *bytes.Buffer + reader io.Reader +} + +func newMultiRendererFile(f fs.File) *multiRendererFile { + if _, ok := f.(io.Seeker); ok { + return &multiRendererFile{ + File: f, + reader: f, + } + } + + var buf bytes.Buffer + return &multiRendererFile{ + File: f, + reader: io.TeeReader(f, &buf), + buf: &buf, + } +} + +func (f *multiRendererFile) Read(p []byte) (int, error) { + return f.reader.Read(p) +} + +func (f *multiRendererFile) Reset() error { + if s, ok := f.File.(io.Seeker); ok { + _, err := s.Seek(0, io.SeekStart) + return err + } + var buf bytes.Buffer + r := io.MultiReader(f.buf, f.File) + + f.reader = io.TeeReader(r, &buf) + f.buf = &buf + + return nil +}