diff --git a/blogo/LICENSE b/blogo/LICENSE deleted file mode 100644 index 57bc88a..0000000 --- a/blogo/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. - diff --git a/blogo/README.md b/blogo/README.md deleted file mode 100644 index 4272aa4..0000000 --- a/blogo/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Blo.go - -> This package is still in heavy development and it is being dog fed -> in [capytal/www](https://forge.capytal.company/capytal/www). - -## License - -© 2025-present Gustavo "Guz" L de Mello -© 2025-present The Lored.dev Contributors - -Licensed under the Apache License, Version 2.0 (the "License"), unless otherwise -explicitly noted. You may obtain a copy of the License at [LICENSE](./LICENSE) or -[http://www.apache.org/licenses/LICENSE-2.0](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. diff --git a/blogo/blogo.go b/blogo/blogo.go deleted file mode 100644 index e50aada..0000000 --- a/blogo/blogo.go +++ /dev/null @@ -1,423 +0,0 @@ -// 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/x/blogo/core" - "forge.capytal.company/loreddev/x/blogo/plugin" - "forge.capytal.company/loreddev/x/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 -} diff --git a/blogo/core/core.go b/blogo/core/core.go deleted file mode 100644 index c090409..0000000 --- a/blogo/core/core.go +++ /dev/null @@ -1,332 +0,0 @@ -// 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 core - -import ( - "fmt" - "io" - "io/fs" - "log/slog" - "net/http" - "strings" - - "forge.capytal.company/loreddev/x/blogo/plugin" - "forge.capytal.company/loreddev/x/tinyssert" -) - -// Creates a implementation of [http.Handler] that maps the [(*http.Request).Path] to a file of the -// same name in the file system provided by the sourcer. Use [Opts] to have more fine grained control -// over some additional behaviour of the implementation. -func NewServer( - sourcer plugin.Sourcer, - renderer plugin.Renderer, - onerror plugin.ErrorHandler, - opts ...ServerOpts, -) http.Handler { - opt := ServerOpts{} - 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{})) - } - - var filesystem fs.FS - if opt.SourceOnInit { - fs, err := sourcer.Source() - if err != nil { - panic(fmt.Sprintf("Failed to source files on initialization due to error: %s", - err.Error(), - )) - } - filesystem = fs - } - - return &server{ - files: filesystem, - - sourcer: sourcer, - renderer: renderer, - onerror: onerror, - - assert: opt.Assertions, - log: opt.Logger, - } -} - -// Options used in the construction of the server/[http.Handler] in [NewServer] to better -// control additional behaviour of the implementation. -type ServerOpts struct { - // Call [(plugin.Sourcer).Source] on construction of the implementation on [NewServer]? - // Panics if the it returns a error. By default sourcing of files is done on the first - // request. - SourceOnInit bool - // [tinyssert.Assertions] implementation used by server for it's Assertions, by default - // uses [tinyssert.NewDisabledAssertions] to effectively disable assertions. Use this - // if you want to the server to fail-fast on incorrect states. - 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. - Logger *slog.Logger -} - -type server struct { - files fs.FS - - sourcer plugin.Sourcer - renderer plugin.Renderer - onerror plugin.ErrorHandler - - assert tinyssert.Assertions - log *slog.Logger -} - -func (srv *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - srv.assert.NotNil(srv.log) - srv.assert.NotNil(w) - srv.assert.NotNil(r) - - log := srv.log.With(slog.String("path", r.URL.Path)) - log.Debug("Serving endpoint") - - if srv.files == nil { - err := srv.serveHTTPSource(w, r) - if err != nil { - return - } - } - - path := strings.Trim(r.URL.Path, "/") - if path == "" || path == "/" { - path = "." - } - - file, err := srv.serveHTTPOpenFile(path, w, r) - if err != nil { - return - } - - // Defers the closing of the file to prevent memory being held if a renderer - // does not properly closes the file. - defer file.Close() - - err = srv.serveHTTPRender(file, w, r) - if err != nil { - return - } - - log.Debug("Finished serving endpoint") -} - -func (srv *server) serveHTTPSource(w http.ResponseWriter, r *http.Request) error { - srv.assert.NotNil(srv.sourcer, "A sourcer needs to be available") - srv.assert.NotNil(srv.onerror, "An error handler needs to be available in cases of errors") - srv.assert.NotNil(srv.log) - srv.assert.NotNil(w) - srv.assert.NotNil(r) - - log := srv.log.With(slog.String("path", r.URL.Path), slog.String("sourcer", srv.sourcer.Name())) - log.Debug("Initializing file system") - - fs, err := srv.sourcer.Source() - if err != nil { - log := log.With( - slog.String("err", err.Error()), - slog.String("errorhandler", srv.onerror.Name()), - ) - - log.Error( - "Failed to get file system, handling error to ErrorHandler", - ) - - recovr, ok := srv.onerror.Handle(&ServeError{ - Res: w, - Req: r, - Err: &SourceError{ - Sourcer: srv.sourcer, - Err: err, - }, - }) - - if !ok { - log.Error("Failed to handle error with plugin") - - w.WriteHeader(http.StatusInternalServerError) - _, err = w.Write([]byte(fmt.Sprintf( - "Failed to handle error %q with plugin %q", - err.Error(), - srv.onerror.Name(), - ))) - srv.assert.Nil(err) - - return err - } - - r, ok := recovr.(plugin.Sourcer) - - if !ok { - return err - } - - fs, err = r.Source() - srv.assert.Nil(err) - } - - srv.files = fs - - return nil -} - -func (srv *server) serveHTTPOpenFile( - name string, - w http.ResponseWriter, - r *http.Request, -) (fs.File, error) { - srv.assert.NotZero(name, "Name of file should not be empty") - srv.assert.NotNil(srv.files, "A file system needs to be present to open a file") - srv.assert.NotNil(srv.onerror, "An error handler needs to be available in cases of errors") - srv.assert.NotNil(srv.log) - srv.assert.NotNil(w) - srv.assert.NotNil(r) - - log := srv.log.With( - slog.String("path", r.URL.Path), - slog.String("filename", name), - slog.String("sourcer", srv.sourcer.Name()), - ) - log.Debug("Opening file") - - f, err := srv.files.Open(name) - - if err != nil || f == nil { - if err == nil && f == nil { - err = fmt.Errorf( - "file system returned a nil file using sourcer %q", - srv.sourcer.Name(), - ) - } - - log := log.With( - slog.String("err", err.Error()), - slog.String("errorhandler", srv.onerror.Name()), - ) - - log.Warn( - "Failed to open file, handling error to ErrorHandler", - ) - - recovr, ok := srv.onerror.Handle(ServeError{ - Res: w, - Req: r, - Err: SourceError{ - Sourcer: srv.sourcer, - Err: err, - }, - }) - - if !ok { - log.Error("Failed to handle error with plugin") - w.WriteHeader(http.StatusInternalServerError) - _, err = w.Write([]byte(fmt.Sprintf( - "Failed to handle error %q with plugin %q", - err.Error(), - srv.onerror.Name(), - ))) - srv.assert.Nil(err) - - return nil, err - } - - r, ok := recovr.(fs.FS) - - if !ok { - return nil, err - } - - f, err = r.Open(name) - srv.assert.Nil(err) - - } - - return f, err -} - -func (srv *server) serveHTTPRender(file fs.File, w http.ResponseWriter, r *http.Request) error { - srv.assert.NotNil(file, "A file needs to be present to it to be rendered") - srv.assert.NotNil(srv.renderer, "A renderer needs to be present to render a file") - srv.assert.NotNil(srv.onerror, "An error handler needs to be available in cases of errors") - srv.assert.NotNil(srv.log) - srv.assert.NotNil(w) - srv.assert.NotNil(r) - - log := srv.log.With( - slog.String("path", r.URL.Path), - slog.String("renderer", srv.renderer.Name()), - ) - log.Debug("Rendering file") - - err := srv.renderer.Render(file, w) - if err != nil { - log := log.With( - slog.String("err", err.Error()), - slog.String("errorhandler", srv.onerror.Name()), - ) - - log.Error( - "Failed to render file, handling error to ErrorHandler", - ) - - recovr, ok := srv.onerror.Handle(ServeError{ - Res: w, - Req: r, - Err: RenderError{ - Renderer: srv.renderer, - File: file, - Err: err, - }, - }) - - if !ok { - log.Error("Failed to handle error with plugin") - - w.WriteHeader(http.StatusInternalServerError) - _, err = w.Write([]byte(fmt.Sprintf( - "Failed to handle error %q with plugin %q", - err.Error(), - srv.onerror.Name(), - ))) - srv.assert.Nil(err) - - return err - } - - r, ok := recovr.(plugin.Renderer) - - if !ok { - return err - } - - err = r.Render(file, w) - srv.assert.Nil(err) - - } - - return nil -} diff --git a/blogo/core/errors.go b/blogo/core/errors.go deleted file mode 100644 index 32da8a5..0000000 --- a/blogo/core/errors.go +++ /dev/null @@ -1,65 +0,0 @@ -// 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 core - -import ( - "fmt" - "io/fs" - "net/http" - - "forge.capytal.company/loreddev/x/blogo/plugin" -) - -type ServeError struct { - Res http.ResponseWriter - Req *http.Request - Err error -} - -func (e ServeError) Error() string { - return fmt.Sprintf("failed to serve file on path %q", e.Req.URL.Path) -} - -func (e ServeError) Unwrap() error { - return e.Err -} - -type SourceError struct { - Sourcer plugin.Sourcer - Err error -} - -func (e SourceError) Error() string { - return fmt.Sprintf("failed to source files with sourcer %q", e.Sourcer.Name()) -} - -func (e SourceError) Unwrap() error { - return e.Err -} - -type RenderError struct { - Renderer plugin.Renderer - File fs.File - Err error -} - -func (e RenderError) Error() string { - return fmt.Sprintf("failed to source files with renderer %q", e.Renderer.Name()) -} - -func (e RenderError) Unwrap() error { - return e.Err -} diff --git a/blogo/metadata/metadata.go b/blogo/metadata/metadata.go deleted file mode 100644 index 4791db4..0000000 --- a/blogo/metadata/metadata.go +++ /dev/null @@ -1,320 +0,0 @@ -// 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. - -// [Metadata] is a simple key-value data structure that should be used by -// plugins to pass data between their processes and between plugins. -// -// This package provides a collection of types, interfaces and functions to -// help create and manipulate said data structure. -package metadata - -import ( - "errors" - "reflect" -) - -var ( - ErrImmutable = errors.New("metadata is immutable") - ErrInvalidType = errors.New("key is not of specified type") - ErrNotFound = errors.New("value not found metadata") - ErrNoMetadata = errors.New("type does not implement or has metadata") -) - -// Gets a value from a [Metadata] or [WithMetadata] objects and tries -// to convert it to the specified type. If m implements [TypedMetadata], -// tries to use the typed methods directly. -// -// For more information, see [Get]. -// -// If the value is not of the specified type, returns [ErrInvalidType]. -func GetTyped[T any](m any, key string) (T, error) { - var z T - - if m, ok := m.(TypedMetadata); ok { - v, err := getTypedFromTyped[T](m, key) - if v, ok := v.(T); ok && err == nil { - return v, err - } - } - - v, err := Get(m, key) - if err != nil { - return z, err - } - - if v, ok := v.(T); ok { - return v, nil - } - - vv, zv := reflect.ValueOf(v), reflect.ValueOf(z) - vt, zt := vv.Type(), zv.Type() - if vt.ConvertibleTo(zt) { - v = vv.Convert(zt).Interface() - if v, ok := v.(T); ok { - return v, nil - } - } - - return z, ErrInvalidType -} - -func getTypedFromTyped[T any](m TypedMetadata, key string) (any, error) { - var z T - - t := reflect.TypeOf(z) - if t == nil { - return z, ErrInvalidType - } - - switch t.Kind() { - case reflect.Bool: - return m.GetBool(key) - - case reflect.String: - return m.GetString(key) - - case reflect.Int: - return m.GetInt(key) - case reflect.Int8: - return m.GetInt8(key) - case reflect.Int16: - return m.GetInt16(key) - case reflect.Int32: - return m.GetInt32(key) - case reflect.Int64: - return m.GetInt64(key) - - case reflect.Uint: - return m.GetInt(key) - case reflect.Uint8: - return m.GetUint8(key) - case reflect.Uint16: - return m.GetUint16(key) - case reflect.Uint32: - return m.GetUint32(key) - case reflect.Uint64: - return m.GetUint64(key) - case reflect.Uintptr: - return m.GetUintptr(key) - - case reflect.Float32: - return m.GetFloat32(key) - case reflect.Float64: - return m.GetFloat64(key) - - case reflect.Complex64: - return m.GetComplex64(key) - case reflect.Complex128: - return m.GetComplex128(key) - - default: - return m.Get(key) - } -} - -// Gets a value from m, if it implements [Metadata] or [WithMetadata], otherwise returns -// [ErrNoMetadata]. -// -// If there is metadata, but there isn't any value associated with the specified key, -// returns [ErrNotFound]. More information at [Metadata]'s Get method. -func Get(m any, key string) (any, error) { - data, err := GetMetadata(m) - if err != nil { - return nil, err - } - return data.Get(key) -} - -// Sets a value of m, if it implements [Metadata] or [WithMetadata], otherwise returns -// otherwise returns [ErrNoMetadata]. -// -// If the underlying metadata is [Immutable], returns [ErrImmutable]. See [Metadata]'s -// Set method for more information. -func Set(m any, key string, v any) error { - data, err := GetMetadata(m) - if err != nil { - return err - } - return data.Set(key, v) -} - -// Deletes a value of m, if it implements [Metadata] or [WithMetadata], otherwise returns -// otherwise returns [ErrNoMetadata]. -// -// If the underlying metadata is [Immutable], returns [ErrImmutable]. See [Metadata]'s -// Delete method for more information. -func Delete(m any, key string) error { - data, err := GetMetadata(m) - if err != nil { - return err - } - return data.Delete(key) -} - -// Gets the underlying [Metadata] of m. If m implements [Metadata], returns it unchanged, -// otherwise uses the Metadata method if it implements [WithMetadata]. -// -// If m doesn't implement any of the interfaces, returns [ErrNoMetadata]. -func GetMetadata(m any) (Metadata, error) { - var data Metadata - - if mt, ok := m.(Metadata); ok { - data = mt - } else if mfile, ok := m.(WithMetadata); ok { - data = mfile.Metadata() - } else { - return nil, ErrNoMetadata - } - - return data, nil -} - -// Types may implement this interface to add [Metadata] to their objects that can -// be easily accessed via [Get], [Set], [Delete] and [GetMetadata]. -type WithMetadata interface { - // Returns the underlying [Metadata] of the type. - // - // If the [Metadata] is empty and/or the type doesn't have any data associated with - // it, implementations should return a empty [Metadata] (such as Map(map[string]any{})) - // and should never return a nil interface. - Metadata() Metadata -} - -// Minimal interface for the Metadata data. This data is used to easily pass information -// between [plugin.Plugin]'s files and file systems. -// -// Implementations of this interface can store their metadata in any way possible, -// may it be with a simple underlying map[string]any or a network accessed storage. -// -// Plugins may add prefixed to their keys to minimize conflicts between other plugins' -// data. The convention for key strings is ".", all lowercase and -// kabeb-cased. -// -// Other objects and interfaces, such as [fs.FS] and [fs.File] may implement this -// interface directly or use the [WithMetadata] interface. -// -// [TypedMetadata] may also be implemented to optimize calls via [GetTyped]. -type Metadata interface { - // Gets the value of the specified key. - // - // Implementations should return [ErrNotFound] if the provided key doesn't have - // any associated value with it. - Get(key string) (any, error) - // Sets the value of the specified key. - // - // If the key cannot be created or have it's underlying value changed, - // implementations should return [ErrImmutable]. - // - // If the type of v is different from the stored value, implementations may return - // [ErrInvalidType] if they can't accept different types. - Set(key string, v any) error - // Deletes the key from metadata. Implementations that cannot delete the key directly, - // should set the value a appropriated zero or implementations' default value for that key. - // - // If the key cannot be created or have it's underlying value changed, - // implementations should return [ErrImmutable]. - Delete(key string) error -} - -// Type adapter to allow the use of ordinary maps as [Metadata] implementations. -// -// If map is nil, Get always returns [ErrNotFound], Set and Delete return [ErrImmutable]. -type Map map[string]any - -func (m Map) Get(key string) (any, error) { - if m == nil { - return nil, ErrNotFound - } - - if v, ok := m[key]; ok { - return v, nil - } else { - return nil, ErrNotFound - } -} - -func (m Map) Set(key string, v any) error { - if m == nil { - return ErrImmutable - } - m[key] = v - return nil -} - -func (m Map) Delete(key string) error { - if m == nil { - return ErrImmutable - } - delete(m, key) - return nil -} - -// Joins multiple [Metadata] objects together so their values can be easily -// accessed using just one call. -// -// [Get]: -// Iterates over all Metadatas until it finds one that returns a nil-error. -// -// [Set]: -// Sets the specified key on all underlying Metadatas. Ignores errors. -// -// [Delete]: -// Deletes the specified key on all underlying Metadatas. Ignores errors. -func Join(ms ...Metadata) Metadata { - ms = append([]Metadata{Map(make(map[string]any))}, ms...) - return joined(ms) -} - -type joined []Metadata - -func (m joined) Get(key string) (any, error) { - for _, m := range m { - v, err := m.Get(key) - if err == nil { - return v, nil - } - } - return nil, ErrNotFound -} - -func (m joined) Set(key string, v any) error { - for _, m := range m { - _ = m.Set(key, v) - } - return nil -} - -func (m joined) Delete(key string) error { - for _, m := range m { - _ = m.Delete(key) - } - return nil -} - -type immutable struct{ Metadata } - -// Converts all keys from m to immutable. All calls to Set and Delete -// are responded with [ErrImmutable]. -func Immutable(m Metadata) Metadata { - return &immutable{m} -} - -func (m *immutable) Set(key string, v any) error { - return ErrImmutable -} - -func (m *immutable) Delete(key string) error { - return ErrImmutable -} diff --git a/blogo/metadata/typed.go b/blogo/metadata/typed.go deleted file mode 100644 index 5bd0895..0000000 --- a/blogo/metadata/typed.go +++ /dev/null @@ -1,136 +0,0 @@ -// 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 metadata - -// TypedMetadata expands the [Metadata] interface to add helper methods for -// Go's primitive types. -// -// [GetTyped] uses this interface for optimization. -type TypedMetadata interface { - Metadata - - GetBool(key string) (bool, error) - - GetString(key string) (string, error) - - GetInt(key string) (int, error) - GetInt8(key string) (int8, error) - GetInt16(key string) (int16, error) - GetInt32(key string) (int32, error) - GetInt64(key string) (int64, error) - - GetUint(key string) (uint, error) - GetUint8(key string) (uint8, error) - GetUint16(key string) (uint16, error) - GetUint32(key string) (uint32, error) - GetUint64(key string) (uint64, error) - GetUintptr(key string) (uintptr, error) - - GetByte(key string) (byte, error) - - GetRune(key string) (rune, error) - - GetFloat32(key string) (float32, error) - GetFloat64(key string) (float64, error) - - GetComplex64(key string) (complex64, error) - GetComplex128(key string) (complex128, error) -} - -func Typed(m Metadata) TypedMetadata { - if m, ok := m.(TypedMetadata); ok { - return m - } - return &typedMetadata{m} -} - -type typedMetadata struct{ Metadata } - -func (m *typedMetadata) GetBool(key string) (bool, error) { - return GetTyped[bool](m, key) -} - -func (m *typedMetadata) GetString(key string) (string, error) { - return GetTyped[string](m, key) -} - -func (m *typedMetadata) GetInt(key string) (int, error) { - return GetTyped[int](m, key) -} - -func (m *typedMetadata) GetInt8(key string) (int8, error) { - return GetTyped[int8](m, key) -} - -func (m *typedMetadata) GetInt16(key string) (int16, error) { - return GetTyped[int16](m, key) -} - -func (m *typedMetadata) GetInt32(key string) (int32, error) { - return GetTyped[int32](m, key) -} - -func (m *typedMetadata) GetInt64(key string) (int64, error) { - return GetTyped[int64](m, key) -} - -func (m *typedMetadata) GetUint(key string) (uint, error) { - return GetTyped[uint](m, key) -} - -func (m *typedMetadata) GetUint8(key string) (uint8, error) { - return GetTyped[uint8](m, key) -} - -func (m *typedMetadata) GetUint16(key string) (uint16, error) { - return GetTyped[uint16](m, key) -} - -func (m *typedMetadata) GetUint32(key string) (uint32, error) { - return GetTyped[uint32](m, key) -} - -func (m *typedMetadata) GetUint64(key string) (uint64, error) { - return GetTyped[uint64](m, key) -} - -func (m *typedMetadata) GetUintptr(key string) (uintptr, error) { - return GetTyped[uintptr](m, key) -} - -func (m *typedMetadata) GetByte(key string) (byte, error) { - return GetTyped[byte](m, key) -} - -func (m *typedMetadata) GetRune(key string) (rune, error) { - return GetTyped[rune](m, key) -} - -func (m *typedMetadata) GetFloat32(key string) (float32, error) { - return GetTyped[float32](m, key) -} - -func (m *typedMetadata) GetFloat64(key string) (float64, error) { - return GetTyped[float64](m, key) -} - -func (m *typedMetadata) GetComplex64(key string) (complex64, error) { - return GetTyped[complex64](m, key) -} - -func (m *typedMetadata) GetComplex128(key string) (complex128, error) { - return GetTyped[complex128](m, key) -} diff --git a/blogo/plugin/group.go b/blogo/plugin/group.go deleted file mode 100644 index 7c0ec26..0000000 --- a/blogo/plugin/group.go +++ /dev/null @@ -1,44 +0,0 @@ -// 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 plugin - -const pluginGroupName = "blogo-plugingroup-group" - -type Group interface { - Plugin - WithPlugins - Plugins() []Plugin -} - -type pluginGroup struct { - plugins []Plugin -} - -func NewGroup(plugins ...Plugin) Group { - return &pluginGroup{plugins} -} - -func (p *pluginGroup) Name() string { - return pluginGroupName -} - -func (p *pluginGroup) Use(plugin Plugin) { - p.plugins = append(p.plugins, plugin) -} - -func (p *pluginGroup) Plugins() []Plugin { - return p.plugins -} diff --git a/blogo/plugin/plugin.go b/blogo/plugin/plugin.go deleted file mode 100644 index 334470d..0000000 --- a/blogo/plugin/plugin.go +++ /dev/null @@ -1,45 +0,0 @@ -// 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 plugin - -import ( - "io" - "io/fs" -) - -type Plugin interface { - Name() string -} - -type WithPlugins interface { - Plugin - Use(Plugin) -} - -type Renderer interface { - Plugin - Render(src fs.File, out io.Writer) error -} - -type Sourcer interface { - Plugin - Source() (fs.FS, error) -} - -type ErrorHandler interface { - Plugin - Handle(error) (recovr any, handled bool) -} diff --git a/blogo/plugins/buffered_multi_renderer.go b/blogo/plugins/buffered_multi_renderer.go deleted file mode 100644 index 4c218bd..0000000 --- a/blogo/plugins/buffered_multi_renderer.go +++ /dev/null @@ -1,300 +0,0 @@ -// 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 plugins - -import ( - "bytes" - "errors" - "io" - "io/fs" - "log/slog" - - "forge.capytal.company/loreddev/x/blogo/plugin" - "forge.capytal.company/loreddev/x/tinyssert" -) - -const bufferedMultiRendererName = "blogo-buffer-renderer" - -func NewBufferedMultiRenderer(opts ...BufferedMultiRendererOpts) BufferedMultiRenderer { - opt := BufferedMultiRendererOpts{} - 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{})) - } - - return &bufferedMultiRenderer{ - plugins: []plugin.Renderer{}, - - assert: opt.Assertions, - log: opt.Logger, - } -} - -type BufferedMultiRenderer interface { - plugin.Renderer - plugin.WithPlugins -} - -type BufferedMultiRendererOpts struct { - Assertions tinyssert.Assertions - Logger *slog.Logger -} - -type bufferedMultiRenderer struct { - plugins []plugin.Renderer - - assert tinyssert.Assertions - log *slog.Logger -} - -func (r *bufferedMultiRenderer) Name() string { - return bufferedMultiRendererName -} - -func (r *bufferedMultiRenderer) Use(p plugin.Plugin) { - r.assert.NotNil(r.plugins, "Plugins slice needs to be not-nil") - r.assert.NotNil(r.log) - - log := r.log.With(slog.String("plugin", p.Name())) - log.Debug("Adding plugin") - - if p, ok := p.(plugin.Group); ok { - log.Debug("Plugin implements plugin.Group, using it's plugins") - for _, p := range p.Plugins() { - r.Use(p) - } - } - - if p, ok := p.(plugin.Renderer); ok { - log.Debug("Adding plugin") - r.plugins = append(r.plugins, p) - } else { - log.Warn("Plugin does not implement Renderer, ignoring") - } -} - -func (r *bufferedMultiRenderer) Render(src fs.File, w io.Writer) error { - r.assert.NotNil(r.plugins, "Plugins slice needs to be not-nil") - r.assert.NotNil(r.log) - - log := r.log.With() - - log.Debug("Creating buffered file") - bf := newBufferedFile(src) - - var buf bytes.Buffer - - for _, p := range r.plugins { - log := log.With(slog.String("plugin", p.Name())) - log.Debug("Trying to render with plugin") - - err := p.Render(bf, &buf) - if err == nil { - log.Debug("Successfully rendered with plugin") - break - } - - log.Debug("Unable to render with plugin, resetting file and writer") - - if err = bf.Reset(); err != nil { - log.Error("Failed to reset file", slog.String("err", err.Error())) - return errors.Join(errors.New("failed to reset buffered file"), err) - } - - buf.Reset() - } - - log.Debug("Copying response to final writer") - if _, err := io.Copy(w, &buf); err != nil { - log.Error("Failed to copy response to final writer") - return errors.Join(errors.New("failed to copy response to final writer"), err) - } - - return nil -} - -func newBufferedFile(src fs.File) bufferedFile { - var buf bytes.Buffer - r := io.TeeReader(src, &buf) - - if d, ok := src.(fs.ReadDirFile); ok { - return &bufDirFile{ - file: d, - buffer: &buf, - reader: r, - - entries: []fs.DirEntry{}, - eof: false, - n: 0, - } - } - - return &bufFile{ - file: src, - buffer: &buf, - reader: r, - } -} - -type bufferedFile interface { - fs.File - Reset() error -} - -type bufFile struct { - file fs.File - buffer *bytes.Buffer - reader io.Reader -} - -func (f *bufFile) Read(p []byte) (int, error) { - return f.reader.Read(p) -} - -func (f *bufFile) Close() error { - return nil -} - -func (f *bufFile) Stat() (fs.FileInfo, error) { - return f.file.Stat() -} - -func (f *bufFile) Reset() error { - _, err := io.ReadAll(f.reader) - if err != nil { - return err - } - - var buf bytes.Buffer - r := io.TeeReader(f.buffer, &buf) - - f.buffer = &buf - f.reader = r - - return nil -} - -type bufDirFile struct { - file fs.ReadDirFile - - buffer *bytes.Buffer - reader io.Reader - - entries []fs.DirEntry - eof bool - n int -} - -func (f *bufDirFile) Read(p []byte) (int, error) { - return f.reader.Read(p) -} - -func (f *bufDirFile) Close() error { - return nil -} - -func (f *bufDirFile) Stat() (fs.FileInfo, error) { - return f.file.Stat() -} - -func (f *bufDirFile) ReadDir(n int) ([]fs.DirEntry, error) { - start, end := f.n, f.n+n - - var err error - - // If EOF is true, it means we already read all the content from the - // source directory, so we can use the entries slice directly. Otherwise, we - // may need to read more from the source directly if the provided end index - // is bigger than what we already have. - if end > len(f.entries) && !f.eof { - e, err := f.file.ReadDir(n) - - if e != nil { - // Add the entries to our buffer so we can access them even after a reset. - f.entries = append(f.entries, e...) - } - - if err != nil && !errors.Is(err, io.EOF) { - return []fs.DirEntry{}, err - } - - // If we reached EOF, we don't need to call the source directory anymore - // and can just use the slice directly - if errors.Is(err, io.EOF) { - f.eof = true - } - } - - // Reading all contents from the directory needs us to have all values inside - // our buffer/slice, if EOF isn't already reached, we need to get the rest of - // the content from the source directory. - if n <= 0 && !f.eof { - e, err := f.file.ReadDir(n) - - if e != nil { - f.entries = append(f.entries, e...) - } - - if err != nil && !errors.Is(err, io.EOF) { - return []fs.DirEntry{}, err - } - - if errors.Is(err, io.EOF) { - f.eof = true - } - } - - if n <= 0 { - start, end = 0, len(f.entries) - } else if end > len(f.entries) { - end = len(f.entries) - err = io.EOF - } - - e := f.entries[start:end] - - f.n = end - - return e, err -} - -func (f *bufDirFile) Reset() error { - // To reset the ReadDir of the file, we pretty much just need to set - // the start offset to 0, so any subsequent reads will start at the first - // item, that is probably already buffered on f.entries. - f.n = 0 - - // Reset the Read buffer, the directory file implementation may have contents - // on it's Read function. - _, err := io.ReadAll(f.reader) - if err != nil { - return err - } - - var buf bytes.Buffer - r := io.TeeReader(f.buffer, &buf) - - f.buffer = &buf - f.reader = r - - return nil -} diff --git a/blogo/plugins/empty_sourcer.go b/blogo/plugins/empty_sourcer.go deleted file mode 100644 index 2dae197..0000000 --- a/blogo/plugins/empty_sourcer.go +++ /dev/null @@ -1,44 +0,0 @@ -// 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 plugins - -import ( - "io/fs" - - "forge.capytal.company/loreddev/x/blogo/plugin" -) - -const emptySourcerPluginName = "blogo-empty-sourcer" - -type emptySourcer struct{} - -func NewEmptySourcer() plugin.Sourcer { - return &emptySourcer{} -} - -func (p *emptySourcer) Name() string { - return emptySourcerPluginName -} - -func (p *emptySourcer) Source() (fs.FS, error) { - return emptyFS{}, nil -} - -type emptyFS struct{} - -func (f emptyFS) Open(name string) (fs.File, error) { - return nil, fs.ErrNotExist -} diff --git a/blogo/plugins/folding_renderer.go b/blogo/plugins/folding_renderer.go deleted file mode 100644 index 79d4a7b..0000000 --- a/blogo/plugins/folding_renderer.go +++ /dev/null @@ -1,179 +0,0 @@ -// 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 plugins - -import ( - "bytes" - "fmt" - "io" - "io/fs" - "log/slog" - - "forge.capytal.company/loreddev/x/blogo/plugin" - "forge.capytal.company/loreddev/x/tinyssert" -) - -const foldingRendererPluginName = "blogo-foldingrenderer-renderer" - -func NewFoldingRenderer(opts ...FoldingRendererOpts) FoldingRenderer { - opt := FoldingRendererOpts{} - 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{})) - } - - return &foldingRenderer{ - plugins: []plugin.Renderer{}, - - assert: opt.Assertions, - log: opt.Logger, - } -} - -type FoldingRenderer interface { - plugin.WithPlugins - plugin.Renderer -} - -type FoldingRendererOpts struct { - PanicOnInit bool - - Assertions tinyssert.Assertions - Logger *slog.Logger -} - -type foldingRenderer struct { - plugins []plugin.Renderer - - assert tinyssert.Assertions - log *slog.Logger -} - -func (r *foldingRenderer) Name() string { - return foldingRendererPluginName -} - -func (r *foldingRenderer) Use(p plugin.Plugin) { - r.assert.NotNil(p) - r.assert.NotNil(r.plugins) - r.assert.NotNil(r.log) - - log := r.log.With(slog.String("plugin", p.Name())) - - if pr, ok := p.(plugin.Renderer); ok { - r.plugins = append(r.plugins, pr) - } else { - log.Error(fmt.Sprintf( - "Failed to add plugin %q, since it doesn't implement plugin.Renderer", - p.Name(), - )) - } -} - -func (r *foldingRenderer) Render(src fs.File, w io.Writer) error { - r.assert.NotNil(r.plugins) - r.assert.NotNil(r.log) - r.assert.NotNil(src) - r.assert.NotNil(w) - - log := r.log.With() - - if len(r.plugins) == 0 { - log.Debug("No renderers found, copying file contents to writer") - - _, err := io.Copy(w, src) - return err - } - - log.Debug("Creating folding file") - - f, err := newFoldignFile(src) - if err != nil { - log.Error("Failed to create folding file", slog.String("err", err.Error())) - - return err - } - - for _, p := range r.plugins { - log := log.With(slog.String("plugin", p.Name())) - - log.Debug("Rendering with plugin") - - err := p.Render(f, f) - if err != nil { - log.Error("Failed to render with plugin", slog.String("err", err.Error())) - return err - } - - log.Debug("Folding file to next render") - - if err := f.Fold(); err != nil { - log.Error("Failed to fold file", slog.String("err", err.Error())) - return err - } - } - - log.Debug("Writing final file to Writer") - - _, err = io.Copy(w, f) - return err -} - -type foldingFile struct { - fs.File - read *bytes.Buffer - writer *bytes.Buffer -} - -func newFoldignFile(f fs.File) (*foldingFile, error) { - var r, w bytes.Buffer - - if _, err := io.Copy(&r, f); err != nil { - return nil, err - } - if err := f.Close(); err != nil { - return nil, err - } - - return &foldingFile{File: f, read: &r, writer: &w}, nil -} - -func (f *foldingFile) Close() error { - return nil -} - -func (f *foldingFile) Read(p []byte) (int, error) { - return f.read.Read(p) -} - -func (f *foldingFile) Write(p []byte) (int, error) { - return f.writer.Write(p) -} - -func (f *foldingFile) Fold() error { - f.read.Reset() - if _, err := io.Copy(f.read, f.writer); err != nil { - return err - } - f.writer.Reset() - return nil -} diff --git a/blogo/plugins/gitea/TODO.md b/blogo/plugins/gitea/TODO.md deleted file mode 100644 index 13ade74..0000000 --- a/blogo/plugins/gitea/TODO.md +++ /dev/null @@ -1,2 +0,0 @@ -- [ ] Handle symlinks -- [ ] Handle submodules diff --git a/blogo/plugins/gitea/client.go b/blogo/plugins/gitea/client.go deleted file mode 100644 index 5438fb6..0000000 --- a/blogo/plugins/gitea/client.go +++ /dev/null @@ -1,249 +0,0 @@ -// By contributing to, or using this source code, you agree with the terms of the -// MIT-style licensed that can be found below: -// -// Copyright (c) 2025-present Gustavo "Guz" L. de Mello -// Copyright (c) 2025-present The Lored.dev Contributors -// Copyright (c) 2016 The Gitea Authors -// Copyright (c) 2014 The Gogs Authors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -// Sections of the contents of this file were sourced from the official Gitea SDK for Go, -// which can be found at https://gitea.com/gitea/go-sdk and is licensed under a MIT-style -// licensed stated at the start of this file. - -package gitea - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "time" -) - -type client struct { - endpoint string - http *http.Client -} - -func newClient(endpoint string, http *http.Client) *client { - return &client{endpoint: endpoint, http: http} -} - -func (c *client) GetContents( - owner, repo, ref, filepath string, -) (*contentsResponse, *http.Response, error) { - data, res, err := c.get( - fmt.Sprintf("/repos/%s/%s/contents/%s?ref=%s", owner, repo, filepath, url.QueryEscape(ref)), - ) - if err != nil { - return &contentsResponse{}, res, err - } - - file := new(contentsResponse) - if err := json.Unmarshal(data, &file); err != nil { - return &contentsResponse{}, res, errors.Join( - errors.New("failed to parse JSON response from API"), - err, - ) - } - - return file, res, nil -} - -func (c *client) ListContents( - owner, repo, ref, filepath string, -) ([]*contentsResponse, *http.Response, error) { - endpoint := fmt.Sprintf( - "/repos/%s/%s/contents/%s?ref=%s", - owner, - repo, - filepath, - url.QueryEscape(ref), - ) - if filepath == "" || filepath == "." { - endpoint = fmt.Sprintf( - "/repos/%s/%s/contents?ref=%s", - owner, - repo, - url.QueryEscape(ref), - ) - } - - data, res, err := c.get(endpoint) - if err != nil { - return []*contentsResponse{}, res, err - } - - directory := make([]*contentsResponse, 0) - if err := json.Unmarshal(data, &directory); err != nil { - return []*contentsResponse{}, res, errors.Join( - errors.New("failed to parse JSON response from API"), - err, - ) - } - - return directory, res, nil -} - -func (c *client) GetSingleCommit(user, repo, commitID string) (*commit, *http.Response, error) { - data, res, err := c.get( - fmt.Sprintf("/repos/%s/%s/git/commits/%s", user, repo, commitID), - ) - if err != nil { - return &commit{}, res, err - } - - var commit *commit - if err := json.Unmarshal(data, commit); err != nil { - return commit, res, errors.Join( - errors.New("failed to parse JSON response from API"), - err, - ) - } - - return commit, res, err -} - -func (c *client) GetFileReader( - owner, repo, ref, filepath string, - resolveLFS ...bool, -) (io.ReadCloser, *http.Response, error) { - if len(resolveLFS) != 0 && resolveLFS[0] { - return c.getResponseReader( - fmt.Sprintf( - "/repos/%s/%s/media/%s?ref=%s", - owner, - repo, - filepath, - url.QueryEscape(ref), - ), - ) - } - - return c.getResponseReader( - fmt.Sprintf( - "/repos/%s/%s/raw/%s?ref=%s", - owner, - repo, - filepath, - url.QueryEscape(ref), - ), - ) -} - -func (c *client) get(path string) ([]byte, *http.Response, error) { - body, res, err := c.getResponseReader(path) - if err != nil { - return nil, res, err - } - defer body.Close() - - data, err := io.ReadAll(body) - if err != nil { - return nil, res, err - } - - return data, res, err -} - -func (c *client) getResponseReader(path string) (io.ReadCloser, *http.Response, error) { - res, err := c.http.Get(c.endpoint + path) - if err != nil { - return nil, nil, errors.Join(errors.New("failed to request"), err) - } - - data, err := statusCodeToErr(res) - if err != nil { - return io.NopCloser(bytes.NewReader(data)), res, err - } - - return res.Body, res, err -} - -func statusCodeToErr(resp *http.Response) (body []byte, err error) { - if resp.StatusCode/100 == 2 { - return nil, nil - } - - defer resp.Body.Close() - data, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("body read on HTTP error %d: %v", resp.StatusCode, err) - } - - errMap := make(map[string]interface{}) - if err = json.Unmarshal(data, &errMap); err != nil { - return data, fmt.Errorf( - "Unknown API Error: %d Request Path: '%s'\nResponse body: '%s'", - resp.StatusCode, - resp.Request.URL.Path, - string(data), - ) - } - - if msg, ok := errMap["message"]; ok { - return data, fmt.Errorf("%v", msg) - } - - return data, fmt.Errorf("%s: %s", resp.Status, string(data)) -} - -type contentsResponse struct { - Name string `json:"name"` - Path string `json:"path"` - SHA string `json:"sha"` - LastCommitSha string `json:"last_commit_sha"` - - // NOTE: can be "file", "dir", "symlink" or "submodule" - Type string `json:"type"` - Size int64 `json:"size"` - // NOTE: populated just when `type` is "file" - Encoding *string `json:"encoding"` - // NOTE: populated just when `type` is "file" - Content *string `json:"content"` - // NOTE: populated just when `type` is "link" - Target *string `json:"target"` - - URL *string `json:"url"` - HTMLURL *string `json:"html_url"` - GitURL *string `json:"git_url"` - DownloadURL *string `json:"download_url"` - - // NOTE: populated just when `type` is "submodule" - SubmoduleGitURL *string `json:"submodule_giit_url"` - - Links *fileLinksResponse `json:"_links"` -} - -type fileLinksResponse struct { - Self *string `json:"self"` - GitURL *string `json:"git"` - HTMLURL *string `json:"html"` -} - -type commit struct { - URL string `json:"url"` - SHA string `json:"sha"` - Created time.Time `json:"created"` -} diff --git a/blogo/plugins/gitea/fs.go b/blogo/plugins/gitea/fs.go deleted file mode 100644 index c1fbb5c..0000000 --- a/blogo/plugins/gitea/fs.go +++ /dev/null @@ -1,348 +0,0 @@ -// 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 gitea - -import ( - "bytes" - "encoding/base64" - "errors" - "io" - "io/fs" - "net/http" - "os" - "path" - "slices" - "syscall" - "time" - - "forge.capytal.company/loreddev/x/blogo/metadata" -) - -type repositoryFS struct { - metadata map[string]any - - owner string - repo string - ref string - - client *client -} - -func newRepositoryFS(owner, repo, ref string, client *client) fs.FS { - return &repositoryFS{ - owner: owner, - repo: repo, - ref: ref, - client: client, - } -} - -func (fsys *repositoryFS) Metadata() metadata.Metadata { - // TODO: Properly implement metadata with contents from the API - if fsys.metadata == nil || (fsys.metadata != nil && len(fsys.metadata) == 0) { - m := map[string]any{} - m["gitea.owner"] = fsys.owner - m["gitea.repository"] = fsys.repo - - if fsys.ref != "" { - m["gitea.ref"] = fsys.ref - } - - fsys.metadata = m - } - return metadata.Map(fsys.metadata) -} - -func (fsys *repositoryFS) Open(name string) (fs.File, error) { - if !fs.ValidPath(name) { - return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid} - } - - file, _, err := fsys.client.GetContents(fsys.owner, fsys.repo, fsys.ref, name) - if err == nil { - return &repositoryFile{ - contentsResponse: *file, - - owner: fsys.owner, - repo: fsys.repo, - ref: fsys.ref, - client: fsys.client, - - contents: nil, - }, nil - } - - // If previous call returned a error, it may be because the file is a directory, - // so we will call from it's parent directory to be able to get it's metadata. - path := path.Dir(name) - if path == "." { - path = "" - } - - list, res, err := fsys.client.ListContents(fsys.owner, fsys.repo, fsys.ref, path) - if err != nil { - if res.StatusCode == http.StatusUnauthorized { - return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrPermission} - } else if res.StatusCode == http.StatusNotFound { - return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} - } - return nil, &fs.PathError{Op: "open", Path: name, Err: err} - } - - // If the function is being called to open the root directory, return the - // repository as a root directory. We are returning it here since we can get - // a SHA of the past returned files. - if name == "." { - sha := "" - if len(list) > 0 { - sha = list[0].LastCommitSha - } - - return &repositoryDirFile{repositoryFile{ - contentsResponse: contentsResponse{ - Name: fsys.repo, - Path: ".", - SHA: sha, - LastCommitSha: sha, - Type: "dir", - }, - - owner: fsys.owner, - repo: fsys.repo, - ref: fsys.ref, - - client: fsys.client, - - contents: nil, - }, 0}, nil - } - - i := slices.IndexFunc(list, func(i *contentsResponse) bool { - return i.Path == name - }) - if i == -1 { - return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} - } - - dir := list[i] - if dir.Type != "dir" { - return nil, &fs.PathError{ - Op: "open", - Path: name, - Err: errors.New("unexpected, directory found is not of type 'dir'"), - } - } - - f := &repositoryFile{ - contentsResponse: *dir, - - owner: fsys.owner, - repo: fsys.repo, - ref: fsys.ref, - client: fsys.client, - - contents: nil, - } - - return &repositoryDirFile{*f, 0}, nil -} - -// Implements fs.File to represent a remote file in the repository. The contents of -// the file are filled on the first Read call, reusing the base64-encoded -// *contentsResponse.Content if available, if not, the file calls the API to retrieve -// the raw contents. -// -// To prevent possible content changes after this object has been initialized, if none -// ref is provided, it uses the *contentsResponse.LastCommitSha as a ref. -type repositoryFile struct { - contentsResponse - - metadata map[string]any - - owner string - repo string - ref string - - client *client - - contents io.ReadCloser -} - -func (f *repositoryFile) Metadata() metadata.Metadata { - // TODO: Properly implement metadata with contents from the API - if f.metadata == nil || (f.metadata != nil && len(f.metadata) == 0) { - m := map[string]any{} - m["gitea.owner"] = f.owner - m["gitea.repository"] = f.repo - - if f.ref != "" { - m["gitea.ref"] = f.ref - } - - f.metadata = m - } - return metadata.Map(f.metadata) -} - -func (f *repositoryFile) Stat() (fs.FileInfo, error) { - return &repositoryFileInfo{*f}, nil -} - -func (f *repositoryFile) Read(p []byte) (int, error) { - var err error - - if f.contents == nil && f.Type == "file" { - f.contents, err = f.getFileContents() - } - - if err != nil { - return 0, errors.Join(errors.New("failed to fetch file contents from API"), err) - } - - return f.contents.Read(p) -} - -func (f *repositoryFile) Close() error { - return f.contents.Close() -} - -func (f *repositoryFile) getFileContents() (io.ReadCloser, error) { - if *f.Content != "" && f.Encoding != nil && *f.Encoding == "base64" { - b, err := base64.StdEncoding.DecodeString(*f.Content) - if err == nil { - return io.NopCloser(bytes.NewReader(b)), nil - } - } - - ref := f.ref - if ref == "" { - ref = f.contentsResponse.LastCommitSha - } - - r, _, err := f.client.GetFileReader(f.owner, f.repo, ref, f.Path, true) - return r, err -} - -// Implements fs.ReadDirFile for the underlying 'repositoryFile'. -// 'repositoryFile' should be of type "dir", and not a list of said directory -// content. -type repositoryDirFile struct { - repositoryFile - n int -} - -func (f *repositoryDirFile) Read(p []byte) (int, error) { - return 0, io.EOF -} - -func (f *repositoryDirFile) Close() error { - return nil -} - -func (f *repositoryDirFile) ReadDir(n int) ([]fs.DirEntry, error) { - list, _, err := f.client.ListContents(f.owner, f.repo, f.ref, f.Path) - if err != nil { - return []fs.DirEntry{}, err - } - - start, end := f.n, f.n+n - if n <= 0 { - start, end = 0, len(list) - } else if end > len(list) { - end = len(list) - err = io.EOF - } - - list = list[start:end] - entries := make([]fs.DirEntry, len(list)) - for i, v := range list { - entries[i] = &repositoryDirEntry{repositoryFile{ - contentsResponse: *v, - - owner: f.owner, - repo: f.repo, - ref: f.ref, - client: f.client, - }} - } - - f.n = end - - return entries, err -} - -// Implements fs.DirEntry for the embedded 'repositoryFile' -type repositoryDirEntry struct { - repositoryFile -} - -func (e *repositoryDirEntry) Name() string { - i, _ := e.Info() - return i.Name() -} - -func (e *repositoryDirEntry) IsDir() bool { - i, _ := e.Info() - return i.IsDir() -} - -func (e *repositoryDirEntry) Type() fs.FileMode { - i, _ := e.Info() - return i.Mode().Type() -} - -func (e *repositoryDirEntry) Info() (fs.FileInfo, error) { - return &repositoryFileInfo{e.repositoryFile}, nil -} - -// Implements fs.FileInfo, getting information from the embedded 'repositoryFile' -type repositoryFileInfo struct { - repositoryFile -} - -func (fi *repositoryFileInfo) Name() string { - return fi.contentsResponse.Name -} - -func (fi *repositoryFileInfo) Size() int64 { - return fi.contentsResponse.Size -} - -func (fi *repositoryFileInfo) Mode() fs.FileMode { - if fi.Type == "symlink" { - return os.FileMode(fs.ModeSymlink | syscall.S_IRUSR | syscall.S_IRGRP | syscall.S_IROTH) - } else if fi.IsDir() { - return os.FileMode(fs.ModeDir | syscall.S_IRUSR | syscall.S_IRGRP | syscall.S_IROTH) - } - return os.FileMode(syscall.S_IRUSR | syscall.S_IRGRP | syscall.S_IROTH) -} - -func (fi *repositoryFileInfo) ModTime() time.Time { - commit, _, err := fi.client.GetSingleCommit(fi.owner, fi.repo, fi.LastCommitSha) - if err != nil { - return time.Time{} - } - - return commit.Created -} - -func (fi *repositoryFileInfo) IsDir() bool { - return fi.Type == "dir" -} - -func (fi *repositoryFileInfo) Sys() any { - return nil -} diff --git a/blogo/plugins/gitea/gitea.go b/blogo/plugins/gitea/gitea.go deleted file mode 100644 index 665ecef..0000000 --- a/blogo/plugins/gitea/gitea.go +++ /dev/null @@ -1,88 +0,0 @@ -// 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 gitea - -import ( - "fmt" - "io/fs" - "net/http" - "net/url" - "strings" - - "forge.capytal.company/loreddev/x/blogo/plugin" -) - -const pluginName = "blogo-gitea-sourcer" - -type p struct { - client *client - - owner string - repo string - ref string -} - -type Opts struct { - HTTPClient *http.Client - Ref string -} - -func New(owner, repo, apiUrl string, opts ...Opts) plugin.Plugin { - opt := Opts{} - if len(opts) > 0 { - opt = opts[0] - } - - if opt.HTTPClient == nil { - opt.HTTPClient = http.DefaultClient - } - - u, err := url.Parse(apiUrl) - if err != nil { - panic( - fmt.Sprintf( - "%s: %q is not a valid URL. Err: %q", - pluginName, - apiUrl, - err.Error(), - ), - ) - } - - if u.Path == "" || u.Path == "/" { - u.Path = "/api/v1" - } else { - u.Path = strings.TrimSuffix(u.Path, "/api/v1") - } - - client := newClient(u.String(), opt.HTTPClient) - - return &p{ - client: client, - - owner: owner, - repo: repo, - ref: opt.Ref, - } -} - -func (p *p) Name() string { - return pluginName -} - -func (p *p) Source() (fs.FS, error) { - return newRepositoryFS(p.owner, p.repo, p.ref, p.client), nil -} diff --git a/blogo/plugins/gitea/gitea_test.go b/blogo/plugins/gitea/gitea_test.go deleted file mode 100644 index 8f4e0c9..0000000 --- a/blogo/plugins/gitea/gitea_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package gitea_test - -import ( - "io" - "testing" - - "forge.capytal.company/loreddev/x/blogo/plugin" - "forge.capytal.company/loreddev/x/blogo/plugins/gitea" -) - -func TestSource(t *testing.T) { - p := gitea.New("loreddev", "x", "https://forge.capytal.company") - - s := p.(plugin.Sourcer) - - fs, err := s.Source() - if err != nil { - t.Fatalf("Failed to source file system: %s %v", err.Error(), err) - } - - file, err := fs.Open("blogo/LICENSE") - if err != nil { - t.Fatalf("Failed to open file: %s %v", err.Error(), err) - } - - contents, err := io.ReadAll(file) - if err != nil { - t.Fatalf("Failed to read contents of file: %s %v", err.Error(), err) - } - - t.Logf("Successfully read contents of file: %s", string(contents)) -} diff --git a/blogo/plugins/logger_error_handler.go b/blogo/plugins/logger_error_handler.go deleted file mode 100644 index d6c5330..0000000 --- a/blogo/plugins/logger_error_handler.go +++ /dev/null @@ -1,66 +0,0 @@ -// 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 plugins - -import ( - "fmt" - "log/slog" - - "forge.capytal.company/loreddev/x/blogo/plugin" -) - -const loggerErrorHandlerName = "blogo-loggererrorhandler-errorhandler" - -func NewLoggerErrorHandler(logger *slog.Logger, level ...slog.Level) plugin.ErrorHandler { - l := slog.LevelError - if len(level) > 0 { - l = level[0] - } - - if logger == nil { - panic(fmt.Sprintf("%s: Failed to construct LoggerErrorHandler, logger needs to be non-nil", - loggerErrorHandlerName)) - } - - return &loggerErrorHandler{logger: logger, level: l} -} - -type loggerErrorHandler struct { - logger *slog.Logger - level slog.Level -} - -func (h *loggerErrorHandler) Name() string { - return loggerErrorHandlerName -} - -func (h *loggerErrorHandler) log(msg string, args ...any) { - switch h.level { - case slog.LevelDebug: - h.logger.Debug(msg, args...) - case slog.LevelInfo: - h.logger.Info(msg, args...) - case slog.LevelWarn: - h.logger.Warn(msg, args...) - default: - h.logger.Error(msg, args...) - } -} - -func (h *loggerErrorHandler) Handle(err error) (recovr any, handled bool) { - h.log("BLOGO ERROR", slog.String("err", err.Error())) - return nil, true -} diff --git a/blogo/plugins/markdown/markdown.go b/blogo/plugins/markdown/markdown.go deleted file mode 100644 index 003475c..0000000 --- a/blogo/plugins/markdown/markdown.go +++ /dev/null @@ -1,60 +0,0 @@ -package markdown - -import ( - "errors" - "io" - "io/fs" - "strings" - - "github.com/yuin/goldmark" - meta "github.com/yuin/goldmark-meta" - "github.com/yuin/goldmark/extension" - "github.com/yuin/goldmark/parser" - "github.com/yuin/goldmark/renderer" - "github.com/yuin/goldmark/text" - - "forge.capytal.company/loreddev/x/blogo/plugin" -) - -const pluginName = "blogo-markdown-renderer" - -type p struct { - parser parser.Parser - renderer renderer.Renderer -} - -func New() plugin.Plugin { - m := goldmark.New( - goldmark.WithExtensions( - extension.NewLinkify(), - meta.Meta, - ), - ) - - return &p{ - parser: m.Parser(), - renderer: m.Renderer(), - } -} - -func (p *p) Name() string { - return pluginName -} - -func (p *p) Render(f fs.File, w io.Writer) error { - stat, err := f.Stat() - if err != nil || !strings.HasSuffix(stat.Name(), ".md") { - return errors.New("does not support file") - } - - src, err := io.ReadAll(f) - if err != nil { - return err - } - - txt := text.NewReader(src) - - ast := p.parser.Parse(txt) - - return p.renderer.Render(w, src, ast) -} diff --git a/blogo/plugins/multi_error_handler.go b/blogo/plugins/multi_error_handler.go deleted file mode 100644 index 23a4648..0000000 --- a/blogo/plugins/multi_error_handler.go +++ /dev/null @@ -1,111 +0,0 @@ -// 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 plugins - -import ( - "io" - "log/slog" - - "forge.capytal.company/loreddev/x/blogo/plugin" - "forge.capytal.company/loreddev/x/tinyssert" -) - -const multiErrorHandlerName = "blogo-multierrorhandler-errorhandler" - -func NewMultiErrorHandler(opts ...MultiErrorHandlerOpts) MultiErrorHandler { - opt := MultiErrorHandlerOpts{} - 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{})) - } - - return &multiErrorHandler{ - handlers: []plugin.ErrorHandler{}, - - assert: opt.Assertions, - log: opt.Logger, - } -} - -type MultiErrorHandler interface { - plugin.ErrorHandler - plugin.WithPlugins -} - -type MultiErrorHandlerOpts struct { - Assertions tinyssert.Assertions - Logger *slog.Logger -} - -type multiErrorHandler struct { - handlers []plugin.ErrorHandler - - assert tinyssert.Assertions - log *slog.Logger -} - -func (h *multiErrorHandler) Name() string { - return multiErrorHandlerName -} - -func (h *multiErrorHandler) Use(p plugin.Plugin) { - h.assert.NotNil(h.handlers, "Error handlers slice should not be nil") - h.assert.NotNil(h.log) - - log := h.log.With(slog.String("plugin", p.Name())) - log.Debug("Adding plugin") - - if p, ok := p.(plugin.Group); ok { - log.Debug("Plugin is a group, using children plugins") - for _, p := range p.Plugins() { - h.Use(p) - } - } - - if p, ok := p.(plugin.ErrorHandler); ok { - h.handlers = append(h.handlers, p) - } else { - log.Debug("Plugin does not implement ErrorHandler, ignoring") - } -} - -func (h *multiErrorHandler) Handle(err error) (recovr any, handled bool) { - h.assert.NotNil(h.handlers, "Error handlers slice should not be nil") - h.assert.NotNil(h.log) - - log := h.log.With(slog.String("err", err.Error())) - log.Debug("Handling error") - - for _, handler := range h.handlers { - log := log.With(slog.String("plugin", handler.Name())) - log.Debug("Handling error with plugin") - - recovr, ok := handler.Handle(err) - if ok { - log.Debug("Error successfully handled with plugin") - return recovr, ok - } - } - - log.Debug("Failed to handle error with any plugin") - return nil, false -} diff --git a/blogo/plugins/multi_renderer.go b/blogo/plugins/multi_renderer.go deleted file mode 100644 index b1f5834..0000000 --- a/blogo/plugins/multi_renderer.go +++ /dev/null @@ -1,176 +0,0 @@ -// 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 plugins - -import ( - "bytes" - "errors" - "fmt" - "io" - "io/fs" - "log/slog" - - "forge.capytal.company/loreddev/x/blogo/plugin" - "forge.capytal.company/loreddev/x/tinyssert" -) - -const multiRendererName = "blogo-multirenderer-renderer" - -func NewMultiRenderer(opts ...MultiRendererOpts) MultiRenderer { - opt := MultiRendererOpts{} - 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{})) - } - - return &multiRenderer{ - plugins: []plugin.Renderer{}, - - assert: opt.Assertions, - log: opt.Logger, - } -} - -type MultiRenderer interface { - plugin.Renderer - plugin.WithPlugins -} - -type MultiRendererOpts struct { - Assertions tinyssert.Assertions - Logger *slog.Logger -} - -type multiRenderer struct { - plugins []plugin.Renderer - - assert tinyssert.Assertions - log *slog.Logger -} - -func (r *multiRenderer) Name() string { - return multiRendererName -} - -func (r *multiRenderer) Use(p plugin.Plugin) { - r.assert.NotNil(p) - r.assert.NotNil(r.plugins) - r.assert.NotNil(r.log) - - log := r.log.With(slog.String("plugin", p.Name())) - - if p, ok := p.(plugin.Group); ok { - log.Debug("Plugin is a group, using children plugins") - for _, p := range p.Plugins() { - r.Use(p) - } - } - - if pr, ok := p.(plugin.Renderer); ok { - log.Debug("Added renderer plugin") - r.plugins = append(r.plugins, pr) - } else { - log.Error(fmt.Sprintf( - "Failed to add plugin %q, since it doesn't implement plugin.Renderer", - p.Name(), - )) - } -} - -func (r *multiRenderer) Render(src fs.File, w io.Writer) error { - r.assert.NotNil(r.plugins) - r.assert.NotNil(r.log) - r.assert.NotNil(src) - r.assert.NotNil(w) - - log := r.log.With() - - if len(r.plugins) == 0 { - log.Debug("No renderers found, copying file contents to writer") - - _, err := io.Copy(w, src) - return err - } - - mf := newMultiRendererFile(src) - - for _, pr := range r.plugins { - log := log.With(slog.String("plugin", pr.Name())) - - log.Debug("Trying to render with plugin") - err := pr.Render(src, w) - - if err == nil { - break - } - - 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 -} diff --git a/blogo/plugins/multi_sourcer.go b/blogo/plugins/multi_sourcer.go deleted file mode 100644 index 307cb1f..0000000 --- a/blogo/plugins/multi_sourcer.go +++ /dev/null @@ -1,176 +0,0 @@ -// 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 plugins - -import ( - "errors" - "fmt" - "io" - "io/fs" - "log/slog" - - "forge.capytal.company/loreddev/x/blogo/metadata" - "forge.capytal.company/loreddev/x/blogo/plugin" - "forge.capytal.company/loreddev/x/tinyssert" -) - -const multiSourcerName = "blogo-multisourcer-sourcer" - -func NewMultiSourcer(opts ...MultiSourcerOpts) MultiSourcer { - opt := MultiSourcerOpts{} - 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{})) - } - - return &multiSourcer{ - plugins: []plugin.Sourcer{}, - - skipOnSourceError: opt.SkipOnSourceError, - skipOnFSError: opt.SkipOnFSError, - - log: opt.Logger, - } -} - -type MultiSourcer interface { - plugin.Sourcer - plugin.WithPlugins -} - -type MultiSourcerOpts struct { - SkipOnSourceError bool - SkipOnFSError bool - - Assertions tinyssert.Assertions - Logger *slog.Logger -} - -type multiSourcer struct { - plugins []plugin.Sourcer - - skipOnSourceError bool - skipOnFSError bool - - assert tinyssert.Assertions - log *slog.Logger -} - -func (s *multiSourcer) Name() string { - return multiSourcerName -} - -func (s *multiSourcer) Use(p plugin.Plugin) { - s.assert.NotNil(p) - s.assert.NotNil(s.plugins) - s.assert.NotNil(s.log) - - log := s.log.With(slog.String("plugin", p.Name())) - - if p, ok := p.(plugin.Group); ok { - log.Debug("Plugin is a group, using children plugins") - for _, p := range p.Plugins() { - s.Use(p) - } - } - - if plg, ok := p.(plugin.Sourcer); ok { - log.Debug("Added sourcer plugin") - s.plugins = append(s.plugins, plg) - } else { - log.Error(fmt.Sprintf( - "Failed to add plugin %q, since it doesn't implement plugin.Sourcer", - p.Name(), - )) - } -} - -func (s *multiSourcer) Source() (fs.FS, error) { - s.assert.NotNil(s.plugins) - s.assert.NotNil(s.log) - - log := s.log.With() - - fileSystems := []fs.FS{} - - for _, ps := range s.plugins { - log = log.With(slog.String("plugin", ps.Name())) - log.Info("Sourcing file system of plugin") - - f, err := ps.Source() - if err != nil && s.skipOnSourceError { - log.Warn( - "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)) - for i := range f { - f[i] = fileSystems[i] - } - - return &multiSourcerFS{ - fileSystems: f, - skipOnError: s.skipOnFSError, - }, nil -} - -type multiSourcerFS struct { - fileSystems []fs.FS - skipOnError bool -} - -func (pf *multiSourcerFS) Metadata() metadata.Metadata { - ms := []metadata.Metadata{} - for _, v := range pf.fileSystems { - if m, err := metadata.GetMetadata(v); err == nil { - ms = append(ms, m) - } - } - return metadata.Join(ms...) -} - -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 -} diff --git a/blogo/plugins/not_found_error_handler.go b/blogo/plugins/not_found_error_handler.go deleted file mode 100644 index 24aacae..0000000 --- a/blogo/plugins/not_found_error_handler.go +++ /dev/null @@ -1,132 +0,0 @@ -// 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 plugins - -import ( - "errors" - "html/template" - "io" - "io/fs" - "log/slog" - "net/http" - - "forge.capytal.company/loreddev/x/blogo/core" - "forge.capytal.company/loreddev/x/blogo/plugin" - "forge.capytal.company/loreddev/x/tinyssert" -) - -const notFoundErrorHandlerName = "blogo-notfounderrorhandler-errorhandler" - -func NewNotFoundErrorHandler( - templt template.Template, - opts ...TemplateErrorHandlerOpts, -) plugin.ErrorHandler { - opt := TemplateErrorHandlerOpts{} - 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{})) - } - - return ¬FoundErrorHandler{ - templt: templt, - - assert: opt.Assertions, - log: opt.Logger, - } -} - -type NotFoundErrorHandlerOpts struct { - Assertions tinyssert.Assertions - Logger *slog.Logger -} - -type NotFoundErrorHandlerInfo struct { - Plugin string - Path string - FilePath string - Error error - ErrorMsg string -} - -type notFoundErrorHandler struct { - templt template.Template - - assert tinyssert.Assertions - log *slog.Logger -} - -func (h *notFoundErrorHandler) Name() string { - return notFoundErrorHandlerName -} - -func (h *notFoundErrorHandler) Handle(err error) (recovr any, handled bool) { - h.assert.NotNil(err, "Error should not be nil") - h.assert.NotNil(h.templt, "notFound should not be nil") - h.assert.NotNil(h.log) - - log := h.log.With(slog.String("err", err.Error())) - - var serr core.ServeError - if !errors.As(err, &serr) { - log.Debug("Error is not a core.ServeError, ignoring error") - return nil, false - } - - log = h.log.With(slog.String("serveerr", serr.Error())) - - var sourceErr core.SourceError - if !errors.As(serr.Err, &sourceErr) { - log.Debug("Error is not a core.SourceError, ignoring error") - return nil, false - } - - log = h.log.With(slog.String("sourceerr", sourceErr.Error())) - - pathErr, ok := sourceErr.Err.(*fs.PathError) - if !ok { - log.Debug("Error is not a *fs.PathError, ignoring error") - return nil, false - } else if pathErr.Err != fs.ErrNotExist { - log.Debug("Error is not fs.ErrNotExist, ignoring error") - return nil, false - } - - log = h.log.With(slog.String("patherr", pathErr.Error())) - - log.Debug("Handling error") - - w, r := serr.Res, serr.Req - - w.WriteHeader(http.StatusNotFound) - if err := h.templt.Execute(w, NotFoundErrorHandlerInfo{ - Plugin: sourceErr.Sourcer.Name(), - Path: r.URL.Path, - FilePath: pathErr.Path, - Error: serr.Err, - ErrorMsg: serr.Err.Error(), - }); err != nil { - log.Error("Failed to execute notFound and respond error") - return nil, false - } - - return nil, true -} diff --git a/blogo/plugins/plain_text.go b/blogo/plugins/plain_text.go deleted file mode 100644 index e2171f6..0000000 --- a/blogo/plugins/plain_text.go +++ /dev/null @@ -1,86 +0,0 @@ -// 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 plugins - -import ( - "errors" - "fmt" - "io" - "io/fs" - - "forge.capytal.company/loreddev/x/blogo/plugin" - "forge.capytal.company/loreddev/x/tinyssert" -) - -const plainTextName = "blogo-plaintext-renderer" - -func NewPlainText(opts ...PlainTextOpts) plugin.Renderer { - opt := PlainTextOpts{} - if len(opts) > 0 { - opt = opts[0] - } - - if opt.Assertions == nil { - opt.Assertions = tinyssert.NewDisabledAssertions() - } - - return &painText{ - assert: opt.Assertions, - } -} - -type PlainTextOpts struct { - Assertions tinyssert.Assertions -} - -type painText struct { - assert tinyssert.Assertions -} - -func (p *painText) Name() string { - return plainTextName -} - -func (p *painText) Render(src fs.File, w io.Writer) error { - p.assert.NotNil(src) - p.assert.NotNil(w) - - if d, ok := src.(fs.ReadDirFile); ok { - return p.renderDirectory(d, w) - } - - _, err := io.Copy(w, src) - return err -} - -func (p *painText) renderDirectory(f fs.ReadDirFile, w io.Writer) error { - es, err := f.ReadDir(-1) - if err != nil { - return err - } - - for _, e := range es { - _, err := w.Write([]byte(fmt.Sprintf("%s\n", e.Name()))) - if err != nil { - return errors.Join( - fmt.Errorf("failed to write directory file list, file %s", e.Name()), - err, - ) - } - } - - return nil -} diff --git a/blogo/plugins/plugins.go b/blogo/plugins/plugins.go deleted file mode 100644 index d95b720..0000000 --- a/blogo/plugins/plugins.go +++ /dev/null @@ -1,16 +0,0 @@ -// 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 plugins diff --git a/blogo/plugins/prefixed_sourcer.go b/blogo/plugins/prefixed_sourcer.go deleted file mode 100644 index e7b47be..0000000 --- a/blogo/plugins/prefixed_sourcer.go +++ /dev/null @@ -1,186 +0,0 @@ -// 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 plugins - -import ( - "fmt" - "io" - "io/fs" - "log/slog" - "strings" - - "forge.capytal.company/loreddev/x/blogo/metadata" - "forge.capytal.company/loreddev/x/blogo/plugin" - "forge.capytal.company/loreddev/x/tinyssert" -) - -const prefixedSourcerName = "blogo-prefixedsourcer-sourcer" - -func NewPrefixedSourcer(opts ...PrefixedSourcerOpts) PrefixedSourcer { - opt := PrefixedSourcerOpts{} - if len(opts) > 0 { - opt = opts[0] - } - - if opt.PrefixSeparator == "" { - opt.PrefixSeparator = "/" - } - - if opt.Assertions == nil { - opt.Assertions = tinyssert.NewDisabledAssertions() - } - if opt.Logger == nil { - opt.Logger = slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) - } - - return &prefixedSourcer{ - plugins: map[string]plugin.Sourcer{}, - - prefixSeparator: opt.PrefixSeparator, - acceptDuplicated: opt.AcceptDuplicated, - - skipOnSourceError: opt.SkipOnSourceError, - skipOnFSError: opt.SkipOnFSError, - - assert: opt.Assertions, - log: opt.Logger, - } -} - -type PrefixedSourcerOpts struct { - PrefixSeparator string - AcceptDuplicated bool - - SkipOnSourceError bool - SkipOnFSError bool - - Assertions tinyssert.Assertions - Logger *slog.Logger -} - -type PrefixedSourcer interface { - plugin.Sourcer - plugin.WithPlugins - UseNamed(string, plugin.Plugin) -} - -type prefixedSourcer struct { - plugins map[string]plugin.Sourcer - - prefixSeparator string - acceptDuplicated bool - - skipOnSourceError bool - skipOnFSError bool - - assert tinyssert.Assertions - log *slog.Logger -} - -func (s *prefixedSourcer) Name() string { - return prefixedSourcerName -} - -func (s *prefixedSourcer) Use(plugin plugin.Plugin) { - s.UseNamed(plugin.Name(), plugin) -} - -func (s *prefixedSourcer) UseNamed(prefix string, p plugin.Plugin) { - s.assert.NotZero(prefix, "Prefix of plugin should not be empty") - s.assert.NotNil(p) - s.assert.NotNil(s.plugins) - s.assert.NotNil(s.log) - - log := s.log.With(slog.String("plugin", p.Name()), slog.String("prefix", prefix)) - log.Debug("Adding plugin") - - var sourcer plugin.Sourcer - if ps, ok := p.(plugin.Sourcer); ok { - sourcer = ps - } else { - log.Error(fmt.Sprintf( - "Failed to add plugin %q (with prefix %q), since it doesn't implement SourcerPlugin", - p.Name(), prefix, - )) - return - } - - if _, ok := s.plugins[prefix]; ok && !s.acceptDuplicated { - log.Error("Duplicated prefix, skipping plugin") - return - } - - s.plugins[prefix] = sourcer -} - -func (s *prefixedSourcer) Source() (fs.FS, error) { - s.assert.NotNil(s.plugins) - s.assert.NotNil(s.log) - - log := s.log.With() - - fileSystems := make(map[string]fs.FS, len(s.plugins)) - - for a, ps := range s.plugins { - log = log.With(slog.String("plugin", ps.Name()), slog.String("prefix", a)) - log.Info("Sourcing file system of plugin") - - f, err := ps.Source() - if err != nil && s.skipOnSourceError { - log.Warn("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: s.prefixSeparator, - }, nil -} - -type prefixedSourcerFS struct { - fileSystems map[string]fs.FS - prefixSeparator string -} - -func (pf *prefixedSourcerFS) Metadata() metadata.Metadata { - ms := []metadata.Metadata{} - for _, v := range pf.fileSystems { - if m, err := metadata.GetMetadata(v); err == nil { - ms = append(ms, m) - } - } - return metadata.Join(ms...) -} - -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} -} diff --git a/blogo/plugins/priority_list.go b/blogo/plugins/priority_list.go deleted file mode 100644 index 5f4b33e..0000000 --- a/blogo/plugins/priority_list.go +++ /dev/null @@ -1,72 +0,0 @@ -// 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 plugins - -import ( - "cmp" - "slices" - - "forge.capytal.company/loreddev/x/blogo/plugin" -) - -const priorityGroupName = "blogo-prioritygroup-group" - -func NewPriorityGroup(plugins ...plugin.Plugin) PriorityGroup { - return &priorityGroup{plugins} -} - -type PriorityGroup interface { - plugin.WithPlugins -} - -type priorityGroup struct { - plugins []plugin.Plugin -} - -func (p *priorityGroup) Name() string { - return priorityGroupName -} - -func (p *priorityGroup) Use(plugin plugin.Plugin) { - p.plugins = append(p.plugins, plugin) -} - -func (p *priorityGroup) Plugins() []plugin.Plugin { - slices.SortStableFunc(p.plugins, func(a plugin.Plugin, b plugin.Plugin) int { - return cmp.Compare(p.getPriority(a, b), p.getPriority(b, a)) - }) - return p.plugins -} - -func (p *priorityGroup) getPriority(plugin plugin.Plugin, cmp plugin.Plugin) int { - if plg, ok := plugin.(PluginWithDynamicPriority); ok { - return plg.Priority(cmp) - } else if plg, ok := plugin.(PluginWithPriority); ok { - return plg.Priority() - } else { - return 0 - } -} - -type PluginWithPriority interface { - plugin.Plugin - Priority() int -} - -type PluginWithDynamicPriority interface { - plugin.Plugin - Priority(plugin.Plugin) int -} diff --git a/blogo/plugins/template_error_handler.go b/blogo/plugins/template_error_handler.go deleted file mode 100644 index 639565b..0000000 --- a/blogo/plugins/template_error_handler.go +++ /dev/null @@ -1,106 +0,0 @@ -// 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 plugins - -import ( - "errors" - "html/template" - "io" - "log/slog" - "net/http" - - "forge.capytal.company/loreddev/x/blogo/core" - "forge.capytal.company/loreddev/x/blogo/plugin" - "forge.capytal.company/loreddev/x/tinyssert" -) - -const templateErrorHandlerName = "blogo-templateerrorhandler-errorhandler" - -func NewTemplateErrorHandler( - templt template.Template, - opts ...TemplateErrorHandlerOpts, -) plugin.ErrorHandler { - opt := TemplateErrorHandlerOpts{} - 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{})) - } - - return &templateErrorHandler{ - templt: templt, - - assert: opt.Assertions, - log: opt.Logger, - } -} - -type TemplateErrorHandlerOpts struct { - Assertions tinyssert.Assertions - Logger *slog.Logger -} - -type TemplateErrorHandlerInfo struct { - Path string - Error error - ErrorMsg string -} - -type templateErrorHandler struct { - templt template.Template - - assert tinyssert.Assertions - log *slog.Logger -} - -func (h *templateErrorHandler) Name() string { - return templateErrorHandlerName -} - -func (h *templateErrorHandler) Handle(err error) (recovr any, handled bool) { - h.assert.NotNil(err, "Error should not be nil") - h.assert.NotNil(h.templt, "Template should not be nil") - h.assert.NotNil(h.log) - - log := h.log.With(slog.String("err", err.Error())) - - var serr core.ServeError - if !errors.As(err, &serr) { - log.Debug("Error is not a core.ServeError, ignoring error") - return nil, false - } - - log.Debug("Handling error") - - w, r := serr.Res, serr.Req - - w.WriteHeader(http.StatusInternalServerError) - if err := h.templt.Execute(w, TemplateErrorHandlerInfo{ - Path: r.URL.Path, - Error: serr.Err, - ErrorMsg: serr.Err.Error(), - }); err != nil { - log.Error("Failed to execute template and respond error") - return nil, false - } - - return nil, true -}