chore: initial commit, move from loreddev/x
Moved the source code to a new repository outside from loreddev/x. To
see the commit history before this commit, see
1f823aa099
This commit is contained in:
423
blogo.go
Normal file
423
blogo.go
Normal file
@@ -0,0 +1,423 @@
|
||||
// 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 (
|
||||
"html/template"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"forge.capytal.company/loreddev/blogo/core"
|
||||
"forge.capytal.company/loreddev/blogo/plugin"
|
||||
"forge.capytal.company/loreddev/blogo/plugins"
|
||||
"forge.capytal.company/loreddev/x/tinyssert"
|
||||
)
|
||||
|
||||
var defaultNotFoundTemplate = template.Must(
|
||||
template.New("not-found").Parse("404: Blog post {{.Path}} not found"),
|
||||
)
|
||||
|
||||
var defaultInternalErrTemplate = template.Must(
|
||||
template.New("internal-err").
|
||||
Parse("500: Failed to get blog post {{.Path}} due to error {{.ErrorMsg}}\n{{.Error}}"),
|
||||
)
|
||||
|
||||
// The main function of the package. Creates a new [Blogo] implementation.
|
||||
//
|
||||
// This implementation automatically adds fallbacks and uses built-in [plugins] to handle
|
||||
// multiple sources, renderers and error handlers.
|
||||
//
|
||||
// Use [Opts] to more fine grained control of what plugins are used on initialization. To have
|
||||
// complete control over how plugins are managed, use the [core] package and [plugins]
|
||||
// for more information and building blocks to create a custom [Blogo] implementation.
|
||||
func New(opts ...Opts) Blogo {
|
||||
opt := Opts{}
|
||||
if len(opts) > 0 {
|
||||
opt = opts[0]
|
||||
}
|
||||
|
||||
if opt.Assertions == nil {
|
||||
opt.Assertions = tinyssert.NewDisabledAssertions()
|
||||
}
|
||||
if opt.Logger == nil {
|
||||
opt.Logger = slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
|
||||
}
|
||||
|
||||
if opt.FallbackRenderer == nil {
|
||||
opt.FallbackRenderer = plugins.NewPlainText(plugins.PlainTextOpts{
|
||||
Assertions: opt.Assertions,
|
||||
})
|
||||
}
|
||||
if opt.MultiRenderer == nil {
|
||||
opt.MultiRenderer = plugins.NewBufferedMultiRenderer(plugins.BufferedMultiRendererOpts{
|
||||
Assertions: opt.Assertions,
|
||||
Logger: opt.Logger.WithGroup("multi-renderer"),
|
||||
})
|
||||
}
|
||||
|
||||
if opt.FallbackSourcer == nil {
|
||||
opt.FallbackSourcer = plugins.NewEmptySourcer()
|
||||
}
|
||||
if opt.MultiSourcer == nil {
|
||||
opt.MultiSourcer = plugins.NewMultiSourcer(plugins.MultiSourcerOpts{
|
||||
SkipOnSourceError: true,
|
||||
SkipOnFSError: true,
|
||||
|
||||
Assertions: opt.Assertions,
|
||||
Logger: opt.Logger.WithGroup("multi-sourcer"),
|
||||
})
|
||||
}
|
||||
|
||||
if opt.FallbackErrorHandler == nil {
|
||||
logger := opt.Logger.WithGroup("errors")
|
||||
|
||||
f := plugins.NewMultiErrorHandler(plugins.MultiErrorHandlerOpts{
|
||||
Assertions: opt.Assertions,
|
||||
Logger: logger,
|
||||
})
|
||||
|
||||
f.Use(plugins.NewNotFoundErrorHandler(
|
||||
*defaultNotFoundTemplate,
|
||||
plugins.TemplateErrorHandlerOpts{
|
||||
Assertions: opt.Assertions,
|
||||
Logger: logger.WithGroup("not-found"),
|
||||
},
|
||||
))
|
||||
|
||||
f.Use(plugins.NewTemplateErrorHandler(
|
||||
*defaultInternalErrTemplate,
|
||||
plugins.TemplateErrorHandlerOpts{
|
||||
Assertions: opt.Assertions,
|
||||
Logger: logger.WithGroup("internal-err"),
|
||||
},
|
||||
))
|
||||
|
||||
f.Use(plugins.NewLoggerErrorHandler(logger.WithGroup("logger"), slog.LevelError))
|
||||
|
||||
opt.FallbackErrorHandler = f
|
||||
}
|
||||
if opt.MultiErrorHandler == nil {
|
||||
opt.MultiErrorHandler = plugins.NewMultiErrorHandler(plugins.MultiErrorHandlerOpts{
|
||||
Assertions: opt.Assertions,
|
||||
Logger: opt.Logger.WithGroup("errors"),
|
||||
})
|
||||
}
|
||||
|
||||
return &blogo{
|
||||
plugins: []plugin.Plugin{},
|
||||
|
||||
fallbackRenderer: opt.FallbackRenderer,
|
||||
multiRenderer: opt.MultiRenderer,
|
||||
fallbackSourcer: opt.FallbackSourcer,
|
||||
multiSourcer: opt.MultiSourcer,
|
||||
fallbackErrorHandler: opt.FallbackErrorHandler,
|
||||
multiErrorHandler: opt.MultiErrorHandler,
|
||||
|
||||
assert: opt.Assertions,
|
||||
log: opt.Logger,
|
||||
}
|
||||
}
|
||||
|
||||
// The simplest interface of the [blogo] package's blogging engine.
|
||||
//
|
||||
// Users should use the [New] function to easily have a implementation
|
||||
// that handles plugins and initialization out-of-the-box, or implement
|
||||
// their own to have more fine grained control over the package.
|
||||
type Blogo interface {
|
||||
// Adds a new plugin to the engine.
|
||||
//
|
||||
// Implementations may accept any type of plugin interface. The default
|
||||
// implementation accepts [plugin.Sourcer], [plugin.Renderer], [plugin.ErrorHandler],
|
||||
// and [plugin.Group], ignoring any other plugins or nil values silently.
|
||||
Use(plugin.Plugin)
|
||||
// Initialize the plugins or internal state if necessary.
|
||||
//
|
||||
// Implementations may call it on the fist request and/or panic on failed initialization.
|
||||
// The default implementation calls Init on the first call to ServeHTTP
|
||||
Init()
|
||||
// The main entry point to access all blog posts.
|
||||
//
|
||||
// Implementations may not expect to the ServeHTTP's request to have a path different from
|
||||
// "/". Users should use http.StripPrefix if they are using it on a defined path, for example:
|
||||
//
|
||||
// http.Handle("/blog", http.StripPrefix("/blog/", blogo))
|
||||
//
|
||||
// Implementations of this interface may add other method to access the blog posts
|
||||
// besides just http requests. Plugins that register API endpoints will handle them
|
||||
// inside this handler, in other words, endpoints paths will be appended to any path
|
||||
// that this Handler is being used in.
|
||||
http.Handler
|
||||
}
|
||||
|
||||
// Options used by [New] to better fine grain the default plugins used by the
|
||||
// default [Blogo] implementation.
|
||||
type Opts struct {
|
||||
// The plugin that will be used if no [plugin.Renderer] is provided.
|
||||
// Defaults to [plugins.NewPlainText].
|
||||
FallbackRenderer plugin.Renderer
|
||||
|
||||
// What plugin will be used to combine multiple renderers if necessary.
|
||||
MultiRenderer interface {
|
||||
plugin.Renderer
|
||||
plugin.WithPlugins
|
||||
}
|
||||
|
||||
// The plugin that will be used if no [plugin.Sourcer] is provided.
|
||||
// Defaults to [plugins.NewEmptySourcer].
|
||||
FallbackSourcer plugin.Sourcer
|
||||
|
||||
// What plugin will be used to combine multiple sourcers if necessary.
|
||||
MultiSourcer interface {
|
||||
plugin.Sourcer
|
||||
plugin.WithPlugins
|
||||
}
|
||||
|
||||
// The plugin that will be used if no [plugin.ErrorHandler] is provided.
|
||||
// Defaults to a MultiErrorHandler with [plugins.NewNotFoundErrorHandler],
|
||||
// [plugins.NewTemplateErrorHandler] and [plugins.NewLoggerErrorHandler].
|
||||
FallbackErrorHandler plugin.ErrorHandler
|
||||
|
||||
// What plugin will be used to combine multiple error handlers.
|
||||
MultiErrorHandler interface {
|
||||
plugin.ErrorHandler
|
||||
plugin.WithPlugins
|
||||
}
|
||||
|
||||
// [tinyssert.Assertions] implementation used Assertions, by default
|
||||
// uses [tinyssert.NewDisabledAssertions] to effectively disable assertions.
|
||||
// Use this if to fail-fast on incorrect states. This is also passed to the
|
||||
// default built-in plugins on initialization.
|
||||
Assertions tinyssert.Assertions
|
||||
|
||||
// Logger to be used to send error, warns and debug messages, useful for plugin
|
||||
// development and debugging the pipeline of files. By default it uses a logger
|
||||
// that writes to [io.Discard], effectively disabling logging. This is passed
|
||||
// to the default built-in plugins on initialization.
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
type blogo struct {
|
||||
plugins []plugin.Plugin
|
||||
|
||||
fallbackRenderer plugin.Renderer
|
||||
multiRenderer interface {
|
||||
plugin.Renderer
|
||||
plugin.WithPlugins
|
||||
}
|
||||
fallbackSourcer plugin.Sourcer
|
||||
multiSourcer interface {
|
||||
plugin.Sourcer
|
||||
plugin.WithPlugins
|
||||
}
|
||||
fallbackErrorHandler plugin.ErrorHandler
|
||||
multiErrorHandler interface {
|
||||
plugin.ErrorHandler
|
||||
plugin.WithPlugins
|
||||
}
|
||||
|
||||
server http.Handler
|
||||
|
||||
assert tinyssert.Assertions
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func (b *blogo) Use(p plugin.Plugin) {
|
||||
b.assert.NotNil(p, "Plugin definition should not be nil")
|
||||
b.assert.NotNil(b.plugins, "Plugins needs to be not-nil")
|
||||
b.assert.NotNil(b.log)
|
||||
|
||||
log := b.log.With(slog.String("plugin", p.Name()))
|
||||
|
||||
if p, ok := p.(plugin.Group); ok {
|
||||
log.Debug("Plugin group found, adding it's plugins")
|
||||
for _, p := range p.Plugins() {
|
||||
b.Use(p)
|
||||
}
|
||||
}
|
||||
|
||||
if p != nil {
|
||||
b.plugins = append(b.plugins, p)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *blogo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
b.assert.NotNil(b.log)
|
||||
b.assert.NotNil(w)
|
||||
b.assert.NotNil(r)
|
||||
|
||||
if b.server != nil {
|
||||
b.server.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
log := b.log.With()
|
||||
log.Debug("Core server not initialized")
|
||||
|
||||
b.Init()
|
||||
|
||||
b.server.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (b *blogo) Init() {
|
||||
b.assert.NotNil(b.plugins, "Plugins needs to be not-nil")
|
||||
b.assert.NotNil(b.log)
|
||||
|
||||
log := b.log.With()
|
||||
log.Debug("Initializing Blogo plugins")
|
||||
|
||||
sourcer := b.initSourcer()
|
||||
renderer := b.initRenderer()
|
||||
errorHandler := b.initErrorHandler()
|
||||
|
||||
log.Debug("Constructing Blogo server")
|
||||
|
||||
b.server = core.NewServer(sourcer, renderer, errorHandler, core.ServerOpts{
|
||||
Assertions: b.assert,
|
||||
Logger: b.log.WithGroup("server"),
|
||||
})
|
||||
|
||||
log.Debug("Server constructed")
|
||||
}
|
||||
|
||||
func (b *blogo) initRenderer() plugin.Renderer {
|
||||
b.assert.NotNil(b.plugins, "Plugins needs to be not-nil")
|
||||
b.assert.NotNil(b.fallbackRenderer, "FallbackRenderer needs to be not-nil")
|
||||
b.assert.NotNil(b.multiRenderer, "MultiRenderer needs to be not-nil")
|
||||
b.assert.NotNil(b.log)
|
||||
|
||||
log := b.log.With()
|
||||
log.Debug("Initializing Blogo Renderer plugins")
|
||||
|
||||
renderers := []plugin.Renderer{}
|
||||
|
||||
for _, p := range b.plugins {
|
||||
if r, ok := p.(plugin.Renderer); ok {
|
||||
log.Debug("Adding Renderer", slog.String("sourcer", r.Name()))
|
||||
|
||||
renderers = append(renderers, r)
|
||||
}
|
||||
}
|
||||
|
||||
if len(renderers) == 0 {
|
||||
log.Debug("No Renderer avaiable, using %q as fallback",
|
||||
slog.String("renderer", b.fallbackRenderer.Name()))
|
||||
|
||||
return b.fallbackRenderer
|
||||
}
|
||||
|
||||
if len(renderers) == 1 {
|
||||
log.Debug("Just one Renderer found, using it directly",
|
||||
slog.String("renderer", renderers[0].Name()))
|
||||
|
||||
return renderers[0]
|
||||
}
|
||||
|
||||
log.Debug("Multiple Renderers found, using MultiRenderer to combine them",
|
||||
slog.String("renderer", b.multiRenderer.Name()),
|
||||
)
|
||||
for _, r := range renderers {
|
||||
b.multiRenderer.Use(r)
|
||||
}
|
||||
|
||||
return b.multiRenderer
|
||||
}
|
||||
|
||||
func (b *blogo) initSourcer() plugin.Sourcer {
|
||||
b.assert.NotNil(b.plugins, "Plugins needs to be not-nil")
|
||||
b.assert.NotNil(b.fallbackSourcer, "FallbackSourcer needs to be not-nil")
|
||||
b.assert.NotNil(b.multiSourcer, "MultiSourcer needs to be not-nil")
|
||||
b.assert.NotNil(b.log)
|
||||
|
||||
log := b.log.With()
|
||||
log.Debug("Initializing Blogo Sourcer plugins")
|
||||
|
||||
sourcers := []plugin.Sourcer{}
|
||||
|
||||
for _, p := range b.plugins {
|
||||
if s, ok := p.(plugin.Sourcer); ok {
|
||||
log.Debug("Adding Sourcer", slog.String("sourcer", s.Name()))
|
||||
|
||||
sourcers = append(sourcers, s)
|
||||
}
|
||||
}
|
||||
|
||||
if len(sourcers) == 0 {
|
||||
log.Debug("No Sourcer avaiable, using %q as fallback",
|
||||
slog.String("sourcer", b.fallbackSourcer.Name()))
|
||||
|
||||
return b.fallbackSourcer
|
||||
}
|
||||
|
||||
if len(sourcers) == 1 {
|
||||
log.Debug("Just one Sourcer found, using it directly",
|
||||
slog.String("sourcer", sourcers[0].Name()))
|
||||
|
||||
return sourcers[0]
|
||||
}
|
||||
|
||||
log.Debug("Multiple Sourcers found, using MultiSourcer to combine them",
|
||||
slog.String("sourcer", b.multiSourcer.Name()),
|
||||
)
|
||||
for _, s := range sourcers {
|
||||
b.multiSourcer.Use(s)
|
||||
}
|
||||
|
||||
return b.multiSourcer
|
||||
}
|
||||
|
||||
func (b *blogo) initErrorHandler() plugin.ErrorHandler {
|
||||
b.assert.NotNil(b.plugins, "Plugins needs to be not-nil")
|
||||
b.assert.NotNil(b.fallbackErrorHandler, "FallbackErrorHandler needs to be not-nil")
|
||||
b.assert.NotNil(b.multiErrorHandler, "MultiErrorHandler needs to be not-nil")
|
||||
b.assert.NotNil(b.log)
|
||||
|
||||
log := b.log.With()
|
||||
log.Debug("Initializing Blogo ErrorHandler plugins")
|
||||
|
||||
errorHandlers := []plugin.ErrorHandler{}
|
||||
|
||||
for _, p := range b.plugins {
|
||||
if s, ok := p.(plugin.ErrorHandler); ok {
|
||||
log.Debug("Adding ErrorHandler", slog.String("errorHandler", s.Name()))
|
||||
|
||||
errorHandlers = append(errorHandlers, s)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errorHandlers) == 0 {
|
||||
log.Debug("No ErrorHandler avaiable, using %q as fallback",
|
||||
slog.String("errorHandler", b.fallbackErrorHandler.Name()))
|
||||
|
||||
return b.fallbackErrorHandler
|
||||
}
|
||||
|
||||
if len(errorHandlers) == 1 {
|
||||
log.Debug("Just one ErrorHandler found, using it directly",
|
||||
slog.String("errorHandler", errorHandlers[0].Name()))
|
||||
|
||||
return errorHandlers[0]
|
||||
}
|
||||
|
||||
log.Debug("Multiple ErrorHandlers found, using MultiSourcer to combine them",
|
||||
slog.String("errorHandler", b.multiErrorHandler.Name()),
|
||||
)
|
||||
for _, s := range errorHandlers {
|
||||
b.multiErrorHandler.Use(s)
|
||||
}
|
||||
|
||||
return b.multiErrorHandler
|
||||
}
|
||||
Reference in New Issue
Block a user