Compare commits

...

33 Commits

Author SHA1 Message Date
9f85a243e8 v0.0.520
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m7s
2024-10-05 01:02:25 +02:00
dc6cb274ee v0.0.519
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-10-05 00:58:15 +02:00
f6b47792a4 v0.0.518 Improve sq db-listener interface (breaking)
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m11s
2024-10-05 00:45:55 +02:00
295b3ef793 v0.0.517 add constructor funcs for tuples
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m21s
2024-10-02 11:31:34 +02:00
721c176337 v0.0.516
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m25s
2024-09-25 21:43:41 +02:00
ebba6545a3 v0.0.515
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 5m35s
2024-09-16 17:39:51 +02:00
19c7e22ced v0.0.514 fix mongo filter where the primary sort key is null in db (fallback to secondary)
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-09-16 17:39:18 +02:00
9f883b458f v0.0.513
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m55s
2024-09-16 15:27:32 +02:00
1f456c5134 v0.0.512
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 5m6s
2024-09-15 21:25:21 +02:00
d7fbef37db v0.0.511
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m10s
2024-09-15 18:22:07 +02:00
a1668b6e5a v0.0.510
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m24s
2024-09-13 18:06:49 +02:00
3a17edfaf0 v0.0.509
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 6m2s
2024-08-26 14:35:49 +02:00
3320a9c19d v0.0.508
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m25s
2024-08-25 17:36:20 +02:00
8dcd8a270a v0.0.507 fix jsonfilter:"-" not working
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 7m7s
2024-08-25 15:41:17 +02:00
03a9b276d8 v0.0.506 allow empty-string as value for enum
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 7m36s
2024-08-22 11:45:02 +02:00
9c8cde384f v0.0.505
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 6m17s
2024-08-08 15:57:05 +02:00
99b000ecf4 v0.0.504
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m53s
2024-08-07 19:44:45 +02:00
a173e30090 v0.0.503
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m52s
2024-08-07 19:37:38 +02:00
a3481a7d2d v0.0.502
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-08-07 19:35:23 +02:00
a8e6f98a89 v0.0.501
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-08-07 19:31:36 +02:00
ab805403b9 v0.0.500
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-08-07 19:30:38 +02:00
1e98d351ce v0.0.499
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 5m24s
2024-08-07 18:34:22 +02:00
c40bdc8e9e v0.0.498
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m13s
2024-08-07 17:26:35 +02:00
7204562879 v0.0.497
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m33s
2024-08-07 17:04:59 +02:00
741611a2e1 v0.0.496 wpdf fixes and wpdf test.go
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m58s
2024-08-07 15:34:06 +02:00
133aeb8374 v0.0.495
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m44s
2024-08-07 14:00:02 +02:00
b78a468632 v0.0.494 add tables to wpdf
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-08-07 13:57:29 +02:00
f1b4480e0f v0.0.493 fix panic in RegisterImage for very short images
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 5m22s
2024-08-07 09:22:37 +02:00
ffffe4bf24 v0.0.492
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 5m32s
2024-08-02 16:19:21 +02:00
413bf3c848 v0.0.491 small optimization in Paginate method
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 8m31s
2024-07-31 00:15:09 +02:00
646990b549 v0.0.490 documentation and extra-params in exerr
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m2s
2024-07-27 23:44:18 +02:00
e5818146a8 v0.0.489
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m54s
2024-07-23 14:21:03 +02:00
1310054121 v0.0.488 fix wpdf with 16bpp images
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m9s
2024-07-22 15:16:28 +02:00
45 changed files with 1965 additions and 192 deletions

View File

@@ -8,7 +8,7 @@ This should not have any heavy dependencies (gin, mongo, etc) and add missing ba
Potentially needs `export GOPRIVATE="gogs.mikescher.com"`
### Packages:
## Packages:
| Name | Maintainer | Description |
|-------------|------------|---------------------------------------------------------------------------------------------------------------|
@@ -21,7 +21,7 @@ Potentially needs `export GOPRIVATE="gogs.mikescher.com"`
| reflectext | Mike | Utility for golang reflection |
| fsext | Mike | Utility for filesytem access |
| | | |
| mongoext | Mike | Utility/Helper functions for mongodb |
| mongoext | Mike | Utility/Helper functions for mongodb (kinda abandoned) |
| cursortoken | Mike | MongoDB cursortoken implementation |
| pagination | Mike | Pagination implementation |
| | | |
@@ -42,4 +42,69 @@ Potentially needs `export GOPRIVATE="gogs.mikescher.com"`
| wmo | Mike | Mongo Wrapper, wraps mongodb with a better interface |
| | | |
| scn | Mike | SimpleCloudNotifier |
| | | |
| | | |
## Usage:
### exerr
- see **mongoext/builder.go** for full info
Short summary:
- An better error package with metadata, listener, api-output and error-traces
- Initialize with `exerr.Init()`
- *Never* return `err` direct, always use exerr.Wrap(err, "...") - add metadata where applicable
- at the end either Print(), Fatal() or Output() your error (print = stdout, fatal = panic, output = json API response)
- You can add listeners with exerr.RegisterListener(), and save the full errors to a db or smth
### wmo
- A typed wrapper around the official mongo-go-driver
- Use `wmo.W[...](...)` to wrap the collections and type-ify them
- The new collections have all the usual methods, but types
- Also they have List() and Paginate() methods for paginated listings (witehr with a cursortoken or page/limit)
- Register additional hooks with `WithDecodeFunc`, `WithUnmarshalHook`, `WithMarshalHook`, `WithModifyingPipeline`, `WithModifyingPipelineFunc`
- List(), Paginate(), etc support filter interfaces
- Rule(s) of thumb:
- filter the results in the filter interface
- sort the results in the sort function of the filter interface
- add joins ($lookup's) in the `WithModifyingPipelineFunc`/`WithModifyingPipeline`
#### ginext
- A wrapper around gin-gonic/gin
- create the gin engine with `ginext.NewEngine`
- Add routes with `engine.Routes()...`
- `.Use(..)` adds a middleware
- `.Group(..)` adds a group
- `.Get().Handle(..)` adds a handler
- Handler return values (in contract to ginext) - values implement the `ginext.HTTPResponse` interface
- Every handler starts with something like:
```go
func (handler Handler) CommunityMetricsValues(pctx ginext.PreContext) ginext.HTTPResponse {
type communityURI struct {
Version string `uri:"version"`
CommunityID models.CommunityID `uri:"cid"`
}
type body struct {
UserID models.UserID `json:"userID"`
EventID models.EventID `json:"eventID"`
}
var u uri
var b body
ctx, gctx, httpErr := pctx.URI(&u).Body(&b).Start() // can have more unmarshaller, like header, form, etc
if httpErr != nil {
return *httpErr
}
defer ctx.Cancel()
// do stuff
}
```
#### sq
- TODO (like mongoext for sqlite/sql databases)

View File

@@ -46,7 +46,7 @@ var rexEnumPackage = rext.W(regexp.MustCompile(`^package\s+(?P<name>[A-Za-z0-9_]
var rexEnumDef = rext.W(regexp.MustCompile(`^\s*type\s+(?P<name>[A-Za-z0-9_]+)\s+(?P<type>[A-Za-z0-9_]+)\s*//\s*(@enum:type).*$`))
var rexEnumValueDef = rext.W(regexp.MustCompile(`^\s*(?P<name>[A-Za-z0-9_]+)\s+(?P<type>[A-Za-z0-9_]+)\s*=\s*(?P<value>("[A-Za-z0-9_:\s\-.]+"|[0-9]+))\s*(//(?P<comm>.*))?.*$`))
var rexEnumValueDef = rext.W(regexp.MustCompile(`^\s*(?P<name>[A-Za-z0-9_]+)\s+(?P<type>[A-Za-z0-9_]+)\s*=\s*(?P<value>("[A-Za-z0-9_:\s\-.]*"|[0-9]+))\s*(//(?P<comm>.*))?.*$`))
var rexEnumChecksumConst = rext.W(regexp.MustCompile(`const ChecksumEnumGenerator = "(?P<cs>[A-Za-z0-9_]*)"`))

View File

@@ -3,8 +3,8 @@ package cursortoken
import (
"encoding/base32"
"encoding/json"
"errors"
"go.mongodb.org/mongo-driver/bson/primitive"
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
"strings"
"time"
)
@@ -127,7 +127,7 @@ func Decode(tok string) (CursorToken, error) {
}
if !strings.HasPrefix(tok, "tok_") {
return CursorToken{}, errors.New("could not decode token, missing prefix")
return CursorToken{}, exerr.New(exerr.TypeCursorTokenDecode, "could not decode token, missing prefix").Str("token", tok).Build()
}
body, err := base32.StdEncoding.DecodeString(tok[len("tok_"):])
@@ -138,7 +138,7 @@ func Decode(tok string) (CursorToken, error) {
var tokenDeserialize cursorTokenSerialize
err = json.Unmarshal(body, &tokenDeserialize)
if err != nil {
return CursorToken{}, err
return CursorToken{}, exerr.Wrap(err, "failed to deserialize token").Str("token", tok).Build()
}
token := CursorToken{Mode: CTMNormal}

View File

@@ -57,3 +57,11 @@ func (m JsonOpt[T]) MustValue() T {
}
return m.value
}
func (m JsonOpt[T]) IfSet(fn func(v T)) bool {
if !m.isSet {
return false
}
fn(m.value)
return true
}

View File

@@ -19,6 +19,14 @@ func (s Single[T1]) TupleValues() []any {
return []any{s.V1}
}
func NewSingle[T1 any](v1 T1) Single[T1] {
return Single[T1]{V1: v1}
}
func NewTuple1[T1 any](v1 T1) Single[T1] {
return Single[T1]{V1: v1}
}
// ----------------------------------------------------------------------------
type Tuple[T1 any, T2 any] struct {
@@ -34,6 +42,14 @@ func (t Tuple[T1, T2]) TupleValues() []any {
return []any{t.V1, t.V2}
}
func NewTuple[T1 any, T2 any](v1 T1, v2 T2) Tuple[T1, T2] {
return Tuple[T1, T2]{V1: v1, V2: v2}
}
func NewTuple2[T1 any, T2 any](v1 T1, v2 T2) Tuple[T1, T2] {
return Tuple[T1, T2]{V1: v1, V2: v2}
}
// ----------------------------------------------------------------------------
type Triple[T1 any, T2 any, T3 any] struct {
@@ -50,6 +66,14 @@ func (t Triple[T1, T2, T3]) TupleValues() []any {
return []any{t.V1, t.V2, t.V3}
}
func NewTriple[T1 any, T2 any, T3 any](v1 T1, v2 T2, v3 T3) Triple[T1, T2, T3] {
return Triple[T1, T2, T3]{V1: v1, V2: v2, V3: v3}
}
func NewTuple3[T1 any, T2 any, T3 any](v1 T1, v2 T2, v3 T3) Triple[T1, T2, T3] {
return Triple[T1, T2, T3]{V1: v1, V2: v2, V3: v3}
}
// ----------------------------------------------------------------------------
type Quadruple[T1 any, T2 any, T3 any, T4 any] struct {
@@ -67,6 +91,14 @@ func (t Quadruple[T1, T2, T3, T4]) TupleValues() []any {
return []any{t.V1, t.V2, t.V3, t.V4}
}
func NewQuadruple[T1 any, T2 any, T3 any, T4 any](v1 T1, v2 T2, v3 T3, v4 T4) Quadruple[T1, T2, T3, T4] {
return Quadruple[T1, T2, T3, T4]{V1: v1, V2: v2, V3: v3, V4: v4}
}
func NewTuple4[T1 any, T2 any, T3 any, T4 any](v1 T1, v2 T2, v3 T3, v4 T4) Quadruple[T1, T2, T3, T4] {
return Quadruple[T1, T2, T3, T4]{V1: v1, V2: v2, V3: v3, V4: v4}
}
// ----------------------------------------------------------------------------
type Quintuple[T1 any, T2 any, T3 any, T4 any, T5 any] struct {
@@ -86,6 +118,14 @@ func (t Quintuple[T1, T2, T3, T4, T5]) TupleValues() []any {
}
func NewQuintuple[T1 any, T2 any, T3 any, T4 any, T5 any](v1 T1, v2 T2, v3 T3, v4 T4, v5 T5) Quintuple[T1, T2, T3, T4, T5] {
return Quintuple[T1, T2, T3, T4, T5]{V1: v1, V2: v2, V3: v3, V4: v4, V5: v5}
}
func NewTuple5[T1 any, T2 any, T3 any, T4 any, T5 any](v1 T1, v2 T2, v3 T3, v4 T4, v5 T5) Quintuple[T1, T2, T3, T4, T5] {
return Quintuple[T1, T2, T3, T4, T5]{V1: v1, V2: v2, V3: v3, V4: v4, V5: v5}
}
// ----------------------------------------------------------------------------
type Sextuple[T1 any, T2 any, T3 any, T4 any, T5 any, T6 any] struct {
@@ -106,6 +146,14 @@ func (t Sextuple[T1, T2, T3, T4, T5, T6]) TupleValues() []any {
}
func NewSextuple[T1 any, T2 any, T3 any, T4 any, T5 any, T6 any](v1 T1, v2 T2, v3 T3, v4 T4, v5 T5, v6 T6) Sextuple[T1, T2, T3, T4, T5, T6] {
return Sextuple[T1, T2, T3, T4, T5, T6]{V1: v1, V2: v2, V3: v3, V4: v4, V5: v5, V6: v6}
}
func NewTuple6[T1 any, T2 any, T3 any, T4 any, T5 any, T6 any](v1 T1, v2 T2, v3 T3, v4 T4, v5 T5, v6 T6) Sextuple[T1, T2, T3, T4, T5, T6] {
return Sextuple[T1, T2, T3, T4, T5, T6]{V1: v1, V2: v2, V3: v3, V4: v4, V5: v5, V6: v6}
}
// ----------------------------------------------------------------------------
type Septuple[T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any] struct {
@@ -126,6 +174,14 @@ func (t Septuple[T1, T2, T3, T4, T5, T6, T7]) TupleValues() []any {
return []any{t.V1, t.V2, t.V3, t.V4, t.V5, t.V6, t.V7}
}
func NewSeptuple[T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any](v1 T1, v2 T2, v3 T3, v4 T4, v5 T5, v6 T6, v7 T7) Septuple[T1, T2, T3, T4, T5, T6, T7] {
return Septuple[T1, T2, T3, T4, T5, T6, T7]{V1: v1, V2: v2, V3: v3, V4: v4, V5: v5, V6: v6, V7: v7}
}
func NewTuple7[T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any](v1 T1, v2 T2, v3 T3, v4 T4, v5 T5, v6 T6, v7 T7) Septuple[T1, T2, T3, T4, T5, T6, T7] {
return Septuple[T1, T2, T3, T4, T5, T6, T7]{V1: v1, V2: v2, V3: v3, V4: v4, V5: v5, V6: v6, V7: v7}
}
// ----------------------------------------------------------------------------
type Octuple[T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any] struct {
@@ -147,6 +203,14 @@ func (t Octuple[T1, T2, T3, T4, T5, T6, T7, T8]) TupleValues() []any {
return []any{t.V1, t.V2, t.V3, t.V4, t.V5, t.V6, t.V7, t.V8}
}
func NewOctuple[T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any](v1 T1, v2 T2, v3 T3, v4 T4, v5 T5, v6 T6, v7 T7, v8 T8) Octuple[T1, T2, T3, T4, T5, T6, T7, T8] {
return Octuple[T1, T2, T3, T4, T5, T6, T7, T8]{V1: v1, V2: v2, V3: v3, V4: v4, V5: v5, V6: v6, V7: v7, V8: v8}
}
func NewTuple8[T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any](v1 T1, v2 T2, v3 T3, v4 T4, v5 T5, v6 T6, v7 T7, v8 T8) Octuple[T1, T2, T3, T4, T5, T6, T7, T8] {
return Octuple[T1, T2, T3, T4, T5, T6, T7, T8]{V1: v1, V2: v2, V3: v3, V4: v4, V5: v5, V6: v6, V7: v7, V8: v8}
}
// ----------------------------------------------------------------------------
type Nonuple[T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any, T9 any] struct {
@@ -168,3 +232,10 @@ func (t Nonuple[T1, T2, T3, T4, T5, T6, T7, T8, T9]) TupleLength() int {
func (t Nonuple[T1, T2, T3, T4, T5, T6, T7, T8, T9]) TupleValues() []any {
return []any{t.V1, t.V2, t.V3, t.V4, t.V5, t.V6, t.V7, t.V8, t.V9}
}
func NewNonuple[T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any, T9 any](v1 T1, v2 T2, v3 T3, v4 T4, v5 T5, v6 T6, v7 T7, v8 T8, v9 T9) Nonuple[T1, T2, T3, T4, T5, T6, T7, T8, T9] {
return Nonuple[T1, T2, T3, T4, T5, T6, T7, T8, T9]{V1: v1, V2: v2, V3: v3, V4: v4, V5: v5, V6: v6, V7: v7, V8: v8, V9: v9}
}
func NewTuple9[T1 any, T2 any, T3 any, T4 any, T5 any, T6 any, T7 any, T8 any, T9 any](v1 T1, v2 T2, v3 T3, v4 T4, v5 T5, v6 T6, v7 T7, v8 T8, v9 T9) Nonuple[T1, T2, T3, T4, T5, T6, T7, T8, T9] {
return Nonuple[T1, T2, T3, T4, T5, T6, T7, T8, T9]{V1: v1, V2: v2, V3: v3, V4: v4, V5: v5, V6: v6, V7: v7, V8: v8, V9: v9}
}

View File

@@ -30,6 +30,10 @@ import (
// If possible add metadata to the error (eg the id that was not found, ...), the methods are the same as in zerolog
// return nil, exerror.Wrap(err, "do something failed").Str("someid", id).Int("count", in.Count).Build()
//
// You can also add extra-data to an error with Extra(..)
// in contrast to metadata is extradata always printed in the resulting error and is more intended for additional (programmatically readable) data in addition to the errortype
// (metadata is more internal debug info/help)
//
// You can change the errortype with `.User()` and `.System()` (User-errors are 400 and System-errors 500)
// You can also manually set the statuscode with `.WithStatuscode(http.NotFound)`
// You can set the type with `WithType(..)`
@@ -76,12 +80,14 @@ func Wrap(err error, msg string) *Builder {
return &Builder{errorData: newExErr(CatSystem, TypeInternal, msg)} // prevent NPE if we call Wrap with err==nil
}
v := FromError(err)
if !pkgconfig.RecursiveErrors {
v := FromError(err)
v.Message = msg
return &Builder{wrappedErr: err, errorData: v}
} else {
return &Builder{wrappedErr: err, errorData: wrapExErr(v, msg, CatWrap, 1)}
}
return &Builder{wrappedErr: err, errorData: wrapExErr(FromError(err), msg, CatWrap, 1)}
}
// ----------------------------------------------------------------------------
@@ -368,29 +374,6 @@ func (b *Builder) CtxData(method Method, ctx context.Context) *Builder {
return b
}
func formatHeader(header map[string][]string) string {
ml := 1
for k, _ := range header {
if len(k) > ml {
ml = len(k)
}
}
r := ""
for k, v := range header {
if r != "" {
r += "\n"
}
for _, hval := range v {
value := hval
value = strings.ReplaceAll(value, "\n", "\\n")
value = strings.ReplaceAll(value, "\r", "\\r")
value = strings.ReplaceAll(value, "\t", "\\t")
r += langext.StrPadRight(k, " ", ml) + " := " + value
}
}
return r
}
func extractHeader(header map[string][]string) []string {
r := make([]string, 0, len(header))
for k, v := range header {
@@ -407,6 +390,16 @@ func extractHeader(header map[string][]string) []string {
// ----------------------------------------------------------------------------
// Extra adds additional data to the error
// this is not like the other metadata (like Id(), Str(), etc)
// this data is public and will be printed/outputted
func (b *Builder) Extra(key string, val any) *Builder {
b.errorData.Extra[key] = val
return b
}
// ----------------------------------------------------------------------------
// Build creates a new error, ready to pass up the stack
// If the errors is not SevWarn or SevInfo it gets also logged (in short form, without stacktrace) onto stdout
// Can be gloablly configured with ZeroLogErrTraces and ZeroLogAllTraces
@@ -468,6 +461,10 @@ func (b *Builder) Print(ctxs ...context.Context) {
b.errorData.Log(pkgconfig.ZeroLogger.Error())
} else if b.errorData.Severity == SevWarn {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Warn())
} else if b.errorData.Severity == SevInfo {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Info())
} else {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Debug())
}
b.errorData.CallListener(MethodPrint)

View File

@@ -12,6 +12,8 @@ import (
var reflectTypeStr = reflect.TypeOf("")
func FromError(err error) *ExErr {
//goland:noinspection GoTypeAssertionOnErrors
if verr, ok := err.(*ExErr); ok {
// A simple ExErr
return verr
@@ -31,6 +33,7 @@ func FromError(err error) *ExErr {
Caller: "",
OriginalError: nil,
Meta: getForeignMeta(err),
Extra: make(map[string]any),
}
}
@@ -48,6 +51,7 @@ func newExErr(cat ErrorCategory, errtype ErrorType, msg string) *ExErr {
Caller: callername(2),
OriginalError: nil,
Meta: make(map[string]MetaValue),
Extra: make(map[string]any),
}
}
@@ -65,6 +69,7 @@ func wrapExErr(e *ExErr, msg string, cat ErrorCategory, stacktraceskip int) *ExE
Caller: callername(1 + stacktraceskip),
OriginalError: e,
Meta: make(map[string]MetaValue),
Extra: langext.CopyMap(langext.ForceMap(e.Extra)),
}
}

View File

@@ -1,6 +1,7 @@
package exerr
import (
"fmt"
"github.com/rs/xid"
"github.com/rs/zerolog"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
@@ -26,7 +27,8 @@ type ExErr struct {
OriginalError *ExErr `json:"originalError"`
Meta MetaMap `json:"meta"`
Extra map[string]any `json:"extra"`
Meta MetaMap `json:"meta"`
}
func (ee *ExErr) Error() string {
@@ -36,6 +38,13 @@ func (ee *ExErr) Error() string {
// Unwrap must be implemented so that some error.XXX methods work
func (ee *ExErr) Unwrap() error {
if ee.OriginalError == nil {
if ee.WrappedErr != nil {
if werr, ok := ee.WrappedErr.(error); ok {
return werr
}
}
return nil // this is neccessary - otherwise we return a wrapped nil and the `x == nil` comparison fails (= panic in errors.Is and other failures)
}
return ee.OriginalError
@@ -81,6 +90,23 @@ func (ee *ExErr) Log(evt *zerolog.Event) {
}
func (ee *ExErr) FormatLog(lvl LogPrintLevel) string {
// [LogPrintShort]
//
// - Only print message and type
// - Used e.g. for logging to the console when Build is called
// - also used in Print() if level == Warn/Info
//
// [LogPrintOverview]
//
// - print message, extra and errortrace
//
// [LogPrintFull]
//
// - print full error, with meta and extra, and trace, etc
// - Used in Output() and Print()
//
if lvl == LogPrintShort {
msg := ee.Message
@@ -101,6 +127,10 @@ func (ee *ExErr) FormatLog(lvl LogPrintLevel) string {
str := "[" + ee.RecursiveType().Key + "] <" + ee.UniqueID + "> " + strings.ReplaceAll(ee.RecursiveMessage(), "\n", " ") + "\n"
for exk, exv := range ee.Extra {
str += fmt.Sprintf(" # [[[ %s ==> %v ]]]\n", exk, exv)
}
indent := ""
for curr := ee; curr != nil; curr = curr.OriginalError {
indent += " "
@@ -122,6 +152,10 @@ func (ee *ExErr) FormatLog(lvl LogPrintLevel) string {
str := "[" + ee.RecursiveType().Key + "] <" + ee.UniqueID + "> " + strings.ReplaceAll(ee.RecursiveMessage(), "\n", " ") + "\n"
for exk, exv := range ee.Extra {
str += fmt.Sprintf(" # [[[ %s ==> %v ]]]\n", exk, exv)
}
indent := ""
for curr := ee; curr != nil; curr = curr.OriginalError {
indent += " "
@@ -328,6 +362,14 @@ func (ee *ExErr) GetMetaTime(key string) (time.Time, bool) {
return time.Time{}, false
}
func (ee *ExErr) GetExtra(key string) (any, bool) {
if v, ok := ee.Extra[key]; ok {
return v, true
}
return nil, false
}
// contains test if the supplied error is contained in this error (anywhere in the chain)
func (ee *ExErr) contains(original *ExErr) (*ExErr, bool) {
if original == nil {

View File

@@ -90,6 +90,20 @@ func (ee *ExErr) ToAPIJson(applyExtendListener bool, includeWrappedErrors bool,
apiOutput["__data"] = ee.toJson(0, applyExtendListener, includeMetaFields)
}
for exkey, exval := range ee.Extra {
// ensure we do not override existing values
for {
if _, ok := apiOutput[exkey]; ok {
exkey = "_" + exkey
} else {
break
}
}
apiOutput[exkey] = exval
}
if applyExtendListener {
pkgconfig.ExtendGinOutput(ee, apiOutput)
}

View File

@@ -86,3 +86,28 @@ func MessageMatch(e error, matcher func(string) bool) bool {
return false
}
// OriginalError returns the lowest level error, probably the original/external error that was originally wrapped
func OriginalError(e error) error {
if e == nil {
return nil
}
//goland:noinspection GoTypeAssertionOnErrors
bmerr, ok := e.(*ExErr)
for !ok {
return e
}
for bmerr.OriginalError != nil {
bmerr = bmerr.OriginalError
}
if bmerr.WrappedErr != nil {
if werr, ok := bmerr.WrappedErr.(error); ok {
return werr
}
}
return bmerr
}

9
ginext/jsonFilter.go Normal file
View File

@@ -0,0 +1,9 @@
package ginext
import "github.com/gin-gonic/gin"
var jsonFilterKey = "goext.jsonfilter"
func SetJSONFilter(g *gin.Context, filter string) {
g.Set(jsonFilterKey, filter)
}

View File

@@ -7,17 +7,21 @@ import (
)
type jsonHTTPResponse struct {
statusCode int
data any
headers []headerval
cookies []cookieval
statusCode int
data any
headers []headerval
cookies []cookieval
filterOverride *string
}
func (j jsonHTTPResponse) jsonRenderer(g *gin.Context) json.GoJsonRender {
var f *string
if jsonfilter := g.GetString("goext.jsonfilter"); jsonfilter != "" {
if jsonfilter := g.GetString(jsonFilterKey); jsonfilter != "" {
f = &jsonfilter
}
if j.filterOverride != nil {
f = j.filterOverride
}
return json.GoJsonRender{Data: j.data, NilSafeSlices: true, NilSafeMaps: true, Filter: f}
}
@@ -68,3 +72,7 @@ func (j jsonHTTPResponse) Headers() []string {
func JSON(sc int, data any) HTTPResponse {
return &jsonHTTPResponse{statusCode: sc, data: data}
}
func JSONWithFilter(sc int, data any, f string) HTTPResponse {
return &jsonHTTPResponse{statusCode: sc, data: data, filterOverride: &f}
}

View File

@@ -57,7 +57,7 @@ func (w *GinRoutesWrapper) Use(middleware ...gin.HandlerFunc) *GinRoutesWrapper
}
func (w *GinRoutesWrapper) WithJSONFilter(filter string) *GinRoutesWrapper {
return w.Use(func(g *gin.Context) { g.Set("goext.jsonfilter", filter) })
return w.Use(func(g *gin.Context) { g.Set(jsonFilterKey, filter) })
}
func (w *GinRoutesWrapper) GET(relativePath string) *GinRouteBuilder {
@@ -112,7 +112,7 @@ func (w *GinRouteBuilder) Use(middleware ...gin.HandlerFunc) *GinRouteBuilder {
}
func (w *GinRouteBuilder) WithJSONFilter(filter string) *GinRouteBuilder {
return w.Use(func(g *gin.Context) { g.Set("goext.jsonfilter", filter) })
return w.Use(func(g *gin.Context) { g.Set(jsonFilterKey, filter) })
}
func (w *GinRouteBuilder) Handle(handler WHandlerFunc) {

34
go.mod
View File

@@ -6,38 +6,38 @@ require (
github.com/gin-gonic/gin v1.10.0
github.com/glebarez/go-sqlite v1.22.0 // only needed for tests -.-
github.com/jmoiron/sqlx v1.4.0
github.com/rs/xid v1.5.0
github.com/rs/xid v1.6.0
github.com/rs/zerolog v1.33.0
go.mongodb.org/mongo-driver v1.16.0
golang.org/x/crypto v0.25.0
golang.org/x/sys v0.22.0
golang.org/x/term v0.22.0
go.mongodb.org/mongo-driver v1.17.1
golang.org/x/crypto v0.28.0
golang.org/x/sys v0.26.0
golang.org/x/term v0.25.0
)
require (
github.com/disintegration/imaging v1.6.2
github.com/jung-kurt/gofpdf v1.16.2
golang.org/x/sync v0.7.0
golang.org/x/sync v0.8.0
)
require (
github.com/bytedance/sonic v1.11.9 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/bytedance/sonic v1.12.3 // indirect
github.com/bytedance/sonic/loader v0.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.22.0 // indirect
github.com/go-playground/validator/v10 v10.22.1 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/compress v1.17.10 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
@@ -45,18 +45,18 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/text v0.16.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
golang.org/x/arch v0.11.0 // indirect
golang.org/x/image v0.21.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/text v0.19.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.37.6 // indirect

74
go.sum
View File

@@ -24,9 +24,19 @@ github.com/bytedance/sonic v1.11.8 h1:Zw/j1KfiS+OYTi9lyB3bb0CFxPJVkM17k1wyDG32LR
github.com/bytedance/sonic v1.11.8/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic v1.11.9 h1:LFHENlIY/SLzDWverzdOvgMztTxcfcF+cqNsz9pK5zg=
github.com/bytedance/sonic v1.11.9/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic v1.12.0 h1:YGPgxF9xzaCNvd/ZKdQ28yRovhfMFZQjuk6fKBzZ3ls=
github.com/bytedance/sonic v1.12.0/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic v1.12.1 h1:jWl5Qz1fy7X1ioY74WqO0KjAMtAGQs4sYnjiEBiyX24=
github.com/bytedance/sonic v1.12.1/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic v1.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg=
github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU=
github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic/loader v0.1.0/go.mod h1:UmRT+IRTGKz/DAkzcEGzyVqQFJ7H9BqwBO3pm9H/+HY=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
@@ -57,6 +67,8 @@ github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uq
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
@@ -85,6 +97,8 @@ github.com/go-playground/validator/v10 v10.21.0 h1:4fZA11ovvtkdgaeev9RGWPgc1uj3H
github.com/go-playground/validator/v10 v10.21.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
@@ -126,6 +140,8 @@ github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0N
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0=
github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
@@ -167,6 +183,8 @@ github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtos
github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -176,6 +194,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
@@ -213,6 +233,8 @@ github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqTosly
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76 h1:tBiBTKHnIjovYoLX/TPkcf+OjqqKGQrPtGT3Foz+Pgo=
github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76/go.mod h1:SQliXeA7Dhkt//vS29v3zpbEwoa+zb2Cn5xj5uO4K5U=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk=
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
@@ -224,11 +246,23 @@ go.mongodb.org/mongo-driver v1.15.1 h1:l+RvoUOoMXFmADTLfYDm7On9dRm7p4T80/lEQM+r7
go.mongodb.org/mongo-driver v1.15.1/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
go.mongodb.org/mongo-driver v1.16.0 h1:tpRsfBJMROVHKpdGyc1BBEzzjDUWjItxbVSZ8Ls4BQ4=
go.mongodb.org/mongo-driver v1.16.0/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw=
go.mongodb.org/mongo-driver v1.16.1 h1:rIVLL3q0IHM39dvE+z2ulZLp9ENZKThVfuvN/IiN4l8=
go.mongodb.org/mongo-driver v1.16.1/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw=
go.mongodb.org/mongo-driver v1.17.0 h1:Hp4q2MCjvY19ViwimTs00wHi7G4yzxh4/2+nTx8r40k=
go.mongodb.org/mongo-driver v1.17.0/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM=
go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.9.0 h1:ub9TgUInamJ8mrZIGlBG6/4TqWeMszd4N8lNorbrr6k=
golang.org/x/arch v0.9.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8=
golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -251,6 +285,12 @@ golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -260,6 +300,12 @@ golang.org/x/image v0.17.0 h1:nTRVVdajgB8zCMZVsViyzhnMKPwYeroEERRC64JuLco=
golang.org/x/image v0.17.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ=
golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys=
golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=
golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM=
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -282,12 +328,20 @@ golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -313,6 +367,14 @@ golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
@@ -329,6 +391,12 @@ golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -341,6 +409,12 @@ golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

View File

@@ -1,5 +1,5 @@
package goext
const GoextVersion = "0.0.487"
const GoextVersion = "0.0.520"
const GoextVersionTimestamp = "2024-07-18T17:45:56+0200"
const GoextVersionTimestamp = "2024-10-05T01:02:25+0200"

View File

@@ -788,7 +788,7 @@ FieldLoop:
if f.omitEmpty && isEmptyValue(fv) {
continue
} else if opts.filter != nil && len(f.jsonfilter) > 0 && !f.jsonfilter.Contains(*opts.filter) {
} else if !matchesJSONFilter(f.jsonfilter, opts.filter) {
continue
}
e.WriteByte(next)
@@ -808,6 +808,30 @@ FieldLoop:
}
}
func matchesJSONFilter(filter jsonfilter, value *string) bool {
if len(filter) == 0 {
return true // no filter in struct
}
if value == nil || *value == "" {
return false // no filter set, but struct has filter, return false
}
if len(filter) == 1 && filter[0] == "-" {
return false
}
if filter.Contains(*value) {
return true
}
if filter.Contains("*") {
return true
}
return false
}
func newStructEncoder(t reflect.Type, tagkey string) encoderFunc {
se := structEncoder{fields: cachedTypeFields(t, tagkey)}
return se.encode
@@ -1333,7 +1357,7 @@ func typeFields(t reflect.Type, tagkey string) structFields {
var jsonfilter []string
jsonfilterTag := sf.Tag.Get("jsonfilter")
if jsonfilterTag != "" && jsonfilterTag != "-" {
if jsonfilterTag != "" {
jsonfilter = strings.Split(jsonfilterTag, ",")
}

View File

@@ -169,7 +169,7 @@ func EncodeImage(img image.Image, compression ImageCompresson) (bytes.Buffer, st
}
}
func ObjectFitImage(img image.Image, bbw float64, bbh float64, fit ImageFit, fillColor color.Color) (image.Image, error) {
func ObjectFitImage(img image.Image, bbw float64, bbh float64, fit ImageFit, fillColor color.Color) (image.Image, PercentageRectangle, error) {
iw := img.Bounds().Size().X
ih := img.Bounds().Size().Y
@@ -214,12 +214,12 @@ func ObjectFitImage(img image.Image, bbw float64, bbh float64, fit ImageFit, fil
draw.Draw(newImg, newImg.Bounds(), &image.Uniform{C: fillColor}, image.Pt(0, 0), draw.Src)
draw.Draw(newImg, newImg.Bounds(), img, image.Pt(0, 0), draw.Over)
return newImg, nil
return newImg, PercentageRectangle{0, 0, 1, 1}, nil
}
if fit == ImageFitContainCenter || fit == ImageFitContainTopLeft || fit == ImageFitContainTopRight || fit == ImageFitContainBottomLeft || fit == ImageFitContainBottomRight {
// image-fit:cover fills the target-bounding-box with the image, there is potentially empty-space, it potentially cuts parts of the image away
// image-fit:contain fills the target-bounding-box with the image, there is potentially empty-space, it potentially cuts parts of the image away
// we use the bigger (!) value of facW and facH,
// because the image is made to fit the bounding-box, the bigger factor (= the dimension the image is stretched less) is relevant
@@ -266,7 +266,7 @@ func ObjectFitImage(img image.Image, bbw float64, bbh float64, fit ImageFit, fil
draw.Draw(newImg, newImg.Bounds(), &image.Uniform{C: fillColor}, image.Pt(0, 0), draw.Src)
draw.Draw(newImg, destBounds, img, image.Pt(0, 0), draw.Over)
return newImg, nil
return newImg, calcRelativeRect(destBounds, newImg.Bounds()), nil
}
if fit == ImageFitStretch {
@@ -293,10 +293,10 @@ func ObjectFitImage(img image.Image, bbw float64, bbh float64, fit ImageFit, fil
draw.Draw(newImg, newImg.Bounds(), &image.Uniform{C: fillColor}, image.Pt(0, 0), draw.Src)
draw.Draw(newImg, newImg.Bounds(), img, image.Pt(0, 0), draw.Over)
return newImg, nil
return newImg, PercentageRectangle{0, 0, 1, 1}, nil
}
return nil, exerr.New(exerr.TypeInternal, fmt.Sprintf("unknown image-fit: '%s'", fit)).Build()
return nil, PercentageRectangle{}, exerr.New(exerr.TypeInternal, fmt.Sprintf("unknown image-fit: '%s'", fit)).Build()
}
func VerifyAndDecodeImage(data io.Reader, mime string) (image.Image, error) {

35
imageext/types.go Normal file
View File

@@ -0,0 +1,35 @@
package imageext
import "image"
type Rectangle struct {
X float64
Y float64
W float64
H float64
}
type PercentageRectangle struct {
X float64 // [0..1]
Y float64 // [0..1]
W float64 // [0..1]
H float64 // [0..1]
}
func (r PercentageRectangle) Of(ref Rectangle) Rectangle {
return Rectangle{
X: ref.X + r.X*ref.W,
Y: ref.Y + r.Y*ref.H,
W: r.W * ref.W,
H: r.H * ref.H,
}
}
func calcRelativeRect(inner image.Rectangle, outer image.Rectangle) PercentageRectangle {
return PercentageRectangle{
X: float64(inner.Min.X-outer.Min.X) / float64(outer.Dx()),
Y: float64(inner.Min.Y-outer.Min.Y) / float64(outer.Dy()),
W: float64(inner.Dx()) / float64(outer.Dx()),
H: float64(inner.Dy()) / float64(outer.Dy()),
}
}

View File

@@ -323,6 +323,16 @@ func ArrMap[T1 any, T2 any](arr []T1, conv func(v T1) T2) []T2 {
return r
}
func ArrDeRef[T1 any](arr []*T1) []T1 {
r := make([]T1, 0, len(arr))
for _, v := range arr {
if v != nil {
r = append(r, *v)
}
}
return r
}
func MapMap[TK comparable, TV any, TR any](inmap map[TK]TV, conv func(k TK, v TV) TR) []TR {
r := make([]TR, 0, len(inmap))
for k, v := range inmap {

15
langext/io.go Normal file
View File

@@ -0,0 +1,15 @@
package langext
import "io"
type nopCloser struct {
io.Writer
}
func (n nopCloser) Close() error {
return nil // no op
}
func WriteNopCloser(w io.Writer) io.WriteCloser {
return nopCloser{w}
}

View File

@@ -66,7 +66,7 @@ func CopyMap[K comparable, V any](a map[K]V) map[K]V {
func ForceMap[K comparable, V any](v map[K]V) map[K]V {
if v == nil {
return make(map[K]V, 0)
return make(map[K]V)
} else {
return v
}

View File

@@ -7,6 +7,7 @@ import (
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"sync"
"time"
)
type DB interface {
@@ -57,89 +58,121 @@ func (db *database) AddListener(listener Listener) {
func (db *database) Exec(ctx context.Context, sqlstr string, prep PP) (sql.Result, error) {
origsql := sqlstr
t0 := time.Now()
preMeta := PreExecMeta{Context: ctx, TransactionConstructorContext: nil}
for _, v := range db.lstr {
err := v.PreExec(ctx, nil, &sqlstr, &prep)
err := v.PreExec(ctx, nil, &sqlstr, &prep, preMeta)
if err != nil {
return nil, exerr.Wrap(err, "failed to call SQL pre-exec listener").Str("original_sql", origsql).Str("sql", sqlstr).Any("sql_params", prep).Build()
}
}
t1 := time.Now()
res, err := db.db.NamedExecContext(ctx, sqlstr, prep)
postMeta := PostExecMeta{Context: ctx, TransactionConstructorContext: nil, Init: t0, Start: t1, End: time.Now()}
for _, v := range db.lstr {
v.PostExec(nil, origsql, sqlstr, prep)
v.PostExec(nil, origsql, sqlstr, prep, postMeta)
}
if err != nil {
return nil, exerr.Wrap(err, "Failed to [exec] sql statement").Str("original_sql", origsql).Str("sql", sqlstr).Any("sql_params", prep).Build()
}
return res, nil
}
func (db *database) Query(ctx context.Context, sqlstr string, prep PP) (*sqlx.Rows, error) {
origsql := sqlstr
t0 := time.Now()
preMeta := PreQueryMeta{Context: ctx, TransactionConstructorContext: nil}
for _, v := range db.lstr {
err := v.PreQuery(ctx, nil, &sqlstr, &prep)
err := v.PreQuery(ctx, nil, &sqlstr, &prep, preMeta)
if err != nil {
return nil, exerr.Wrap(err, "failed to call SQL pre-query listener").Str("original_sql", origsql).Str("sql", sqlstr).Any("sql_params", prep).Build()
}
}
t1 := time.Now()
rows, err := sqlx.NamedQueryContext(ctx, db.db, sqlstr, prep)
postMeta := PostQueryMeta{Context: ctx, TransactionConstructorContext: nil, Init: t0, Start: t1, End: time.Now()}
for _, v := range db.lstr {
v.PostQuery(nil, origsql, sqlstr, prep)
v.PostQuery(nil, origsql, sqlstr, prep, postMeta)
}
if err != nil {
return nil, exerr.Wrap(err, "Failed to [query] sql statement").Str("original_sql", origsql).Str("sql", sqlstr).Any("sql_params", prep).Build()
}
return rows, nil
}
func (db *database) Ping(ctx context.Context) error {
t0 := time.Now()
preMeta := PrePingMeta{Context: ctx}
for _, v := range db.lstr {
err := v.PrePing(ctx)
err := v.PrePing(ctx, preMeta)
if err != nil {
return err
}
}
t1 := time.Now()
err := db.db.PingContext(ctx)
postMeta := PostPingMeta{Context: ctx, Init: t0, Start: t1, End: time.Now()}
for _, v := range db.lstr {
v.PostPing(err)
v.PostPing(err, postMeta)
}
if err != nil {
return exerr.Wrap(err, "Failed to [ping] sql database").Build()
}
return nil
}
func (db *database) BeginTransaction(ctx context.Context, iso sql.IsolationLevel) (Tx, error) {
t0 := time.Now()
db.lock.Lock()
txid := db.txctr
db.txctr += 1 // with overflow !
db.lock.Unlock()
preMeta := PreTxBeginMeta{Context: ctx}
for _, v := range db.lstr {
err := v.PreTxBegin(ctx, txid)
err := v.PreTxBegin(ctx, txid, preMeta)
if err != nil {
return nil, err
}
}
t1 := time.Now()
xtx, err := db.db.BeginTxx(ctx, &sql.TxOptions{Isolation: iso})
postMeta := PostTxBeginMeta{Context: ctx, Init: t0, Start: t1, End: time.Now()}
for _, v := range db.lstr {
v.PostTxBegin(txid, err, postMeta)
}
if err != nil {
return nil, exerr.Wrap(err, "Failed to start sql transaction").Build()
}
for _, v := range db.lstr {
v.PostTxBegin(txid, err)
}
return NewTransaction(xtx, txid, db), nil
return newTransaction(ctx, xtx, txid, db), nil
}
func (db *database) Exit() error {

View File

@@ -1,187 +1,265 @@
package sq
import "context"
import (
"context"
"time"
)
type PrePingMeta struct {
Context context.Context
}
type PreTxBeginMeta struct {
Context context.Context
ConstructorContext context.Context
}
type PreTxCommitMeta struct {
ConstructorContext context.Context
}
type PreTxRollbackMeta struct {
ConstructorContext context.Context
}
type PreQueryMeta struct {
Context context.Context
TransactionConstructorContext context.Context
}
type PreExecMeta struct {
Context context.Context
TransactionConstructorContext context.Context
}
type PostPingMeta struct {
Context context.Context
Init time.Time
Start time.Time
End time.Time
}
type PostTxBeginMeta struct {
Context context.Context
Init time.Time
Start time.Time
End time.Time
}
type PostTxCommitMeta struct {
ConstructorContext context.Context
Init time.Time
Start time.Time
End time.Time
ExecCounter int
QueryCounter int
}
type PostTxRollbackMeta struct {
ConstructorContext context.Context
Init time.Time
Start time.Time
End time.Time
ExecCounter int
QueryCounter int
}
type PostQueryMeta struct {
Context context.Context
TransactionConstructorContext context.Context
Init time.Time
Start time.Time
End time.Time
}
type PostExecMeta struct {
Context context.Context
TransactionConstructorContext context.Context
Init time.Time
Start time.Time
End time.Time
}
type Listener interface {
PrePing(ctx context.Context) error
PreTxBegin(ctx context.Context, txid uint16) error
PreTxCommit(txid uint16) error
PreTxRollback(txid uint16) error
PreQuery(ctx context.Context, txID *uint16, sql *string, params *PP) error
PreExec(ctx context.Context, txID *uint16, sql *string, params *PP) error
PrePing(ctx context.Context, meta PrePingMeta) error
PreTxBegin(ctx context.Context, txid uint16, meta PreTxBeginMeta) error
PreTxCommit(txid uint16, meta PreTxCommitMeta) error
PreTxRollback(txid uint16, meta PreTxRollbackMeta) error
PreQuery(ctx context.Context, txID *uint16, sql *string, params *PP, meta PreQueryMeta) error
PreExec(ctx context.Context, txID *uint16, sql *string, params *PP, meta PreExecMeta) error
PostPing(result error)
PostTxBegin(txid uint16, result error)
PostTxCommit(txid uint16, result error)
PostTxRollback(txid uint16, result error)
PostQuery(txID *uint16, sqlOriginal string, sqlReal string, params PP)
PostExec(txID *uint16, sqlOriginal string, sqlReal string, params PP)
PostPing(result error, meta PostPingMeta)
PostTxBegin(txid uint16, result error, meta PostTxBeginMeta)
PostTxCommit(txid uint16, result error, meta PostTxCommitMeta)
PostTxRollback(txid uint16, result error, meta PostTxRollbackMeta)
PostQuery(txID *uint16, sqlOriginal string, sqlReal string, params PP, meta PostQueryMeta)
PostExec(txID *uint16, sqlOriginal string, sqlReal string, params PP, meta PostExecMeta)
}
type genListener struct {
prePing func(ctx context.Context) error
preTxBegin func(ctx context.Context, txid uint16) error
preTxCommit func(txid uint16) error
preTxRollback func(txid uint16) error
preQuery func(ctx context.Context, txID *uint16, sql *string, params *PP) error
preExec func(ctx context.Context, txID *uint16, sql *string, params *PP) error
postPing func(result error)
postTxBegin func(txid uint16, result error)
postTxCommit func(txid uint16, result error)
postTxRollback func(txid uint16, result error)
postQuery func(txID *uint16, sqlOriginal string, sqlReal string, params PP)
postExec func(txID *uint16, sqlOriginal string, sqlReal string, params PP)
prePing func(ctx context.Context, meta PrePingMeta) error
preTxBegin func(ctx context.Context, txid uint16, meta PreTxBeginMeta) error
preTxCommit func(txid uint16, meta PreTxCommitMeta) error
preTxRollback func(txid uint16, meta PreTxRollbackMeta) error
preQuery func(ctx context.Context, txID *uint16, sql *string, params *PP, meta PreQueryMeta) error
preExec func(ctx context.Context, txID *uint16, sql *string, params *PP, meta PreExecMeta) error
postPing func(result error, meta PostPingMeta)
postTxBegin func(txid uint16, result error, meta PostTxBeginMeta)
postTxCommit func(txid uint16, result error, meta PostTxCommitMeta)
postTxRollback func(txid uint16, result error, meta PostTxRollbackMeta)
postQuery func(txID *uint16, sqlOriginal string, sqlReal string, params PP, meta PostQueryMeta)
postExec func(txID *uint16, sqlOriginal string, sqlReal string, params PP, meta PostExecMeta)
}
func (g genListener) PrePing(ctx context.Context) error {
func (g genListener) PrePing(ctx context.Context, meta PrePingMeta) error {
if g.prePing != nil {
return g.prePing(ctx)
return g.prePing(ctx, meta)
} else {
return nil
}
}
func (g genListener) PreTxBegin(ctx context.Context, txid uint16) error {
func (g genListener) PreTxBegin(ctx context.Context, txid uint16, meta PreTxBeginMeta) error {
if g.preTxBegin != nil {
return g.preTxBegin(ctx, txid)
return g.preTxBegin(ctx, txid, meta)
} else {
return nil
}
}
func (g genListener) PreTxCommit(txid uint16) error {
func (g genListener) PreTxCommit(txid uint16, meta PreTxCommitMeta) error {
if g.preTxCommit != nil {
return g.preTxCommit(txid)
return g.preTxCommit(txid, meta)
} else {
return nil
}
}
func (g genListener) PreTxRollback(txid uint16) error {
func (g genListener) PreTxRollback(txid uint16, meta PreTxRollbackMeta) error {
if g.preTxRollback != nil {
return g.preTxRollback(txid)
return g.preTxRollback(txid, meta)
} else {
return nil
}
}
func (g genListener) PreQuery(ctx context.Context, txID *uint16, sql *string, params *PP) error {
func (g genListener) PreQuery(ctx context.Context, txID *uint16, sql *string, params *PP, meta PreQueryMeta) error {
if g.preQuery != nil {
return g.preQuery(ctx, txID, sql, params)
return g.preQuery(ctx, txID, sql, params, meta)
} else {
return nil
}
}
func (g genListener) PreExec(ctx context.Context, txID *uint16, sql *string, params *PP) error {
func (g genListener) PreExec(ctx context.Context, txID *uint16, sql *string, params *PP, meta PreExecMeta) error {
if g.preExec != nil {
return g.preExec(ctx, txID, sql, params)
return g.preExec(ctx, txID, sql, params, meta)
} else {
return nil
}
}
func (g genListener) PostPing(result error) {
func (g genListener) PostPing(result error, meta PostPingMeta) {
if g.postPing != nil {
g.postPing(result)
g.postPing(result, meta)
}
}
func (g genListener) PostTxBegin(txid uint16, result error) {
func (g genListener) PostTxBegin(txid uint16, result error, meta PostTxBeginMeta) {
if g.postTxBegin != nil {
g.postTxBegin(txid, result)
g.postTxBegin(txid, result, meta)
}
}
func (g genListener) PostTxCommit(txid uint16, result error) {
func (g genListener) PostTxCommit(txid uint16, result error, meta PostTxCommitMeta) {
if g.postTxCommit != nil {
g.postTxCommit(txid, result)
g.postTxCommit(txid, result, meta)
}
}
func (g genListener) PostTxRollback(txid uint16, result error) {
func (g genListener) PostTxRollback(txid uint16, result error, meta PostTxRollbackMeta) {
if g.postTxRollback != nil {
g.postTxRollback(txid, result)
g.postTxRollback(txid, result, meta)
}
}
func (g genListener) PostQuery(txID *uint16, sqlOriginal string, sqlReal string, params PP) {
func (g genListener) PostQuery(txID *uint16, sqlOriginal string, sqlReal string, params PP, meta PostQueryMeta) {
if g.postQuery != nil {
g.postQuery(txID, sqlOriginal, sqlReal, params)
g.postQuery(txID, sqlOriginal, sqlReal, params, meta)
}
}
func (g genListener) PostExec(txID *uint16, sqlOriginal string, sqlReal string, params PP) {
func (g genListener) PostExec(txID *uint16, sqlOriginal string, sqlReal string, params PP, meta PostExecMeta) {
if g.postExec != nil {
g.postExec(txID, sqlOriginal, sqlReal, params)
g.postExec(txID, sqlOriginal, sqlReal, params, meta)
}
}
func NewPrePingListener(f func(ctx context.Context) error) Listener {
func NewPrePingListener(f func(ctx context.Context, meta PrePingMeta) error) Listener {
return genListener{prePing: f}
}
func NewPreTxBeginListener(f func(ctx context.Context, txid uint16) error) Listener {
func NewPreTxBeginListener(f func(ctx context.Context, txid uint16, meta PreTxBeginMeta) error) Listener {
return genListener{preTxBegin: f}
}
func NewPreTxCommitListener(f func(txid uint16) error) Listener {
func NewPreTxCommitListener(f func(txid uint16, meta PreTxCommitMeta) error) Listener {
return genListener{preTxCommit: f}
}
func NewPreTxRollbackListener(f func(txid uint16) error) Listener {
func NewPreTxRollbackListener(f func(txid uint16, meta PreTxRollbackMeta) error) Listener {
return genListener{preTxRollback: f}
}
func NewPreQueryListener(f func(ctx context.Context, txID *uint16, sql *string, params *PP) error) Listener {
func NewPreQueryListener(f func(ctx context.Context, txID *uint16, sql *string, params *PP, meta PreQueryMeta) error) Listener {
return genListener{preQuery: f}
}
func NewPreExecListener(f func(ctx context.Context, txID *uint16, sql *string, params *PP) error) Listener {
func NewPreExecListener(f func(ctx context.Context, txID *uint16, sql *string, params *PP, meta PreExecMeta) error) Listener {
return genListener{preExec: f}
}
func NewPreListener(f func(ctx context.Context, cmdtype string, txID *uint16, sql *string, params *PP) error) Listener {
return genListener{
preExec: func(ctx context.Context, txID *uint16, sql *string, params *PP) error {
preExec: func(ctx context.Context, txID *uint16, sql *string, params *PP, meta PreExecMeta) error {
return f(ctx, "EXEC", txID, sql, params)
},
preQuery: func(ctx context.Context, txID *uint16, sql *string, params *PP) error {
preQuery: func(ctx context.Context, txID *uint16, sql *string, params *PP, meta PreQueryMeta) error {
return f(ctx, "QUERY", txID, sql, params)
},
}
}
func NewPostPingListener(f func(result error)) Listener {
func NewPostPingListener(f func(result error, meta PostPingMeta)) Listener {
return genListener{postPing: f}
}
func NewPostTxBeginListener(f func(txid uint16, result error)) Listener {
func NewPostTxBeginListener(f func(txid uint16, result error, meta PostTxBeginMeta)) Listener {
return genListener{postTxBegin: f}
}
func NewPostTxCommitListener(f func(txid uint16, result error)) Listener {
func NewPostTxCommitListener(f func(txid uint16, result error, meta PostTxCommitMeta)) Listener {
return genListener{postTxCommit: f}
}
func NewPostTxRollbackListener(f func(txid uint16, result error)) Listener {
func NewPostTxRollbackListener(f func(txid uint16, result error, meta PostTxRollbackMeta)) Listener {
return genListener{postTxRollback: f}
}
func NewPostQueryListener(f func(txID *uint16, sqlOriginal string, sqlReal string, params PP)) Listener {
func NewPostQueryListener(f func(txID *uint16, sqlOriginal string, sqlReal string, params PP, meta PostQueryMeta)) Listener {
return genListener{postQuery: f}
}
func NewPostExecListener(f func(txID *uint16, sqlOriginal string, sqlReal string, params PP)) Listener {
func NewPostExecListener(f func(txID *uint16, sqlOriginal string, sqlReal string, params PP, meta PostExecMeta)) Listener {
return genListener{postExec: f}
}
func NewPostListener(f func(cmdtype string, txID *uint16, sqlOriginal string, sqlReal string, params PP)) Listener {
return genListener{
postExec: func(txID *uint16, sqlOriginal string, sqlReal string, params PP) {
postExec: func(txID *uint16, sqlOriginal string, sqlReal string, params PP, meta PostExecMeta) {
f("EXEC", txID, sqlOriginal, sqlReal, params)
},
postQuery: func(txID *uint16, sqlOriginal string, sqlReal string, params PP) {
postQuery: func(txID *uint16, sqlOriginal string, sqlReal string, params PP, meta PostQueryMeta) {
f("QUERY", txID, sqlOriginal, sqlReal, params)
},
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/jmoiron/sqlx"
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"time"
)
type TxStatus string
@@ -26,62 +27,78 @@ type Tx interface {
}
type transaction struct {
tx *sqlx.Tx
id uint16
status TxStatus
execCtr int
queryCtr int
db *database
constructorContext context.Context
tx *sqlx.Tx
id uint16
status TxStatus
execCtr int
queryCtr int
db *database
}
func NewTransaction(xtx *sqlx.Tx, txid uint16, db *database) Tx {
func newTransaction(ctx context.Context, xtx *sqlx.Tx, txid uint16, db *database) Tx {
return &transaction{
tx: xtx,
id: txid,
status: TxStatusInitial,
execCtr: 0,
queryCtr: 0,
db: db,
constructorContext: ctx,
tx: xtx,
id: txid,
status: TxStatusInitial,
execCtr: 0,
queryCtr: 0,
db: db,
}
}
func (tx *transaction) Rollback() error {
t0 := time.Now()
preMeta := PreTxRollbackMeta{ConstructorContext: tx.constructorContext}
for _, v := range tx.db.lstr {
err := v.PreTxRollback(tx.id)
err := v.PreTxRollback(tx.id, preMeta)
if err != nil {
return exerr.Wrap(err, "failed to call SQL pre-rollback listener").Int("tx.id", int(tx.id)).Build()
}
}
t1 := time.Now()
result := tx.tx.Rollback()
if result == nil {
tx.status = TxStatusRollback
}
postMeta := PostTxRollbackMeta{ConstructorContext: tx.constructorContext, Init: t0, Start: t1, End: time.Now(), ExecCounter: tx.execCtr, QueryCounter: tx.queryCtr}
for _, v := range tx.db.lstr {
v.PostTxRollback(tx.id, result)
v.PostTxRollback(tx.id, result, postMeta)
}
return result
}
func (tx *transaction) Commit() error {
t0 := time.Now()
preMeta := PreTxCommitMeta{ConstructorContext: tx.constructorContext}
for _, v := range tx.db.lstr {
err := v.PreTxCommit(tx.id)
err := v.PreTxCommit(tx.id, preMeta)
if err != nil {
return exerr.Wrap(err, "failed to call SQL pre-commit listener").Int("tx.id", int(tx.id)).Build()
}
}
t1 := time.Now()
result := tx.tx.Commit()
if result == nil {
tx.status = TxStatusComitted
}
postMeta := PostTxCommitMeta{ConstructorContext: tx.constructorContext, Init: t0, Start: t1, End: time.Now(), ExecCounter: tx.execCtr, QueryCounter: tx.queryCtr}
for _, v := range tx.db.lstr {
v.PostTxRollback(tx.id, result)
v.PostTxCommit(tx.id, result, postMeta)
}
return result
@@ -89,21 +106,29 @@ func (tx *transaction) Commit() error {
func (tx *transaction) Exec(ctx context.Context, sqlstr string, prep PP) (sql.Result, error) {
origsql := sqlstr
t0 := time.Now()
preMeta := PreExecMeta{Context: ctx, TransactionConstructorContext: tx.constructorContext}
for _, v := range tx.db.lstr {
err := v.PreExec(ctx, langext.Ptr(tx.id), &sqlstr, &prep)
err := v.PreExec(ctx, langext.Ptr(tx.id), &sqlstr, &prep, preMeta)
if err != nil {
return nil, exerr.Wrap(err, "failed to call SQL pre-exec listener").Int("tx.id", int(tx.id)).Str("original_sql", origsql).Str("sql", sqlstr).Any("sql_params", prep).Build()
}
}
t1 := time.Now()
res, err := tx.tx.NamedExecContext(ctx, sqlstr, prep)
tx.execCtr++
if tx.status == TxStatusInitial && err == nil {
tx.status = TxStatusActive
}
postMeta := PostExecMeta{Context: ctx, TransactionConstructorContext: tx.constructorContext, Init: t0, Start: t1, End: time.Now()}
for _, v := range tx.db.lstr {
v.PostExec(langext.Ptr(tx.id), origsql, sqlstr, prep)
v.PostExec(langext.Ptr(tx.id), origsql, sqlstr, prep, postMeta)
}
if err != nil {
@@ -114,21 +139,29 @@ func (tx *transaction) Exec(ctx context.Context, sqlstr string, prep PP) (sql.Re
func (tx *transaction) Query(ctx context.Context, sqlstr string, prep PP) (*sqlx.Rows, error) {
origsql := sqlstr
t0 := time.Now()
preMeta := PreQueryMeta{Context: ctx, TransactionConstructorContext: tx.constructorContext}
for _, v := range tx.db.lstr {
err := v.PreQuery(ctx, langext.Ptr(tx.id), &sqlstr, &prep)
err := v.PreQuery(ctx, langext.Ptr(tx.id), &sqlstr, &prep, preMeta)
if err != nil {
return nil, exerr.Wrap(err, "failed to call SQL pre-query listener").Int("tx.id", int(tx.id)).Str("original_sql", origsql).Str("sql", sqlstr).Any("sql_params", prep).Build()
}
}
t1 := time.Now()
rows, err := sqlx.NamedQueryContext(ctx, tx.tx, sqlstr, prep)
tx.queryCtr++
if tx.status == TxStatusInitial && err == nil {
tx.status = TxStatusActive
}
postMeta := PostQueryMeta{Context: ctx, TransactionConstructorContext: tx.constructorContext, Init: t0, Start: t1, End: time.Now()}
for _, v := range tx.db.lstr {
v.PostQuery(langext.Ptr(tx.id), origsql, sqlstr, prep)
v.PostQuery(langext.Ptr(tx.id), origsql, sqlstr, prep, postMeta)
}
if err != nil {

View File

@@ -2,6 +2,9 @@ package timeext
import "time"
// YearDifference calculates the difference between two timestamps in years.
// = t1 - t2
// returns a float value
func YearDifference(t1 time.Time, t2 time.Time, tz *time.Location) float64 {
yDelta := float64(t1.Year() - t2.Year())
@@ -11,3 +14,31 @@ func YearDifference(t1 time.Time, t2 time.Time, tz *time.Location) float64 {
return yDelta + (processT1 - processT2)
}
// MonthDifference calculates the difference between two timestamps in months.
// = t1 - t2
// returns a float value
func MonthDifference(t1 time.Time, t2 time.Time) float64 {
yDelta := float64(t1.Year() - t2.Year())
mDelta := float64(t1.Month() - t2.Month())
dDelta := float64(0)
t1MonthDays := DaysInMonth(t1)
t2MonthDays := DaysInMonth(t2)
if t2.Year() > t1.Year() || (t2.Year() == t1.Year() && t2.Month() > t1.Month()) {
dDelta -= 1
dDelta += float64(t1MonthDays-t1.Day()) / float64(t1MonthDays)
dDelta += float64(t2.Day()) / float64(t2MonthDays)
} else if t2.Year() < t1.Year() || (t2.Year() == t1.Year() && t2.Month() < t1.Month()) {
dDelta -= 1
dDelta += float64(t1.Day()) / float64(t1MonthDays)
dDelta += float64(t2MonthDays-t2.Day()) / float64(t2MonthDays)
} else {
dDelta += float64(t1.Day()-t2.Day()) / float64(t1MonthDays)
}
return yDelta*12 + mDelta + dDelta
}

View File

@@ -81,3 +81,63 @@ func epsilonEquals(a, b float64) bool {
epsilon := 0.01
return math.Abs(a-b) < epsilon
}
func TestMonthDifferenceSameDate(t *testing.T) {
t1 := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
t2 := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
expected := 0.0
result := MonthDifference(t2, t1)
if !epsilonEquals(result, expected) {
t.Errorf("Expected %v, got %v", expected, result)
}
}
func TestMonthDifferenceSameMonth(t *testing.T) {
t1 := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
t2 := time.Date(2022, 1, 31, 0, 0, 0, 0, time.UTC)
expected := 0.967741935483871 // Approximation of 30/31 days
result := MonthDifference(t2, t1)
if !epsilonEquals(result, expected) {
t.Errorf("Expected %v, got %v", expected, result)
}
}
func TestMonthDifferenceDifferentMonthsSameYear(t *testing.T) {
t1 := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
t2 := time.Date(2022, 3, 1, 0, 0, 0, 0, time.UTC)
expected := 2.0
result := MonthDifference(t2, t1)
if !epsilonEquals(result, expected) {
t.Errorf("Expected %v, got %v", expected, result)
}
}
func TestMonthDifferenceDifferentYears(t *testing.T) {
t1 := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
t2 := time.Date(2022, 2, 1, 0, 0, 0, 0, time.UTC)
expected := 2.0
result := MonthDifference(t2, t1)
if !epsilonEquals(result, expected) {
t.Errorf("Expected %v, got %v", expected, result)
}
}
func TestMonthDifferenceT1BeforeT2(t *testing.T) {
t1 := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
t2 := time.Date(2022, 6, 1, 0, 0, 0, 0, time.UTC)
expected := 5.0
result := MonthDifference(t2, t1)
if !epsilonEquals(result, expected) {
t.Errorf("Expected %v, got %v", expected, result)
}
}
func TestMonthDifferenceT1AfterT2(t *testing.T) {
t1 := time.Date(2022, 6, 1, 0, 0, 0, 0, time.UTC)
t2 := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
expected := -5.0
result := MonthDifference(t2, t1)
if !epsilonEquals(result, expected) {
t.Errorf("Expected %v, got %v", expected, result)
}
}

View File

@@ -184,3 +184,10 @@ func AddYears(t time.Time, yearCount float64, tz *time.Location) time.Time {
return t.Add(time.Duration(float64(t1.Sub(t0)) * floatCount))
}
func DaysInMonth(t time.Time) int {
// https://stackoverflow.com/a/73882035/1761622
y, m, _ := t.Date()
return time.Date(y, m+1, 0, 0, 0, 0, 0, time.UTC).Day()
}

View File

@@ -191,3 +191,39 @@ func TestCombineDateAndTime_CombineDifferentParts(t *testing.T) {
t.Errorf("Expected %v, got %v", expected, result)
}
}
func TestDaysInMonth_31Days(t *testing.T) {
date := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC) // January
expected := 31
result := DaysInMonth(date)
if result != expected {
t.Errorf("Expected %d but got %d", expected, result)
}
}
func TestDaysInMonth_30Days(t *testing.T) {
date := time.Date(2022, 4, 1, 0, 0, 0, 0, time.UTC) // April
expected := 30
result := DaysInMonth(date)
if result != expected {
t.Errorf("Expected %d but got %d", expected, result)
}
}
func TestDaysInMonth_FebruaryLeapYear(t *testing.T) {
date := time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC) // February in a leap year
expected := 29
result := DaysInMonth(date)
if result != expected {
t.Errorf("Expected %d but got %d", expected, result)
}
}
func TestDaysInMonth_FebruaryNonLeapYear(t *testing.T) {
date := time.Date(2021, 2, 1, 0, 0, 0, 0, time.UTC) // February in a non-leap year
expected := 28
result := DaysInMonth(date)
if result != expected {
t.Errorf("Expected %d but got %d", expected, result)
}
}

View File

@@ -12,7 +12,11 @@ import (
func (c *Coll[TData]) FindOne(ctx context.Context, filter bson.M) (TData, error) {
r, err := c.findOneInternal(ctx, filter, false)
if err != nil {
return *new(TData), exerr.Wrap(err, "mongo-query[find-one] failed").Str("collection", c.Name()).Build()
if filterId, ok := filter["_id"]; ok {
return *new(TData), exerr.Wrap(err, "mongo-query[find-one] failed").Str("collection", c.Name()).Any("filter", filter).Any("filter_id", filterId).Build()
} else {
return *new(TData), exerr.Wrap(err, "mongo-query[find-one] failed").Str("collection", c.Name()).Any("filter", filter).Build()
}
}
return *r, nil
@@ -21,7 +25,7 @@ func (c *Coll[TData]) FindOne(ctx context.Context, filter bson.M) (TData, error)
func (c *Coll[TData]) FindOneOpt(ctx context.Context, filter bson.M) (*TData, error) {
r, err := c.findOneInternal(ctx, filter, true)
if err != nil {
return nil, exerr.Wrap(err, "mongo-query[find-one-opt] failed").Str("collection", c.Name()).Build()
return nil, exerr.Wrap(err, "mongo-query[find-one-opt] failed").Str("collection", c.Name()).Any("filter", filter).Build()
}
return r, nil
@@ -58,7 +62,11 @@ func (c *Coll[TData]) findOneInternal(ctx context.Context, filter bson.M, allowN
return nil, nil
}
if err != nil {
return nil, exerr.Wrap(err, "mongo-query[find-one] failed").Any("filter", filter).Str("collection", c.Name()).NoLog().Build()
if filterId, ok := filter["_id"]; ok {
return nil, exerr.Wrap(err, "mongo-query[find-one|internal] failed").Str("collection", c.Name()).Any("filter", filter).Any("filter_id", filterId).NoLog().Build()
} else {
return nil, exerr.Wrap(err, "mongo-query[find-one|internal] failed").Str("collection", c.Name()).Any("filter", filter).NoLog().Build()
}
}
return &res, nil

View File

@@ -215,7 +215,7 @@ func createPaginationPipeline[TData any](coll *Coll[TData], token ct.CursorToken
// the conflict-resolution condition, for entries with the _same_ <field> as the $primary we take the ones with a greater $secondary (= newer)
cond = append(cond, bson.M{"$and": bson.A{
bson.M{fieldPrimary: valuePrimary},
bson.M{"$or": bson.A{bson.M{fieldPrimary: valuePrimary}, bson.M{fieldPrimary: nil}, bson.M{fieldPrimary: bson.M{"$exists": false}}}},
bson.M{*fieldSecondary: bson.M{"$gt": valueSecondary}},
}})
@@ -225,7 +225,7 @@ func createPaginationPipeline[TData any](coll *Coll[TData], token ct.CursorToken
// the conflict-resolution condition, for entries with the _same_ <field> as the $primary we take the ones with a smaller $secondary (= older)
cond = append(cond, bson.M{"$and": bson.A{
bson.M{fieldPrimary: valuePrimary},
bson.M{"$or": bson.A{bson.M{fieldPrimary: valuePrimary}, bson.M{fieldPrimary: nil}, bson.M{fieldPrimary: bson.M{"$exists": false}}}},
bson.M{*fieldSecondary: bson.M{"$lt": valueSecondary}},
}})

View File

@@ -60,19 +60,27 @@ func (c *Coll[TData]) Paginate(ctx context.Context, filter pag.MongoFilter, page
return nil, pag.Pagination{}, exerr.Wrap(err, "failed to all-decode entities").Build()
}
cursorTotalCount, err := c.coll.Aggregate(ctx, pipelineTotalCount)
if err != nil {
return nil, pag.Pagination{}, exerr.Wrap(err, "mongo-aggregation failed").Any("pipeline", pipelineTotalCount).Str("collection", c.Name()).Build()
}
var tcRes totalCountResult
if cursorTotalCount.Next(ctx) {
err = cursorTotalCount.Decode(&tcRes)
if err != nil {
return nil, pag.Pagination{}, exerr.Wrap(err, "failed to decode mongo-aggregation $count result").Any("pipeline", pipelineTotalCount).Str("collection", c.Name()).Build()
}
if limit == nil {
// optimization, limit==nil, so we query all entities anyway, just use the array length
tcRes.Count = len(entities)
} else {
tcRes.Count = 0 // no entries in DB
cursorTotalCount, err := c.coll.Aggregate(ctx, pipelineTotalCount)
if err != nil {
return nil, pag.Pagination{}, exerr.Wrap(err, "mongo-aggregation failed").Any("pipeline", pipelineTotalCount).Str("collection", c.Name()).Build()
}
if cursorTotalCount.Next(ctx) {
err = cursorTotalCount.Decode(&tcRes)
if err != nil {
return nil, pag.Pagination{}, exerr.Wrap(err, "failed to decode mongo-aggregation $count result").Any("pipeline", pipelineTotalCount).Str("collection", c.Name()).Build()
}
} else {
tcRes.Count = 0 // no entries in DB
}
}
paginationObj := pag.Pagination{

1
wpdf/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
wpdf_test.pdf

BIN
wpdf/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -3,6 +3,7 @@ package wpdf
import (
"bytes"
"github.com/jung-kurt/gofpdf"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
)
type WPDFBuilder struct {
@@ -13,6 +14,7 @@ type WPDFBuilder struct {
fontName PDFFontFamily
fontStyle PDFFontStyle
fontSize float64
debug bool
}
type PDFMargins struct {
@@ -61,6 +63,19 @@ func (b *WPDFBuilder) SetMargins(v PDFMargins) {
func (b *WPDFBuilder) AddPage() {
b.b.AddPage()
if b.debug {
ml, mt, mr, mb := b.GetMargins()
pw, ph := b.GetPageSize()
b.Rect(pw-ml-mr, ph-mt-mb, RectOutline, NewPDFRectOpt().X(ml).Y(mt).LineWidth(0.25).DrawColor(0, 0, 128))
b.Rect(pw, mt, RectFill, NewPDFRectOpt().X(0).Y(0).FillColor(0, 0, 255).Alpha(0.2, BlendNormal))
b.Rect(ml, ph-mt-mb, RectFill, NewPDFRectOpt().X(0).Y(mt).FillColor(0, 0, 255).Alpha(0.2, BlendNormal))
b.Rect(mr, ph-mt-mb, RectFill, NewPDFRectOpt().X(pw-mr).Y(mt).FillColor(0, 0, 255).Alpha(0.2, BlendNormal))
b.Rect(pw, mb, RectFill, NewPDFRectOpt().X(0).Y(ph-mb).FillColor(0, 0, 255).Alpha(0.2, BlendNormal))
}
}
func (b *WPDFBuilder) SetTextColor(cr, cg, cb int) {
@@ -105,12 +120,38 @@ func (b *WPDFBuilder) SetFont(fontName PDFFontFamily, fontStyle PDFFontStyle, fo
b.cellHeight = b.b.PointConvert(fontSize)
}
func (b *WPDFBuilder) GetFontSize() float64 {
return b.fontSize
}
func (b *WPDFBuilder) GetFontFamily() PDFFontStyle {
return b.fontStyle
}
func (b *WPDFBuilder) GetFontStyle() float64 {
return b.fontSize
}
func (b *WPDFBuilder) SetCellSpacing(h float64) {
b.cellSpacing = h
}
func (b *WPDFBuilder) Ln(h float64) {
xBefore, yBefore := b.GetXY()
b.b.Ln(h)
yAfter := b.GetY()
if b.debug {
_, _, mr, _ := b.GetMargins()
pw, _ := b.GetPageSize()
b.Rect(pw-mr-xBefore, yAfter-yBefore, RectOutline, NewPDFRectOpt().X(xBefore).Y(yBefore).LineWidth(0.25).DrawColor(128, 128, 0).Alpha(0.5, BlendNormal))
b.Rect(pw-mr-xBefore, yAfter-yBefore, RectFill, NewPDFRectOpt().X(xBefore).Y(yBefore).LineWidth(0.25).FillColor(128, 128, 0).Alpha(0.1, BlendNormal))
b.Line(xBefore, yBefore, pw-mr, yAfter, NewPDFLineOpt().LineWidth(0.25).DrawColor(128, 128, 0))
}
}
func (b *WPDFBuilder) Build() ([]byte, error) {
@@ -192,6 +233,48 @@ func (b *WPDFBuilder) GetWorkAreaWidth() float64 {
return b.GetPageWidth() - b.GetMarginLeft() - b.GetMarginRight()
}
func (b *WPDFBuilder) GetStringWidth(str string) float64 {
func (b *WPDFBuilder) SetAutoPageBreak(auto bool, margin float64) {
b.b.SetAutoPageBreak(auto, margin)
}
func (b *WPDFBuilder) SetFooterFunc(fnc func()) {
b.b.SetFooterFunc(fnc)
}
func (b *WPDFBuilder) PageNo() int {
return b.b.PageNo()
}
func (b *WPDFBuilder) Bookmark(txtStr string, level int, y float64) {
b.b.Bookmark(b.tr(txtStr), level, y)
}
func (b *WPDFBuilder) GetStringWidth(str string, opts ...PDFCellOpt) float64 {
var fontNameOverride *PDFFontFamily
var fontStyleOverride *PDFFontStyle
var fontSizeOverride *float64
for _, opt := range opts {
fontNameOverride = langext.CoalesceOpt(opt.fontNameOverride, fontNameOverride)
fontStyleOverride = langext.CoalesceOpt(opt.fontStyleOverride, fontStyleOverride)
fontSizeOverride = langext.CoalesceOpt(opt.fontSizeOverride, fontSizeOverride)
}
if fontNameOverride != nil || fontStyleOverride != nil || fontSizeOverride != nil {
oldFontName := b.fontName
oldFontStyle := b.fontStyle
oldFontSize := b.fontSize
newFontName := langext.Coalesce(fontNameOverride, oldFontName)
newFontStyle := langext.Coalesce(fontStyleOverride, oldFontStyle)
newFontSize := langext.Coalesce(fontSizeOverride, oldFontSize)
b.SetFont(newFontName, newFontStyle, newFontSize)
defer func() { b.SetFont(oldFontName, oldFontStyle, oldFontSize) }()
}
return b.b.GetStringWidth(str)
}
func (b *WPDFBuilder) Debug(v bool) {
b.debug = v
}

View File

@@ -1,6 +1,9 @@
package wpdf
import "gogs.mikescher.com/BlackForestBytes/goext/langext"
import (
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
)
type PDFCellOpt struct {
width *float64
@@ -14,6 +17,7 @@ type PDFCellOpt struct {
fontNameOverride *PDFFontFamily
fontStyleOverride *PDFFontStyle
fontSizeOverride *float64
alphaOverride *dataext.Tuple[float64, PDFBlendMode]
extraLn *float64
x *float64
autoWidth *bool
@@ -21,6 +25,7 @@ type PDFCellOpt struct {
borderColor *PDFColor
fillColor *PDFColor
autoWidthPaddingX *float64
debug *bool
}
func NewPDFCellOpt() *PDFCellOpt {
@@ -149,12 +154,45 @@ func (opt *PDFCellOpt) FillColorHex(c uint32) *PDFCellOpt {
return opt
}
func (opt *PDFCellOpt) Alpha(alpha float64, blendMode PDFBlendMode) *PDFCellOpt {
opt.alphaOverride = &dataext.Tuple[float64, PDFBlendMode]{V1: alpha, V2: blendMode}
return opt
}
func (opt *PDFCellOpt) Debug(v bool) *PDFCellOpt {
opt.debug = &v
return opt
}
func (opt *PDFCellOpt) Copy() *PDFCellOpt {
c := *opt
return &c
}
func (opt *PDFCellOpt) ToMulti() *PDFMultiCellOpt {
return &PDFMultiCellOpt{
width: opt.width,
height: opt.height,
border: opt.border,
align: opt.align,
fill: opt.fill,
fontNameOverride: opt.fontNameOverride,
fontStyleOverride: opt.fontStyleOverride,
fontSizeOverride: opt.fontSizeOverride,
extraLn: opt.extraLn,
x: opt.x,
textColor: opt.textColor,
borderColor: opt.borderColor,
fillColor: opt.fillColor,
}
}
func (b *WPDFBuilder) Cell(txt string, opts ...*PDFCellOpt) {
txtTR := b.tr(txt)
width := float64(0)
height := b.cellHeight + b.cellSpacing
var height *float64 = nil
border := BorderNone
ln := BreakToNextLine
align := AlignLeft
@@ -164,6 +202,7 @@ func (b *WPDFBuilder) Cell(txt string, opts ...*PDFCellOpt) {
var fontNameOverride *PDFFontFamily
var fontStyleOverride *PDFFontStyle
var fontSizeOverride *float64
var alphaOverride *dataext.Tuple[float64, PDFBlendMode]
extraLn := float64(0)
var x *float64
autoWidth := false
@@ -171,10 +210,11 @@ func (b *WPDFBuilder) Cell(txt string, opts ...*PDFCellOpt) {
var borderColor *PDFColor
var fillColor *PDFColor
autoWidthPaddingX := float64(0)
debug := b.debug
for _, opt := range opts {
width = langext.Coalesce(opt.width, width)
height = langext.Coalesce(opt.height, height)
height = langext.CoalesceOpt(opt.height, height)
border = langext.Coalesce(opt.border, border)
ln = langext.Coalesce(opt.ln, ln)
align = langext.Coalesce(opt.align, align)
@@ -184,6 +224,7 @@ func (b *WPDFBuilder) Cell(txt string, opts ...*PDFCellOpt) {
fontNameOverride = langext.CoalesceOpt(opt.fontNameOverride, fontNameOverride)
fontStyleOverride = langext.CoalesceOpt(opt.fontStyleOverride, fontStyleOverride)
fontSizeOverride = langext.CoalesceOpt(opt.fontSizeOverride, fontSizeOverride)
alphaOverride = langext.CoalesceOpt(opt.alphaOverride, alphaOverride)
extraLn = langext.Coalesce(opt.extraLn, extraLn)
x = langext.CoalesceOpt(opt.x, x)
autoWidth = langext.Coalesce(opt.autoWidth, autoWidth)
@@ -191,6 +232,7 @@ func (b *WPDFBuilder) Cell(txt string, opts ...*PDFCellOpt) {
borderColor = langext.CoalesceOpt(opt.borderColor, borderColor)
fillColor = langext.CoalesceOpt(opt.fillColor, fillColor)
autoWidthPaddingX = langext.Coalesce(opt.autoWidthPaddingX, autoWidthPaddingX)
debug = langext.Coalesce(opt.debug, debug)
}
if fontNameOverride != nil || fontStyleOverride != nil || fontSizeOverride != nil {
@@ -204,6 +246,11 @@ func (b *WPDFBuilder) Cell(txt string, opts ...*PDFCellOpt) {
defer func() { b.SetFont(oldFontName, oldFontStyle, oldFontSize) }()
}
if height == nil {
// (do after SetFont, so that b.cellHeight is correctly set to fontOverride)
height = langext.Ptr(b.cellHeight + b.cellSpacing)
}
if textColor != nil {
oldColorR, oldColorG, oldColorB := b.b.GetTextColor()
b.SetTextColor(textColor.R, textColor.G, textColor.B)
@@ -222,15 +269,33 @@ func (b *WPDFBuilder) Cell(txt string, opts ...*PDFCellOpt) {
defer func() { b.SetFillColor(oldColorR, oldColorG, oldColorB) }()
}
if alphaOverride != nil {
oldA, oldBMS := b.b.GetAlpha()
b.b.SetAlpha(alphaOverride.V1, string(alphaOverride.V2))
defer func() { b.b.SetAlpha(oldA, oldBMS) }()
}
if x != nil {
b.b.SetX(*x)
}
if autoWidth {
width = b.b.GetStringWidth(txtTR) + autoWidthPaddingX
width = b.GetStringWidth(txtTR, langext.ArrDeRef(opts)...) + autoWidthPaddingX
}
b.b.CellFormat(width, height, txtTR, string(border), int(ln), string(align), fill, link, linkStr)
xBefore, yBefore := b.b.GetXY()
b.b.CellFormat(width, *height, txtTR, string(border), int(ln), string(align), fill, link, linkStr)
if debug {
if ln == BreakToNextLine {
b.Rect(b.GetPageWidth()-xBefore-b.GetMarginRight(), *height, RectOutline, NewPDFRectOpt().X(xBefore).Y(yBefore).LineWidth(0.25).DrawColor(0, 128, 0))
} else if ln == BreakToRight {
b.Rect(b.GetX()-xBefore, *height, RectOutline, NewPDFRectOpt().X(xBefore).Y(yBefore).LineWidth(0.25).DrawColor(0, 128, 0))
} else if ln == BreakToBelow {
b.Rect(b.GetPageWidth()-xBefore-b.GetMarginRight(), *height, RectOutline, NewPDFRectOpt().X(xBefore).Y(yBefore).LineWidth(0.25).DrawColor(0, 128, 0))
}
}
if extraLn != 0 {
b.b.Ln(extraLn)

View File

@@ -74,6 +74,35 @@ const (
RectFillOutline PDFRectStyle = "FD"
)
type PDFBlendMode string
const (
BlendNormal PDFBlendMode = "Normal"
BlendMultiply PDFBlendMode = "Multiply"
BlendScreen PDFBlendMode = "Screen"
BlendOverlay PDFBlendMode = "Overlay"
BlendDarken PDFBlendMode = "Darken"
BlendLighten PDFBlendMode = "Lighten"
BlendColorDodge PDFBlendMode = "ColorDodge"
BlendColorBurn PDFBlendMode = "ColorBurn"
BlendHardLight PDFBlendMode = "HardLight"
BlendSoftLight PDFBlendMode = "SoftLight"
BlendDifference PDFBlendMode = "Difference"
BlendExclusion PDFBlendMode = "Exclusion"
BlendHue PDFBlendMode = "Hue"
BlendSaturation PDFBlendMode = "Saturation"
BlendColor PDFBlendMode = "Color"
BlendLuminosity PDFBlendMode = "Luminosity"
)
type PDFLineCapStyle string
const (
CapButt PDFLineCapStyle = "butt"
CapRound PDFLineCapStyle = "round"
CapSquare PDFLineCapStyle = "square"
)
const (
BackgroundFill = true
BackgroundTransparent = false

View File

@@ -3,10 +3,12 @@ package wpdf
import (
"bytes"
"github.com/jung-kurt/gofpdf"
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/imageext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"image"
"image/color"
"image/draw"
"net/http"
)
@@ -64,7 +66,12 @@ func (b *WPDFBuilder) RegisterImage(bin []byte, opts ...*PDFImageRegisterOpt) *P
}
if imageType == "" {
ct := http.DetectContentType(bin[:512])
ct := ""
if len(bin) > 512 {
ct = http.DetectContentType(bin[:512])
} else {
ct = http.DetectContentType(bin)
}
switch ct {
case "image/jpg":
imageType = "JPG"
@@ -124,6 +131,8 @@ type PDFImageOpt struct {
compression *imageext.ImageCompresson
reEncodePixelPerMM *float64
crop *imageext.ImageCrop
alphaOverride *dataext.Tuple[float64, PDFBlendMode]
debug *bool
}
func NewPDFImageOpt() *PDFImageOpt {
@@ -150,6 +159,11 @@ func (opt *PDFImageOpt) Height(v float64) *PDFImageOpt {
return opt
}
func (opt *PDFImageOpt) Debug(v bool) *PDFImageOpt {
opt.debug = &v
return opt
}
func (opt *PDFImageOpt) Flow(v bool) *PDFImageOpt {
opt.flow = &v
return opt
@@ -211,6 +225,11 @@ func (opt *PDFImageOpt) Crop(cropX float64, cropY float64, cropWidth float64, cr
return opt
}
func (opt *PDFImageOpt) Alpha(alpha float64, blendMode PDFBlendMode) *PDFImageOpt {
opt.alphaOverride = &dataext.Tuple[float64, PDFBlendMode]{V1: alpha, V2: blendMode}
return opt
}
func (b *WPDFBuilder) Image(img *PDFImageRef, opts ...*PDFImageOpt) {
var err error
@@ -228,7 +247,9 @@ func (b *WPDFBuilder) Image(img *PDFImageRef, opts ...*PDFImageOpt) {
var imageFit *imageext.ImageFit = nil
var fillColor color.Color = color.Transparent
compression := imageext.CompressionPNGSpeed
debug := b.debug
var crop *imageext.ImageCrop = nil
var alphaOverride *dataext.Tuple[float64, PDFBlendMode]
for _, opt := range opts {
x = langext.Coalesce(opt.x, x)
@@ -246,10 +267,18 @@ func (b *WPDFBuilder) Image(img *PDFImageRef, opts ...*PDFImageOpt) {
compression = langext.Coalesce(opt.compression, compression)
reEncodePixelPerMM = langext.Coalesce(opt.reEncodePixelPerMM, reEncodePixelPerMM)
crop = langext.CoalesceOpt(opt.crop, crop)
debug = langext.Coalesce(opt.debug, debug)
alphaOverride = langext.CoalesceOpt(opt.alphaOverride, alphaOverride)
}
if flow {
y = b.GetY()
}
regName := img.Name
var subImageBounds *imageext.PercentageRectangle = nil
if imageFit != nil || fillColor != nil || crop != nil {
var dataimg image.Image
@@ -277,11 +306,21 @@ func (b *WPDFBuilder) Image(img *PDFImageRef, opts ...*PDFImageOpt) {
pxw := w * pdfPixelPerMillimeter
pxh := h * pdfPixelPerMillimeter
dataimg, err = imageext.ObjectFitImage(dataimg, pxw, pxh, *imageFit, fillColor)
var dataImgRect imageext.PercentageRectangle
dataimg, dataImgRect, err = imageext.ObjectFitImage(dataimg, pxw, pxh, *imageFit, fillColor)
if err != nil {
b.b.SetError(err)
return
}
subImageBounds = &dataImgRect
}
if dataimg.ColorModel() != color.RGBAModel && dataimg.ColorModel() != color.NRGBAModel {
// the image cannto be 16bpp or similar - otherwise fpdf errors out
dataImgRGBA := image.NewNRGBA(image.Rect(0, 0, dataimg.Bounds().Dx(), dataimg.Bounds().Dy()))
draw.Draw(dataImgRGBA, dataImgRGBA.Bounds(), dataimg, dataimg.Bounds().Min, draw.Src)
dataimg = dataImgRGBA
}
bfr, imgMime, err := imageext.EncodeImage(dataimg, compression)
@@ -305,6 +344,12 @@ func (b *WPDFBuilder) Image(img *PDFImageRef, opts ...*PDFImageOpt) {
}
if alphaOverride != nil {
oldA, oldBMS := b.b.GetAlpha()
b.b.SetAlpha(alphaOverride.V1, string(alphaOverride.V2))
defer func() { b.b.SetAlpha(oldA, oldBMS) }()
}
fpdfOpt := gofpdf.ImageOptions{
ImageType: imageType,
ReadDpi: readDpi,
@@ -312,4 +357,16 @@ func (b *WPDFBuilder) Image(img *PDFImageRef, opts ...*PDFImageOpt) {
}
b.b.ImageOptions(regName, x, y, w, h, flow, fpdfOpt, link, linkStr)
if debug {
b.Rect(w, h, RectOutline, NewPDFRectOpt().X(x).Y(y).LineWidth(0.25).DrawColor(255, 0, 0))
if subImageBounds != nil {
r := subImageBounds.Of(imageext.Rectangle{X: x, Y: y, W: w, H: h})
b.Rect(r.W, r.H, RectOutline, NewPDFRectOpt().X(r.X).Y(r.Y).LineWidth(0.25).DrawColor(255, 0, 0))
b.Rect(r.W, r.H, RectFill, NewPDFRectOpt().X(r.X).Y(r.Y).FillColor(255, 0, 0).Alpha(0.2, BlendNormal))
b.Line(r.X, r.Y, r.X+r.W, r.Y+r.H, NewPDFLineOpt().LineWidth(0.25).DrawColor(255, 0, 0))
b.Line(r.X+r.W, r.Y, r.X, r.Y+r.H, NewPDFLineOpt().LineWidth(0.25).DrawColor(255, 0, 0))
}
}
}

96
wpdf/wpdfLine.go Normal file
View File

@@ -0,0 +1,96 @@
package wpdf
import (
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
)
type PDFLineOpt struct {
lineWidth *float64
drawColor *PDFColor
alpha *dataext.Tuple[float64, PDFBlendMode]
capStyle *PDFLineCapStyle
debug *bool
}
func NewPDFLineOpt() *PDFLineOpt {
return &PDFLineOpt{}
}
func (opt *PDFLineOpt) LineWidth(v float64) *PDFLineOpt {
opt.lineWidth = &v
return opt
}
func (opt *PDFLineOpt) DrawColor(cr, cg, cb int) *PDFLineOpt {
opt.drawColor = langext.Ptr(rgbToColor(cr, cg, cb))
return opt
}
func (opt *PDFLineOpt) DrawColorHex(c uint32) *PDFLineOpt {
opt.drawColor = langext.Ptr(hexToColor(c))
return opt
}
func (opt *PDFLineOpt) Alpha(alpha float64, blendMode PDFBlendMode) *PDFLineOpt {
opt.alpha = &dataext.Tuple[float64, PDFBlendMode]{V1: alpha, V2: blendMode}
return opt
}
func (opt *PDFLineOpt) CapButt() *PDFLineOpt {
opt.capStyle = langext.Ptr(CapButt)
return opt
}
func (opt *PDFLineOpt) CapSquare() *PDFLineOpt {
opt.capStyle = langext.Ptr(CapSquare)
return opt
}
func (opt *PDFLineOpt) CapRound() *PDFLineOpt {
opt.capStyle = langext.Ptr(CapRound)
return opt
}
func (opt *PDFLineOpt) Debug(v bool) *PDFLineOpt {
opt.debug = &v
return opt
}
func (b *WPDFBuilder) Line(x1 float64, y1 float64, x2 float64, y2 float64, opts ...*PDFLineOpt) {
var lineWidth *float64
var drawColor *PDFColor
var alphaOverride *dataext.Tuple[float64, PDFBlendMode]
capStyle := CapButt
debug := b.debug
for _, opt := range opts {
lineWidth = langext.CoalesceOpt(opt.lineWidth, lineWidth)
drawColor = langext.CoalesceOpt(opt.drawColor, drawColor)
alphaOverride = langext.CoalesceOpt(opt.alpha, alphaOverride)
capStyle = langext.Coalesce(opt.capStyle, capStyle)
debug = langext.Coalesce(opt.debug, debug)
}
if lineWidth != nil {
old := b.GetLineWidth()
b.SetLineWidth(*lineWidth)
defer func() { b.SetLineWidth(old) }()
}
if drawColor != nil {
oldR, oldG, oldB := b.GetDrawColor()
b.SetDrawColor(drawColor.R, drawColor.G, drawColor.B)
defer func() { b.SetDrawColor(oldR, oldG, oldB) }()
}
if alphaOverride != nil {
oldA, oldBMS := b.b.GetAlpha()
b.b.SetAlpha(alphaOverride.V1, string(alphaOverride.V2))
defer func() { b.b.SetAlpha(oldA, oldBMS) }()
}
b.b.SetLineCapStyle(string(capStyle))
b.b.Line(x1, y1, x2, y2)
}

View File

@@ -1,6 +1,9 @@
package wpdf
import "gogs.mikescher.com/BlackForestBytes/goext/langext"
import (
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
)
type PDFMultiCellOpt struct {
width *float64
@@ -11,11 +14,13 @@ type PDFMultiCellOpt struct {
fontNameOverride *PDFFontFamily
fontStyleOverride *PDFFontStyle
fontSizeOverride *float64
alphaOverride *dataext.Tuple[float64, PDFBlendMode]
extraLn *float64
x *float64
textColor *PDFColor
borderColor *PDFColor
fillColor *PDFColor
debug *bool
}
func NewPDFMultiCellOpt() *PDFMultiCellOpt {
@@ -119,6 +124,21 @@ func (opt *PDFMultiCellOpt) FillColorHex(c uint32) *PDFMultiCellOpt {
return opt
}
func (opt *PDFMultiCellOpt) Alpha(alpha float64, blendMode PDFBlendMode) *PDFMultiCellOpt {
opt.alphaOverride = &dataext.Tuple[float64, PDFBlendMode]{V1: alpha, V2: blendMode}
return opt
}
func (opt *PDFMultiCellOpt) Debug(v bool) *PDFMultiCellOpt {
opt.debug = &v
return opt
}
func (opt *PDFMultiCellOpt) Copy() *PDFMultiCellOpt {
c := *opt
return &c
}
func (b *WPDFBuilder) MultiCell(txt string, opts ...*PDFMultiCellOpt) {
txtTR := b.tr(txt)
@@ -131,11 +151,13 @@ func (b *WPDFBuilder) MultiCell(txt string, opts ...*PDFMultiCellOpt) {
var fontNameOverride *PDFFontFamily
var fontStyleOverride *PDFFontStyle
var fontSizeOverride *float64
var alphaOverride *dataext.Tuple[float64, PDFBlendMode]
extraLn := float64(0)
var x *float64
var textColor *PDFColor
var borderColor *PDFColor
var fillColor *PDFColor
debug := b.debug
for _, opt := range opts {
width = langext.Coalesce(opt.width, width)
@@ -146,11 +168,13 @@ func (b *WPDFBuilder) MultiCell(txt string, opts ...*PDFMultiCellOpt) {
fontNameOverride = langext.CoalesceOpt(opt.fontNameOverride, fontNameOverride)
fontStyleOverride = langext.CoalesceOpt(opt.fontStyleOverride, fontStyleOverride)
fontSizeOverride = langext.CoalesceOpt(opt.fontSizeOverride, fontSizeOverride)
alphaOverride = langext.CoalesceOpt(opt.alphaOverride, alphaOverride)
extraLn = langext.Coalesce(opt.extraLn, extraLn)
x = langext.CoalesceOpt(opt.x, x)
textColor = langext.CoalesceOpt(opt.textColor, textColor)
borderColor = langext.CoalesceOpt(opt.borderColor, borderColor)
fillColor = langext.CoalesceOpt(opt.fillColor, fillColor)
debug = langext.Coalesce(opt.debug, debug)
}
if fontNameOverride != nil || fontStyleOverride != nil || fontSizeOverride != nil {
@@ -182,12 +206,24 @@ func (b *WPDFBuilder) MultiCell(txt string, opts ...*PDFMultiCellOpt) {
defer func() { b.SetFillColor(oldColorR, oldColorG, oldColorB) }()
}
if alphaOverride != nil {
oldA, oldBMS := b.b.GetAlpha()
b.b.SetAlpha(alphaOverride.V1, string(alphaOverride.V2))
defer func() { b.b.SetAlpha(oldA, oldBMS) }()
}
if x != nil {
b.b.SetX(*x)
}
xBefore, yBefore := b.b.GetXY()
b.b.MultiCell(width, height, txtTR, string(border), string(align), fill)
if debug {
b.Rect(b.GetPageWidth()-xBefore-b.GetMarginRight(), b.GetY()-yBefore, RectOutline, NewPDFRectOpt().X(xBefore).Y(yBefore).LineWidth(0.25).DrawColor(0, 128, 0))
}
if extraLn != 0 {
b.b.Ln(extraLn)
}

View File

@@ -1,6 +1,9 @@
package wpdf
import "gogs.mikescher.com/BlackForestBytes/goext/langext"
import (
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
)
type PDFRectOpt struct {
x *float64
@@ -8,10 +11,12 @@ type PDFRectOpt struct {
lineWidth *float64
drawColor *PDFColor
fillColor *PDFColor
alpha *dataext.Tuple[float64, PDFBlendMode]
radiusTL *float64
radiusTR *float64
radiusBR *float64
radiusBL *float64
debug *bool
}
func NewPDFRectOpt() *PDFRectOpt {
@@ -81,16 +86,28 @@ func (opt *PDFRectOpt) RadiusBR(radius float64) *PDFRectOpt {
return opt
}
func (opt *PDFRectOpt) Alpha(alpha float64, blendMode PDFBlendMode) *PDFRectOpt {
opt.alpha = &dataext.Tuple[float64, PDFBlendMode]{V1: alpha, V2: blendMode}
return opt
}
func (opt *PDFRectOpt) Debug(v bool) *PDFRectOpt {
opt.debug = &v
return opt
}
func (b *WPDFBuilder) Rect(w float64, h float64, styleStr PDFRectStyle, opts ...*PDFRectOpt) {
x := b.GetX()
y := b.GetY()
var lineWidth *float64
var drawColor *PDFColor
var fillColor *PDFColor
var alphaOverride *dataext.Tuple[float64, PDFBlendMode]
radiusTL := float64(0)
radiusTR := float64(0)
radiusBR := float64(0)
radiusBL := float64(0)
debug := b.debug
for _, opt := range opts {
x = langext.Coalesce(opt.x, x)
@@ -98,10 +115,12 @@ func (b *WPDFBuilder) Rect(w float64, h float64, styleStr PDFRectStyle, opts ...
lineWidth = langext.CoalesceOpt(opt.lineWidth, lineWidth)
drawColor = langext.CoalesceOpt(opt.drawColor, drawColor)
fillColor = langext.CoalesceOpt(opt.fillColor, fillColor)
alphaOverride = langext.CoalesceOpt(opt.alpha, alphaOverride)
radiusTL = langext.Coalesce(opt.radiusTL, radiusTL)
radiusTR = langext.Coalesce(opt.radiusTR, radiusTR)
radiusBR = langext.Coalesce(opt.radiusBR, radiusBR)
radiusBL = langext.Coalesce(opt.radiusBL, radiusBL)
debug = langext.Coalesce(opt.debug, debug)
}
if lineWidth != nil {
@@ -122,5 +141,11 @@ func (b *WPDFBuilder) Rect(w float64, h float64, styleStr PDFRectStyle, opts ...
defer func() { b.SetFillColor(oldR, oldG, oldB) }()
}
if alphaOverride != nil {
oldA, oldBMS := b.b.GetAlpha()
b.b.SetAlpha(alphaOverride.V1, string(alphaOverride.V2))
defer func() { b.b.SetAlpha(oldA, oldBMS) }()
}
b.b.RoundedRectExt(x, y, w, h, radiusTL, radiusTR, radiusBR, radiusBL, string(styleStr))
}

341
wpdf/wpdfTable.go Normal file
View File

@@ -0,0 +1,341 @@
package wpdf
import (
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/rext"
"regexp"
"strconv"
)
// Column specifier:
//
// - `{number}`: Use this amount of space
// - `auto`: Use the needed space for the content
// - `*` / `fr`: Use the remaining space, evenly distributed, shrink down to auto
// - `{num}fr` / `{num}*`: Use the remaining space, evenly distributed (weighted), shrink down to auto
//
// # TableBuilder
// - PadX/PadY: Padding between cells
// - DefaultStyle: Default style for cells
//
// # TableCellStyleOpt
// - MultiCell: Use wpdf.MultiCell() instead of wpdf.Cell() --> supports linebreaks
// - Ellipsize: Ellipsize text if too long
// - PaddingHorz: Additional horizontal padding inside of cell to space text around
// - PDFCellOpt: Normal styling options (evtl not all are supported, depending on MultiCell: true/false)
var regexTableColumnSpecFr = rext.W(regexp.MustCompile(`^(?P<num>[0-9]*)(fr|\*)$`))
type TableBuilder struct {
builder *WPDFBuilder
padx float64
pady float64
rows []tableRow
defaultCellStyle *TableCellStyleOpt
columnWidths *[]string
debug *bool
}
func (r tableRow) maxFontSize(defaultFontSize float64) float64 {
mfs := defaultFontSize
for _, cell := range r.cells {
if cell.Style.fontSizeOverride != nil {
mfs = max(mfs, *cell.Style.fontSizeOverride)
}
}
return mfs
}
func (b *TableBuilder) Widths(v ...string) *TableBuilder {
b.columnWidths = &v
return b
}
func (b *TableBuilder) DefaultStyle(s *TableCellStyleOpt) *TableBuilder {
b.defaultCellStyle = s
return b
}
func (b *TableBuilder) PadX(v float64) *TableBuilder {
b.padx = v
return b
}
func (b *TableBuilder) PadY(v float64) *TableBuilder {
b.pady = v
return b
}
func (b *TableBuilder) AddRow(cells ...TableCell) *TableBuilder {
b.rows = append(b.rows, tableRow{cells: cells})
return b
}
func (b *TableBuilder) AddRowWithStyle(style *TableCellStyleOpt, cells ...string) *TableBuilder {
tcels := make([]TableCell, 0, len(cells))
for _, cell := range cells {
tcels = append(tcels, TableCell{Content: cell, Style: *style})
}
b.rows = append(b.rows, tableRow{cells: tcels})
return b
}
func (b *TableBuilder) AddRowDefaultStyle(cells ...string) *TableBuilder {
tcels := make([]TableCell, 0, len(cells))
for _, cell := range cells {
tcels = append(tcels, TableCell{Content: cell, Style: langext.Coalesce(b.defaultCellStyle, TableCellStyleOpt{})})
}
b.rows = append(b.rows, tableRow{cells: tcels})
return b
}
func (b *TableBuilder) BuildRow() *TableRowBuilder {
return &TableRowBuilder{tabbuilder: b, cells: make([]TableCell, 0)}
}
func (b *TableBuilder) Build() {
builder := b.builder
debug := langext.Coalesce(b.debug, b.builder.debug)
if len(b.rows) == 0 {
return // nothing to do
}
_, pageHeight := builder.FPDF().GetPageSize()
pbEnabled, pbMargin := builder.FPDF().GetAutoPageBreak()
builder.FPDF().SetAutoPageBreak(false, 0) // manually handle pagebreak in tables
defer func() { builder.FPDF().SetAutoPageBreak(pbEnabled, pbMargin) }()
columnWidths := b.calculateColumns()
columnCount := len(columnWidths)
for i, dat := range b.rows {
if len(dat.cells) != columnCount {
builder.FPDF().SetError(exerr.New(exerr.TypeInternal, "data must have the same length as header").Int("idx", i).Build())
return
}
}
defaultFontSize, _ := builder.FPDF().GetFontSize()
for rowIdx, row := range b.rows {
nextY := builder.GetY()
for cellIdx, cell := range row.cells {
str := cell.Content
style := cell.Style
ellipsize := langext.Coalesce(style.ellipsize, true)
cellPaddingHorz := langext.Coalesce(style.paddingHorz, 2)
fillHeight := langext.Coalesce(style.fillHeight, false)
bx := builder.GetX()
by := builder.GetY()
cellWidth := columnWidths[cellIdx]
_ = fillHeight // TODO implement, but how?? ( cells with fillHeight=true should have a border of the full column height, even if another column is growing it, but we do not know teh height beforehand ... )
if langext.Coalesce(style.multiCell, true) {
builder.MultiCell(str, style.PDFCellOpt.Copy().ToMulti().Width(cellWidth).Debug(debug))
} else {
if ellipsize {
if builder.GetStringWidth(str, style.PDFCellOpt) > (cellWidth - cellPaddingHorz) {
for builder.GetStringWidth(str+"...", style.PDFCellOpt) > (cellWidth-cellPaddingHorz) && len(str) > 0 {
str = str[:len(str)-1]
}
str += "..."
}
}
builder.Cell(str, style.PDFCellOpt.Copy().Width(cellWidth).Debug(debug))
}
nextY = max(nextY, builder.GetY())
builder.SetXY(bx+cellWidth+b.padx, by)
}
builder.SetY(nextY + b.pady)
if rowIdx < len(b.rows)-1 && pbEnabled && (builder.GetY()+b.rows[rowIdx+1].maxFontSize(defaultFontSize)) > (pageHeight-pbMargin) {
builder.FPDF().AddPage()
}
}
}
func (b *TableBuilder) calculateColumns() []float64 {
pageWidthTotal, _ := b.builder.FPDF().GetPageSize()
marginLeft, _, marginRight, _ := b.builder.FPDF().GetMargins()
pageWidth := pageWidthTotal - marginLeft - marginRight
columnDef := make([]string, 0)
if b.columnWidths != nil {
columnDef = *b.columnWidths
} else if len(b.rows) > 0 {
columnDef = make([]string, len(b.rows[0].cells))
for i := range columnDef {
columnDef[i] = "*"
}
} else {
return []float64{}
}
columnWidths := make([]float64, len(columnDef))
frColumnWidthCount := 0
frColumnWeights := make([]float64, len(columnDef))
remainingWidth := pageWidth - (float64(len(columnDef)-1) * b.padx)
autoWidths := make([]float64, len(columnDef))
for colIdx := range columnDef {
w := float64(0)
for _, row := range b.rows {
if len(row.cells) > colIdx {
w = max(w, b.builder.GetStringWidth(row.cells[colIdx].Content, row.cells[colIdx].Style.PDFCellOpt))
}
}
autoWidths[colIdx] = w
}
for colIdx, col := range columnDef {
maxPadHorz := float64(0)
minWidth := float64(0)
for _, row := range b.rows {
if len(row.cells) > colIdx {
ph := langext.Coalesce(row.cells[colIdx].Style.paddingHorz, 2)
mw := langext.Coalesce(row.cells[colIdx].Style.minWidth, 0)
minWidth = max(minWidth, ph+mw)
maxPadHorz = max(maxPadHorz, ph)
}
}
if col == "auto" {
w := max(autoWidths[colIdx]+maxPadHorz, minWidth)
columnWidths[colIdx] = w
remainingWidth -= w
} else if match, ok := regexTableColumnSpecFr.MatchFirst(col); ok {
if match.GroupByName("num").Value() == "" {
w := minWidth
frColumnWidthCount += 1
frColumnWeights[colIdx] = 1
columnWidths[colIdx] = w
remainingWidth -= w
} else {
w := minWidth
n, _ := strconv.Atoi(match.GroupByName("num").Value())
frColumnWidthCount += n
frColumnWeights[colIdx] = float64(n)
columnWidths[colIdx] = w
remainingWidth -= w
}
} else {
if w, err := strconv.ParseFloat(col, 64); err == nil {
w = max(w, minWidth)
columnWidths[colIdx] = w
remainingWidth -= w
} else {
b.builder.FPDF().SetError(exerr.New(exerr.TypeInternal, "invalid column width").Str("width", col).Build())
w = max(w, minWidth)
columnWidths[colIdx] = w
remainingWidth -= w
return nil
}
}
}
if remainingWidth < 0 {
// no remaining space to distribute
return columnWidths
}
{
rmSub := 0.0
for i := range columnDef {
if frColumnWeights[i] != 0 {
w := min(autoWidths[i], (remainingWidth/float64(frColumnWidthCount))*frColumnWeights[i])
rmSub += w - columnWidths[i]
columnWidths[i] = w
}
}
remainingWidth -= rmSub
}
if remainingWidth > 0.01 {
rmSub := 0.0
for i, _ := range columnDef {
if frColumnWeights[i] != 0 {
addW := (remainingWidth / float64(frColumnWidthCount)) * frColumnWeights[i]
rmSub += addW
columnWidths[i] += addW
}
}
remainingWidth -= rmSub
}
return columnWidths
}
func (b *TableBuilder) RowCount() int {
return len(b.rows)
}
func (b *TableBuilder) Debug(v bool) *TableBuilder {
b.debug = &v
return b
}
func (b *WPDFBuilder) Table() *TableBuilder {
return &TableBuilder{
builder: b,
rows: make([]tableRow, 0),
pady: 2,
padx: 2,
defaultCellStyle: defaultTableStyle(),
}
}
func defaultTableStyle() *TableCellStyleOpt {
return &TableCellStyleOpt{
PDFCellOpt: *NewPDFCellOpt().
FontSize(float64(8)).
Border(BorderFull).
BorderColorHex(uint32(0x666666)).
FillColorHex(uint32(0xF0F0F0)).
TextColorHex(uint32(0x000000)).
FillBackground(true),
minWidth: langext.Ptr(float64(5)),
ellipsize: langext.PTrue,
multiCell: langext.PFalse,
}
}

187
wpdf/wpdfTableCell.go Normal file
View File

@@ -0,0 +1,187 @@
package wpdf
import (
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
)
type TableCell struct {
Content string
Style TableCellStyleOpt
}
type TableCellStyleOpt struct {
multiCell *bool
ellipsize *bool
paddingHorz *float64
minWidth *float64
fillHeight *bool
PDFCellOpt
}
func NewTableCellStyleOpt() *TableCellStyleOpt {
return &TableCellStyleOpt{}
}
func (o *TableCellStyleOpt) FillHeight(b bool) *TableCellStyleOpt {
o.fillHeight = &b
return o
}
func (o *TableCellStyleOpt) MultiCell(v bool) *TableCellStyleOpt {
o.multiCell = &v
return o
}
func (o *TableCellStyleOpt) Ellipsize(v bool) *TableCellStyleOpt {
o.ellipsize = &v
return o
}
func (o *TableCellStyleOpt) PaddingHorz(v float64) *TableCellStyleOpt {
o.paddingHorz = &v
return o
}
func (o *TableCellStyleOpt) MinWidth(v float64) *TableCellStyleOpt {
o.minWidth = &v
return o
}
func (o *TableCellStyleOpt) CellStyle(v PDFCellOpt) *TableCellStyleOpt {
o.PDFCellOpt = v
return o
}
func (o *TableCellStyleOpt) Width(v float64) *TableCellStyleOpt {
o.PDFCellOpt.width = &v
return o
}
func (o *TableCellStyleOpt) Height(v float64) *TableCellStyleOpt {
o.PDFCellOpt.height = &v
return o
}
func (o *TableCellStyleOpt) Border(v PDFBorder) *TableCellStyleOpt {
o.PDFCellOpt.border = &v
return o
}
func (o *TableCellStyleOpt) LnPos(v PDFTextBreak) *TableCellStyleOpt {
o.PDFCellOpt.ln = &v
return o
}
func (o *TableCellStyleOpt) Align(v PDFTextAlign) *TableCellStyleOpt {
o.PDFCellOpt.align = &v
return o
}
func (o *TableCellStyleOpt) FillBackground(v bool) *TableCellStyleOpt {
o.PDFCellOpt.fill = &v
return o
}
func (o *TableCellStyleOpt) Link(v int) *TableCellStyleOpt {
o.PDFCellOpt.link = &v
return o
}
func (o *TableCellStyleOpt) LinkStr(v string) *TableCellStyleOpt {
o.PDFCellOpt.linkStr = &v
return o
}
func (o *TableCellStyleOpt) Font(fontName PDFFontFamily, fontStyle PDFFontStyle, fontSize float64) *TableCellStyleOpt {
o.PDFCellOpt.fontNameOverride = &fontName
o.PDFCellOpt.fontStyleOverride = &fontStyle
o.PDFCellOpt.fontSizeOverride = &fontSize
return o
}
func (o *TableCellStyleOpt) FontName(v PDFFontFamily) *TableCellStyleOpt {
o.PDFCellOpt.fontNameOverride = &v
return o
}
func (o *TableCellStyleOpt) FontStyle(v PDFFontStyle) *TableCellStyleOpt {
o.PDFCellOpt.fontStyleOverride = &v
return o
}
func (o *TableCellStyleOpt) FontSize(v float64) *TableCellStyleOpt {
o.PDFCellOpt.fontSizeOverride = &v
return o
}
func (o *TableCellStyleOpt) Bold() *TableCellStyleOpt {
o.PDFCellOpt.fontStyleOverride = langext.Ptr(Bold)
return o
}
func (o *TableCellStyleOpt) Italic() *TableCellStyleOpt {
o.PDFCellOpt.fontStyleOverride = langext.Ptr(Italic)
return o
}
func (o *TableCellStyleOpt) LnAfter(v float64) *TableCellStyleOpt {
o.PDFCellOpt.extraLn = &v
return o
}
func (o *TableCellStyleOpt) X(v float64) *TableCellStyleOpt {
o.PDFCellOpt.x = &v
return o
}
func (o *TableCellStyleOpt) AutoWidth() *TableCellStyleOpt {
o.PDFCellOpt.autoWidth = langext.PTrue
return o
}
func (o *TableCellStyleOpt) AutoWidthPaddingX(v float64) *TableCellStyleOpt {
o.PDFCellOpt.autoWidthPaddingX = &v
return o
}
func (o *TableCellStyleOpt) TextColor(cr, cg, cb int) *TableCellStyleOpt {
o.PDFCellOpt.textColor = langext.Ptr(rgbToColor(cr, cg, cb))
return o
}
func (o *TableCellStyleOpt) TextColorHex(c uint32) *TableCellStyleOpt {
o.PDFCellOpt.textColor = langext.Ptr(hexToColor(c))
return o
}
func (o *TableCellStyleOpt) BorderColor(cr, cg, cb int) *TableCellStyleOpt {
o.PDFCellOpt.borderColor = langext.Ptr(rgbToColor(cr, cg, cb))
return o
}
func (o *TableCellStyleOpt) BorderColorHex(c uint32) *TableCellStyleOpt {
o.PDFCellOpt.borderColor = langext.Ptr(hexToColor(c))
return o
}
func (o *TableCellStyleOpt) FillColor(cr, cg, cb int) *TableCellStyleOpt {
o.PDFCellOpt.fillColor = langext.Ptr(rgbToColor(cr, cg, cb))
return o
}
func (o *TableCellStyleOpt) FillColorHex(c uint32) *TableCellStyleOpt {
o.PDFCellOpt.fillColor = langext.Ptr(hexToColor(c))
return o
}
func (o *TableCellStyleOpt) Alpha(alpha float64, blendMode PDFBlendMode) *TableCellStyleOpt {
o.PDFCellOpt.alphaOverride = &dataext.Tuple[float64, PDFBlendMode]{V1: alpha, V2: blendMode}
return o
}
func (o *TableCellStyleOpt) Debug(v bool) *TableCellStyleOpt {
o.PDFCellOpt.debug = &v
return o
}

52
wpdf/wpdfTableRow.go Normal file
View File

@@ -0,0 +1,52 @@
package wpdf
import "gogs.mikescher.com/BlackForestBytes/goext/langext"
type tableRow struct {
cells []TableCell
}
type TableRowBuilder struct {
tabbuilder *TableBuilder
defaultStyle *TableCellStyleOpt
cells []TableCell
}
func (r *TableRowBuilder) RowStyle(style *TableCellStyleOpt) *TableRowBuilder {
r.defaultStyle = style
return r
}
func (r *TableRowBuilder) Cell(cell string) *TableRowBuilder {
r.cells = append(r.cells, TableCell{Content: cell, Style: langext.Coalesce3(r.defaultStyle, r.tabbuilder.defaultCellStyle, TableCellStyleOpt{})})
return r
}
func (r *TableRowBuilder) Cells(cells ...string) *TableRowBuilder {
for _, cell := range cells {
r.cells = append(r.cells, TableCell{Content: cell, Style: langext.Coalesce3(r.defaultStyle, r.tabbuilder.defaultCellStyle, TableCellStyleOpt{})})
}
return r
}
func (r *TableRowBuilder) CellObject(cell TableCell) *TableRowBuilder {
r.cells = append(r.cells, cell)
return r
}
func (r *TableRowBuilder) CellObjects(cells ...TableCell) *TableRowBuilder {
for _, cell := range cells {
r.cells = append(r.cells, cell)
}
return r
}
func (r *TableRowBuilder) CellWithStyle(cell string, style *TableCellStyleOpt) *TableRowBuilder {
r.cells = append(r.cells, TableCell{Content: cell, Style: *style})
return r
}
func (r *TableRowBuilder) BuildRow() *TableBuilder {
r.tabbuilder.AddRow(r.cells...)
return r.tabbuilder
}

105
wpdf/wpdf_test.go Normal file
View File

@@ -0,0 +1,105 @@
package wpdf
import (
_ "embed"
"fmt"
"gogs.mikescher.com/BlackForestBytes/goext/imageext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"os"
"path"
"testing"
)
//go:embed logo.png
var logoData []byte
func TestPDFBuilder(t *testing.T) {
builder := NewPDFBuilder(Portrait, SizeA4, true)
builder.Debug(true)
logoRef := builder.RegisterImage(logoData)
builder.SetMargins(PDFMargins{Left: 15, Top: 40, Right: 10})
builder.AddPage()
builder.SetFont(FontHelvetica, Normal, 10)
builder.Cell("Neueinrichtung deiner Entgeltumwandlung", NewPDFCellOpt().Bold().FontSize(20))
builder.Ln(10)
builder.SetFont(FontHelvetica, Normal, 10)
builder.Cell("Hello World", NewPDFCellOpt().Width(50).Align(AlignHorzCenter).LnPos(BreakToRight))
builder.IncX(10)
builder.Cell("Second Text", NewPDFCellOpt().AutoWidth().AutoWidthPaddingX(2).LnPos(BreakToRight))
builder.Ln(10)
builder.MultiCell("Im Fall einer individuellen Entgeltumwandlung ist die Zuschussverpflichtung auf der Grundlage des Betriebsrentenstärkungsgesetzes in der gesetzlich vorgeschriebenen Höhe (§ 1a Abs. 1a BetrAVG), über den arbeitgeberfinanzierten Zuschuss erfüllt.")
builder.Ln(4)
builder.Image(logoRef, NewPDFImageOpt().X(90).Y(160).Width(70).Height(30).ImageFit(imageext.ImageFitContainCenter))
builder.Ln(4)
cellStyleHeader := &TableCellStyleOpt{
PDFCellOpt: *NewPDFCellOpt().
FontSize(float64(8)).
BorderColorHex(uint32(0x666666)).
Border(BorderFull).
FillColorHex(uint32(0xC0C0C0)).
FillBackground(true).
TextColorHex(uint32(0x000000)).
Align(AlignHorzCenter).
Bold(),
minWidth: langext.Ptr(float64(5)),
ellipsize: langext.PTrue,
multiCell: langext.PFalse,
}
cellStyleMulti := &TableCellStyleOpt{
PDFCellOpt: *NewPDFCellOpt().
FontSize(float64(8)).
BorderColorHex(uint32(0x666666)).
Border(BorderFull).
FillColorHex(uint32(0xC060C0)).
FillBackground(true).
TextColorHex(uint32(0x000000)),
minWidth: langext.Ptr(float64(5)),
ellipsize: langext.PFalse,
multiCell: langext.PTrue,
}
builder.Table().
Widths("auto", "20", "1fr", "20").
PadX(2).
PadY(2).
AddRowWithStyle(cellStyleHeader, "test", "hello", "123", "end").
AddRowDefaultStyle("test", "hello", "123", "end").
AddRowDefaultStyle("123", "helasdsalo", "a", "enwqad").
AddRowDefaultStyle("123asd", "TrimMeTrimMeTrimMeTrimMe", "a", "enwqad").
AddRowWithStyle(cellStyleMulti, "123", "helasdsalo", "a", "MultiCell: enwqad enw\nqad enwqad enwqad enwqad enwqad").
AddRowDefaultStyle("123", "helasdsalo", "a", "enwqad").
Debug(false).
Build()
builder.Ln(8)
builder.Table().
Widths("auto", "20", "1fr", "20").
PadX(2).
PadY(2).
BuildRow().RowStyle(cellStyleHeader).Cells("test", "hello", "123", "end").BuildRow().
BuildRow().Cells("test", "hello", "123", "end").BuildRow().
BuildRow().RowStyle(cellStyleMulti.FillHeight(true)).Cell("123").Cell("helasdsalo").Cell("a").Cell("MultiCell: enwqad enw\nqad enwqad enwqad enwqad enwqad").BuildRow().
AddRowDefaultStyle("123", "helasdsalo", "a", "enwqad").
Debug(false).
Build()
bin, err := builder.Build()
if err != nil {
t.Fatal(err)
}
fn := "wpdf_test.pdf"
_ = os.WriteFile(fn, bin, 0644)
fmt.Println("file://" + path.Join(langext.Must(os.Getwd()), fn))
}