diff --git a/blogo/blogo.go b/blogo/blogo.go index 82f7b72..1f348e7 100644 --- a/blogo/blogo.go +++ b/blogo/blogo.go @@ -153,7 +153,7 @@ func (b *Blogo) Init() error { b.Use(renderer) } - fs, err := b.sources[0].Source() // TODO: Support for multiple sources (via another plugin or built-in, with prefixes or not) + fs, err := b.source() if err != nil { return errors.Join(errors.New("failed to source files"), err) } @@ -162,6 +162,42 @@ func (b *Blogo) Init() error { return nil } +func (b *Blogo) source() (fs.FS, error) { + log := b.log.With(slog.String("step", "SOURCING")) + + if len(b.sources) == 1 { + log.Debug( + "Just one sources found, using it directly", + slog.String("plugin", b.sources[0].Name()), + ) + return b.sources[0].Source() + } + + log.Debug( + fmt.Sprintf( + "Multiple sources found, initializing built-in %q plugin", + multiSourcerPluginName, + ), + ) + + multi := NewMultiSourcer(MultiSourcerOpts{ + NotPanicOnInit: true, + NotSkipOnFSError: false, + NotSkipOnSourceError: false, + Logger: log, + }) + + for _, s := range b.sources { + log.Debug("Adding plugin to multi-sourcer", slog.String("plugin", s.Name())) + multi.Use(s) + } + + b.sources = make([]SourcerPlugin, 1) + b.sources[0] = multi + + return b.sources[0].Source() +} + func (b *Blogo) render(src fs.File, w io.Writer) error { log := b.log.With(slog.String("step", "RENDERING")) diff --git a/blogo/sourcer.go b/blogo/sourcer.go new file mode 100644 index 0000000..edf1f53 --- /dev/null +++ b/blogo/sourcer.go @@ -0,0 +1,133 @@ +package blogo + +import ( + "errors" + "fmt" + "io" + "io/fs" + "log/slog" +) + +const multiSourcerPluginName = "blogo-multisourcer-sourcer" + +type MultiSourcer interface { + SourcerPlugin + Use(Plugin) +} + +type multiSourcer struct { + sources []SourcerPlugin + + panicOnInit bool + skipOnSourceError bool + skipOnFSError bool + + log *slog.Logger +} + +type MultiSourcerOpts struct { + NotPanicOnInit bool + NotSkipOnSourceError bool + NotSkipOnFSError bool + + Logger *slog.Logger +} + +func NewMultiSourcer(opts ...MultiSourcerOpts) MultiSourcer { + opt := MultiSourcerOpts{} + if len(opts) > 0 { + opt = opts[0] + } + + if opt.Logger == nil { + opt.Logger = slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) + } + opt.Logger = opt.Logger.WithGroup(multiSourcerPluginName) + + return &multiSourcer{ + sources: []SourcerPlugin{}, + + panicOnInit: !opt.NotPanicOnInit, + skipOnSourceError: !opt.NotSkipOnSourceError, + skipOnFSError: !opt.NotSkipOnFSError, + + log: opt.Logger, + } +} + +func (p *multiSourcer) Name() string { + return multiSourcerPluginName +} + +func (p *multiSourcer) Use(plugin Plugin) { + log := p.log.With(slog.String("plugin", plugin.Name())) + + if plg, ok := plugin.(SourcerPlugin); ok { + log.Debug("Added renderer plugin") + p.sources = append(p.sources, plg) + } else { + m := fmt.Sprintf("failed to add plugin %q, since it doesn't implement SourcerPlugin", plugin.Name()) + log.Error(m) + if p.panicOnInit { + panic(fmt.Sprintf("%s: %s", multiRendererPluginName, m)) + } + } +} + +func (p *multiSourcer) Source() (fs.FS, error) { + log := p.log + + fileSystems := []fs.FS{} + + for _, s := range p.sources { + log = log.With(slog.String("plugin", p.Name())) + 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 = append(fileSystems, f) + } + + f := make([]fs.FS, len(fileSystems), len(fileSystems)) + for i := range f { + f[i] = fileSystems[i] + } + + return &multiSourcerFS{ + fileSystems: f, + skipOnError: p.skipOnFSError, + }, nil +} + +type multiSourcerFS struct { + fileSystems []fs.FS + skipOnError bool +} + +func (mf *multiSourcerFS) Open(name string) (fs.File, error) { + for _, f := range mf.fileSystems { + file, err := f.Open(name) + + if err != nil && !errors.Is(err, fs.ErrNotExist) && !mf.skipOnError { + return file, err + } + + if err == nil { + return file, err + } + } + + return nil, fs.ErrNotExist +}