diff --git a/smalltrip/LICENSE b/smalltrip/LICENSE deleted file mode 100644 index 57bc88a..0000000 --- a/smalltrip/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/smalltrip/README.md b/smalltrip/README.md deleted file mode 100644 index df030b1..0000000 --- a/smalltrip/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Smalltrip - -## License - -Copyright © 2025 Gustavo "Guz" L. de Mello • Copyright © 2025 The Lored.dev Contributors - -Licensed and distributed under the [Apache License (Version 2.0)](./LICENSE). - diff --git a/smalltrip/middleware/cache.go b/smalltrip/middleware/cache.go deleted file mode 100644 index f68b9c1..0000000 --- a/smalltrip/middleware/cache.go +++ /dev/null @@ -1,219 +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 middleware - -import ( - "fmt" - "net/http" - "strings" - "time" -) - -func Cache(options ...CacheOption) Middleware { - d := defaultCacheDirectives - - for _, option := range options { - option(&d) - } - - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Cache-Control", d.String()) - next.ServeHTTP(w, r) - }) - } -} - -func DisableCache() Middleware { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Cache-Control", "no-store") - next.ServeHTTP(w, r) - }) - } -} - -// TODO: SmartCache is a smarter implementation of Cache that handles requests -// with authorization, Cache-Control from the client, and others. -func SmartCache(options ...CacheOption) Middleware { - return Cache(options...) -} - -// TODO: PersistentCache is a smarter implementation of SmartCache that handles requests -// with authorization, Cache-Control from the client, and stores responses into -// a persistent storage solution like Redis. -func PersistentCache(options ...CacheOption) Middleware { - return SmartCache(options...) -} - -type CacheOption func(*directives) - -func CacheMaxAge(t time.Duration) CacheOption { - return func(d *directives) { d.maxAge = &t } -} - -func CacheSMaxAge(t time.Duration) CacheOption { - return func(d *directives) { d.sMaxage = &t } -} - -func CacheNoCache(b ...bool) CacheOption { - bool := optionalTrue(b) - return func(d *directives) { d.noCache = &bool } -} - -func CacheNoStore(b ...bool) CacheOption { - bool := optionalTrue(b) - return func(d *directives) { d.noStore = &bool } -} - -func CacheNoTransform(b ...bool) CacheOption { - bool := optionalTrue(b) - return func(d *directives) { d.noTransform = &bool } -} - -func CacheMustRevalidate(b ...bool) CacheOption { - bool := optionalTrue(b) - return func(d *directives) { d.mustRevalidate = &bool } -} - -func CacheProxyRevalidate(b ...bool) CacheOption { - bool := optionalTrue(b) - return func(d *directives) { d.proxyRevalidate = &bool } -} - -func CacheMustUnderstand(b ...bool) CacheOption { - bool := optionalTrue(b) - return func(d *directives) { d.mustUnderstand = &bool } -} - -func CachePrivate(b ...bool) CacheOption { - bool := optionalTrue(b) - return func(d *directives) { d.private = &bool } -} - -func CachePublic(b ...bool) CacheOption { - bool := optionalTrue(b) - return func(d *directives) { d.public = &bool } -} - -func CacheImmutable(b ...bool) CacheOption { - bool := optionalTrue(b) - return func(d *directives) { d.immutable = &bool } -} - -func CacheStaleWhileRevalidate(t time.Duration) CacheOption { - return func(d *directives) { d.staleWhileRevalidate = &t } -} - -func CacheStaleIfError(t time.Duration) CacheOption { - return func(d *directives) { d.staleIfError = &t } -} - -func optionalTrue(b []bool) bool { - bl := true - if len(b) > 0 { - bl = b[1] - } - return bl -} - -var ( - defaultCacheDirectives = directives{ - maxAge: &day, - sMaxage: &day, - - mustRevalidate: &tru, - private: &tru, - - staleWhileRevalidate: &twoDays, - staleIfError: &twoDays, - } - tru, fals = true, false - day = time.Duration(time.Hour * 24) - twoDays = time.Duration(time.Hour * 48) -) - -type directives struct { - maxAge *time.Duration - sMaxage *time.Duration - - noCache *bool - noStore *bool - noTransform *bool - - mustRevalidate *bool - proxyRevalidate *bool - mustUnderstand *bool - - private *bool - public *bool - immutable *bool - - staleWhileRevalidate *time.Duration - staleIfError *time.Duration -} - -var _ fmt.Stringer = directives{} - -func (d directives) String() string { - ds := []string{} - - if d.maxAge != nil { - ds = append(ds, fmt.Sprintf("max-age=%d", int(d.maxAge.Seconds()))) - } - if d.sMaxage != nil { - ds = append(ds, fmt.Sprintf("s-maxage=%d", int(d.sMaxage.Seconds()))) - } - - if d.noCache != nil && *d.noCache { - ds = append(ds, "no-cache") - } - if d.noStore != nil && *d.noStore { - ds = append(ds, "no-store") - } - if d.noTransform != nil && *d.noTransform { - ds = append(ds, "no-transform") - } - - if d.mustRevalidate != nil && *d.mustRevalidate { - ds = append(ds, "must-revalidate") - } - if d.proxyRevalidate != nil && *d.proxyRevalidate { - ds = append(ds, "proxy-revalidate") - } - if d.mustUnderstand != nil && *d.mustRevalidate { - ds = append(ds, "must-understand") - } - - if d.private != nil && *d.private { - ds = append(ds, "private") - } - if d.public != nil && *d.public { - ds = append(ds, "public") - } - if d.immutable != nil && *d.immutable { - ds = append(ds, "immutable") - } - - if d.staleWhileRevalidate != nil { - ds = append(ds, fmt.Sprintf("stale-while-revalidate=%d", int(d.staleWhileRevalidate.Seconds()))) - } - if d.staleIfError != nil { - ds = append(ds, fmt.Sprintf("stale-if-error=%d", int(d.staleIfError.Seconds()))) - } - - return strings.Join(ds, ", ") -} diff --git a/smalltrip/middleware/logger.go b/smalltrip/middleware/logger.go deleted file mode 100644 index c047877..0000000 --- a/smalltrip/middleware/logger.go +++ /dev/null @@ -1,93 +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 middleware - -import ( - "fmt" - "log/slog" - "math/rand" - "net/http" -) - -func Logger(logger *slog.Logger) Middleware { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - lw := &loggerResponseWriter{w, 0} - - addr := loggerGetAddr(r) - - log := logger.With( - slog.String("id", randHash(5)), - slog.String("method", fmt.Sprintf("%4s", r.Method)), - slog.String("addr", addr), - slog.String("path", r.URL.Path), - ) - - log.Debug("NEW REQUEST", slog.String("status", "000")) - - next.ServeHTTP(lw, r) - - log = log.With(slog.String("status", fmt.Sprintf("%3d", lw.statusCode))) - - switch { - case lw.statusCode >= 500: - log.Warn("ERR REQUEST") - case lw.statusCode >= 400: - log.Info("INV REQUEST") - case lw.statusCode >= 200: - log.Debug("END REQUEST") - default: - log.Debug("MSC REQUEST") - } - }) - } -} - -func loggerGetAddr(r *http.Request) string { - if i := r.Header.Get("CF-Connecting-IP"); i != "" { - return i - } - if i := r.Header.Get("X-Forwarded-For"); i != "" { - return i - } - if i := r.Header.Get("X-Real-IP"); i != "" { - return i - } - return r.RemoteAddr -} - -type loggerResponseWriter struct { - http.ResponseWriter - statusCode int -} - -func (w *loggerResponseWriter) WriteHeader(s int) { - w.statusCode = s - w.ResponseWriter.WriteHeader(s) -} - -const hashChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - -// This is not the most performant function, as a TODO we could -// improve based on this Stackoberflow thread: -// https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go -func randHash(n int) string { - b := make([]byte, n) - for i := range b { - b[i] = hashChars[rand.Int63()%int64(len(hashChars))] - } - return string(b) -} diff --git a/smalltrip/middleware/middleware.go b/smalltrip/middleware/middleware.go deleted file mode 100644 index b5520a3..0000000 --- a/smalltrip/middleware/middleware.go +++ /dev/null @@ -1,20 +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 middleware - -import "net/http" - -type Middleware = func(next http.Handler) http.Handler diff --git a/smalltrip/middleware/request.go b/smalltrip/middleware/request.go deleted file mode 100644 index 79032ff..0000000 --- a/smalltrip/middleware/request.go +++ /dev/null @@ -1,17 +0,0 @@ -package middleware - -import ( - "net/http" - "strings" -) - -func FormMethod(key string) Middleware { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if v := r.FormValue(key); v != "" { - r.Method = strings.ToUpper(v) - } - next.ServeHTTP(w, r) - }) - } -} diff --git a/smalltrip/multiplexer/multiplexer.go b/smalltrip/multiplexer/multiplexer.go deleted file mode 100644 index 306c65d..0000000 --- a/smalltrip/multiplexer/multiplexer.go +++ /dev/null @@ -1,29 +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 multiplexer - -import "net/http" - -type Multiplexer interface { - HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) - Handle(pattern string, handler http.Handler) - Handler(r *http.Request) (h http.Handler, pattern string) - http.Handler -} - -func New() Multiplexer { - return http.NewServeMux() -} diff --git a/smalltrip/multiplexer/patterns.go b/smalltrip/multiplexer/patterns.go deleted file mode 100644 index 08162a2..0000000 --- a/smalltrip/multiplexer/patterns.go +++ /dev/null @@ -1,153 +0,0 @@ -package multiplexer - -import ( - "fmt" - "net/http" - "slices" - "strings" -) - -func WithPatternsOptions(mux Multiplexer, opts ...PatternOption) Multiplexer { - return &patternsMux{opts: opts, Multiplexer: mux} -} - -func AddTrailingSlash() PatternOption { - return func(s string) string { - if strings.HasSuffix(s, "...}") { - return s - } - // If the pattern is /image.html{$}, we modify it to be /image.html/{$} - if strings.HasSuffix(s, "{$}") && !strings.HasSuffix(s, "/{$}") { - return strings.TrimSuffix(s, "{$}") + "/{$}" - } - if !strings.HasSuffix(s, "/") { - return s + "/" - } - return s - } -} - -func RemoveTrailingSlash() PatternOption { - return func(s string) string { - s = strings.TrimSuffix(s, "/") - if strings.HasSuffix(s, "/{$}") { - return strings.TrimSuffix(s, "/{$}") + "{$}" - } - return s - } -} - -func AddStrictEnd() PatternOption { - return func(s string) string { - if strings.HasSuffix(s, "{$}") { - return s - } - return s + "{$}" - } -} - -func RemoteStrictEnd() PatternOption { - return func(s string) string { - return strings.TrimSuffix(s, "{$}") - } -} - -type PatternOption func(string) string - -func WithPatternRules(mux Multiplexer, rules ...PatternRule) Multiplexer { - opts := make([]PatternOption, len(rules)) - for i, r := range rules { - opts[i] = func(s string) string { r(s); return s } - } - return &patternsMux{Multiplexer: mux, opts: opts} -} - -func NoTrailingSlash() PatternRule { - return func(s string) { - if strings.HasSuffix(s, "/") || strings.HasSuffix(s, "/{$}") { - panic(fmt.Sprintf("no-trailing-slash: pattern %q has trailing slash", s)) - } - } -} - -func EnsureTrailingSlash() PatternRule { - return func(s string) { - if !strings.HasSuffix(s, "/{$}") && !strings.HasSuffix(s, "/") && !strings.HasSuffix(s, "...}") { - panic(fmt.Sprintf("trailing-slash: pattern %q doesn't has a trailing slash", s)) - } - } -} - -func NoMethod() PatternRule { - return func(s string) { - if len(strings.Split(s, " ")) > 1 { - panic(fmt.Sprintf("no-method: pattern %q has a method", s)) - } - } -} - -func EnsureMethod(methods ...string) PatternRule { - if len(methods) == 0 && methods != nil { - methods = DefaultMethods - } - return func(s string) { - sp := strings.Split(s, " ") - if len(sp) <= 0 { - panic(fmt.Sprintf("method: pattern %q doesn't has a method", s)) - } - if methods != nil { - if slices.Contains(methods, sp[0]) { - panic(fmt.Sprintf("method: pattern %q doesn't has a valid method, valid methods are: %s", s, strings.Join(methods, ", "))) - } - } - } -} - -var DefaultMethods = []string{ - http.MethodConnect, - http.MethodDelete, - http.MethodGet, - http.MethodHead, - http.MethodOptions, - http.MethodPatch, - http.MethodPost, - http.MethodPut, - http.MethodTrace, -} - -func EnsureStrictEnd() PatternRule { - return func(s string) { - if !strings.HasSuffix(s, "{$}") && !strings.HasSuffix(s, "...}") { - panic(fmt.Sprintf(`strict-end: pattern %q doesn't end with "{$}"`, s)) - } - } -} - -func NoStrictEnd() PatternRule { - return func(s string) { - if strings.HasSuffix(s, "{$}") { - panic(fmt.Sprintf(`no-strict-end: pattern %q ends with "{$}"`, s)) - } - } -} - -type PatternRule func(string) - -type patternsMux struct { - opts []PatternOption - Multiplexer -} - -func (pm *patternsMux) HandleFunc(p string, h func(http.ResponseWriter, *http.Request)) { - for _, po := range pm.opts { - p = po(p) - } - pm.Multiplexer.HandleFunc(p, h) -} - -func (pm *patternsMux) Handle(p string, h http.Handler) { - for _, po := range pm.opts { - p = po(p) - } - pm.Multiplexer.Handle(p, h) -} diff --git a/smalltrip/multiplexer/request.go b/smalltrip/multiplexer/request.go deleted file mode 100644 index 6b287d9..0000000 --- a/smalltrip/multiplexer/request.go +++ /dev/null @@ -1,22 +0,0 @@ -package multiplexer - -import ( - "net/http" - "strings" -) - -func WithFormMethod(mux Multiplexer, key string) Multiplexer { - return &formMethodMux{key: key, Multiplexer: mux} -} - -type formMethodMux struct { - key string - Multiplexer -} - -func (mux *formMethodMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if v := r.FormValue(mux.key); v != "" { - r.Method = strings.ToUpper(v) - } - mux.Multiplexer.ServeHTTP(w, r) -} diff --git a/smalltrip/options.go b/smalltrip/options.go deleted file mode 100644 index 0907ebd..0000000 --- a/smalltrip/options.go +++ /dev/null @@ -1,43 +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 smalltrip - -import ( - "log/slog" - - "forge.capytal.company/loreddev/x/smalltrip/middleware" - "forge.capytal.company/loreddev/x/smalltrip/multiplexer" -) - -type Option func(*router) - -func WithLogger(logger *slog.Logger) Option { - return func(r *router) { - r.log = logger - } -} - -func WithMultiplexer(mux multiplexer.Multiplexer) Option { - return func(r *router) { - r.mux = mux - } -} - -func WithMiddleware(m middleware.Middleware) Option { - return func(r *router) { - r.Use(m) - } -} diff --git a/smalltrip/problem/400.go b/smalltrip/problem/400.go deleted file mode 100644 index ba58622..0000000 --- a/smalltrip/problem/400.go +++ /dev/null @@ -1,337 +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 problem - -import ( - "fmt" - "net/http" - "strings" - "time" -) - -func NewBadRequest(detail string, opts ...Option) BadRequest { - return BadRequest{NewDetailed(http.StatusBadRequest, detail, opts...)} -} - -type BadRequest struct{ RegisteredProblem } - -func NewUnauthorized(scheme AuthScheme, opts ...Option) Unauthorized { - return Unauthorized{ - RegisteredProblem: NewDetailed( - http.StatusUnauthorized, - fmt.Sprintf("You must authenticate using the %q scheme", scheme.Title), - opts..., - ), - Authentication: scheme, - } -} - -type Unauthorized struct { - RegisteredProblem - Authentication AuthScheme `json:"authentication,omitempty" xml:"authentication,omitempty"` -} - -func (p Unauthorized) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if p.Authentication.Title != "" { - w.Header().Set("WWW-Authenticate", p.Authentication.Title) - } - p.Handler(p).ServeHTTP(w, r) -} - -func NewPaymentRequired(opts ...Option) PaymentRequired { - return PaymentRequired{NewStatus(http.StatusPaymentRequired, opts...)} -} - -type PaymentRequired struct{ RegisteredProblem } - -func NewForbidden(opts ...Option) Forbidden { - return Forbidden{NewStatus(http.StatusForbidden, opts...)} -} - -type Forbidden struct{ RegisteredProblem } - -func NewNotFound(opts ...Option) NotFound { - return NotFound{NewStatus(http.StatusNotFound, opts...)} -} - -type NotFound struct{ RegisteredProblem } - -func NewMethodNotAllowed[T string | []string](allow T, opts ...Option) MethodNotAllowed { - p := MethodNotAllowed{ - RegisteredProblem: NewStatus(http.StatusMethodNotAllowed, opts...), - } - if as, ok := any(allow).([]string); ok { - p.Allowed = as - } else { - p.Allowed = []string{any(allow).(string)} - } - return p -} - -type MethodNotAllowed struct { - RegisteredProblem - Allowed []string `json:"allowed,omitempty" xml:"allowed,omitempty"` -} - -func (p MethodNotAllowed) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if len(p.Allowed) > 0 { - w.Header().Set("Allow", strings.Join(p.Allowed, ", ")) - } - p.Handler(p).ServeHTTP(w, r) -} - -func NewNotAcceptable[T string | []string](header NegotiationHeader, allow T, opts ...Option) NotAcceptable { - p := NotAcceptable{ - RegisteredProblem: NewDetailed(http.StatusMethodNotAllowed, fmt.Sprintf( - "Cannot provide a response matching the list of acceptable values defined by %q header", header), - opts..., - ), - } - if as, ok := any(allow).([]string); ok { - p.Allowed = as - } else { - p.Allowed = []string{any(allow).(string)} - } - return p -} - -type NotAcceptable struct { - RegisteredProblem - Allowed []string `json:"allowed,omitempty" xml:"allowed,omitempty"` -} - -func (p NotAcceptable) ServeHTTP(w http.ResponseWriter, r *http.Request) { - p.Handler(p).ServeHTTP(w, r) -} - -const ( - NegotiationHeaderAccept NegotiationHeader = "Accept" - NegotiationHeaderAcceptEncoding NegotiationHeader = "AcceptEncoding" - NegotiationHeaderAcceptLanguage NegotiationHeader = "AcceptLanguage" -) - -type NegotiationHeader string - -func NewProxyAuthRequired(scheme AuthScheme, opts ...Option) ProxyAuthRequired { - return ProxyAuthRequired{ - RegisteredProblem: NewDetailed( - http.StatusProxyAuthRequired, - fmt.Sprintf("You must authenticate on the proxy using the %q scheme", scheme.Title), - opts..., - ), - Authentication: scheme, - } -} - -type ProxyAuthRequired struct { - RegisteredProblem - Authentication AuthScheme `json:"authentication,omitempty" xml:"authentication,omitempty"` -} - -func (p ProxyAuthRequired) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if p.Authentication.Title != "" { - w.Header().Set("Proxy-Authenticate", p.Authentication.Title) - } - p.Handler(p).ServeHTTP(w, r) -} - -var ( - AuthSchemeBasic = AuthScheme{Title: "Basic", Type: "https://datatracker.ietf.org/doc/html/rfc7617"} - AuthSchemeBearer = AuthScheme{Title: "Bearer", Type: "https://datatracker.ietf.org/doc/html/rfc6750"} - AuthSchemeDigest = AuthScheme{Title: "Digest", Type: "https://datatracker.ietf.org/doc/html/rfc7616"} - AuthSchemeHOBA = AuthScheme{Title: "HOBA", Type: "https://datatracker.ietf.org/doc/html/rfc7486"} - AuthSchemeMutual = AuthScheme{Title: "Mutual", Type: "https://datatracker.ietf.org/doc/html/rfc8120"} - AuthSchemeNegotiate = AuthScheme{Title: "Negotiate", Type: "https://datatracker.ietf.org/doc/html/rfc4599"} - AuthSchemeVAPID = AuthScheme{Title: "VAPID", Type: "https://datatracker.ietf.org/doc/html/rfc8292"} - AuthSchemeSCRAM = AuthScheme{Title: "SCRAM", Type: "https://datatracker.ietf.org/doc/html/rfc8292"} - AuthSchemeAWS4HMACSHA256 = AuthScheme{Title: "AWS4-HMAC-SHA256", Type: "https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html"} -) - -type AuthScheme struct { - Type string `json:"type,omitempty" xml:"type,omitempty"` - Title string `json:"title,omitempty" xml:"title,omitempty"` -} - -func NewRequestTimeout(opts ...Option) RequestTimeout { - return RequestTimeout{NewStatus(http.StatusRequestTimeout, opts...)} -} - -type RequestTimeout struct{ RegisteredProblem } - -func (p RequestTimeout) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Connection", "close") - p.Handler(p).ServeHTTP(w, r) -} - -func NewConflict(opts ...Option) Conflict { - return Conflict{NewStatus(http.StatusConflict, opts...)} -} - -type Conflict struct{ RegisteredProblem } - -func NewGone(opts ...Option) Gone { - return Gone{NewStatus(http.StatusGone, opts...)} -} - -type Gone struct{ RegisteredProblem } - -func NewLengthRequired(opts ...Option) LengthRequired { - return LengthRequired{NewStatus(http.StatusLengthRequired, opts...)} -} - -type LengthRequired struct{ RegisteredProblem } - -func NewPreconditionFailed(opts ...Option) PreconditionFailed { - return PreconditionFailed{NewStatus(http.StatusPreconditionFailed, opts...)} -} - -type PreconditionFailed struct{ RegisteredProblem } - -func NewContentTooLarge(opts ...Option) ContentTooLarge { - return ContentTooLarge{NewStatus(http.StatusRequestEntityTooLarge, opts...)} -} - -type ContentTooLarge struct{ RegisteredProblem } - -func NewURITooLong(opts ...Option) URITooLong { - return URITooLong{NewStatus(http.StatusRequestURITooLong, opts...)} -} - -type URITooLong struct{ RegisteredProblem } - -func NewUnsupportedMediaType(opts ...Option) UnsupportedMediaType { - return UnsupportedMediaType{NewStatus(http.StatusUnsupportedMediaType, opts...)} -} - -type UnsupportedMediaType struct{ RegisteredProblem } - -func NewRangeNotSatisfiable(unit string, contentRange int, opts ...Option) RangeNotSatisfiable { - return RangeNotSatisfiable{ - RegisteredProblem: NewStatus(http.StatusGone, opts...), - Range: fmt.Sprintf("%s */%d", unit, contentRange), - } -} - -type RangeNotSatisfiable struct { - RegisteredProblem - Range string `json:"range" xml:"range"` -} - -func (p RangeNotSatisfiable) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Range", p.Range) - p.Handler(p).ServeHTTP(w, r) -} - -func NewExpectationFailed(opts ...Option) ExpectationFailed { - return ExpectationFailed{NewStatus(http.StatusExpectationFailed, opts...)} -} - -type ExpectationFailed struct{ RegisteredProblem } - -func NewTeapot(opts ...Option) Teapot { - return Teapot{NewStatus(http.StatusTeapot, opts...)} -} - -type Teapot struct{ RegisteredProblem } - -func NewMisdirectedRequest(opts ...Option) MisdirectedRequest { - return MisdirectedRequest{NewStatus(http.StatusMisdirectedRequest, opts...)} -} - -type MisdirectedRequest struct{ RegisteredProblem } - -func NewUnprocessableContent(opts ...Option) UnprocessableContent { - return UnprocessableContent{NewStatus(http.StatusUnprocessableEntity, opts...)} -} - -type UnprocessableContent struct{ RegisteredProblem } - -// TODO?: Should the response of this be different and follow WebDAV's XML format? -func NewLocked(opts ...Option) Locked { - return Locked{NewStatus(http.StatusLocked, opts...)} -} - -type Locked struct{ RegisteredProblem } - -func NewFailedDependency(opts ...Option) FailedDependency { - return FailedDependency{NewStatus(http.StatusFailedDependency, opts...)} -} - -type FailedDependency struct{ RegisteredProblem } - -func NewTooEarly(opts ...Option) TooEarly { - return TooEarly{NewStatus(http.StatusTooEarly, opts...)} -} - -type TooEarly struct{ RegisteredProblem } - -func NewUpgradeRequired(protocol Protocol, opts ...Option) UpgradeRequired { - return UpgradeRequired{ - RegisteredProblem: NewStatus(http.StatusUpgradeRequired, opts...), - Protocol: protocol, - } -} - -type UpgradeRequired struct { - RegisteredProblem - Protocol Protocol `json:"protocol" xml:"protocol"` -} - -func (p UpgradeRequired) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Upgrade", string(p.Protocol)) - p.Handler(p).ServeHTTP(w, r) -} - -const ( - Http1_1 Protocol = "HTTP/1.1" - Http2_0 Protocol = "HTTP/2.0" - Http3_0 Protocol = "HTTP/3.0" -) - -type Protocol string - -func NewPreconditionRequired(opts ...Option) PreconditionRequired { - return PreconditionRequired{NewStatus(http.StatusPreconditionRequired, opts...)} -} - -type PreconditionRequired struct{ RegisteredProblem } - -func NewTooManyRequests[T time.Time | time.Duration](retryAfter T, opts ...Option) TooManyRequests[T] { - p := NewStatus(http.StatusTooManyRequests, opts...) - return TooManyRequests[T]{RegisteredProblem: p, RetryAfter: RetryAfter[T]{time: retryAfter}} -} - -type TooManyRequests[T time.Time | time.Duration] struct { - RegisteredProblem - RetryAfter RetryAfter[T] `json:"retryAfter" xml:"retry-after"` -} - -func (p TooManyRequests[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Retry-After", p.RetryAfter.String()) - p.Handler(p).ServeHTTP(w, r) -} - -func NewRequestHeaderFieldsTooLarge(opts ...Option) RequestHeaderFieldsTooLarge { - return RequestHeaderFieldsTooLarge{NewStatus(http.StatusRequestHeaderFieldsTooLarge, opts...)} -} - -type RequestHeaderFieldsTooLarge struct{ RegisteredProblem } - -func NewUnavailableForLegalReasons(opts ...Option) UnavailableForLegalReasons { - return UnavailableForLegalReasons{NewStatus(http.StatusUnavailableForLegalReasons, opts...)} -} - -type UnavailableForLegalReasons struct{ RegisteredProblem } diff --git a/smalltrip/problem/500.go b/smalltrip/problem/500.go deleted file mode 100644 index ef61c05..0000000 --- a/smalltrip/problem/500.go +++ /dev/null @@ -1,152 +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 problem - -import ( - "encoding/xml" - "errors" - "net/http" - "time" -) - -func NewInternalServerError(err error, opts ...Option) InternalServerError { - return InternalServerError{ - RegisteredProblem: NewDetailed(http.StatusInternalServerError, err.Error(), opts...), - Errors: newErrorTree(err).Errors, - error: err, - } -} - -type InternalServerError struct { - RegisteredProblem - Errors []ErrorTree `json:"errors" xml:"errors"` - - error error `json:"-" xml:"-"` -} - -var _ error = InternalServerError{} - -func (p InternalServerError) ServeHTTP(w http.ResponseWriter, r *http.Request) { - p.Handler(p).ServeHTTP(w, r) -} - -func (p InternalServerError) Error() string { - return p.error.Error() -} - -func newErrorTree(err error) ErrorTree { - i := ErrorTree{Detail: err.Error(), Errors: []ErrorTree{}, error: err} - if us, ok := err.(interface{ Unwrap() []error }); ok { - for _, e := range us.Unwrap() { - i.Errors = append(i.Errors, newErrorTree(e)) - } - } else if e := errors.Unwrap(err); e != nil { - i.Errors = append(i.Errors, newErrorTree(e)) - } - return i -} - -type ErrorTree struct { - Detail string `json:"detail" xml:"detail"` - Errors []ErrorTree `json:"errors" xml:"errors"` - - XMLName xml.Name `json:"-" xml:"errors"` - error error `json:"-" xml:"-"` -} - -var _ error = ErrorTree{} - -func (i ErrorTree) Error() string { - return i.error.Error() -} - -func NewNotImplemented[T time.Time | time.Duration](retryAfter T, opts ...Option) NotImplemented[T] { - p := NewStatus(http.StatusNotImplemented, opts...) - return NotImplemented[T]{RegisteredProblem: p, RetryAfter: RetryAfter[T]{time: retryAfter}} -} - -type NotImplemented[T time.Time | time.Duration] struct { - RegisteredProblem - RetryAfter RetryAfter[T] `json:"retryAfter" xml:"retry-after"` -} - -func (p NotImplemented[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Retry-After", p.RetryAfter.String()) - p.Handler(p).ServeHTTP(w, r) -} - -func NewBadGateway(opts ...Option) BadGateway { - return BadGateway{NewStatus(http.StatusBadGateway, opts...)} -} - -type BadGateway struct{ RegisteredProblem } - -func NewServiceUnavailable[T time.Time | time.Duration](retryAfter T, opts ...Option) ServiceUnavailable[T] { - p := NewStatus(http.StatusNotImplemented, opts...) - return ServiceUnavailable[T]{RegisteredProblem: p, RetryAfter: RetryAfter[T]{time: retryAfter}} -} - -type ServiceUnavailable[T time.Time | time.Duration] struct { - RegisteredProblem - RetryAfter RetryAfter[T] `json:"retryAfter" xml:"retry-after"` -} - -func (p ServiceUnavailable[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Retry-After", p.RetryAfter.String()) - p.handler(p).ServeHTTP(w, r) -} - -func NewGatewayTimeout(opts ...Option) GatewayTimeout { - return GatewayTimeout{NewStatus(http.StatusGatewayTimeout, opts...)} -} - -type GatewayTimeout struct{ RegisteredProblem } - -func NewHTTPVersionNotSupported(opts ...Option) HTTPVersionNotSupported { - return HTTPVersionNotSupported{NewStatus(http.StatusHTTPVersionNotSupported, opts...)} -} - -type HTTPVersionNotSupported struct{ RegisteredProblem } - -func NewVariantAlsoNegotiates(opts ...Option) VariantAlsoNegotiates { - return VariantAlsoNegotiates{NewStatus(http.StatusVariantAlsoNegotiates, opts...)} -} - -type VariantAlsoNegotiates struct{ RegisteredProblem } - -func NewInsufficientStorage(opts ...Option) InsufficientStorage { - return InsufficientStorage{NewStatus(http.StatusInsufficientStorage, opts...)} -} - -type InsufficientStorage struct{ RegisteredProblem } - -func NewLoopDetected(opts ...Option) LoopDetected { - return LoopDetected{NewStatus(http.StatusLoopDetected, opts...)} -} - -type LoopDetected struct{ RegisteredProblem } - -func NewNotExtended(opts ...Option) NotExtended { - return NotExtended{NewStatus(http.StatusNotExtended, opts...)} -} - -type NotExtended struct{ RegisteredProblem } - -func NewNetworkAuthenticationRequired(opts ...Option) NetworkAuthenticationRequired { - return NetworkAuthenticationRequired{NewStatus(http.StatusNetworkAuthenticationRequired, opts...)} -} - -type NetworkAuthenticationRequired struct{ RegisteredProblem } diff --git a/smalltrip/problem/handlers.go b/smalltrip/problem/handlers.go deleted file mode 100644 index 42a847b..0000000 --- a/smalltrip/problem/handlers.go +++ /dev/null @@ -1,155 +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 problem - -import ( - "encoding/json" - "encoding/xml" - "fmt" - "io" - "net/http" - "strings" -) - -type Handler func(p Problem) http.Handler - -// TODO: HandlerDevpage, this handler will be a complete handler with the smalltrip logo and a stylized page showing the error, JSON and XML values, and accepting a custom -// template to add custom styling. - -func HandlerBrowser(template Template) Handler { - return func(p Problem) http.Handler { - h := HandlerContentType(map[string]Handler{ - "text/html": HandlerTemplate(template), - "application/xml+html": HandlerTemplate(template), - "application/xml": HandlerXML, - ProblemMediaTypeXML: HandlerXML, - "application/json": HandlerJSON, - ProblemMediaTypeJSON: HandlerJSON, - }, HandlerJSON) - return h(p) - } -} - -func HandlerTemplate(template Template) Handler { - return func(p Problem) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(p.Status()) - if err := template.Execute(w, p); err != nil { - _, _ = w.Write(fmt.Appendf([]byte{}, "Failed to execute problem template: %s\n\nPlease report this error to the site's admin.", err.Error())) - } - }) - } -} - -type Template interface { - Execute(w io.Writer, data any) error -} - -func HandlerContentType(handlers map[string]Handler, fallback ...Handler) Handler { - return func(p Problem) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - for t, h := range handlers { - if strings.Contains(r.Header.Get("Accept"), t) { - h(p).ServeHTTP(w, r) - return - } - } - if len(fallback) > 0 { - fallback[0](p).ServeHTTP(w, r) - } - }) - } -} - -func HandlerXML(p Problem) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", ProblemMediaTypeXML) - - b, err := xml.Marshal(p) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write(fmt.Appendf([]byte{}, "Failed to marshal problem XML: %s\n\nPlease report this error to the site's admin.", err.Error())) - } - - w.WriteHeader(p.Status()) - - _, err = w.Write(b) - if err != nil { - _, _ = w.Write(fmt.Appendf([]byte{}, "Failed to write problem XML: %s\n\nPlease report this error to the site's admin.", err.Error())) - } - }) -} - -func HandlerJSON(p Problem) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", ProblemMediaTypeJSON) - - b, err := json.Marshal(p) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write(fmt.Appendf([]byte{}, "Failed to marshal problem JSON: %s\n\nPlease report this error to the site's admin.", err.Error())) - } - - w.WriteHeader(p.Status()) - - _, err = w.Write(b) - if err != nil { - _, _ = w.Write(fmt.Appendf([]byte{}, "Failed to write problem JSON: %s\n\nPlease report this error to the site's admin.", err.Error())) - } - }) -} - -func HandlerText(p Problem) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "text/plain") - - w.WriteHeader(p.Status()) - - s := fmt.Sprintf( - "Type: %s\n"+ - "Status: %3d\n"+ - "Title: %s\n"+ - "Detail: %s\n"+ - "Instance: %s\n\n"+ - p.Type(), - p.Status(), - p.Title(), - p.Detail(), - p.Instance(), - ) - - _, err := w.Write(fmt.Appendf([]byte{}, "%s%+v\n\n%#v", s, p, p)) - if err != nil { - _, _ = w.Write(fmt.Append([]byte{}, - "Ok, what should we do at this point? You fucked up so bad that this message "+ - "shouldn't even be able to be sent in the first place. If you are a normal user I'm "+ - "so sorry for you to be reading this. If you're a developer, go fix your ResponseWriter "+ - "implementation, because this should never happen in any normal codebase. "+ - "I hope for the life of anyone you love you don't use this message in some "+ - "error checking or any sort of API-contract, because there will be no more hope "+ - "for you or your project. May God or any other divinity that you may "+ - "or may not believe be with you when trying to fix this mistake, you will need it.", - // If someone use this as part of the API-contract I'll not even be surprised. - // So any change to this message is still considered a breaking change. - )) - } - }) -} - -const ( - ProblemMediaTypeJSON = "application/problem+json" - ProblemMediaTypeXML = "application/problem+xml" -) diff --git a/smalltrip/problem/headers.go b/smalltrip/problem/headers.go deleted file mode 100644 index 54bbc81..0000000 --- a/smalltrip/problem/headers.go +++ /dev/null @@ -1,48 +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 problem - -import ( - "encoding" - "fmt" - "net/http" - "time" -) - -type RetryAfter[T time.Time | time.Duration] struct { - time T -} - -var ( - _ encoding.TextMarshaler = RetryAfter[time.Time]{} - _ fmt.Stringer = RetryAfter[time.Time]{} -) - -func (h RetryAfter[T]) MarshalText() ([]byte, error) { - return []byte(h.String()), nil -} - -func (h RetryAfter[T]) String() string { - switch t := any(h.time).(type) { - case time.Time: - if !t.IsZero() { - return t.Format(http.TimeFormat) - } - case time.Duration: - return fmt.Sprintf("%.0f", t.Seconds()) - } - return "" -} diff --git a/smalltrip/problem/middleware.go b/smalltrip/problem/middleware.go deleted file mode 100644 index aa56b77..0000000 --- a/smalltrip/problem/middleware.go +++ /dev/null @@ -1,67 +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 problem - -import ( - "context" - "fmt" - "net/http" - - "forge.capytal.company/loreddev/x/smalltrip/middleware" -) - -type ContextKey string - -var DefaultContextKey ContextKey = "x-smalltrip-problems-middleware-handler" - -// TODO?: BufferedMiddleware, a middleware which can respond or redirect to -// a error page even after the first Write - -func PanicMiddleware() middleware.Middleware { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer func() { - if rv := recover(); rv != nil { - err := fmt.Errorf("panic recovered: %+v", rv) - NewInternalServerError(err).ServeHTTP(w, r) - } - }() - next.ServeHTTP(w, r) - }) - } -} - -func Middleware(h Handler) middleware.Middleware { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := context.WithValue(r.Context(), DefaultContextKey, h) - next.ServeHTTP(w, r.WithContext(ctx)) - }) - } -} - -func HandlerMiddleware(fallback ...Handler) Handler { - return func(p Problem) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handler := r.Context().Value(DefaultContextKey) - if h, ok := handler.(Handler); handler != nil && ok { - h(p).ServeHTTP(w, r) - } else if len(fallback) > 0 { - fallback[0](p).ServeHTTP(w, r) - } - }) - } -} diff --git a/smalltrip/problem/multiplexer.go b/smalltrip/problem/multiplexer.go deleted file mode 100644 index 76d973f..0000000 --- a/smalltrip/problem/multiplexer.go +++ /dev/null @@ -1,148 +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 problem - -import ( - "net/http" - "strings" - - "forge.capytal.company/loreddev/x/smalltrip/multiplexer" -) - -func Multiplexer(m multiplexer.Multiplexer, opts ...MultiplexerOption) multiplexer.Multiplexer { - mux := &mux{Multiplexer: m, methodList: DefaultMethods} - - for _, opt := range opts { - opt(mux) - } - - return mux -} - -type mux struct { - notFound http.Handler - methodNotAllowed http.Handler - methodList []string - - multiplexer.Multiplexer -} - -func (m *mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { - i := &interceptor{ - notFound: m.notFound, - methodNotAllowed: m.methodNotAllowed, - methodList: m.methodList, - - mux: m, - - w: w, - r: r, - } - m.Multiplexer.ServeHTTP(i, r) -} - -type interceptor struct { - notFound http.Handler - methodNotAllowed http.Handler - methodList []string - - mux multiplexer.Multiplexer - - statusCode int - intercept bool - - w http.ResponseWriter - r *http.Request -} - -var _ http.ResponseWriter = (*interceptor)(nil) - -func (e *interceptor) Header() http.Header { - return e.w.Header() -} - -func (e *interceptor) WriteHeader(statusCode int) { - if statusCode > 399 && strings.Contains(e.w.Header().Get("Content-Type"), "text/plain") { - e.w.Header().Del("Content-Type") - e.intercept = true - e.statusCode = statusCode - return - } - e.w.WriteHeader(statusCode) -} - -func (e *interceptor) Write(data []byte) (int, error) { - if e.intercept && e.statusCode == http.StatusMethodNotAllowed { - method := e.r.Method - _, current := e.mux.Handler(e.r) - - var allowed []string - for _, m := range e.methodList { - e.r.Method = m - if _, p := e.mux.Handler(e.r); p != current { - allowed = append(allowed, m) - } - } - - e.r.Method = method - - if e.methodNotAllowed != nil { - e.w.Header().Set("Allow", strings.Join(allowed, ", ")) - e.methodNotAllowed.ServeHTTP(e.w, e.r) - } else { - NewMethodNotAllowed(allowed).ServeHTTP(e.w, e.r) - } - return len(data), nil - - } else if e.intercept && e.statusCode == http.StatusNotFound && e.notFound != nil { - e.notFound.ServeHTTP(e.w, e.r) - return len(data), nil - - } else if e.intercept { - NewDetailed(e.statusCode, strings.TrimSpace(string(data))).ServeHTTP(e.w, e.r) - return len(data), nil - } - - return e.w.Write(data) -} - -var DefaultMethods = multiplexer.DefaultMethods - -type MultiplexerOption func(*mux) - -func WithNotFound(h http.Handler) MultiplexerOption { - return func(m *mux) { - m.notFound = h - } -} - -func WithMethodNotAllowed(h http.Handler) MultiplexerOption { - return func(m *mux) { - m.methodNotAllowed = h - } -} - -func WithMethod(method string) MultiplexerOption { - return func(m *mux) { - m.methodList = append(m.methodList, method) - } -} - -func WithMethodList(list []string) MultiplexerOption { - return func(m *mux) { - m.methodList = list - } -} diff --git a/smalltrip/problem/problem.go b/smalltrip/problem/problem.go deleted file mode 100644 index 3419261..0000000 --- a/smalltrip/problem/problem.go +++ /dev/null @@ -1,169 +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 problem - -import ( - "encoding/xml" - "fmt" - "net/http" - "slices" - "text/template" -) - -type Problem interface { - Type() string - Title() string - Status() int - Detail() string - Instance() string - - Handler(self Problem) http.Handler - - http.Handler -} - -type RegisteredProblem struct { - TypeURI string `json:"type,omitempty" xml:"type,omitempty"` - TypeTitle string `json:"title,omitempty" xml:"title,omitempty"` - StatusCode int `json:"status,omitempty" xml:"status,omitempty"` - DetailMessage string `json:"detail,omitempty" xml:"detail,omitempty"` - InstanceURI string `json:"instance,omitempty" xml:"instance,omitempty"` - - XMLName xml.Name `json:"-" xml:"problem"` - - handler Handler `json:"-" xml:"-"` -} - -func NewStatus(s int, opts ...Option) RegisteredProblem { - return New(slices.Concat([]Option{WithStatus(s)}, opts)...) -} - -func NewDetailed(s int, detail string, opts ...Option) RegisteredProblem { - return New(slices.Concat([]Option{WithStatus(s), WithDetail(detail)}, opts)...) -} - -func New(opts ...Option) RegisteredProblem { - p := RegisteredProblem{ - TypeURI: DefaultTypeURI, - handler: DefaultHandler, - } - - for _, opt := range opts { - opt(&p) - } - return p -} - -var ( - DefaultTypeURI = "about:blank" - DefaultTemplate = template.Must(template.New("x-smalltrip-problem-template").Parse(` - - - - {{ .Status }} - {{ .Title }} - - -

{{.Status}} - {{ .Title }}

-

{{ .Type }}

-

{{ .Detail }}

- {{if .Instance}} -

Instance: {{ .Instance }}

- {{end}} - {{printf "%#v" .}} - - -`)) - DefaultHandler = HandlerMiddleware(HandlerBrowser(DefaultTemplate)) -) - -func (p RegisteredProblem) Type() string { - return p.TypeURI -} - -func (p RegisteredProblem) Title() string { - return p.TypeTitle -} - -func (p RegisteredProblem) Status() int { - return p.StatusCode -} - -func (p RegisteredProblem) Detail() string { - return p.DetailMessage -} - -func (p RegisteredProblem) Instance() string { - return p.InstanceURI -} - -func (p RegisteredProblem) Handler(self Problem) http.Handler { - return p.handler(self) -} - -func (p RegisteredProblem) ServeHTTP(w http.ResponseWriter, r *http.Request) { - p.Handler(p).ServeHTTP(w, r) -} - -func WithType(t string) Option { - return func(p *RegisteredProblem) { - p.TypeURI = t - } -} - -func WithTitle(t string) Option { - return func(p *RegisteredProblem) { - p.TypeTitle = t - } -} - -func WithStatus(s int) Option { - return func(p *RegisteredProblem) { - if p.TypeTitle == "" { - p.TypeTitle = http.StatusText(s) - } - p.StatusCode = s - } -} - -func WithDetail(d string) Option { - return func(p *RegisteredProblem) { - if p.DetailMessage != "" { - p.DetailMessage = fmt.Sprintf("%s: %s", p.DetailMessage, d) - } else { - p.DetailMessage = d - } - } -} - -func WithDetailf(f string, args ...any) Option { - return func(p *RegisteredProblem) { - WithDetail(fmt.Sprintf(f, args...))(p) - } -} - -func WithError(err error) Option { - return func(p *RegisteredProblem) { - WithDetail(err.Error())(p) - } -} - -func WithInstance(i string) Option { - return func(p *RegisteredProblem) { - p.InstanceURI = i - } -} - -type Option func(*RegisteredProblem) diff --git a/smalltrip/smalltrip.go b/smalltrip/smalltrip.go deleted file mode 100644 index f34e513..0000000 --- a/smalltrip/smalltrip.go +++ /dev/null @@ -1,125 +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 smalltrip - -import ( - "fmt" - "io" - "log/slog" - "net/http" - "reflect" - "runtime" - - "forge.capytal.company/loreddev/x/smalltrip/middleware" - "forge.capytal.company/loreddev/x/smalltrip/multiplexer" -) - -type Router interface { - multiplexer.Multiplexer - Use(middleware.Middleware) -} - -type router struct { - mux multiplexer.Multiplexer - mws []middleware.Middleware - log *slog.Logger -} - -var _ Router = (*router)(nil) - -func NewRouter(options ...Option) Router { - r := &router{ - mux: http.NewServeMux(), - mws: []middleware.Middleware{}, - log: slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})).WithGroup("smalltrip-router"), - } - - for _, option := range options { - option(r) - } - - return r -} - -func (router *router) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) { - log := router.log.With(slog.String("pattern", pattern), slog.String("handler", getValueType(handler))) - log.Info("Adding route") - - var hf http.Handler = http.HandlerFunc(handler) - - for _, m := range router.mws { - log.Debug("Wrapping with middleware", slog.String("middleware", getValueType(m))) - hf = m(hf) - } - - router.mux.Handle(pattern, hf) -} - -func (router *router) Handle(pattern string, handler http.Handler) { - log := router.log.With(slog.String("pattern", pattern), slog.String("handler", getValueType(handler))) - log.Info("Adding route") - - for _, m := range router.mws { - log.Debug("Wrapping with middleware", slog.String("middleware", getValueType(m))) - handler = m(handler) - } - - router.mux.Handle(pattern, handler) -} - -func (router *router) Use(m middleware.Middleware) { - router.log.Info("Middleware added", slog.String("middleware", getValueType(m))) - - if router.mws == nil { - router.mws = []middleware.Middleware{} - } - router.mws = append(router.mws, m) -} - -func (router *router) Handler(r *http.Request) (http.Handler, string) { - return router.mux.Handler(r) -} - -func (router *router) ServeHTTP(w http.ResponseWriter, r *http.Request) { - router.mux.ServeHTTP(w, r) -} - -func getValueType[T any](value T) (name string) { - defer func() { - if rc := recover(); rc != nil { - name = fmt.Sprintf("%T", value) - } - }() - - v := reflect.ValueOf(value) - - if v.Kind() == reflect.Pointer { - return getValueType(v.Elem().Interface()) - } - - if v.Kind() == reflect.Func { - fc := runtime.FuncForPC(v.Pointer()) - if fc != nil { - return fc.Name() - } - } - - if p, n := v.Type().PkgPath(), v.Type().Name(); p != "" && n != "" { - return fmt.Sprintf("%s.%s", p, n) - } - - return fmt.Sprintf("%T", value) -}