From 3ad1c99c416b10ccf2065f1d5f5483d7b3be3600 Mon Sep 17 00:00:00 2001 From: "Gustavo L de Mello (Guz)" Date: Mon, 13 Jan 2025 22:35:07 -0300 Subject: [PATCH] feat(blogo,plugins,prefixedsourcer): new prefixed sourcer, a multisourcer alternative --- blogo/plugin_prefixedsourcer.go | 196 ++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 blogo/plugin_prefixedsourcer.go diff --git a/blogo/plugin_prefixedsourcer.go b/blogo/plugin_prefixedsourcer.go new file mode 100644 index 0000000..da03581 --- /dev/null +++ b/blogo/plugin_prefixedsourcer.go @@ -0,0 +1,196 @@ +// Copyright 2025-present Gustavo "Guz" L. de Mello +// Copyright 2025-present The Lored.dev Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package blogo + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "io" + "io/fs" + "log/slog" + "strings" +) + +const prefixedSourcerPluginName = "blogo-prefixedsourcer-sourcer" + +type PrefixedSourcer interface { + SourcerPlugin + Use(Plugin) + UseNamed(string, Plugin) +} + +type prefixedSourcer struct { + sources map[string]SourcerPlugin + + prefixSeparator string + acceptDuplicated bool + + panicOnInit bool + skipOnHexError bool + skipOnSourceError bool + skipOnFSError bool + + hexFunc HexFunc + + log *slog.Logger +} + +type PrefixedSourcerOpts struct { + PrefixSeparator string + AcceptDuplicated bool + + NotPanicOnInit bool + NotSkipOnHexError bool + NotSkipOnSourceError bool + NotSkipOnFSError bool + + HexFunc HexFunc + + Logger *slog.Logger +} + +type HexFunc = func(n int) (string, error) + +func hexFunc(n int) (string, error) { + bytes := make([]byte, n) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + return hex.EncodeToString(bytes), nil +} + +func NewPrefixedSourcer(opts ...PrefixedSourcerOpts) PrefixedSourcer { + opt := PrefixedSourcerOpts{} + if len(opts) > 0 { + opt = opts[0] + } + + if opt.PrefixSeparator == "" { + opt.PrefixSeparator = "/" + } + + if opt.HexFunc == nil { + opt.HexFunc = hexFunc + } + + if opt.Logger == nil { + opt.Logger = slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) + } + opt.Logger = opt.Logger.WithGroup(prefixedSourcerPluginName) + + return &prefixedSourcer{ + sources: map[string]SourcerPlugin{}, + + prefixSeparator: opt.PrefixSeparator, + acceptDuplicated: opt.AcceptDuplicated, + + panicOnInit: !opt.NotPanicOnInit, + skipOnSourceError: !opt.NotSkipOnSourceError, + skipOnFSError: !opt.NotSkipOnFSError, + + log: opt.Logger, + } +} + +func (p *prefixedSourcer) Name() string { + return prefixedSourcerPluginName +} + +func (p *prefixedSourcer) Use(plugin Plugin) { + p.UseNamed(plugin.Name(), plugin) +} + +func (p *prefixedSourcer) UseNamed(prefix string, plugin Plugin) { + log := p.log.With(slog.String("plugin", plugin.Name()), slog.String("prefix", prefix)) + + var sourcer SourcerPlugin + if plg, ok := plugin.(SourcerPlugin); ok { + sourcer = plg + } else { + m := fmt.Sprintf("failed to add plugin %q (with prefix %q), since it doesn't implement SourcerPlugin", plugin.Name(), prefix) + log.Error(m) + if p.panicOnInit { + panic(fmt.Sprintf("%s: %s", multiRendererPluginName, m)) + } + } + + if _, ok := p.sources[prefix]; ok && !p.acceptDuplicated { + m := fmt.Sprintf( + "duplicated prefix (%q) for plugin %q", + prefix, + plugin.Name(), + ) + log.Error(m) + if p.panicOnInit { + panic(fmt.Sprintf("%s: %s", multiRendererPluginName, m)) + } + return + } + + log.Debug(fmt.Sprintf("Added sourcer plugin, with prefix %q", prefix)) + p.sources[prefix] = sourcer +} + +func (p *prefixedSourcer) Source() (fs.FS, error) { + log := p.log + + fileSystems := make(map[string]fs.FS, len(p.sources)) + + for a, s := range p.sources { + log = log.With(slog.String("plugin", p.Name()), slog.String("prefix", a)) + log.Info("Sourcing file system of plugin") + + f, err := s.Source() + if err != nil && p.skipOnSourceError { + log.Error( + "Failed to source file system of plugin, skipping", + slog.String("error", err.Error()), + ) + } else if err != nil { + log.Error( + "Failed to source file system of plugin, returning error", + slog.String("error", err.Error()), + ) + return f, err + } + + fileSystems[a] = f + } + + return &prefixedSourcerFS{ + fileSystems: fileSystems, + prefixSeparator: p.prefixSeparator, + }, nil +} + +type prefixedSourcerFS struct { + fileSystems map[string]fs.FS + prefixSeparator string +} + +func (pf *prefixedSourcerFS) Open(name string) (fs.File, error) { + prefix, path, found := strings.Cut(name, pf.prefixSeparator) + if !found { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} + } + + if f, ok := pf.fileSystems[prefix]; ok { + return f.Open(path) + } + + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} +}