Compare commits

...

434 Commits

Author SHA1 Message Date
Mikescher 80cea13437 v0.0.634 add ReplyTo to googleapi.SendMail
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m37s
2026-04-24 13:38:00 +02:00
Mikescher f62e7499ec v0.0.633
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m42s
2026-04-13 16:12:09 +02:00
Mikescher 1edc2712ed v0.0.632 Pubsub.PublishAsync
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m41s
2026-04-13 16:08:28 +02:00
Mikescher 63c28b4141 v0.0.631
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m0s
2026-03-14 15:14:38 +01:00
Mikescher b01e659bb4 v0.0.630
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m24s
2026-03-14 14:10:09 +01:00
Mikescher 0923fa7c09 v0.0.629 excelext
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2026-03-14 14:09:13 +01:00
Mikescher e19cb30713 v0.0.628
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m37s
2026-03-14 01:13:37 +01:00
Mikescher 90dc6079d5 v0.0.627
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2026-03-14 01:13:07 +01:00
Mikescher 5b5c262994 v0.0.626 wsw
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m40s
2026-03-14 00:50:15 +01:00
Mikescher 44ec5e4804 v0.0.625
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m31s
2026-03-11 11:43:26 +01:00
TimoV 2054c04442 added test for StrRemoveControlCharacters func
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m39s
2026-03-10 19:58:04 +01:00
TimoV 373d28d405 added remove control characters func
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m37s
2026-03-10 19:54:28 +01:00
Mikescher 8aaf2cd257 v0.0.623
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m46s
2026-02-03 11:33:03 +01:00
Mikescher a373876d32 Remove redundant failure-mail step from pipeline (now handled by gitea itself)
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m40s
2026-01-23 10:16:30 +01:00
Mikescher 12324ba60d v0.0.622 fix tests
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m38s
2026-01-09 07:01:41 +01:00
Mikescher a032d09ea2 v0.0.621
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m34s
2026-01-08 23:04:46 +01:00
Mikescher 4bebc9ea38 v0.0.620
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2026-01-08 23:03:55 +01:00
Mikescher 580906ea08 v0.0.619 better handle error in ginext.JSON() with invalid data
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m26s
2026-01-08 22:58:50 +01:00
Mikescher 0f28e3d11b v0.0.618 remove deprecated param 'user_id' from scn.send()
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m46s
2025-12-06 22:20:02 +01:00
Mikescher 55f80432bb v0.0.617
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m18s
2025-12-06 11:54:36 +01:00
Mikescher d6daf0e285 v0.0.616
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2025-12-06 11:53:59 +01:00
Mikescher 1c2bc060da v0.0.615 MultiMutex
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m55s
2025-12-06 11:50:54 +01:00
Mikescher 34023dca4c v0.0.614 ArrCount
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m26s
2025-11-29 12:49:17 +01:00
Mikescher 69f2dd73c5 fix tests
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m23s
2025-11-06 09:54:28 +01:00
christopher dce6f634b1 v0.0.613 Add function to convert RFC3339Time To RFC3339NanoTime. Add function to shuffle array
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m37s
2025-11-05 14:48:04 +01:00
christopher 833f74427f Add ToNano method to convert RFC3339Time to RFC3339NanoTime 2025-11-05 14:45:47 +01:00
christopher 535a699584 Add ArrShuffle function to shuffle array elements 2025-11-05 14:45:38 +01:00
Mikescher b439b95f83 v0.0.612 fix sender-name in scn package
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m33s
2025-11-02 23:14:31 +01:00
Mikescher 995a82d90a Update googleapi/README.md
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m43s
2025-10-22 11:39:01 +02:00
Mikescher 0db10845ed v0.0.611 add dataext.MutexSet
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m45s
2025-10-15 09:55:14 +02:00
Mikescher 128ca25aa2 v0.0.610
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m17s
2025-10-04 00:29:57 +02:00
Mikescher f5d13ebe64 v0.0.609
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2025-10-04 00:29:09 +02:00
Mikescher 9730a91ad5 v0.0.608
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2025-10-04 00:27:41 +02:00
Mikescher 8c16e4d982 v0.0.607
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m17s
2025-10-04 00:24:13 +02:00
Mikescher 039a53a395 v0.0.606 add .DataMeta() to enums
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m18s
2025-10-04 00:08:32 +02:00
Mikescher 2cf571579b v0.0.605 do not panic in GoJSONRenderer
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m16s
2025-09-29 16:39:16 +02:00
Mikescher 9a537bb8c2 v0.0.604
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m16s
2025-09-20 15:21:15 +02:00
Mikescher 78ad103151 v0.0.603
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2025-09-20 15:19:09 +02:00
Mikescher c764a946ff v0.0.602 add listener to DelayedCombiningInvoker
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m18s
2025-09-20 15:13:02 +02:00
Mikescher ef59b1241f Fix tests
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m14s
2025-09-13 20:41:37 +02:00
Mikescher a70ab33559 v0.0.601 Add Wait and Update method to Atomic[T]
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m34s
2025-09-13 19:04:52 +02:00
Mikescher a58bb4b14b v0.0.600
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m30s
2025-09-13 18:45:23 +02:00
Mikescher dc62bbe55f v0.0.599 implement dataext.broadcaster
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m37s
2025-09-13 18:42:17 +02:00
Mikescher b832d77d3e v0.0.598 prevent json marshalling of PassHash
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m35s
2025-09-11 11:17:34 +02:00
Mikescher 38467cb4e7 v0.0.597 add update methods to SyncMap
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m47s
2025-09-04 14:25:06 +02:00
TimoV 68b06158b3 v0.0.596 force json map
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m42s
2025-08-26 15:55:57 +02:00
Mikescher 5f51173276 v0.0.595 fix zerlog channel for exerr [ZeroLogErrTraces] output and WRN errors
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m33s
2025-08-20 13:03:21 +02:00
Mikescher 1586314e3e v0.0.594 Add exerr OutputRaw(http.ResponseWriter) method
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m35s
2025-07-16 17:13:07 +02:00
Mikescher 254fe1556a v0.0.593 made PubSub more generic (namespace can be any comparable type)
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m54s
2025-07-16 12:50:36 +02:00
Mikescher 52e74b59f5 v0.0.592
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m30s
2025-07-16 12:46:18 +02:00
Mikescher 64f2cd7219 v0.0.591 implement namespaced PubSub Broker in dataext
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2025-07-16 12:44:55 +02:00
Mikescher a29aec8fb5 v0.0.590 more rfctime equal fixes for chris
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m34s
2025-07-15 14:14:22 +02:00
Mikescher 8ea9b3f79f v0.0.589 improve Equal method of rfctime structs - prevents panic in cmp library
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m48s
2025-07-15 13:46:36 +02:00
Mikescher a4b2a0589f v0.0.588
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m37s
2025-07-11 11:50:29 +02:00
Mikescher 4ef5f6059b v0.0.587
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m37s
2025-07-06 22:24:44 +02:00
Mikescher b23a444aa2 v0.0.586
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m33s
2025-07-04 13:56:53 +02:00
Mikescher 09932046f8 v0.0.585
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m36s
2025-07-04 11:46:00 +02:00
Mikescher 37e52595a2 v0.0.584
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m33s
2025-06-26 16:48:07 +02:00
Mikescher 95d7c90492 v0.0.583
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m31s
2025-06-25 10:59:23 +02:00
Mikescher 23a3235c7e v0.0.582
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m33s
2025-06-25 10:51:38 +02:00
Mikescher 506d276962 v0.0.581
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m39s
2025-06-25 10:28:54 +02:00
Mikescher 2a0cf84416 v0.0.580 Add IsZero() to generated ID types
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m33s
2025-06-16 08:40:07 +02:00
Mikescher 073aa84dd4 v0.0.579 fix StackSkip count on exerr zero-logger for Build()
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m29s
2025-06-13 16:53:47 +02:00
Mikescher a0dc9e92e4 v0.0.578
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m30s
2025-06-11 14:37:51 +02:00
Mikescher 98c591b019 v0.0.577
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m7s
2025-05-18 21:42:18 +02:00
Mikescher a93b93a3cd v0.0.576
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m26s
2025-05-18 14:26:24 +02:00
Mikescher 49bc52d63e v0.0.575 DelayedCombiningInvoker
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m31s
2025-05-11 19:17:05 +02:00
Mikescher 959020e3c0 v0.0.574 add syncMap.clear()
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m37s
2025-05-07 15:28:15 +02:00
TimoV 395e83acf6 panic bf url
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m36s
2025-05-06 19:17:49 +02:00
Mikescher 55ff89f179 v0.0.572 switch to git.blackforestbytes.com as module name
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2025-05-03 16:43:59 +02:00
Mikescher cbaa283f74 v0.0.571 add AsAnyPtr() function to ids
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m45s
2025-04-30 21:06:24 +02:00
Mikescher 20fb1f5601 v0.0.570 add gin_host exerr metadata (for gin-auto-fields)
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m35s
2025-04-25 23:20:25 +02:00
Mikescher cc58639306 v0.0.569
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m30s
2025-04-07 15:47:50 +02:00
Mikescher cea822ffa6 v0.0.568 remove duplicate ids in ExErr.UniqueIDs
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m38s
2025-03-15 22:29:45 +01:00
Mikescher c20ae20cc1 v0.0.567 Add ListenerOpt to exerr.RegisterListener (this is a breking API change !! -- but will prevent more breakage later on...)
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m33s
2025-03-06 12:19:03 +01:00
Mikescher f07cd79b96 v0.0.566
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m35s
2025-02-28 21:46:26 +01:00
Mikescher 164c462b96 v0.0.565
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m33s
2025-02-28 21:43:36 +01:00
Mikescher 5e6cb63f14 v0.0.564 always return non-nil ctx from ginext.Start() (improves nilaway)
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m7s
2025-02-10 13:04:05 +01:00
Mikescher 4832aa9d6c v0.0.563 Add 'ArrContains' alias for 'InArray'
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m7s
2025-01-31 21:16:42 +01:00
Mikescher 4d606d3131 v0.0.562 bf
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m4s
2025-01-29 11:24:20 +01:00
Mikescher be9b9e8ccf v0.0.561 wmo PaginateIterateFunc+PaginateIterate
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m31s
2025-01-29 11:02:41 +01:00
Mikescher 28cdfc5bd2 v0.0.560 wmo ListIterateFunc + ListIterate
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m31s
2025-01-29 10:54:53 +01:00
Mikescher 10a6627323 v0.0.559 Add .Iterate and .IterateFunc methods to wmo
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m33s
2025-01-28 15:55:18 +01:00
Mikescher 06b3b4116e v0.0.558 update gojson (rebase onto go1.23.4)
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m19s
2025-01-10 15:37:04 +01:00
Mikescher ff821390f7 Apply goext specific patches to gojson
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2025-01-10 15:35:14 +01:00
Mikescher c8e9c34706 Reset gojson to golang/go|1.23.4 [removes all custom changes] 2025-01-10 11:49:29 +01:00
Mikescher b7c48cb467 v0.0.556
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m29s
2025-01-09 10:41:00 +01:00
Mikescher a0a80899f5 v0.0.555
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2025-01-09 10:39:56 +01:00
Mikescher 3543441b96 v0.0.554
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2025-01-09 10:39:31 +01:00
Mikescher eef12da4e6 v0.0.553
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m24s
2025-01-09 10:29:22 +01:00
Mikescher d009aafd4e v0.0.552 mathext.ClampOpt
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m50s
2025-01-05 03:40:15 +01:00
Mikescher f7b4aa48d7 v0.0.551 change exerr.RecursiveMessage() logic: use messages of Wrap() if not empty
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m9s
2025-01-04 02:33:49 +01:00
Mikescher 36b092774d v0.0.550 ArrMapSum
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m12s
2024-12-26 00:23:24 +01:00
Mikescher a8c6e39ac5 v0.0.549
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m4s
2024-12-10 13:24:06 +01:00
Mikescher 62f2ce9268 v0.0.548
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m56s
2024-12-09 17:39:35 +01:00
Mikescher 49375e90f0 v0.0.547 allow calling ListWithCount with pageSize=0
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m55s
2024-12-08 18:04:04 +01:00
Mikescher d8cf255c80 v0.0.546 Fix ginext json-parse error when the bufferedReader was read beforehand
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m55s
2024-11-28 12:06:57 +01:00
Mikescher b520282ba0 v0.0.545
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m6s
2024-11-27 13:21:45 +01:00
Mikescher 27cc9366b5 v0.0.544
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m0s
2024-11-26 15:10:27 +01:00
Mikescher d9517fe73c v0.0.543
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m7s
2024-11-13 15:03:51 +01:00
Mikescher 8a92a6cc52 v0.0.542
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m9s
2024-11-07 13:13:12 +01:00
Mikescher 9b2028ab54 v0.0.541
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m3s
2024-11-05 14:38:42 +01:00
Mikescher 207fd331d5 v0.0.540 handle ct=nil same as ct=Start
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m25s
2024-10-28 14:35:05 +01:00
Mikescher 54b0d6701d v0.0.539
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m49s
2024-10-27 02:21:35 +02:00
Mikescher fc2657179b v0.0.538
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m7s
2024-10-27 02:18:45 +02:00
Mikescher d4894e31fe Merge branch 'cursortoken-paginated'
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-10-27 02:18:25 +02:00
Mikescher 0ddfaf666b v0.0.537 fix goext error print always showing error-type of highest-level error
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m12s
2024-10-26 23:38:13 +02:00
Mikescher e154137105 Trying out paginated cursortoken variant [UNTESTED]
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m11s
2024-10-25 09:45:42 +02:00
Mikescher 9b9a79b4ad v0.0.536 revert bfcodegen changes
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m24s
2024-10-22 09:57:06 +02:00
Mikescher 5a8d7110e4 v0.0.535
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m52s
2024-10-22 09:41:59 +02:00
Mikescher d47c84cd47 v0.0.534
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-10-22 09:40:53 +02:00
Mikescher c571f3f888 v0.0.533 Made String() functions in bfcodegen nil-ptr safe
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m9s
2024-10-22 09:36:40 +02:00
Mikescher e884ba6b89 v0.0.532
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m53s
2024-10-13 16:33:39 +02:00
Mikescher 1a8e31e5ef v0.0.531
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m58s
2024-10-13 16:10:55 +02:00
Mikescher eccc0fe9e5 v0.0.530
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m31s
2024-10-09 11:15:26 +02:00
Mikescher c8dec24a0d v0.0.529 handle PanicWrappedErr in exerr.FromError()
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m13s
2024-10-08 19:22:17 +02:00
Mikescher b8cb989e54 v0.0.528
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m38s
2024-10-07 17:20:40 +02:00
Mikescher ec672fbd49 v0.0.527
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-10-07 17:19:30 +02:00
Mikescher cfb0b53fc7 v0.0.526
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m17s
2024-10-05 23:59:23 +02:00
Mikescher a7389f44fa v0.0.525 upgrad go1.22 -> go1.23
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m4s
2024-10-05 02:45:20 +02:00
Mikescher 69f0fedd66 v0.0.524
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m22s
2024-10-05 01:41:10 +02:00
Mikescher 335ef4d8e8 v0.0.523 ringbuffer
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m23s
2024-10-05 01:28:46 +02:00
Mikescher 61801ff20d v0.0.522
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m53s
2024-10-05 01:12:00 +02:00
Mikescher 361dca5c85 v0.0.521 ctxext
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m56s
2024-10-05 01:06:36 +02:00
Mikescher 9f85a243e8 v0.0.520
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m7s
2024-10-05 01:02:25 +02:00
Mikescher dc6cb274ee v0.0.519
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-10-05 00:58:15 +02:00
Mikescher f6b47792a4 v0.0.518 Improve sq db-listener interface (breaking)
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m11s
2024-10-05 00:45:55 +02:00
Mikescher 295b3ef793 v0.0.517 add constructor funcs for tuples
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m21s
2024-10-02 11:31:34 +02:00
Mikescher 721c176337 v0.0.516
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m25s
2024-09-25 21:43:41 +02:00
Mikescher ebba6545a3 v0.0.515
Build Docker and Deploy / Run goext test-suite (push) Successful in 5m35s
2024-09-16 17:39:51 +02:00
Mikescher 19c7e22ced v0.0.514 fix mongo filter where the primary sort key is null in db (fallback to secondary)
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-09-16 17:39:18 +02:00
Mikescher 9f883b458f v0.0.513
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m55s
2024-09-16 15:27:32 +02:00
Mikescher 1f456c5134 v0.0.512
Build Docker and Deploy / Run goext test-suite (push) Successful in 5m6s
2024-09-15 21:25:21 +02:00
Mikescher d7fbef37db v0.0.511
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m10s
2024-09-15 18:22:07 +02:00
Mikescher a1668b6e5a v0.0.510
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m24s
2024-09-13 18:06:49 +02:00
Mikescher 3a17edfaf0 v0.0.509
Build Docker and Deploy / Run goext test-suite (push) Successful in 6m2s
2024-08-26 14:35:49 +02:00
Mikescher 3320a9c19d v0.0.508
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m25s
2024-08-25 17:36:20 +02:00
Mikescher 8dcd8a270a v0.0.507 fix jsonfilter:"-" not working
Build Docker and Deploy / Run goext test-suite (push) Successful in 7m7s
2024-08-25 15:41:17 +02:00
Mikescher 03a9b276d8 v0.0.506 allow empty-string as value for enum
Build Docker and Deploy / Run goext test-suite (push) Failing after 7m36s
2024-08-22 11:45:02 +02:00
Mikescher 9c8cde384f v0.0.505
Build Docker and Deploy / Run goext test-suite (push) Successful in 6m17s
2024-08-08 15:57:05 +02:00
Mikescher 99b000ecf4 v0.0.504
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m53s
2024-08-07 19:44:45 +02:00
Mikescher a173e30090 v0.0.503
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m52s
2024-08-07 19:37:38 +02:00
Mikescher a3481a7d2d v0.0.502
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-08-07 19:35:23 +02:00
Mikescher a8e6f98a89 v0.0.501
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-08-07 19:31:36 +02:00
Mikescher ab805403b9 v0.0.500
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-08-07 19:30:38 +02:00
Mikescher 1e98d351ce v0.0.499
Build Docker and Deploy / Run goext test-suite (push) Successful in 5m24s
2024-08-07 18:34:22 +02:00
Mikescher c40bdc8e9e v0.0.498
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m13s
2024-08-07 17:26:35 +02:00
Mikescher 7204562879 v0.0.497
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m33s
2024-08-07 17:04:59 +02:00
Mikescher 741611a2e1 v0.0.496 wpdf fixes and wpdf test.go
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m58s
2024-08-07 15:34:06 +02:00
Mikescher 133aeb8374 v0.0.495
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m44s
2024-08-07 14:00:02 +02:00
Mikescher b78a468632 v0.0.494 add tables to wpdf
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-08-07 13:57:29 +02:00
Mikescher f1b4480e0f v0.0.493 fix panic in RegisterImage for very short images
Build Docker and Deploy / Run goext test-suite (push) Successful in 5m22s
2024-08-07 09:22:37 +02:00
Mikescher ffffe4bf24 v0.0.492
Build Docker and Deploy / Run goext test-suite (push) Successful in 5m32s
2024-08-02 16:19:21 +02:00
Mikescher 413bf3c848 v0.0.491 small optimization in Paginate method
Build Docker and Deploy / Run goext test-suite (push) Failing after 8m31s
2024-07-31 00:15:09 +02:00
Mikescher 646990b549 v0.0.490 documentation and extra-params in exerr
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m2s
2024-07-27 23:44:18 +02:00
Mikescher e5818146a8 v0.0.489
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m54s
2024-07-23 14:21:03 +02:00
Mikescher 1310054121 v0.0.488 fix wpdf with 16bpp images
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m9s
2024-07-22 15:16:28 +02:00
Mikescher 49d423915c v0.0.487
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m54s
2024-07-18 17:45:56 +02:00
Mikescher 1962cb3c52 v0.0.486 add ginext -> CorsAllowHeader
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m51s
2024-07-18 17:29:18 +02:00
Mikescher 84f124dd4d v0.0.485
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m44s
2024-07-16 15:22:18 +02:00
Mikescher ff8e066135 v0.0.484
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m58s
2024-07-16 15:16:56 +02:00
Mikescher bc5c61e43d v0.0.483
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m37s
2024-07-16 15:08:37 +02:00
Mikescher 6ded615723 v0.0.482 mathext.Percentile
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m46s
2024-07-12 16:33:42 +02:00
Mikescher abc8af525a v0.0.481
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m50s
2024-07-04 16:24:49 +02:00
Mikescher 19d943361b v0.0.480
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m20s
2024-07-02 11:32:22 +02:00
Mikescher b464afae01 v0.0.479 AccessStruct
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m36s
2024-07-02 11:29:47 +02:00
Mikescher 56bc5e8285 v0.0.478
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m41s
2024-07-01 17:23:00 +02:00
Mikescher cb95bb561c v0.0.477 add langext.StrWrap
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m4s
2024-06-29 15:36:39 +02:00
Mikescher dff8941bd3 v0.0.476 Ãproperly close cursor in wmo
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m44s
2024-06-28 18:37:02 +02:00
Mikescher 78e1c33e30 v0.0.475 ArrGroupBy
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m36s
2024-06-16 17:14:21 +02:00
Mikescher d2f2a0558a v0.0.474 Add ZeroLogger config field to exerr.Init to override used zerolog instance
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m27s
2024-06-14 23:18:58 +02:00
Mikescher fc4bed4b9f v0.0.473 add ctx to wmo.FilterQuery|Sort|Pagination
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m23s
2024-06-14 17:24:59 +02:00
julian 94a7bf250d v0.0.472 changed gin engine initialization
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m34s
2024-06-14 14:56:41 +02:00
Mikescher f6121a6961 v0.0.471 Revert "v0.0.470 Add GoextJsonMarshaller interface to call when marshalling json via gojson"
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m50s
2024-06-11 19:39:43 +02:00
Mikescher 7fc73f1e93 v0.0.470 Add GoextJsonMarshaller interface to call when marshalling json via gojson
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m51s
2024-06-11 19:34:48 +02:00
Mikescher 2504ef00a0 v0.0.469
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m26s
2024-06-11 12:10:49 +02:00
TimoV fc5803493c added DblPtrIfNotNil
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m31s
2024-06-05 17:53:57 +02:00
TimoV a9295bfabf added CoalesceDblPtr
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m37s
2024-06-05 15:10:31 +02:00
Mikescher 12fa53d848 v0.0.466 exerr.Wrap now inherits the Severity of the wrapped error
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m24s
2024-06-03 13:48:30 +02:00
Mikescher d2bb362135 v0.0.465
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m39s
2024-06-03 09:39:57 +02:00
Mikescher 9dd81f6bd5 v0.0.464 improve ZeroLogErrTraces/ZeroLogAllTraces output for empty-message wrapped exerrs
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m36s
2024-06-01 02:40:48 +02:00
Mikescher d2c04afcd5 v0.0.463 Fix SubtractYears
Build Docker and Deploy / Run goext test-suite (push) Failing after 3m29s
2024-05-29 20:20:01 +02:00
Mikescher 62980e1489 v0.0.462
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m32s
2024-05-23 14:37:05 +02:00
Mikescher 59963adf74 v0.0.461
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m17s
2024-05-20 00:52:49 +02:00
Mikescher 194ea4ace5 v0.0.460
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m16s
2024-05-20 00:38:04 +02:00
Mikescher 73b80a66bc v0.0.459
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m16s
2024-05-20 00:20:31 +02:00
Mikescher d8b2d01274 v0.0.458 revert 457 and fix ObjectFitImage
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m49s
2024-05-20 00:15:24 +02:00
Mikescher bfa8457e95 v0.0.457 test
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m38s
2024-05-20 00:07:33 +02:00
Mikescher 70106733d9 v0.0.456
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m38s
2024-05-18 23:38:47 +02:00
Mikescher ce7837b9ef v0.0.455 add proper json/bson marshalling to exerr [severity|type|category]
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m39s
2024-05-16 15:38:42 +02:00
Mikescher d0d72167eb v0.0.454
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m17s
2024-05-14 15:10:27 +02:00
Mikescher a55ee1a6ce v0.0.453
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m18s
2024-05-14 14:57:10 +02:00
Mikescher dfc319573c v0.0.452
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m16s
2024-05-14 12:48:43 +02:00
Mikescher 246e555f3f v0.0.451 wpdf image processing
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-05-14 12:46:49 +02:00
Mikescher c28bc086b2 v0.0.450 wpdf
Build Docker and Deploy / Run goext test-suite (push) Failing after 3m33s
2024-05-14 11:52:56 +02:00
Mikescher d44e971325 v0.0.449
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m15s
2024-05-12 16:51:52 +02:00
Mikescher fe4cdc48af v0.0.448 wmo marshalHook
Build Docker and Deploy / Run goext test-suite (push) Failing after 25s
2024-05-12 16:45:45 +02:00
Mikescher 631006a4e1 v0.0.447
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m18s
2024-05-10 21:33:01 +02:00
Mikescher 567ead8697 v0.0.446 syncMap.GetAndSetIfNotContains and CASMutex
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-05-10 21:31:36 +02:00
Mikescher e4886b4a7d v0.0.445 added CtxData() and ExtendGinMeta/ExtendContextMeta to exerr
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m55s
2024-05-03 15:28:53 +02:00
Mikescher dcb5d3d7cd v0.0.444 change gin values in exerr auto meta to not include dots in keys (fucks up mongo)
Build Docker and Deploy / Run goext test-suite (push) Failing after 3m9s
2024-05-03 13:24:08 +02:00
Mikescher 15a639f85a v0.0.443 fix wmo.List with pageSize==0
Build Docker and Deploy / Run goext test-suite (push) Failing after 3m9s
2024-05-03 11:56:29 +02:00
Mikescher 303bd04649 v0.0.442
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m0s
2024-04-29 17:24:10 +02:00
Mikescher 7bda674939 v0.0.441
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m29s
2024-04-29 17:19:55 +02:00
Mikescher 126d4fbd0b v0.0.440 improve exerr.toJson
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m54s
2024-04-29 16:03:58 +02:00
Mikescher fed8bccaab v0.0.439
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m38s
2024-04-25 11:47:16 +02:00
Mikescher 47b6a6b508 v0.0.438
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m20s
2024-04-25 11:40:01 +02:00
Mikescher 764ce79a71 v0.0.437 properly handle $group in wmo extraModPipeline
Build Docker and Deploy / Run goext test-suite (push) Failing after 3m23s
2024-04-23 16:12:17 +02:00
Mikescher b876c64ba2 v0.0.436
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m56s
2024-04-18 14:09:26 +02:00
Mikescher 8d52b41f57 v0.0.435 add ConvertStructToMapOpt.MaxDepth
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m39s
2024-04-15 12:55:44 +02:00
Mikescher f47e2a33fe v0.0.434
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m13s
2024-04-15 10:43:26 +02:00
Mikescher 9321938dad v0.0.433 fix exerr missing gindata when using ginext.Error and add config for Output logging to stderr
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m0s
2024-04-15 10:25:30 +02:00
Mikescher 3828d601a2 v0.0.432 better handling of unmarshall-able values in exerr meta
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m12s
2024-04-13 22:08:45 +02:00
Mikescher 2e713c808d v0.0.431
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m10s
2024-04-10 15:29:59 +02:00
Mikescher 6602f86b43 v0.0.430
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-04-10 15:27:41 +02:00
Mikescher 24d9f0fdc7 v0.0.429
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m23s
2024-04-08 16:33:44 +02:00
Mikescher 8446b2da22 v0.0.428
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-04-08 16:32:34 +02:00
Mikescher 758e5a67b5 v0.0.427
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m30s
2024-04-07 15:10:21 +02:00
Mikescher 678ddd7124 v0.0.426 fix JsonOpt
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m57s
2024-04-01 16:03:00 +02:00
Mikescher 36b71dfaf3 v0.0.425 ArrAppend
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m25s
2024-03-30 14:24:53 +01:00
Mikescher 9491b72b8d v0.0.424 timeext.SubtractYears
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m29s
2024-03-30 03:01:55 +01:00
Mikescher 6c4af4006b v0.0.423 fix createPaginationPipeline - different primary and secondary sort keys broke mongo ??!?? - it actually only sorted by the secondary condition (ignoring the primary?)
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m32s
2024-03-24 15:25:52 +01:00
Mikescher 8bf3a337cf v0.0.422
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m29s
2024-03-23 20:29:46 +01:00
Mikescher 16146494dc v0.0.421
Build Docker and Deploy / Run goext test-suite (push) Failing after 32s
2024-03-23 20:28:51 +01:00
Mikescher b0e443ad99 v0.0.420
Build Docker and Deploy / Run goext test-suite (push) Failing after 36s
2024-03-23 18:01:41 +01:00
Mikescher 9955eacf96 v0.0.419 JsonOpt
Build Docker and Deploy / Run goext test-suite (push) Failing after 39s
2024-03-23 17:49:56 +01:00
Mikescher f0347a9435 v0.0.418 fix tests?
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m1s
2024-03-20 09:42:06 +01:00
Mikescher 7c869c65f3 v0.0.417 add GinWrapper.ForwardRequest
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m26s
2024-03-20 08:58:59 +01:00
Mikescher 14f39a9162 v0.0.416
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m43s
2024-03-18 11:19:01 +01:00
Mikescher dcd106c1cd v0.0.415 add 'tagkey' to gojson.Decoder
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m45s
2024-03-18 10:42:00 +01:00
Mikescher b704e2a362 v0.0.414 fix rfctime.Date bson marshalling for zero value
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m28s
2024-03-16 19:42:59 +01:00
Mikescher 6b4bd5a6f8 v0.0.413 fix tests
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m33s
2024-03-11 21:00:30 +01:00
Mikescher 6df4f5f2a1 v0.0.412 fix GenerateIDSpecs accepting nil for opt
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m32s
2024-03-11 20:58:06 +01:00
Mikescher 780905ba35 v0.0.411
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m33s
2024-03-11 20:43:37 +01:00
Mikescher c679797765 v0.0.410 add ginext.SuppressGinLogs
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-03-11 20:42:12 +01:00
Mikescher 401aad9fa4 v0.0.409
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m41s
2024-03-11 17:05:10 +01:00
Mikescher 645113d553 v0.0.408
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m43s
2024-03-11 16:41:47 +01:00
Mikescher 4a33986b6a v0.0.407 sq.Iterate
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-03-11 16:40:41 +01:00
Mikescher c1c8c64c76 v0.0.406 bf
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m34s
2024-03-10 16:44:21 +01:00
Mikescher 0927fdc4d7 v0.0.405
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m36s
2024-03-10 15:28:26 +01:00
Mikescher 102a280dda v0.0.404
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m37s
2024-03-10 15:25:30 +01:00
Mikescher f13384d794 v0.0.403 bf
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m37s
2024-03-10 12:58:59 +01:00
Mikescher 409d6e108d v0.0.402 add PackageName() and TypeName() to enums_codegen
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m12s
2024-03-10 12:49:31 +01:00
Mikescher ed53f297bd v0.0.401 bf
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m27s
2024-03-09 15:07:03 +01:00
Mikescher 42424f4bc2 v0.0.400 added CommentTrimmer and DBOptions to sq
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m16s
2024-03-09 14:59:32 +01:00
Mikescher 9e5b8c5277 v0.0.399 added sq.NewAutoDBTypeConverter
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m25s
2024-03-09 14:16:35 +01:00
Mikescher 9abe28c490 v0.0.398 added As* version to sort functions
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m13s
2024-03-09 13:36:06 +01:00
Mikescher 422bbd8593 v0.0.397 added wmo.IColl inetrface for tim
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m2s
2024-03-04 12:17:10 +01:00
Mikescher 3956675e04 v0.0.396 sq.ConverterRFCSecondsF64ToString
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m2s
2024-02-28 14:28:48 +01:00
Mikescher 10c3780b52 v0.0.395 MapMerge
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m14s
2024-02-27 13:42:06 +01:00
Mikescher 8edc067a3b v0.0.394
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m31s
2024-02-21 18:40:42 +01:00
Mikescher 1007f2c834 v0.0.393
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m19s
2024-02-21 18:33:18 +01:00
Mikescher c25da03217 v0.0.392
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-02-21 18:32:06 +01:00
Mikescher 4b55dbaacf v0.0.391
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m36s
2024-02-21 16:18:04 +01:00
Mikescher c399fa42ae v0.0.390
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m26s
2024-02-21 16:11:15 +01:00
Mikescher 9e586f7706 v0.0.389
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-02-21 16:10:28 +01:00
Mikescher 3cc8dccc63 v0.0.388 fix ginext.Use loosing absPath information
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m26s
2024-02-20 09:19:06 +01:00
Mikescher 7fedfbca81 v0.0.387 bf
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m24s
2024-02-12 18:17:49 +01:00
Mikescher 3c439ba428 v0.0.386 InsertAndQuerySingle
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m27s
2024-02-09 15:58:21 +01:00
Mikescher ad24f6db44 v0.0.385 UpdateAndQuerySingle
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m29s
2024-02-09 15:40:09 +01:00
Mikescher 1869ff3d75 v0.0.384 QuerySingleOpt
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m28s
2024-02-09 15:20:46 +01:00
Mikescher 30ce8c4b60 v0.0.383 sq.InsertMultiple
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m15s
2024-02-09 15:17:51 +01:00
TimoV 885bb53244 add BuildUrl test
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m59s
2024-02-09 12:27:18 +01:00
TimoV 1c7dc1820a v0.0.382 add build url method
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-02-09 12:25:01 +01:00
Mikescher 7e16e799e4 v0.0.381
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m28s
2024-01-23 17:51:52 +01:00
Mikescher 890e16241d v0.0.380 exerr properly handle inf and nan floats
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m8s
2024-01-22 12:33:41 +01:00
Mikescher b9d0348735 v0.0.379
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m57s
2024-01-19 17:30:20 +01:00
Mikescher b9e9575b9b v0.0.378
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m18s
2024-01-16 15:04:10 +01:00
Mikescher 295a098eb4 v0.0.377 fix sq.ConverterBoolToBit
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m12s
2024-01-14 17:06:42 +01:00
Mikescher b69a082bb1 v0.0.376
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m22s
2024-01-14 01:52:52 +01:00
Mikescher a4a8c83d17 v0.0.375
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m25s
2024-01-14 01:50:48 +01:00
Mikescher e952176bb0 v0.0.374 ppwgen
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m47s
2024-01-14 01:37:38 +01:00
Mikescher d99adb203b v0.0.373 BuildInsertStatement
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m24s
2024-01-14 00:07:01 +01:00
Mikescher f1f91f4cfa v0.0.372 sq.Paginate
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m25s
2024-01-13 21:36:47 +01:00
Mikescher 2afb265ea4 v0.0.371
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m26s
2024-01-13 14:19:19 +01:00
Mikescher be24f7a190 v0.0.370 improve sq errors
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m36s
2024-01-13 14:10:25 +01:00
Mikescher aae8a706e9 v0.0.369 autom. allow usage of existing converter for pointer-types (sq)
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m29s
2024-01-13 02:01:30 +01:00
Mikescher 7d64f18f54 v0.0.368
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m9s
2024-01-13 01:29:40 +01:00
Mikescher d08b2e565a v0.0.367
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m32s
2024-01-12 18:40:29 +01:00
Mikescher d29e84894d v0.0.366 ginext: set cookies
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m28s
2024-01-12 15:10:48 +01:00
Mikescher 617298c366 v0.0.365
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m24s
2024-01-09 18:23:46 +01:00
Tim 668f308565 v0.0.364 add ServerHTTP() to GinWrapper for integration testing
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m59s
2024-01-09 18:17:55 +01:00
Mikescher 240a8ed7aa v0.0.363 wmo.extraModPipeline as func
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m9s
2024-01-09 08:51:46 +01:00
Mikescher 70de8e8d04 v0.0.362
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m27s
2024-01-07 04:18:03 +01:00
Mikescher d38fa60fbc v0.0.361 call exerrListener in ginext.Error
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m37s
2024-01-07 04:01:13 +01:00
Mikescher 5fba7e0e2f v0.0.360 bf
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m26s
2024-01-06 01:31:07 +01:00
Mikescher 8757643399 v0.0.359 fix tests
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m18s
2024-01-05 16:55:53 +01:00
Mikescher 42bd4cf58d v0.0.358
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m22s
2024-01-05 16:53:14 +01:00
Mikescher 413178e2d3 v0.0.357
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m4s
2024-01-05 10:59:06 +01:00
Mikescher 9264a2e99b v0.0.356 exerr.GetMeta
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m9s
2024-01-05 10:43:39 +01:00
Mikescher 2a0471fb3d v0.0.355 sq.Json
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m27s
2024-01-05 10:25:05 +01:00
Mikescher 1497c013f9 v0.0.354
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m13s
2024-01-05 07:21:43 +01:00
Mikescher ef78b7467b v0.0.353 add scn.sendmessage
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m16s
2024-01-04 12:38:03 +01:00
Mikescher 0eda32b725 v0.0.352
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m1s
2023-12-29 19:29:36 +01:00
Mikescher f9ccafb976 v0.0.351 sq value converter
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m30s
2023-12-29 19:25:36 +01:00
Mikescher 6e90239fef v0.0.350
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m2s
2023-12-29 18:06:45 +01:00
Mikescher 05580c384a v0.0.349
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m51s
2023-12-28 01:36:21 +01:00
Mikescher 3188b951fb v0.0.348 added listener and options to goext
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m35s
2023-12-27 20:29:37 +01:00
Mikescher 6b211d1443 fix tests
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m37s
2023-12-17 14:04:35 +01:00
Mikescher b2b9b40792 v0.0.347
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m39s
2023-12-16 17:57:42 +01:00
Mikescher 2f915cb6c1 v0.0.346
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m56s
2023-12-13 16:22:15 +01:00
Mikescher b2b93f570a v0.0.345
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m48s
2023-12-07 19:39:31 +01:00
Mikescher 8247fc4524 v0.0.344
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m11s
2023-12-07 19:36:21 +01:00
Mikescher 5dad44ad09 v0.0.343
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m2s
2023-12-07 18:29:17 +01:00
Mikescher f042183433 v0.0.342 support json data in enum comment
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m29s
2023-12-07 17:57:06 +01:00
Mikescher b0be93a7a0 v0.0.341
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m6s
2023-12-07 14:43:12 +01:00
Mikescher 1c143921e6 v0.0.340
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2023-12-07 14:42:25 +01:00
Mikescher 68e63a9cf6 v0.0.339
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m5s
2023-12-07 10:54:36 +01:00
Mikescher c3162fec95 v0.0.338
Build Docker and Deploy / Run goext test-suite (push) Failing after 59s
2023-12-05 19:50:24 +01:00
Mikescher 1124aa781a v0.0.337
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m6s
2023-12-05 19:45:35 +01:00
Mikescher eef0e9f2aa v0.0.336
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m3s
2023-12-05 19:42:37 +01:00
Mikescher af38b06d22 v0.0.335 added DescriptionMeta to enum codegen
Build Docker and Deploy / Run goext test-suite (push) Failing after 58s
2023-12-05 19:38:03 +01:00
Mikescher 2fad6340c7 v0.0.334 allow dot in enum-value
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m3s
2023-12-05 19:23:27 +01:00
Mikescher 03aa0a2282 Merge branch 'feature/gmail_api'
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m23s
2023-12-04 13:56:18 +01:00
Mikescher 358c238f3d google mail API [[FIN]]
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m28s
2023-12-04 13:55:41 +01:00
Mikescher d65ac8ba2b v0.0.329
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m2s
2023-12-02 13:38:17 +01:00
Mikescher 55d02b8c65 v0.0.328
Build Docker and Deploy / Run goext test-suite (push) Successful in 59s
2023-12-02 13:35:18 +01:00
Mikescher 8a3965f666 v0.0.327
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m44s
2023-12-02 13:15:19 +01:00
Mikescher 4aa2f494b1 v0.0.326 ginext::WithSession
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m38s
2023-12-02 13:07:36 +01:00
Mikescher 8f13eb2f16 google mail API [[[WIP]]] 2023-12-01 18:33:04 +01:00
Mikescher 8f15d42173 v0.0.325
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m48s
2023-11-27 14:14:58 +01:00
Mikescher 07fa21dcca v0.0.324
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m29s
2023-11-25 15:48:28 +01:00
Mikescher e657de7f78 v0.0.323 fix langext.IsNil for reflect.Array
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m35s
2023-11-16 17:15:44 +01:00
Mikescher c534e998e8 v0.0.322 bf SecondsF64
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m1s
2023-11-14 16:31:05 +01:00
Mikescher 88642770c5 v0.0.321 Add .NoLog() to lowest-level query exerr.Wrap in wmo (otherwise we get error logs on stdout even if the callign method allows mongo.ErrNoDocuments)
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m4s
2023-11-14 16:00:14 +01:00
TimoV 8528b5cb66 v0.0.320 bugfix sort
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m14s
2023-11-14 14:50:27 +01:00
Mikescher 5ba84bd8ee v0.0.319 fix error when findoneÃ+pipeline fails
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m15s
2023-11-13 16:45:00 +01:00
Mikescher 1260b2dc77 v0.0.318 add failure mail to testx.yml
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m9s
2023-11-13 15:34:58 +01:00
Mikescher 7d18b913c6 v0.0.317 try fix tests on pipeline
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m38s
2023-11-13 15:28:37 +01:00
TimoV d1f9069f2f v0.0.316 bugfix sorting
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m14s
2023-11-13 15:19:48 +01:00
Mikescher fa6d73301e v0.0.315 atomic
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m26s
2023-11-12 03:10:55 +01:00
Mikescher bfe62799d3 v0.0.314
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m22s
2023-11-10 13:37:55 +01:00
Mikescher ede912eb7b v0.0.313
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m22s
2023-11-10 13:26:30 +01:00
Mikescher ff8f128fe8 v0.0.312 improve exerr.RecursiveMessage()
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m23s
2023-11-10 10:16:31 +01:00
Mikescher 1971f1396f v0.0.311 BF
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m17s
2023-11-09 11:48:45 +01:00
Mikescher bf6c184d12 v0.0.310 debug
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m18s
2023-11-09 11:40:48 +01:00
Mikescher 770f5c5c64 v0.0.309
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m17s
2023-11-09 10:17:29 +01:00
Mikescher 623c021689 v0.0.308
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m20s
2023-11-09 10:02:31 +01:00
Mikescher afcc89bf9e v0.0.307
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m20s
2023-11-09 10:00:01 +01:00
Mikescher 1672e8f8fd v0.0.306
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m12s
2023-11-09 09:36:41 +01:00
Mikescher 398ed56d32 v0.0.305
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m21s
2023-11-09 09:35:56 +01:00
Mikescher f3ecba3883 v0.0.304 add support for WithModifyingPipeline to wmo
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m22s
2023-11-09 09:26:46 +01:00
Mikescher 45031b05cf v0.0.303
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m35s
2023-11-08 19:01:15 +01:00
Mikescher 7413ea045d v0.0.302
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m41s
2023-11-08 18:53:02 +01:00
Mikescher 62c9a4e734 v0.0.301 pagination (page+limit) support in wmo
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m11s
2023-11-08 18:30:30 +01:00
Mikescher 3a8baaa6d9 v0.0.300 add custom unmarshal-hooks to wmo
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m45s
2023-11-04 18:55:44 +01:00
Mikescher 498785e213 v0.0.299 pctx.RawBody( *[]byte )
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m41s
2023-11-03 16:53:41 +01:00
Mikescher 678f95642c v0.0.298 use go/format instead of manual command invocation
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m6s
2023-11-01 04:20:08 +01:00
Mikescher dacc97e2ce v0.0.297
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m1s
2023-11-01 00:31:51 +01:00
Mikescher f8c0c0afa0 v0.0.296 add csid.generateIDFromSeed
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m4s
2023-11-01 00:29:58 +01:00
Mikescher 2fbd5cf965 v0.0.295 added generic base-conversion algorithm
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m4s
2023-11-01 00:23:17 +01:00
Mikescher 75f71fe3db v0.0.294 migrate bfcodegen to templates
Build Docker and Deploy / Run goext test-suite (push) Successful in 58s
2023-10-31 22:58:28 +01:00
Mikescher ab1a1ab6f6 v0.0.293 fix NPE
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m52s
2023-10-30 13:37:31 +01:00
Mikescher 19ee5019ef v0.0.292
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m11s
2023-10-30 10:14:38 +01:00
Mikescher 42b68507f2 v0.0.291
Build Docker and Deploy / Run goext test-suite (push) Successful in 56s
2023-10-26 13:02:45 +02:00
Mikescher 9d0047a11e v0.0.290 csid
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m38s
2023-10-26 13:01:58 +02:00
Mikescher 06d81f1682 v0.0.289 fsext
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m31s
2023-10-26 11:29:08 +02:00
Mikescher 7b8ab03779 v0.0.288 default to recursive-error-msg in exerr.Error()
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m54s
2023-10-19 14:16:01 +02:00
Mikescher 07cbcf5a0a v0.0.287 fix bug in confext::ApplyEnvOverrides if a struct env key exists in the os.env
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m37s
2023-10-12 10:02:42 +02:00
Mikescher da41ec3e84 run CICD tests without doker workaround
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m37s
2023-10-11 15:50:09 +02:00
Mikescher 592fae25af v0.0.286 allow spaces in enum-keys
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m14s
2023-10-11 11:27:18 +02:00
Mikescher 7968460fa2 v0.0.285
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m0s
2023-10-09 15:25:30 +02:00
Mikescher b808c5727c v0.0.284
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m14s
2023-10-09 15:22:57 +02:00
Mikescher 796f7956b8 v0.0.283
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m5s
2023-10-09 15:17:22 +02:00
TimoV 1e6b92d1d9 v0.0.282 ginext bugfix
Build Docker and Deploy / Run goext test-suite (push) Failing after 48s
2023-10-09 09:23:40 +02:00
TimoV 0b85fa5af9 v0.0.281 typo fix
Build Docker and Deploy / Run goext test-suite (push) Failing after 50s
2023-10-09 09:04:07 +02:00
TimoV c3318cc1de v0.0.280 DYN-166 ginext jsonfilter middleware
Build Docker and Deploy / Run goext test-suite (push) Successful in 51s
2023-10-09 09:02:37 +02:00
TimoV fbf4d7b915 v0.0.279 DYN-166 ginext jsonfilter middleware
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m12s
2023-10-09 08:55:22 +02:00
TimoV 9cc0abf9e0 v0.0.278 DYN-166 bugfix jsonfilter
Build Docker and Deploy / Run goext test-suite (push) Failing after 52s
2023-10-05 12:54:07 +02:00
TimoV 7c40bcfd3c v0.0.277 DYN-166 json marshal filter in ginext Write
Build Docker and Deploy / Run goext test-suite (push) Failing after 54s
2023-10-05 12:00:51 +02:00
TimoV 05636a1e4d v0.0.276
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m12s
2023-10-05 10:59:20 +02:00
TimoV 0f52b860ea DYN-166 add jsonfilter to json library
Build Docker and Deploy / Run goext test-suite (push) Successful in 56s
2023-10-05 10:57:34 +02:00
TimoV b5cd116219 DYN-166 add jsonfilter to json library
Build Docker and Deploy / Run goext test-suite (push) Successful in 47s
2023-10-05 10:45:09 +02:00
Mikescher 98486842ae v0.0.275 fix missing returns in (v MetaValue) ShortString
Build Docker and Deploy / Run goext test-suite (push) Successful in 54s
2023-09-29 16:00:40 +02:00
Mikescher 7577a2dd47 v0.0.274 limit exerr log meta values (shortlog) to 240 chars
Build Docker and Deploy / Run goext test-suite (push) Successful in 50s
2023-09-27 16:18:21 +02:00
Mikescher 08681756b6 v0.0.273 add stack to PanicWrappedErr
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m9s
2023-09-27 14:15:59 +02:00
Robin 64772d0474 v0.0.272 WMO: fix FindOneAndReplace not using FindOneAndReplace
Build Docker and Deploy / Run goext test-suite (push) Failing after 50s
2023-09-26 14:41:15 +02:00
Robin 127764556e Merge branch 'master' of ssh://gogs.mikescher.com:8022/BlackForestBytes/goext 2023-09-26 14:41:06 +02:00
Robin 170f43d806 WMO: fix FindOneAndReplace not using FindOneAndReplace 2023-09-26 14:40:56 +02:00
Mikescher 9dffc41274 v0.0.271 return old value in AtomicBool::Set
Build Docker and Deploy / Run goext test-suite (push) Failing after 53s
2023-09-26 14:32:45 +02:00
Mikescher c63cf442f8 try to fix test 'cmdext:TestFailOnStderr'
Build Docker and Deploy / Run goext test-suite (push) Successful in 39s
2023-09-25 18:04:56 +02:00
Robin a2ba283632 v0.0.270 fix inversion of AssertDeepEqual
Build Docker and Deploy / Run goext test-suite (push) Failing after 54s
2023-09-25 11:35:03 +02:00
Robin 4a1fb1ae18 fix inversion of AssertDeepEqual 2023-09-25 11:34:51 +02:00
Robin a127b24e62 v0.0.269 add AssertSetDeepEqual
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m8s
2023-09-25 09:18:22 +02:00
Robin 69d6290376 add AssertSetDeepEqual 2023-09-25 09:18:07 +02:00
Mikescher c08a739158 v0.0.268 added WeekStart() and WeekEnd()
Build Docker and Deploy / Run goext test-suite (push) Successful in 50s
2023-09-21 16:29:23 +02:00
Robin 5f5f0e44f0 v0.0.267 fix AssertDeepEqual
Build Docker and Deploy / Run goext test-suite (push) Failing after 50s
2023-09-21 14:15:02 +02:00
Robin 6e6797eac5 fix AssertDeepEqual 2023-09-21 14:14:51 +02:00
Robin cd9406900a v0.0.266 fix tst showing wrong file:line
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m22s
2023-09-21 13:08:13 +02:00
Robin 6c81f7f6bc fix tst showing wrong file:line, add DeepEqual 2023-09-21 13:07:55 +02:00
Robin d56a0235af v0.0.265 add ListWithCount
Build Docker and Deploy / Run goext test-suite (push) Successful in 49s
2023-09-18 12:57:27 +02:00
Robin de2ca763c1 add function for ListWithCount 2023-09-18 12:56:56 +02:00
Mikescher da52bb5c90 v0.0.264 added Valid() to id-gen
Build Docker and Deploy / Run goext test-suite (push) Successful in 49s
2023-09-18 11:46:17 +02:00
Mikescher 3d4afe7b25 v0.0.263 re-add checksum guard to id-generate
Build Docker and Deploy / Run goext test-suite (push) Failing after 50s
2023-09-18 10:43:29 +02:00
Mikescher f5766d639c v0.0.262 ignore _gen files in bfcodegen checksum-calc
Build Docker and Deploy / Run goext test-suite (push) Successful in 46s
2023-09-18 10:42:43 +02:00
Mikescher cdf2a6e76b v0.0.261 added id-generate.go
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m7s
2023-09-18 10:38:25 +02:00
Robin 6d7cfb86f8 v0.0.260 wmo: fix endless recursion in wmo reflection
Build Docker and Deploy / Run goext test-suite (push) Successful in 50s
2023-09-12 11:40:39 +02:00
Robin 1e9d663ffe fix endless recursion in wmo reflection 2023-09-12 11:39:51 +02:00
Robin 5b8d7ebf87 v0.0.259 wmo: allow fields to pointers to structs
Build Docker and Deploy / Run goext test-suite (push) Failing after 49s
2023-09-12 10:48:57 +02:00
Robin 11dc6d2640 use type instead of value for Reflection in Coll.initFields 2023-09-12 10:47:41 +02:00
Julian 29a3f73f15 v0.0.258
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m22s
2023-09-11 11:28:34 +02:00
Julian 98105642fc removed default sort 2023-09-11 11:28:26 +02:00
Julian 0fd5f3b417 v0.0.257 better handling if pagination is faulty in wmo list
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m2s
2023-09-05 15:01:55 +02:00
Julian 43cac4b3bb v0.0.256 bind header
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m4s
2023-08-28 10:44:38 +02:00
Mikescher cd68af8e66 v0.0.255 tuples
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m43s
2023-08-24 09:47:32 +02:00
Mikescher 113d838876 v0.0.254 revert back to 0.0.250
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m14s
2023-08-22 10:49:57 +02:00
Mikescher 9e5bc0d3ea v0.0.253
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m23s
2023-08-22 10:36:35 +02:00
Mikescher 6d3bd13f61 v0.0.252
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m5s
2023-08-22 10:23:04 +02:00
Mikescher b5ca475b3f v0.0.251 exerr.WithStackSkip
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m9s
2023-08-22 10:21:13 +02:00
Mikescher a75b1291cb v0.0.250
Build Docker and Deploy / Run goext test-suite (push) Failing after 51s
2023-08-21 15:34:27 +02:00
Mikescher 21cd1ee066 v0.0.249 better MDTAny json serialization
Build Docker and Deploy / Run goext test-suite (push) Failing after 54s
2023-08-21 15:19:40 +02:00
Mikescher ae43cbb623 v0.0.248 exerr in wmo package
Build Docker and Deploy / Run goext test-suite (push) Failing after 55s
2023-08-21 15:08:35 +02:00
Mikescher 9b752a911c v0.0.247 -.-
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m12s
2023-08-21 14:23:44 +02:00
Mikescher ec9ac26a4c v0.0.246 timeext
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m11s
2023-08-21 14:15:06 +02:00
Mikescher 39a0b73d56 v0.0.245
Build Docker and Deploy / Run goext test-suite (push) Successful in 40s
2023-08-21 13:27:36 +02:00
Mikescher 2e2e15d4d2 v0.0.244
Build Docker and Deploy / Run goext test-suite (push) Successful in 48s
2023-08-18 13:27:02 +02:00
Mikescher 0d16946aba v0.0.243
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m18s
2023-08-18 13:25:18 +02:00
Mikescher 14441c2378 Adde gitea workflow: tests
Build Docker and Deploy / Run goext test-suite (push) Successful in 57s
2023-08-14 18:39:22 +02:00
Mikescher f6bcdc9903 Merge remote-tracking branch 'origin/master' 2023-08-14 16:33:03 +02:00
Mikescher a95053211c Fix tests 2023-08-14 16:32:39 +02:00
Julian 813ce71e3e v0.0.242 forgot to return something 2023-08-14 16:05:12 +02:00
Julian 56ae0cfc6c v0.0.241 join string array 2023-08-14 15:54:50 +02:00
Mikescher 202afc9068 v0.0.240 2023-08-14 15:36:12 +02:00
Mikescher 56094b3cb6 v0.0.239 pctx.WithTImeout 2023-08-11 16:32:34 +02:00
Mikescher 0da098e9f9 v0.0.238 2023-08-09 19:51:41 +02:00
Mikescher f0881c9fd6 v0.0.237 parse application/x-www-form-urlencoded in ginext 2023-08-09 19:35:01 +02:00
Mikescher 029b408749 v0.0.236 cmdext.FailOnStdErr 2023-08-09 17:48:06 +02:00
Mikescher 84b2be3169 v0.0.235 added .Enum(..) to exerr 2023-08-09 14:40:16 +02:00
Mikescher c872cecc67 v0.0.234 2023-08-09 10:39:14 +02:00
Mikescher 99cd92729e v0.0.233 IncludeMetaInGinOutput 2023-08-09 10:37:59 +02:00
Mikescher ac416f7b69 v0.0.232 2023-08-08 18:01:00 +02:00
Mikescher e10140e143 v0.0.231 2023-08-08 16:10:31 +02:00
Mikescher e165f0f62f v0.0.230 2023-08-08 16:09:02 +02:00
Mikescher 655d4daad9 v0.0.229 2023-08-08 16:05:44 +02:00
251 changed files with 22020 additions and 4061 deletions
+42
View File
@@ -0,0 +1,42 @@
# https://docs.gitea.com/next/usage/actions/quickstart
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
# https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
name: Build Docker and Deploy
run-name: Build & Deploy ${{ gitea.ref }} on ${{ gitea.actor }}
on:
push:
branches:
- '*'
- '**'
jobs:
run_tests:
name: Run goext test-suite
runs-on: bfb-cicd-latest
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Setup go
uses: actions/setup-go@v4
with:
go-version-file: '${{ gitea.workspace }}/go.mod'
- name: Setup packages
uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: curl python3
version: 1.0
- name: go version
run: go version
- name: Run tests
run: cd "${{ gitea.workspace }}" && make test
+2
View File
@@ -6,3 +6,5 @@
# Datasource local storage ignored files # Datasource local storage ignored files
/dataSources/ /dataSources/
/dataSources.local.xml /dataSources.local.xml
# GitHub Copilot persisted chat sessions
/copilot/chatSessions
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AskMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Ask2AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EditMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoImports">
<option name="excludedPackages">
<array>
<option value="github.com/pkg/errors" />
<option value="golang.org/x/net/context" />
</array>
</option>
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoLinterSettings">
<option name="checkGoLinterExe" value="false" />
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/sq/sq_test.go" dialect="SQLite" />
</component>
</project>
+7 -1
View File
@@ -5,7 +5,13 @@ run:
test: test:
# go test ./... # go test ./...
which gotestsum || go install gotest.tools/gotestsum@latest which gotestsum || go install gotest.tools/gotestsum@latest
gotestsum --format "testname" -- -tags="timetzdata sqlite_fts5 sqlite_foreign_keys" "./test" gotestsum --format "testname" -- -tags="timetzdata sqlite_fts5 sqlite_foreign_keys" "./..."
test-in-docker:
tag="goext_temp_test_image:$(shell uuidgen | tr -d '-')"; \
docker build --tag $$tag . -f .gitea/workflows/Dockerfile_tests; \
docker run --rm $$tag; \
docker rmi $$tag
version: version:
_data/version.sh _data/version.sh
+105 -31
View File
@@ -5,37 +5,111 @@ A collection of general & useful library methods
This should not have any heavy dependencies (gin, mongo, etc) and add missing basic language features... This should not have any heavy dependencies (gin, mongo, etc) and add missing basic language features...
Potentially needs `export GOPRIVATE="gogs.mikescher.com"` Potentially needs `export GOPRIVATE="git.blackforestbytes.com"`
### Packages: ## Packages:
| Name | Maintainer | Description | | Name | Maintainer | Description |
|--------------|------------|---------------------------------------------------------------------------------------------------------------| |-------------|------------|---------------------------------------------------------------------------------------------------------------|
| langext | Mike | General uttility/helper functions, (everything thats missing from go standard library) | | langext | Mike | General uttility/helper functions, (everything thats missing from go standard library) |
| mathext | Mike | Utility/Helper functions for math | | mathext | Mike | Utility/Helper functions for math |
| cryptext | Mike | Utility/Helper functions for encryption | | cryptext | Mike | Utility/Helper functions for encryption |
| syncext | Mike | Utility/Helper funtions for multi-threading / mutex / channels | | syncext | Mike | Utility/Helper funtions for multi-threading / mutex / channels |
| dataext | Mike | Various useful data structures | | dataext | Mike | Various useful data structures |
| zipext | Mike | Utility for zip/gzip/tar etc | | zipext | Mike | Utility for zip/gzip/tar etc |
| reflectext | Mike | Utility for golagn reflection | | reflectext | Mike | Utility for golang reflection |
| | | | | fsext | Mike | Utility for filesytem access |
| mongoext | Mike | Utility/Helper functions for mongodb | | ctxext | Mike | Utility for context.Context |
| cursortoken | Mike | MongoDB cursortoken implementation | | | | |
| | | | | mongoext | Mike | Utility/Helper functions for mongodb (kinda abandoned) |
| totpext | Mike | Implementation of TOTP (2-Factor-Auth) | | cursortoken | Mike | MongoDB cursortoken implementation |
| termext | Mike | Utilities for terminals (mostly color output) | | pagination | Mike | Pagination implementation |
| confext | Mike | Parses environment configuration into structs | | | | |
| cmdext | Mike | Runner for external commands/processes | | ginext | Mike | gin wrapper |
| | | | | wsw | Mike | websocket wrapper |
| sq | Mike | Utility functions for sql based databases | | | | |
| tst | Mike | Utility functions for unit tests | | totpext | Mike | Implementation of TOTP (2-Factor-Auth) |
| | | | | termext | Mike | Utilities for terminals (mostly color output) |
| rfctime | Mike | Classes for time seriallization, with different marshallign method for mongo and json | | confext | Mike | Parses environment configuration into structs |
| gojson | Mike | Same interface for marshalling/unmarshalling as go/json, except with proper serialization of null arrays/maps | | cmdext | Mike | Runner for external commands/processes |
| | | | | excelext | Mike | Build Excel files |
| bfcodegen | Mike | Various codegen tools (run via go generate) | | | | |
| | | | | sq | Mike | Utility functions for sql based databases (primarily sqlite) |
| rext | Mike | Regex Wrapper, wraps regexp with a better interface | | tst | Mike | Utility functions for unit tests |
| wmo | Mike | Mongo Wrapper, wraps mongodb with a better interface | | | | |
| | | | | rfctime | Mike | Classes for time seriallization, with different marshallign method for mongo and json |
| gojson | Mike | Same interface for marshalling/unmarshalling as go/json, except with proper serialization of null arrays/maps |
| | | |
| bfcodegen | Mike | Various codegen tools (run via go generate) |
| | | |
| rext | Mike | Regex Wrapper, wraps regexp with a better interface |
| 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)
+4 -8
View File
@@ -2,12 +2,8 @@
- cronext - cronext
- cursortoken - rfctime.HMSTimeOnly
- rfctime.NanoTimeOnly
- typed/geenric mongo wrapper - remove sqlx dependency from sq (unmaintained, and mostly superseeded by our own stuff?)
- Move DBLogger and DBPreprocessor to sq
- error package
- rfctime.DateOnly
- rfctime.HMSTimeOnly
- rfctime.NanoTimeOnly
Binary file not shown.
Binary file not shown.
+197
View File
@@ -0,0 +1,197 @@
package bfcodegen
import (
"bytes"
_ "embed"
"errors"
"fmt"
"go/format"
"git.blackforestbytes.com/BlackForestBytes/goext"
"git.blackforestbytes.com/BlackForestBytes/goext/cryptext"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/rext"
"io"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"text/template"
)
type CSIDDef struct {
File string
FileRelative string
Name string
Prefix string
}
type CSIDGenOptions struct {
DebugOutput *bool
}
var rexCSIDPackage = rext.W(regexp.MustCompile(`^package\s+(?P<name>[A-Za-z0-9_]+)\s*$`))
var rexCSIDDef = rext.W(regexp.MustCompile(`^\s*type\s+(?P<name>[A-Za-z0-9_]+)\s+string\s*//\s*(@csid:type)\s+\[(?P<prefix>[A-Z0-9]{3})].*$`))
var rexCSIDChecksumConst = rext.W(regexp.MustCompile(`const ChecksumCharsetIDGenerator = "(?P<cs>[A-Za-z0-9_]*)"`))
//go:embed csid-generate.template
var templateCSIDGenerateText string
func GenerateCharsetIDSpecs(sourceDir string, destFile string, opt CSIDGenOptions) error {
debugOutput := langext.Coalesce(opt.DebugOutput, false)
files, err := os.ReadDir(sourceDir)
if err != nil {
return err
}
oldChecksum := "N/A"
if _, err := os.Stat(destFile); !os.IsNotExist(err) {
content, err := os.ReadFile(destFile)
if err != nil {
return err
}
if m, ok := rexCSIDChecksumConst.MatchFirst(string(content)); ok {
oldChecksum = m.GroupByName("cs").Value()
}
}
files = langext.ArrFilter(files, func(v os.DirEntry) bool { return v.Name() != path.Base(destFile) })
files = langext.ArrFilter(files, func(v os.DirEntry) bool { return strings.HasSuffix(v.Name(), ".go") })
files = langext.ArrFilter(files, func(v os.DirEntry) bool { return !strings.HasSuffix(v.Name(), "_gen.go") })
langext.SortBy(files, func(v os.DirEntry) string { return v.Name() })
newChecksumStr := goext.GoextVersion
for _, f := range files {
content, err := os.ReadFile(path.Join(sourceDir, f.Name()))
if err != nil {
return err
}
newChecksumStr += "\n" + f.Name() + "\t" + cryptext.BytesSha256(content)
}
newChecksum := cryptext.BytesSha256([]byte(newChecksumStr))
if newChecksum != oldChecksum {
fmt.Printf("[CSIDGenerate] Checksum has changed ( %s -> %s ), will generate new file\n\n", oldChecksum, newChecksum)
} else {
fmt.Printf("[CSIDGenerate] Checksum unchanged ( %s ), nothing to do\n", oldChecksum)
return nil
}
allIDs := make([]CSIDDef, 0)
pkgname := ""
for _, f := range files {
if debugOutput {
fmt.Printf("========= %s =========\n\n", f.Name())
}
fileIDs, pn, err := processCSIDFile(sourceDir, path.Join(sourceDir, f.Name()), debugOutput)
if err != nil {
return err
}
if debugOutput {
fmt.Printf("\n")
}
allIDs = append(allIDs, fileIDs...)
if pn != "" {
pkgname = pn
}
}
if pkgname == "" {
return errors.New("no package name found in any file")
}
fdata, err := format.Source([]byte(fmtCSIDOutput(newChecksum, allIDs, pkgname)))
if err != nil {
return err
}
err = os.WriteFile(destFile, fdata, 0o755)
if err != nil {
return err
}
return nil
}
func processCSIDFile(basedir string, fn string, debugOutput bool) ([]CSIDDef, string, error) {
file, err := os.Open(fn)
if err != nil {
return nil, "", err
}
defer func() { _ = file.Close() }()
bin, err := io.ReadAll(file)
if err != nil {
return nil, "", err
}
lines := strings.Split(string(bin), "\n")
ids := make([]CSIDDef, 0)
pkgname := ""
for i, line := range lines {
if i == 0 && strings.HasPrefix(line, "// Code generated by") {
break
}
if match, ok := rexCSIDPackage.MatchFirst(line); i == 0 && ok {
pkgname = match.GroupByName("name").Value()
continue
}
if match, ok := rexCSIDDef.MatchFirst(line); ok {
rfp, err := filepath.Rel(basedir, fn)
if err != nil {
return nil, "", err
}
def := CSIDDef{
File: fn,
FileRelative: rfp,
Name: match.GroupByName("name").Value(),
Prefix: match.GroupByName("prefix").Value(),
}
if debugOutput {
fmt.Printf("Found ID definition { '%s' }\n", def.Name)
}
ids = append(ids, def)
}
}
return ids, pkgname, nil
}
func fmtCSIDOutput(cs string, ids []CSIDDef, pkgname string) string {
templ := template.Must(template.New("csid-generate").Parse(templateCSIDGenerateText))
buffer := bytes.Buffer{}
err := templ.Execute(&buffer, langext.H{
"PkgName": pkgname,
"Checksum": cs,
"GoextVersion": goext.GoextVersion,
"IDs": ids,
})
if err != nil {
panic(err)
}
return buffer.String()
}
+194
View File
@@ -0,0 +1,194 @@
// Code generated by csid-generate.go DO NOT EDIT.
package {{.PkgName}}
import "crypto/rand"
import "crypto/sha256"
import "fmt"
import "github.com/go-playground/validator/v10"
import "github.com/rs/zerolog/log"
import "git.blackforestbytes.com/BlackForestBytes/goext/exerr"
import "git.blackforestbytes.com/BlackForestBytes/goext/langext"
import "git.blackforestbytes.com/BlackForestBytes/goext/rext"
import "math/big"
import "reflect"
import "regexp"
import "strings"
const ChecksumCharsetIDGenerator = "{{.Checksum}}" // GoExtVersion: {{.GoextVersion}}
const idlen = 24
const checklen = 1
const idCharset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
const idCharsetLen = len(idCharset)
var charSetReverseMap = generateCharsetMap()
const ({{range .IDs}}
prefix{{.Name}} = "{{.Prefix}}" {{end}}
)
var ({{range .IDs}}
regex{{.Name}} = generateRegex(prefix{{.Name}}) {{end}}
)
func generateRegex(prefix string) rext.Regex {
return rext.W(regexp.MustCompile(fmt.Sprintf("^%s[%s]{%d}[%s]{%d}$", prefix, idCharset, idlen-len(prefix)-checklen, idCharset, checklen)))
}
func generateCharsetMap() []int {
result := make([]int, 128)
for i := 0; i < len(result); i++ {
result[i] = -1
}
for idx, chr := range idCharset {
result[int(chr)] = idx
}
return result
}
func generateID(prefix string) string {
k := ""
csMax := big.NewInt(int64(idCharsetLen))
checksum := 0
for i := 0; i < idlen-len(prefix)-checklen; i++ {
v, err := rand.Int(rand.Reader, csMax)
if err != nil {
panic(err)
}
v64 := v.Int64()
k += string(idCharset[v64])
checksum = (checksum + int(v64)) % (idCharsetLen)
}
checkstr := string(idCharset[checksum%idCharsetLen])
return prefix + k + checkstr
}
func generateIDFromSeed(prefix string, seed string) string {
h := sha256.New()
iddata := ""
for len(iddata) < idlen-len(prefix)-checklen {
h.Write([]byte(seed))
bs := h.Sum(nil)
iddata += langext.NewAnyBaseConverter(idCharset).Encode(bs)
}
checksum := 0
for i := 0; i < idlen-len(prefix)-checklen; i++ {
ichr := int(iddata[i])
checksum = (checksum + charSetReverseMap[ichr]) % (idCharsetLen)
}
checkstr := string(idCharset[checksum%idCharsetLen])
return prefix + iddata[:(idlen-len(prefix)-checklen)] + checkstr
}
func validateID(prefix string, value string) error {
if len(value) != idlen {
return exerr.New(exerr.TypeInvalidCSID, "id has the wrong length").Str("value", value).Build()
}
if !strings.HasPrefix(value, prefix) {
return exerr.New(exerr.TypeInvalidCSID, "id is missing the correct prefix").Str("value", value).Str("prefix", prefix).Build()
}
checksum := 0
for i := len(prefix); i < len(value)-checklen; i++ {
ichr := int(value[i])
if ichr < 0 || ichr >= len(charSetReverseMap) || charSetReverseMap[ichr] == -1 {
return exerr.New(exerr.TypeInvalidCSID, "id contains invalid characters").Str("value", value).Build()
}
checksum = (checksum + charSetReverseMap[ichr]) % (idCharsetLen)
}
checkstr := string(idCharset[checksum%idCharsetLen])
if !strings.HasSuffix(value, checkstr) {
return exerr.New(exerr.TypeInvalidCSID, "id checkstring is invalid").Str("value", value).Str("checkstr", checkstr).Build()
}
return nil
}
func getRawData(prefix string, value string) string {
if len(value) != idlen {
return ""
}
return value[len(prefix) : idlen-checklen]
}
func getCheckString(prefix string, value string) string {
if len(value) != idlen {
return ""
}
return value[idlen-checklen:]
}
func ValidateEntityID(vfl validator.FieldLevel) bool {
if !vfl.Field().CanInterface() {
log.Error().Msgf("Failed to validate EntityID (cannot interface ?!?)")
return false
}
ifvalue := vfl.Field().Interface()
if value1, ok := ifvalue.(EntityID); ok {
if vfl.Field().Type().Kind() == reflect.Pointer && langext.IsNil(value1) {
return true
}
if err := value1.Valid(); err != nil {
log.Debug().Msgf("Failed to validate EntityID '%s' (%s)", value1.String(), err.Error())
return false
} else {
return true
}
} else {
log.Error().Msgf("Failed to validate EntityID (wrong type: %T)", ifvalue)
return false
}
}
{{range .IDs}}
// ================================ {{.Name}} ({{.FileRelative}}) ================================
func New{{.Name}}() {{.Name}} {
return {{.Name}}(generateID(prefix{{.Name}}))
}
func (id {{.Name}}) Valid() error {
return validateID(prefix{{.Name}}, string(id))
}
func (i {{.Name}}) String() string {
return string(i)
}
func (i {{.Name}}) Prefix() string {
return prefix{{.Name}}
}
func (id {{.Name}}) Raw() string {
return getRawData(prefix{{.Name}}, string(id))
}
func (id {{.Name}}) CheckString() string {
return getCheckString(prefix{{.Name}}, string(id))
}
func (id {{.Name}}) IsZero() bool {
return id == ""
}
func (id {{.Name}}) Regex() rext.Regex {
return regex{{.Name}}
}
{{end}}
+52
View File
@@ -0,0 +1,52 @@
package bfcodegen
import (
_ "embed"
"fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/cmdext"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
"os"
"path/filepath"
"testing"
"time"
)
//go:embed _test_example_1.tgz
var CSIDExampleModels1 []byte
func TestGenerateCSIDSpecs(t *testing.T) {
tmpFile := filepath.Join(t.TempDir(), langext.MustHexUUID()+".tgz")
tmpDir := filepath.Join(t.TempDir(), langext.MustHexUUID())
err := os.WriteFile(tmpFile, CSIDExampleModels1, 0o777)
tst.AssertNoErr(t, err)
t.Cleanup(func() { _ = os.Remove(tmpFile) })
err = os.Mkdir(tmpDir, 0o777)
tst.AssertNoErr(t, err)
t.Cleanup(func() { _ = os.RemoveAll(tmpFile) })
_, err = cmdext.Runner("tar").Arg("-xvzf").Arg(tmpFile).Arg("-C").Arg(tmpDir).FailOnExitCode().FailOnTimeout().Timeout(time.Minute).Run()
tst.AssertNoErr(t, err)
err = GenerateCharsetIDSpecs(tmpDir, tmpDir+"/csid_gen.go", CSIDGenOptions{DebugOutput: langext.PTrue})
tst.AssertNoErr(t, err)
err = GenerateCharsetIDSpecs(tmpDir, tmpDir+"/csid_gen.go", CSIDGenOptions{DebugOutput: langext.PTrue})
tst.AssertNoErr(t, err)
fmt.Println()
fmt.Println()
fmt.Println()
fmt.Println("=====================================================================================================")
fmt.Println(string(tst.Must(os.ReadFile(tmpDir + "/csid_gen.go"))(t)))
fmt.Println("=====================================================================================================")
fmt.Println()
fmt.Println()
fmt.Println()
}
+214 -202
View File
@@ -1,26 +1,32 @@
package bfcodegen package bfcodegen
import ( import (
"bytes"
_ "embed"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"gogs.mikescher.com/BlackForestBytes/goext" "git.blackforestbytes.com/BlackForestBytes/goext"
"gogs.mikescher.com/BlackForestBytes/goext/cmdext" "git.blackforestbytes.com/BlackForestBytes/goext/cryptext"
"gogs.mikescher.com/BlackForestBytes/goext/cryptext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/rext"
"gogs.mikescher.com/BlackForestBytes/goext/rext" "go/format"
"io" "io"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"reflect"
"regexp" "regexp"
"strings" "strings"
"time" "text/template"
) )
type EnumDefVal struct { type EnumDefVal struct {
VarName string VarName string
Value string Value string
Description *string Description *string
Data *map[string]any
RawComment *string
} }
type EnumDef struct { type EnumDef struct {
@@ -31,20 +37,23 @@ type EnumDef struct {
Values []EnumDefVal Values []EnumDefVal
} }
var rexPackage = rext.W(regexp.MustCompile(`^package\s+(?P<name>[A-Za-z0-9_]+)\s*$`)) type EnumGenOptions struct {
DebugOutput *bool
GoFormat *bool
}
var rexEnumPackage = rext.W(regexp.MustCompile(`^package\s+(?P<name>[A-Za-z0-9_]+)\s*$`))
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 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 rexValueDef = 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_:]+"|[0-9]+))\s*(//(?P<descr>.*))?.*$`)) 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 rexChecksumConst = rext.W(regexp.MustCompile(`const ChecksumGenerator = "(?P<cs>[A-Za-z0-9_]*)"`)) var rexEnumChecksumConst = rext.W(regexp.MustCompile(`const ChecksumEnumGenerator = "(?P<cs>[A-Za-z0-9_]*)"`))
func GenerateEnumSpecs(sourceDir string, destFile string) error { //go:embed enum-generate.template
var templateEnumGenerateText string
files, err := os.ReadDir(sourceDir) func GenerateEnumSpecs(sourceDir string, destFile string, opt EnumGenOptions) error {
if err != nil {
return err
}
oldChecksum := "N/A" oldChecksum := "N/A"
if _, err := os.Stat(destFile); !os.IsNotExist(err) { if _, err := os.Stat(destFile); !os.IsNotExist(err) {
@@ -52,20 +61,45 @@ func GenerateEnumSpecs(sourceDir string, destFile string) error {
if err != nil { if err != nil {
return err return err
} }
if m, ok := rexChecksumConst.MatchFirst(string(content)); ok { if m, ok := rexEnumChecksumConst.MatchFirst(string(content)); ok {
oldChecksum = m.GroupByName("cs").Value() oldChecksum = m.GroupByName("cs").Value()
} }
} }
gocode, _, changed, err := _generateEnumSpecs(sourceDir, destFile, oldChecksum, langext.Coalesce(opt.GoFormat, true), langext.Coalesce(opt.DebugOutput, false))
if err != nil {
return err
}
if !changed {
return nil
}
err = os.WriteFile(destFile, []byte(gocode), 0o755)
if err != nil {
return err
}
return nil
}
func _generateEnumSpecs(sourceDir string, destFile string, oldChecksum string, gofmt bool, debugOutput bool) (string, string, bool, error) {
files, err := os.ReadDir(sourceDir)
if err != nil {
return "", "", false, err
}
files = langext.ArrFilter(files, func(v os.DirEntry) bool { return v.Name() != path.Base(destFile) }) files = langext.ArrFilter(files, func(v os.DirEntry) bool { return v.Name() != path.Base(destFile) })
files = langext.ArrFilter(files, func(v os.DirEntry) bool { return strings.HasSuffix(v.Name(), ".go") }) files = langext.ArrFilter(files, func(v os.DirEntry) bool { return strings.HasSuffix(v.Name(), ".go") })
files = langext.ArrFilter(files, func(v os.DirEntry) bool { return !strings.HasSuffix(v.Name(), "_gen.go") })
langext.SortBy(files, func(v os.DirEntry) string { return v.Name() }) langext.SortBy(files, func(v os.DirEntry) string { return v.Name() })
newChecksumStr := goext.GoextVersion newChecksumStr := goext.GoextVersion
for _, f := range files { for _, f := range files {
content, err := os.ReadFile(path.Join(sourceDir, f.Name())) content, err := os.ReadFile(path.Join(sourceDir, f.Name()))
if err != nil { if err != nil {
return err return "", "", false, err
} }
newChecksumStr += "\n" + f.Name() + "\t" + cryptext.BytesSha256(content) newChecksumStr += "\n" + f.Name() + "\t" + cryptext.BytesSha256(content)
} }
@@ -76,7 +110,7 @@ func GenerateEnumSpecs(sourceDir string, destFile string) error {
fmt.Printf("[EnumGenerate] Checksum has changed ( %s -> %s ), will generate new file\n\n", oldChecksum, newChecksum) fmt.Printf("[EnumGenerate] Checksum has changed ( %s -> %s ), will generate new file\n\n", oldChecksum, newChecksum)
} else { } else {
fmt.Printf("[EnumGenerate] Checksum unchanged ( %s ), nothing to do\n", oldChecksum) fmt.Printf("[EnumGenerate] Checksum unchanged ( %s ), nothing to do\n", oldChecksum)
return nil return "", oldChecksum, false, nil
} }
allEnums := make([]EnumDef, 0) allEnums := make([]EnumDef, 0)
@@ -84,13 +118,18 @@ func GenerateEnumSpecs(sourceDir string, destFile string) error {
pkgname := "" pkgname := ""
for _, f := range files { for _, f := range files {
fmt.Printf("========= %s =========\n\n", f.Name()) if debugOutput {
fileEnums, pn, err := processFile(sourceDir, path.Join(sourceDir, f.Name())) fmt.Printf("========= %s =========\n\n", f.Name())
if err != nil {
return err
} }
fmt.Printf("\n") fileEnums, pn, err := processEnumFile(sourceDir, path.Join(sourceDir, f.Name()), debugOutput)
if err != nil {
return "", "", false, err
}
if debugOutput {
fmt.Printf("\n")
}
allEnums = append(allEnums, fileEnums...) allEnums = append(allEnums, fileEnums...)
@@ -100,32 +139,24 @@ func GenerateEnumSpecs(sourceDir string, destFile string) error {
} }
if pkgname == "" { if pkgname == "" {
return errors.New("no package name found in any file") return "", "", false, errors.New("no package name found in any file")
} }
err = os.WriteFile(destFile, []byte(fmtOutput(newChecksum, allEnums, pkgname)), 0o755) rdata := fmtEnumOutput(newChecksum, allEnums, pkgname)
if !gofmt {
return rdata, newChecksum, true, nil
}
fdata, err := format.Source([]byte(rdata))
if err != nil { if err != nil {
return err return "", "", false, err
} }
res, err := cmdext.RunCommand("go", []string{"fmt", destFile}, langext.Ptr(2*time.Second)) return string(fdata), newChecksum, true, nil
if err != nil {
return err
}
if res.CommandTimedOut {
fmt.Println(res.StdCombined)
return errors.New("go fmt timed out")
}
if res.ExitCode != 0 {
fmt.Println(res.StdCombined)
return errors.New("go fmt did not succeed")
}
return nil
} }
func processFile(basedir string, fn string) ([]EnumDef, string, error) { func processEnumFile(basedir string, fn string, debugOutput bool) ([]EnumDef, string, error) {
file, err := os.Open(fn) file, err := os.Open(fn)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
@@ -149,7 +180,7 @@ func processFile(basedir string, fn string) ([]EnumDef, string, error) {
break break
} }
if match, ok := rexPackage.MatchFirst(line); i == 0 && ok { if match, ok := rexEnumPackage.MatchFirst(line); i == 0 && ok {
pkgname = match.GroupByName("name").Value() pkgname = match.GroupByName("name").Value()
continue continue
} }
@@ -169,15 +200,42 @@ func processFile(basedir string, fn string) ([]EnumDef, string, error) {
Values: make([]EnumDefVal, 0), Values: make([]EnumDefVal, 0),
} }
enums = append(enums, def) enums = append(enums, def)
fmt.Printf("Found enum definition { '%s' -> '%s' }\n", def.EnumTypeName, def.Type)
if debugOutput {
fmt.Printf("Found enum definition { '%s' -> '%s' }\n", def.EnumTypeName, def.Type)
}
} }
if match, ok := rexValueDef.MatchFirst(line); ok { if match, ok := rexEnumValueDef.MatchFirst(line); ok {
typename := match.GroupByName("type").Value() typename := match.GroupByName("type").Value()
comment := match.GroupByNameOrEmpty("comm").ValueOrNil()
var descr *string = nil
var data *map[string]any = nil
if comment != nil {
comment = langext.Ptr(strings.TrimSpace(*comment))
if strings.HasPrefix(*comment, "{") {
if v, ok := tryParseDataComment(*comment); ok {
data = &v
if anyDataDescr, ok := v["description"]; ok {
if dataDescr, ok := anyDataDescr.(string); ok {
descr = &dataDescr
}
}
} else {
descr = comment
}
} else {
descr = comment
}
}
def := EnumDefVal{ def := EnumDefVal{
VarName: match.GroupByName("name").Value(), VarName: match.GroupByName("name").Value(),
Value: match.GroupByName("value").Value(), Value: match.GroupByName("value").Value(),
Description: match.GroupByNameOrEmpty("descr").ValueOrNil(), RawComment: comment,
Description: descr,
Data: data,
} }
found := false found := false
@@ -185,16 +243,21 @@ func processFile(basedir string, fn string) ([]EnumDef, string, error) {
if v.EnumTypeName == typename { if v.EnumTypeName == typename {
enums[i].Values = append(enums[i].Values, def) enums[i].Values = append(enums[i].Values, def)
found = true found = true
if def.Description != nil {
fmt.Printf("Found enum value [%s] for '%s' ('%s')\n", def.Value, def.VarName, *def.Description) if debugOutput {
} else { if def.Description != nil {
fmt.Printf("Found enum value [%s] for '%s'\n", def.Value, def.VarName) fmt.Printf("Found enum value [%s] for '%s' ('%s')\n", def.Value, def.VarName, *def.Description)
} else {
fmt.Printf("Found enum value [%s] for '%s'\n", def.Value, def.VarName)
}
} }
break break
} }
} }
if !found { if !found {
fmt.Printf("Found non-enum value [%s] for '%s' ( looks like enum value, but no matching @enum:type )\n", def.Value, def.VarName) if debugOutput {
fmt.Printf("Found non-enum value [%s] for '%s' ( looks like enum value, but no matching @enum:type )\n", def.Value, def.VarName)
}
} }
} }
} }
@@ -202,160 +265,109 @@ func processFile(basedir string, fn string) ([]EnumDef, string, error) {
return enums, pkgname, nil return enums, pkgname, nil
} }
func fmtOutput(cs string, enums []EnumDef, pkgname string) string { func tryParseDataComment(s string) (map[string]any, bool) {
str := "// Code generated by enum-generate.go DO NOT EDIT.\n"
str += "\n"
str += "package " + pkgname + "\n"
str += "\n"
str += "import \"gogs.mikescher.com/BlackForestBytes/goext/langext\"" + "\n" r := make(map[string]any)
str += "\n"
str += "const ChecksumGenerator = \"" + cs + "\"" + "\n"
str += "\n"
str += "type Enum interface {" + "\n"
str += " Valid() bool" + "\n"
str += " ValuesAny() []any" + "\n"
str += " ValuesMeta() []EnumMetaValue" + "\n"
str += " VarName() string" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "type StringEnum interface {" + "\n"
str += " Enum" + "\n"
str += " String() string" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "type DescriptionEnum interface {" + "\n"
str += " Enum" + "\n"
str += " Description() string" + "\n"
str += "}" + "\n"
str += "\n"
str += "type EnumMetaValue struct {" + "\n"
str += " VarName string `json:\"varName\"`" + "\n"
str += " Value any `json:\"value\"`" + "\n"
str += " Description *string `json:\"description\"`" + "\n"
str += "}" + "\n"
str += "\n"
for _, enumdef := range enums {
hasDescr := langext.ArrAll(enumdef.Values, func(val EnumDefVal) bool { return val.Description != nil })
hasStr := enumdef.Type == "string"
str += "// ================================ " + enumdef.EnumTypeName + " ================================" + "\n"
str += "//" + "\n"
str += "// File: " + enumdef.FileRelative + "\n"
str += "// StringEnum: " + langext.Conditional(hasStr, "true", "false") + "\n"
str += "// DescrEnum: " + langext.Conditional(hasDescr, "true", "false") + "\n"
str += "//" + "\n"
str += "" + "\n"
str += "var __" + enumdef.EnumTypeName + "Values = []" + enumdef.EnumTypeName + "{" + "\n"
for _, v := range enumdef.Values {
str += " " + v.VarName + "," + "\n"
}
str += "}" + "\n"
str += "" + "\n"
if hasDescr {
str += "var __" + enumdef.EnumTypeName + "Descriptions = map[" + enumdef.EnumTypeName + "]string{" + "\n"
for _, v := range enumdef.Values {
str += " " + v.VarName + ": \"" + strings.TrimSpace(*v.Description) + "\"," + "\n"
}
str += "}" + "\n"
str += "" + "\n"
}
str += "var __" + enumdef.EnumTypeName + "Varnames = map[" + enumdef.EnumTypeName + "]string{" + "\n"
for _, v := range enumdef.Values {
str += " " + v.VarName + ": \"" + v.VarName + "\"," + "\n"
}
str += "}" + "\n"
str += "" + "\n"
str += "func (e " + enumdef.EnumTypeName + ") Valid() bool {" + "\n"
str += " return langext.InArray(e, __" + enumdef.EnumTypeName + "Values)" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "func (e " + enumdef.EnumTypeName + ") Values() []" + enumdef.EnumTypeName + " {" + "\n"
str += " return __" + enumdef.EnumTypeName + "Values" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "func (e " + enumdef.EnumTypeName + ") ValuesAny() []any {" + "\n"
str += " return langext.ArrCastToAny(__" + enumdef.EnumTypeName + "Values)" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "func (e " + enumdef.EnumTypeName + ") ValuesMeta() []EnumMetaValue {" + "\n"
str += " return " + enumdef.EnumTypeName + "ValuesMeta()"
str += "}" + "\n"
str += "" + "\n"
if hasStr {
str += "func (e " + enumdef.EnumTypeName + ") String() string {" + "\n"
str += " return string(e)" + "\n"
str += "}" + "\n"
str += "" + "\n"
}
if hasDescr {
str += "func (e " + enumdef.EnumTypeName + ") Description() string {" + "\n"
str += " if d, ok := __" + enumdef.EnumTypeName + "Descriptions[e]; ok {" + "\n"
str += " return d" + "\n"
str += " }" + "\n"
str += " return \"\"" + "\n"
str += "}" + "\n"
str += "" + "\n"
}
str += "func (e " + enumdef.EnumTypeName + ") VarName() string {" + "\n"
str += " if d, ok := __" + enumdef.EnumTypeName + "Varnames[e]; ok {" + "\n"
str += " return d" + "\n"
str += " }" + "\n"
str += " return \"\"" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "func (e " + enumdef.EnumTypeName + ") Meta() EnumMetaValue {" + "\n"
if hasDescr {
str += " return EnumMetaValue{VarName: e.VarName(), Value: e, Description: langext.Ptr(e.Description())}"
} else {
str += " return EnumMetaValue{VarName: e.VarName(), Value: e, Description: nil}"
}
str += "}" + "\n"
str += "" + "\n"
str += "func Parse" + enumdef.EnumTypeName + "(vv string) (" + enumdef.EnumTypeName + ", bool) {" + "\n"
str += " for _, ev := range __" + enumdef.EnumTypeName + "Values {" + "\n"
str += " if string(ev) == vv {" + "\n"
str += " return ev, true" + "\n"
str += " }" + "\n"
str += " }" + "\n"
str += " return \"\", false" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "func " + enumdef.EnumTypeName + "Values() []" + enumdef.EnumTypeName + " {" + "\n"
str += " return __" + enumdef.EnumTypeName + "Values" + "\n"
str += "}" + "\n"
str += "" + "\n"
str += "func " + enumdef.EnumTypeName + "ValuesMeta() []EnumMetaValue {" + "\n"
str += " return []EnumMetaValue{" + "\n"
for _, v := range enumdef.Values {
str += " " + v.VarName + ".Meta(),\n"
}
str += " }" + "\n"
str += "}" + "\n"
str += "" + "\n"
err := json.Unmarshal([]byte(s), &r)
if err != nil {
return nil, false
} }
return str for _, v := range r {
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Ptr && rv.IsNil() {
continue
}
if rv.Kind() == reflect.Bool {
continue
}
if rv.Kind() == reflect.String {
continue
}
if rv.Kind() == reflect.Int64 {
continue
}
if rv.Kind() == reflect.Float64 {
continue
}
return nil, false
}
return r, true
}
func fmtEnumOutput(cs string, enums []EnumDef, pkgname string) string {
templ := template.New("enum-generate")
templ = templ.Funcs(template.FuncMap{
"boolToStr": func(b bool) string { return langext.Conditional(b, "true", "false") },
"deref": func(v *string) string { return *v },
"trimSpace": func(str string) string { return strings.TrimSpace(str) },
"hasStr": func(v EnumDef) bool { return v.Type == "string" },
"hasDescr": func(v EnumDef) bool {
return langext.ArrAll(v.Values, func(val EnumDefVal) bool { return val.Description != nil })
},
"hasData": func(v EnumDef) bool {
return len(v.Values) > 0 && langext.ArrAll(v.Values, func(val EnumDefVal) bool { return val.Data != nil })
},
"gostr": func(v any) string {
return fmt.Sprintf("%#+v", v)
},
"goobj": func(name string, v any) string {
return fmt.Sprintf("%#+v", v)
},
"godatakey": func(v string) string {
return strings.ToUpper(v[0:1]) + v[1:]
},
"godatavalue": func(v any) string {
return fmt.Sprintf("%#+v", v)
},
"godatatype": func(v any) string {
return fmt.Sprintf("%T", v)
},
"mapindex": func(v map[string]any, k string) any {
return v[k]
},
"generalDataKeys": func(v EnumDef) map[string]string {
r0 := make(map[string]int)
for _, eval := range v.Values {
for k := range *eval.Data {
if ctr, ok := r0[k]; ok {
r0[k] = ctr + 1
} else {
r0[k] = 1
}
}
}
r1 := langext.MapToArr(r0)
r2 := langext.ArrFilter(r1, func(p langext.MapEntry[string, int]) bool { return p.Value == len(v.Values) })
r3 := langext.ArrMap(r2, func(p langext.MapEntry[string, int]) string { return p.Key })
r4 := langext.ArrToKVMap(r3, func(p string) string { return p }, func(p string) string { return fmt.Sprintf("%T", (*v.Values[0].Data)[p]) })
return r4
},
})
templ = template.Must(templ.Parse(templateEnumGenerateText))
buffer := bytes.Buffer{}
err := templ.Execute(&buffer, langext.H{
"PkgName": pkgname,
"Checksum": cs,
"GoextVersion": goext.GoextVersion,
"Enums": enums,
})
if err != nil {
panic(err)
}
return buffer.String()
} }
+177
View File
@@ -0,0 +1,177 @@
// Code generated by enum-generate.go DO NOT EDIT.
package {{.PkgName}}
import "git.blackforestbytes.com/BlackForestBytes/goext/langext"
import "git.blackforestbytes.com/BlackForestBytes/goext/enums"
const ChecksumEnumGenerator = "{{.Checksum}}" // GoExtVersion: {{.GoextVersion}}
{{ $pkgname := .PkgName }}
{{range .Enums}}
{{ $hasStr := ( . | hasStr ) }}
{{ $hasDescr := ( . | hasDescr ) }}
{{ $hasData := ( . | hasData ) }}
// ================================ {{.EnumTypeName}} ================================
//
// File: {{.FileRelative}}
// StringEnum: {{$hasStr | boolToStr}}
// DescrEnum: {{$hasDescr | boolToStr}}
// DataEnum: {{$hasData | boolToStr}}
//
{{ $typename := .EnumTypeName }}
{{ $enumdef := . }}
var __{{.EnumTypeName}}Values = []{{.EnumTypeName}}{ {{range .Values}}
{{.VarName}}, {{end}}
}
{{if $hasDescr}}
var __{{.EnumTypeName}}Descriptions = map[{{.EnumTypeName}}]string{ {{range .Values}}
{{.VarName}}: {{.Description | deref | trimSpace | gostr}}, {{end}}
}
{{end}}
{{if $hasData}}
type {{ .EnumTypeName }}Data struct { {{ range $datakey, $datatype := ($enumdef | generalDataKeys) }}
{{ $datakey | godatakey }} {{ $datatype }} `json:"{{ $datakey }}"` {{ end }}
}
var __{{.EnumTypeName}}Data = map[{{.EnumTypeName}}]{{.EnumTypeName}}Data{ {{range .Values}} {{ $enumvalue := . }}
{{.VarName}}: {{ $typename }}Data{ {{ range $datakey, $datatype := $enumdef | generalDataKeys }}
{{ $datakey | godatakey }}: {{ (mapindex $enumvalue.Data $datakey) | godatavalue }}, {{ end }}
}, {{end}}
}
{{end}}
var __{{.EnumTypeName}}Varnames = map[{{.EnumTypeName}}]string{ {{range .Values}}
{{.VarName}}: "{{.VarName}}", {{end}}
}
func (e {{.EnumTypeName}}) Valid() bool {
return langext.InArray(e, __{{.EnumTypeName}}Values)
}
func (e {{.EnumTypeName}}) Values() []{{.EnumTypeName}} {
return __{{.EnumTypeName}}Values
}
func (e {{.EnumTypeName}}) ValuesAny() []any {
return langext.ArrCastToAny(__{{.EnumTypeName}}Values)
}
func (e {{.EnumTypeName}}) ValuesMeta() []enums.EnumMetaValue {
return {{.EnumTypeName}}ValuesMeta()
}
{{if $hasStr}}
func (e {{.EnumTypeName}}) String() string {
return string(e)
}
{{end}}
{{if $hasDescr}}
func (e {{.EnumTypeName}}) Description() string {
if d, ok := __{{.EnumTypeName}}Descriptions[e]; ok {
return d
}
return ""
}
{{end}}
{{if $hasData}}
func (e {{.EnumTypeName}}) Data() {{.EnumTypeName}}Data {
if d, ok := __{{.EnumTypeName}}Data[e]; ok {
return d
}
return {{.EnumTypeName}}Data{}
}
{{end}}
func (e {{.EnumTypeName}}) VarName() string {
if d, ok := __{{.EnumTypeName}}Varnames[e]; ok {
return d
}
return ""
}
func (e {{.EnumTypeName}}) TypeName() string {
return "{{$typename}}"
}
func (e {{.EnumTypeName}}) PackageName() string {
return "{{$pkgname }}"
}
func (e {{.EnumTypeName}}) Meta() enums.EnumMetaValue {
{{if $hasDescr}} return enums.EnumMetaValue{VarName: e.VarName(), Value: e, Description: langext.Ptr(e.Description())} {{else}} return enums.EnumMetaValue{VarName: e.VarName(), Value: e, Description: nil} {{end}}
}
{{if $hasDescr}}
func (e {{.EnumTypeName}}) DescriptionMeta() enums.EnumDescriptionMetaValue {
return enums.EnumDescriptionMetaValue{VarName: e.VarName(), Value: e, Description: e.Description()}
}
{{end}}
{{if $hasData}}
func (e {{.EnumTypeName}}) DataMeta() enums.EnumDataMetaValue {
return enums.EnumDataMetaValue{
VarName: e.VarName(),
Value: e,
{{if $hasDescr}} Description: langext.Ptr(e.Description()), {{else}} Description: nil, {{end}}
Data: map[string]any{
{{ range $datakey, $datatype := $enumdef | generalDataKeys }} "{{ $datakey }}": e.Data().{{ $datakey | godatakey }},
{{ end }}
},
}
}
{{end}}
func Parse{{.EnumTypeName}}(vv string) ({{.EnumTypeName}}, bool) {
for _, ev := range __{{.EnumTypeName}}Values {
if string(ev) == vv {
return ev, true
}
}
return "", false
}
func {{.EnumTypeName}}Values() []{{.EnumTypeName}} {
return __{{.EnumTypeName}}Values
}
func {{.EnumTypeName}}ValuesMeta() []enums.EnumMetaValue {
return []enums.EnumMetaValue{ {{range .Values}}
{{.VarName}}.Meta(), {{end}}
}
}
{{if $hasData}}
func {{.EnumTypeName}}ValuesDataMeta() []enums.EnumDataMetaValue {
return []enums.EnumDataMetaValue{ {{range .Values}}
{{.VarName}}.DataMeta(), {{end}}
}
}
{{end}}
{{if $hasDescr}}
func {{.EnumTypeName}}ValuesDescriptionMeta() []enums.EnumDescriptionMetaValue {
return []enums.EnumDescriptionMetaValue{ {{range .Values}}
{{.VarName}}.DescriptionMeta(), {{end}}
}
}
{{end}}
{{end}}
// ================================ ================= ================================
func AllPackageEnums() []enums.Enum {
return []enums.Enum{ {{range .Enums}}
{{ if gt (len .Values) 0 }} {{ $v := index .Values 0 }} {{ $v.VarName}}, {{end}} // {{ .EnumTypeName }} {{end}}
}
}
+82 -6
View File
@@ -1,15 +1,91 @@
package bfcodegen package bfcodegen
import ( import (
_ "embed"
"fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/cmdext"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
"os"
"path/filepath"
"testing" "testing"
"time"
) )
func TestApplyEnvOverridesSimple(t *testing.T) { //go:embed _test_example_1.tgz
var EnumExampleModels1 []byte
err := GenerateEnumSpecs("/home/mike/Code/reiff/badennet/bnet-backend/models", "/home/mike/Code/reiff/badennet/bnet-backend/models/enums_gen.go") //go:embed _test_example_2.tgz
if err != nil { var EnumExampleModels2 []byte
t.Error(err)
t.Fail()
}
func TestGenerateEnumSpecs(t *testing.T) {
tmpFile := filepath.Join(t.TempDir(), langext.MustHexUUID()+".tgz")
tmpDir := filepath.Join(t.TempDir(), langext.MustHexUUID())
err := os.WriteFile(tmpFile, EnumExampleModels1, 0o777)
tst.AssertNoErr(t, err)
t.Cleanup(func() { _ = os.Remove(tmpFile) })
err = os.Mkdir(tmpDir, 0o777)
tst.AssertNoErr(t, err)
t.Cleanup(func() { _ = os.RemoveAll(tmpFile) })
_, err = cmdext.Runner("tar").Arg("-xvzf").Arg(tmpFile).Arg("-C").Arg(tmpDir).FailOnExitCode().FailOnTimeout().Timeout(time.Minute).Run()
tst.AssertNoErr(t, err)
s1, cs1, _, err := _generateEnumSpecs(tmpDir, "", "N/A", true, true)
tst.AssertNoErr(t, err)
s2, cs2, _, err := _generateEnumSpecs(tmpDir, "", "N/A", true, true)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, cs1, cs2)
tst.AssertEqual(t, s1, s2)
fmt.Println()
fmt.Println()
fmt.Println()
fmt.Println("=====================================================================================================")
fmt.Println(s1)
fmt.Println("=====================================================================================================")
fmt.Println()
fmt.Println()
fmt.Println()
}
func TestGenerateEnumSpecsData(t *testing.T) {
tmpFile := filepath.Join(t.TempDir(), langext.MustHexUUID()+".tgz")
tmpDir := filepath.Join(t.TempDir(), langext.MustHexUUID())
err := os.WriteFile(tmpFile, EnumExampleModels2, 0o777)
tst.AssertNoErr(t, err)
t.Cleanup(func() { _ = os.Remove(tmpFile) })
err = os.Mkdir(tmpDir, 0o777)
tst.AssertNoErr(t, err)
t.Cleanup(func() { _ = os.RemoveAll(tmpFile) })
_, err = cmdext.Runner("tar").Arg("-xvzf").Arg(tmpFile).Arg("-C").Arg(tmpDir).FailOnExitCode().FailOnTimeout().Timeout(time.Minute).Run()
tst.AssertNoErr(t, err)
s1, _, _, err := _generateEnumSpecs(tmpDir, "", "", true, true)
tst.AssertNoErr(t, err)
fmt.Println()
fmt.Println()
fmt.Println()
fmt.Println("=====================================================================================================")
fmt.Println(s1)
fmt.Println("=====================================================================================================")
fmt.Println()
fmt.Println()
fmt.Println()
} }
+198
View File
@@ -0,0 +1,198 @@
package bfcodegen
import (
"bytes"
_ "embed"
"errors"
"fmt"
"go/format"
"git.blackforestbytes.com/BlackForestBytes/goext"
"git.blackforestbytes.com/BlackForestBytes/goext/cryptext"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/rext"
"io"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"text/template"
)
type IDDef struct {
File string
FileRelative string
Name string
}
type IDGenOptions struct {
DebugOutput *bool
}
var rexIDPackage = rext.W(regexp.MustCompile(`^package\s+(?P<name>[A-Za-z0-9_]+)\s*$`))
var rexIDDef = rext.W(regexp.MustCompile(`^\s*type\s+(?P<name>[A-Za-z0-9_]+)\s+string\s*//\s*(@id:type).*$`))
var rexIDChecksumConst = rext.W(regexp.MustCompile(`const ChecksumIDGenerator = "(?P<cs>[A-Za-z0-9_]*)"`))
//go:embed id-generate.template
var templateIDGenerateText string
func GenerateIDSpecs(sourceDir string, destFile string, opt IDGenOptions) error {
debugOutput := langext.Coalesce(opt.DebugOutput, false)
files, err := os.ReadDir(sourceDir)
if err != nil {
return err
}
oldChecksum := "N/A"
if _, err := os.Stat(destFile); !os.IsNotExist(err) {
content, err := os.ReadFile(destFile)
if err != nil {
return err
}
if m, ok := rexIDChecksumConst.MatchFirst(string(content)); ok {
oldChecksum = m.GroupByName("cs").Value()
}
}
files = langext.ArrFilter(files, func(v os.DirEntry) bool { return v.Name() != path.Base(destFile) })
files = langext.ArrFilter(files, func(v os.DirEntry) bool { return strings.HasSuffix(v.Name(), ".go") })
files = langext.ArrFilter(files, func(v os.DirEntry) bool { return !strings.HasSuffix(v.Name(), "_gen.go") })
langext.SortBy(files, func(v os.DirEntry) string { return v.Name() })
newChecksumStr := goext.GoextVersion
for _, f := range files {
content, err := os.ReadFile(path.Join(sourceDir, f.Name()))
if err != nil {
return err
}
newChecksumStr += "\n" + f.Name() + "\t" + cryptext.BytesSha256(content)
}
newChecksum := cryptext.BytesSha256([]byte(newChecksumStr))
if newChecksum != oldChecksum {
fmt.Printf("[IDGenerate] Checksum has changed ( %s -> %s ), will generate new file\n\n", oldChecksum, newChecksum)
} else {
fmt.Printf("[IDGenerate] Checksum unchanged ( %s ), nothing to do\n", oldChecksum)
return nil
}
allIDs := make([]IDDef, 0)
pkgname := ""
for _, f := range files {
if debugOutput {
fmt.Printf("========= %s =========\n\n", f.Name())
}
fileIDs, pn, err := processIDFile(sourceDir, path.Join(sourceDir, f.Name()), debugOutput)
if err != nil {
return err
}
if debugOutput {
fmt.Printf("\n")
}
allIDs = append(allIDs, fileIDs...)
if pn != "" {
pkgname = pn
}
}
if pkgname == "" {
return errors.New("no package name found in any file")
}
fdata, err := format.Source([]byte(fmtIDOutput(newChecksum, allIDs, pkgname)))
if err != nil {
return err
}
err = os.WriteFile(destFile, fdata, 0o755)
if err != nil {
return err
}
return nil
}
func processIDFile(basedir string, fn string, debugOutput bool) ([]IDDef, string, error) {
file, err := os.Open(fn)
if err != nil {
return nil, "", err
}
defer func() { _ = file.Close() }()
bin, err := io.ReadAll(file)
if err != nil {
return nil, "", err
}
lines := strings.Split(string(bin), "\n")
ids := make([]IDDef, 0)
pkgname := ""
for i, line := range lines {
if i == 0 && strings.HasPrefix(line, "// Code generated by") {
break
}
if match, ok := rexIDPackage.MatchFirst(line); i == 0 && ok {
pkgname = match.GroupByName("name").Value()
continue
}
if match, ok := rexIDDef.MatchFirst(line); ok {
rfp, err := filepath.Rel(basedir, fn)
if err != nil {
return nil, "", err
}
def := IDDef{
File: fn,
FileRelative: rfp,
Name: match.GroupByName("name").Value(),
}
if debugOutput {
fmt.Printf("Found ID definition { '%s' }\n", def.Name)
}
ids = append(ids, def)
}
}
return ids, pkgname, nil
}
func fmtIDOutput(cs string, ids []IDDef, pkgname string) string {
templ := template.Must(template.New("id-generate").Parse(templateIDGenerateText))
buffer := bytes.Buffer{}
anyDef := langext.ArrFirstOrNil(ids, func(def IDDef) bool { return def.Name == "AnyID" || def.Name == "AnyId" })
err := templ.Execute(&buffer, langext.H{
"PkgName": pkgname,
"Checksum": cs,
"GoextVersion": goext.GoextVersion,
"IDs": ids,
"AnyDef": anyDef,
})
if err != nil {
panic(err)
}
return buffer.String()
}
+56
View File
@@ -0,0 +1,56 @@
// Code generated by id-generate.go DO NOT EDIT.
package {{.PkgName}}
import "go.mongodb.org/mongo-driver/bson"
import "go.mongodb.org/mongo-driver/bson/bsontype"
import "go.mongodb.org/mongo-driver/bson/primitive"
import "git.blackforestbytes.com/BlackForestBytes/goext/exerr"
const ChecksumIDGenerator = "{{.Checksum}}" // GoExtVersion: {{.GoextVersion}}
{{range .IDs}}
// ================================ {{.Name}} ({{.FileRelative}}) ================================
func (i {{.Name}}) MarshalBSONValue() (bsontype.Type, []byte, error) {
if objId, err := primitive.ObjectIDFromHex(string(i)); err == nil {
return bson.MarshalValue(objId)
} else {
return 0, nil, exerr.New(exerr.TypeMarshalEntityID, "Failed to marshal {{.Name}}("+i.String()+") to ObjectId").Str("value", string(i)).Type("type", i).Build()
}
}
func (i {{.Name}}) String() string {
return string(i)
}
func (i {{.Name}}) ObjID() (primitive.ObjectID, error) {
return primitive.ObjectIDFromHex(string(i))
}
func (i {{.Name}}) Valid() bool {
_, err := primitive.ObjectIDFromHex(string(i))
return err == nil
}
{{if ne $.AnyDef nil}}
func (i {{.Name}}) AsAny() {{$.AnyDef.Name}} {
return {{$.AnyDef.Name}}(i)
}
func (i {{.Name}}) AsAnyPtr() *{{$.AnyDef.Name}} {
v := {{$.AnyDef.Name}}(i)
return &v
}
{{end}}
func (i {{.Name}}) IsZero() bool {
return i == ""
}
func New{{.Name}}() {{.Name}} {
return {{.Name}}(primitive.NewObjectID().Hex())
}
{{end}}
+52
View File
@@ -0,0 +1,52 @@
package bfcodegen
import (
_ "embed"
"fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/cmdext"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
"os"
"path/filepath"
"testing"
"time"
)
//go:embed _test_example_1.tgz
var IDExampleModels1 []byte
func TestGenerateIDSpecs(t *testing.T) {
tmpFile := filepath.Join(t.TempDir(), langext.MustHexUUID()+".tgz")
tmpDir := filepath.Join(t.TempDir(), langext.MustHexUUID())
err := os.WriteFile(tmpFile, IDExampleModels1, 0o777)
tst.AssertNoErr(t, err)
t.Cleanup(func() { _ = os.Remove(tmpFile) })
err = os.Mkdir(tmpDir, 0o777)
tst.AssertNoErr(t, err)
t.Cleanup(func() { _ = os.RemoveAll(tmpFile) })
_, err = cmdext.Runner("tar").Arg("-xvzf").Arg(tmpFile).Arg("-C").Arg(tmpDir).FailOnExitCode().FailOnTimeout().Timeout(time.Minute).Run()
tst.AssertNoErr(t, err)
err = GenerateIDSpecs(tmpDir, tmpDir+"/id_gen.go", IDGenOptions{DebugOutput: langext.PTrue})
tst.AssertNoErr(t, err)
err = GenerateIDSpecs(tmpDir, tmpDir+"/id_gen.go", IDGenOptions{DebugOutput: langext.PTrue})
tst.AssertNoErr(t, err)
fmt.Println()
fmt.Println()
fmt.Println()
fmt.Println("=====================================================================================================")
fmt.Println(string(tst.Must(os.ReadFile(tmpDir + "/id_gen.go"))(t)))
fmt.Println("=====================================================================================================")
fmt.Println()
fmt.Println()
fmt.Println()
}
+8 -1
View File
@@ -2,7 +2,7 @@ package cmdext
import ( import (
"fmt" "fmt"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"time" "time"
) )
@@ -14,6 +14,7 @@ type CommandRunner struct {
listener []CommandListener listener []CommandListener
enforceExitCodes *[]int enforceExitCodes *[]int
enforceNoTimeout bool enforceNoTimeout bool
enforceNoStderr bool
} }
func Runner(program string) *CommandRunner { func Runner(program string) *CommandRunner {
@@ -25,6 +26,7 @@ func Runner(program string) *CommandRunner {
listener: make([]CommandListener, 0), listener: make([]CommandListener, 0),
enforceExitCodes: nil, enforceExitCodes: nil,
enforceNoTimeout: false, enforceNoTimeout: false,
enforceNoStderr: false,
} }
} }
@@ -73,6 +75,11 @@ func (r *CommandRunner) FailOnTimeout() *CommandRunner {
return r return r
} }
func (r *CommandRunner) FailOnStderr() *CommandRunner {
r.enforceNoStderr = true
return r
}
func (r *CommandRunner) Listen(lstr CommandListener) *CommandRunner { func (r *CommandRunner) Listen(lstr CommandListener) *CommandRunner {
r.listener = append(r.listener, lstr) r.listener = append(r.listener, lstr)
return r return r
+47 -5
View File
@@ -2,15 +2,16 @@ package cmdext
import ( import (
"errors" "errors"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/mathext" "git.blackforestbytes.com/BlackForestBytes/goext/mathext"
"gogs.mikescher.com/BlackForestBytes/goext/syncext" "git.blackforestbytes.com/BlackForestBytes/goext/syncext"
"os/exec" "os/exec"
"time" "time"
) )
var ErrExitCode = errors.New("process exited with an unexpected exitcode") var ErrExitCode = errors.New("process exited with an unexpected exitcode")
var ErrTimeout = errors.New("process did not exit after the specified timeout") var ErrTimeout = errors.New("process did not exit after the specified timeout")
var ErrStderrPrint = errors.New("process did print to stderr stream")
type CommandResult struct { type CommandResult struct {
StdOut string StdOut string
@@ -53,12 +54,27 @@ func run(opt CommandRunner) (CommandResult, error) {
err error err error
} }
stderrFailChan := make(chan bool)
outputChan := make(chan resultObj) outputChan := make(chan resultObj)
go func() { go func() {
// we need to first fully read the pipes and then call Wait // we need to first fully read the pipes and then call Wait
// see https://pkg.go.dev/os/exec#Cmd.StdoutPipe // see https://pkg.go.dev/os/exec#Cmd.StdoutPipe
stdout, stderr, stdcombined, err := preader.Read(opt.listener) listener := make([]CommandListener, 0)
listener = append(listener, opt.listener...)
if opt.enforceNoStderr {
listener = append(listener, genericCommandListener{
_readRawStderr: langext.Ptr(func(v []byte) {
if len(v) > 0 {
stderrFailChan <- true
}
}),
})
}
stdout, stderr, stdcombined, err := preader.Read(listener)
if err != nil { if err != nil {
outputChan <- resultObj{stdout, stderr, stdcombined, err} outputChan <- resultObj{stdout, stderr, stdcombined, err}
_ = cmd.Process.Kill() _ = cmd.Process.Kill()
@@ -115,8 +131,34 @@ func run(opt CommandRunner) (CommandResult, error) {
return res, nil return res, nil
} }
case <-stderrFailChan:
_ = cmd.Process.Kill()
if fallback, ok := syncext.ReadChannelWithTimeout(outputChan, 32*time.Millisecond); ok {
// most of the time the cmd.Process.Kill() should also have finished the pipereader
// and we can at least return the already collected stdout, stderr, etc
res := CommandResult{
StdOut: fallback.stdout,
StdErr: fallback.stderr,
StdCombined: fallback.stdcombined,
ExitCode: -1,
CommandTimedOut: false,
}
return res, ErrStderrPrint
} else {
res := CommandResult{
StdOut: "",
StdErr: "",
StdCombined: "",
ExitCode: -1,
CommandTimedOut: false,
}
return res, ErrStderrPrint
}
case outobj := <-outputChan: case outobj := <-outputChan:
if exiterr, ok := outobj.err.(*exec.ExitError); ok { var exiterr *exec.ExitError
if errors.As(outobj.err, &exiterr) {
excode := exiterr.ExitCode() excode := exiterr.ExitCode()
for _, lstr := range opt.listener { for _, lstr := range opt.listener {
lstr.Finished(excode) lstr.Finished(excode)
+37 -12
View File
@@ -1,6 +1,7 @@
package cmdext package cmdext
import ( import (
"errors"
"fmt" "fmt"
"testing" "testing"
"time" "time"
@@ -32,7 +33,7 @@ func TestStdout(t *testing.T) {
func TestStderr(t *testing.T) { func TestStderr(t *testing.T) {
res1, err := Runner("python").Arg("-c").Arg("import sys; print(\"error\", file=sys.stderr, end='')").Run() res1, err := Runner("python3").Arg("-c").Arg("import sys; print(\"error\", file=sys.stderr, end='')").Run()
if err != nil { if err != nil {
t.Errorf("%v", err) t.Errorf("%v", err)
} }
@@ -55,7 +56,7 @@ func TestStderr(t *testing.T) {
} }
func TestStdcombined(t *testing.T) { func TestStdcombined(t *testing.T) {
res1, err := Runner("python"). res1, err := Runner("python3").
Arg("-c"). Arg("-c").
Arg("import sys; import time; print(\"1\", file=sys.stderr, flush=True); time.sleep(0.1); print(\"2\", file=sys.stdout, flush=True); time.sleep(0.1); print(\"3\", file=sys.stderr, flush=True)"). Arg("import sys; import time; print(\"1\", file=sys.stderr, flush=True); time.sleep(0.1); print(\"2\", file=sys.stdout, flush=True); time.sleep(0.1); print(\"3\", file=sys.stderr, flush=True)").
Run() Run()
@@ -81,7 +82,7 @@ func TestStdcombined(t *testing.T) {
} }
func TestPartialRead(t *testing.T) { func TestPartialRead(t *testing.T) {
res1, err := Runner("python"). res1, err := Runner("python3").
Arg("-c"). Arg("-c").
Arg("import sys; import time; print(\"first message\", flush=True); time.sleep(5); print(\"cant see me\", flush=True);"). Arg("import sys; import time; print(\"first message\", flush=True); time.sleep(5); print(\"cant see me\", flush=True);").
Timeout(100 * time.Millisecond). Timeout(100 * time.Millisecond).
@@ -105,7 +106,7 @@ func TestPartialRead(t *testing.T) {
} }
func TestPartialReadStderr(t *testing.T) { func TestPartialReadStderr(t *testing.T) {
res1, err := Runner("python"). res1, err := Runner("python3").
Arg("-c"). Arg("-c").
Arg("import sys; import time; print(\"first message\", file=sys.stderr, flush=True); time.sleep(5); print(\"cant see me\", file=sys.stderr, flush=True);"). Arg("import sys; import time; print(\"first message\", file=sys.stderr, flush=True); time.sleep(5); print(\"cant see me\", file=sys.stderr, flush=True);").
Timeout(100 * time.Millisecond). Timeout(100 * time.Millisecond).
@@ -130,7 +131,7 @@ func TestPartialReadStderr(t *testing.T) {
func TestReadUnflushedStdout(t *testing.T) { func TestReadUnflushedStdout(t *testing.T) {
res1, err := Runner("python").Arg("-c").Arg("import sys; print(\"message101\", file=sys.stdout, end='')").Run() res1, err := Runner("python3").Arg("-c").Arg("import sys; print(\"message101\", file=sys.stdout, end='')").Run()
if err != nil { if err != nil {
t.Errorf("%v", err) t.Errorf("%v", err)
} }
@@ -154,7 +155,7 @@ func TestReadUnflushedStdout(t *testing.T) {
func TestReadUnflushedStderr(t *testing.T) { func TestReadUnflushedStderr(t *testing.T) {
res1, err := Runner("python").Arg("-c").Arg("import sys; print(\"message101\", file=sys.stderr, end='')").Run() res1, err := Runner("python3").Arg("-c").Arg("import sys; print(\"message101\", file=sys.stderr, end='')").Run()
if err != nil { if err != nil {
t.Errorf("%v", err) t.Errorf("%v", err)
} }
@@ -179,7 +180,7 @@ func TestReadUnflushedStderr(t *testing.T) {
func TestPartialReadUnflushed(t *testing.T) { func TestPartialReadUnflushed(t *testing.T) {
t.SkipNow() t.SkipNow()
res1, err := Runner("python"). res1, err := Runner("python3").
Arg("-c"). Arg("-c").
Arg("import sys; import time; print(\"first message\", end=''); time.sleep(5); print(\"cant see me\", end='');"). Arg("import sys; import time; print(\"first message\", end=''); time.sleep(5); print(\"cant see me\", end='');").
Timeout(100 * time.Millisecond). Timeout(100 * time.Millisecond).
@@ -205,7 +206,7 @@ func TestPartialReadUnflushed(t *testing.T) {
func TestPartialReadUnflushedStderr(t *testing.T) { func TestPartialReadUnflushedStderr(t *testing.T) {
t.SkipNow() t.SkipNow()
res1, err := Runner("python"). res1, err := Runner("python3").
Arg("-c"). Arg("-c").
Arg("import sys; import time; print(\"first message\", file=sys.stderr, end=''); time.sleep(5); print(\"cant see me\", file=sys.stderr, end='');"). Arg("import sys; import time; print(\"first message\", file=sys.stderr, end=''); time.sleep(5); print(\"cant see me\", file=sys.stderr, end='');").
Timeout(100 * time.Millisecond). Timeout(100 * time.Millisecond).
@@ -230,7 +231,7 @@ func TestPartialReadUnflushedStderr(t *testing.T) {
func TestListener(t *testing.T) { func TestListener(t *testing.T) {
res1, err := Runner("python"). res1, err := Runner("python3").
Arg("-c"). Arg("-c").
Arg("import sys;" + Arg("import sys;" +
"import time;" + "import time;" +
@@ -263,7 +264,7 @@ func TestListener(t *testing.T) {
func TestLongStdout(t *testing.T) { func TestLongStdout(t *testing.T) {
res1, err := Runner("python"). res1, err := Runner("python3").
Arg("-c"). Arg("-c").
Arg("import sys; import time; print(\"X\" * 125001 + \"\\n\"); print(\"Y\" * 125001 + \"\\n\"); print(\"Z\" * 125001 + \"\\n\");"). Arg("import sys; import time; print(\"X\" * 125001 + \"\\n\"); print(\"Y\" * 125001 + \"\\n\"); print(\"Z\" * 125001 + \"\\n\");").
Timeout(5000 * time.Millisecond). Timeout(5000 * time.Millisecond).
@@ -289,16 +290,40 @@ func TestLongStdout(t *testing.T) {
func TestFailOnTimeout(t *testing.T) { func TestFailOnTimeout(t *testing.T) {
_, err := Runner("sleep").Arg("2").Timeout(200 * time.Millisecond).FailOnTimeout().Run() _, err := Runner("sleep").Arg("2").Timeout(200 * time.Millisecond).FailOnTimeout().Run()
if err != ErrTimeout { if !errors.Is(err, ErrTimeout) {
t.Errorf("wrong err := %v", err) t.Errorf("wrong err := %v", err)
} }
} }
func TestFailOnStderr(t *testing.T) {
res1, err := Runner("python3").Arg("-c").Arg("import sys; print(\"error\", file=sys.stderr, end='')").FailOnStderr().Run()
if err == nil {
t.Errorf("no err")
}
if res1.CommandTimedOut {
t.Errorf("Timeout")
}
if res1.ExitCode != -1 {
t.Errorf("res1.ExitCode == %v", res1.ExitCode)
}
if res1.StdErr != "error" {
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
}
if res1.StdOut != "" {
t.Errorf("res1.StdOut == '%v'", res1.StdOut)
}
if res1.StdCombined != "error\n" {
t.Errorf("res1.StdCombined == '%v'", res1.StdCombined)
}
}
func TestFailOnExitcode(t *testing.T) { func TestFailOnExitcode(t *testing.T) {
_, err := Runner("false").Timeout(200 * time.Millisecond).FailOnExitCode().Run() _, err := Runner("false").Timeout(200 * time.Millisecond).FailOnExitCode().Run()
if err != ErrExitCode { if !errors.Is(err, ErrExitCode) {
t.Errorf("wrong err := %v", err) t.Errorf("wrong err := %v", err)
} }
+7 -7
View File
@@ -2,7 +2,7 @@ package cmdext
import ( import (
"bufio" "bufio"
"gogs.mikescher.com/BlackForestBytes/goext/syncext" "git.blackforestbytes.com/BlackForestBytes/goext/syncext"
"io" "io"
"sync" "sync"
) )
@@ -32,8 +32,8 @@ func (pr *pipeReader) Read(listener []CommandListener) (string, string, string,
stdout := "" stdout := ""
go func() { go func() {
buf := make([]byte, 128) buf := make([]byte, 128)
for true { for {
n, out := pr.stdout.Read(buf) n, err := pr.stdout.Read(buf)
if n > 0 { if n > 0 {
txt := string(buf[:n]) txt := string(buf[:n])
stdout += txt stdout += txt
@@ -42,11 +42,11 @@ func (pr *pipeReader) Read(listener []CommandListener) (string, string, string,
lstr.ReadRawStdout(buf[:n]) lstr.ReadRawStdout(buf[:n])
} }
} }
if out == io.EOF { if err == io.EOF {
break break
} }
if out != nil { if err != nil {
errch <- out errch <- err
break break
} }
} }
@@ -61,7 +61,7 @@ func (pr *pipeReader) Read(listener []CommandListener) (string, string, string,
stderr := "" stderr := ""
go func() { go func() {
buf := make([]byte, 128) buf := make([]byte, 128)
for true { for {
n, err := pr.stderr.Read(buf) n, err := pr.stderr.Read(buf)
if n > 0 { if n > 0 {
+6 -9
View File
@@ -3,7 +3,7 @@ package confext
import ( import (
"errors" "errors"
"fmt" "fmt"
"gogs.mikescher.com/BlackForestBytes/goext/timeext" "git.blackforestbytes.com/BlackForestBytes/goext/timeext"
"math/bits" "math/bits"
"os" "os"
"reflect" "reflect"
@@ -41,12 +41,12 @@ func processEnvOverrides(rval reflect.Value, delim string, prefix string) error
continue continue
} }
if rvfield.Kind() == reflect.Struct { envkey, found := rsfield.Tag.Lookup("env")
if !found || envkey == "-" {
continue
}
envkey, found := rsfield.Tag.Lookup("env") if rvfield.Kind() == reflect.Struct && rvfield.Type() != reflect.TypeOf(time.UnixMilli(0)) {
if !found || envkey == "-" {
continue
}
subPrefix := prefix subPrefix := prefix
if envkey != "" { if envkey != "" {
@@ -57,10 +57,7 @@ func processEnvOverrides(rval reflect.Value, delim string, prefix string) error
if err != nil { if err != nil {
return err return err
} }
}
envkey := rsfield.Tag.Get("env")
if envkey == "" || envkey == "-" {
continue continue
} }
+2 -2
View File
@@ -1,8 +1,8 @@
package confext package confext
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/timeext" "git.blackforestbytes.com/BlackForestBytes/goext/timeext"
"gogs.mikescher.com/BlackForestBytes/goext/tst" "git.blackforestbytes.com/BlackForestBytes/goext/tst"
"testing" "testing"
"time" "time"
) )
+1 -1
View File
@@ -2,7 +2,7 @@ package cryptext
import ( import (
"fmt" "fmt"
"gogs.mikescher.com/BlackForestBytes/goext/tst" "git.blackforestbytes.com/BlackForestBytes/goext/tst"
"testing" "testing"
) )
+1 -1
View File
@@ -1,7 +1,7 @@
package cryptext package cryptext
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/tst" "git.blackforestbytes.com/BlackForestBytes/goext/tst"
"testing" "testing"
) )
+12 -4
View File
@@ -6,13 +6,15 @@ import (
"crypto/sha512" "crypto/sha512"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/totpext"
"golang.org/x/crypto/bcrypt"
"strconv" "strconv"
"strings" "strings"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/totpext"
"golang.org/x/crypto/bcrypt"
) )
const LatestPassHashVersion = 5 const LatestPassHashVersion = 5
@@ -66,7 +68,6 @@ func (ph PassHash) Data() (_version int, _seed []byte, _payload []byte, _totp bo
return int(version), nil, payload, false, nil, true return int(version), nil, payload, false, nil, true
} }
//
if version == 2 { if version == 2 {
if len(split) != 3 { if len(split) != 3 {
return -1, nil, nil, false, nil, false return -1, nil, nil, false, nil, false
@@ -318,6 +319,13 @@ func (ph PassHash) String() string {
return string(ph) return string(ph)
} }
func (ph PassHash) MarshalJSON() ([]byte, error) {
if ph == "" {
return json.Marshal("")
}
return json.Marshal("*****")
}
func HashPassword(plainpass string, totpSecret []byte) (PassHash, error) { func HashPassword(plainpass string, totpSecret []byte) (PassHash, error) {
return HashPasswordV5(plainpass, totpSecret) return HashPasswordV5(plainpass, totpSecret)
} }
+3 -3
View File
@@ -1,9 +1,9 @@
package cryptext package cryptext
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/totpext" "git.blackforestbytes.com/BlackForestBytes/goext/totpext"
"gogs.mikescher.com/BlackForestBytes/goext/tst" "git.blackforestbytes.com/BlackForestBytes/goext/tst"
"testing" "testing"
) )
+263
View File
@@ -0,0 +1,263 @@
package cryptext
import (
"crypto/rand"
"io"
"math/big"
mathrand "math/rand"
"strings"
)
const (
ppStartChar = "BCDFGHJKLMNPQRSTVWXZ"
ppEndChar = "ABDEFIKMNORSTUXYZ"
ppVowel = "AEIOUY"
ppConsonant = "BCDFGHJKLMNPQRSTVWXZ"
ppSegmentLenMin = 3
ppSegmentLenMax = 7
ppMaxRepeatedVowel = 2
ppMaxRepeatedConsonant = 2
)
var ppContinuation = map[uint8]string{
'A': "BCDFGHJKLMNPRSTVWXYZ",
'B': "ADFIKLMNORSTUY",
'C': "AEIKOUY",
'D': "AEILORSUYZ",
'E': "BCDFGHJKLMNPRSTVWXYZ",
'F': "ADEGIKLOPRTUY",
'G': "ABDEFHILMNORSTUY",
'H': "AEIOUY",
'I': "BCDFGHJKLMNPRSTVWXZ",
'J': "AEIOUY",
'K': "ADEFHILMNORSTUY",
'L': "ADEFGIJKMNOPSTUVWYZ",
'M': "ABEFIKOPSTUY",
'N': "ABEFIKOPSTUY",
'O': "BCDFGHJKLMNPRSTVWXYZ",
'P': "AEFIJLORSTUY",
'Q': "AEIOUY",
'R': "ADEFGHIJKLMNOPSTUVYZ",
'S': "ACDEIKLOPTUYZ",
'T': "AEHIJOPRSUWY",
'U': "BCDFGHJKLMNPRSTVWXZ",
'V': "AEIOUY",
'W': "AEIOUY",
'X': "AEIOUY",
'Y': "ABCDFGHKLMNPRSTVXZ",
'Z': "AEILOTUY",
}
var ppLog2Map = map[int]float64{
1: 0.00000000,
2: 1.00000000,
3: 1.58496250,
4: 2.00000000,
5: 2.32192809,
6: 2.58496250,
7: 2.80735492,
8: 3.00000000,
9: 3.16992500,
10: 3.32192809,
11: 3.45943162,
12: 3.58496250,
13: 3.70043972,
14: 3.80735492,
15: 3.90689060,
16: 4.00000000,
17: 4.08746284,
18: 4.16992500,
19: 4.24792751,
20: 4.32192809,
21: 4.39231742,
22: 4.45943162,
23: 4.52356196,
24: 4.58496250,
25: 4.64385619,
26: 4.70043972,
27: 4.75488750,
28: 4.80735492,
29: 4.85798100,
30: 4.90689060,
31: 4.95419631,
32: 5.00000000,
}
var (
ppVowelMap = ppMakeSet(ppVowel)
ppConsonantMap = ppMakeSet(ppConsonant)
ppEndCharMap = ppMakeSet(ppEndChar)
)
func ppMakeSet(v string) map[uint8]bool {
mp := make(map[uint8]bool, len(v))
for _, chr := range v {
mp[uint8(chr)] = true
}
return mp
}
func ppRandInt(rng io.Reader, max int) int {
v, err := rand.Int(rng, big.NewInt(int64(max)))
if err != nil {
panic(err)
}
return int(v.Int64())
}
func ppRand(rng io.Reader, chars string, entropy *float64) uint8 {
chr := chars[ppRandInt(rng, len(chars))]
*entropy = *entropy + ppLog2Map[len(chars)]
return chr
}
func ppCharType(chr uint8) (bool, bool) {
_, ok1 := ppVowelMap[chr]
_, ok2 := ppConsonantMap[chr]
return ok1, ok2
}
func ppCharsetRemove(cs string, set map[uint8]bool, allowEmpty bool) string {
result := ""
for _, chr := range cs {
if _, ok := set[uint8(chr)]; !ok {
result += string(chr)
}
}
if result == "" && !allowEmpty {
return cs
}
return result
}
func ppCharsetFilter(cs string, set map[uint8]bool, allowEmpty bool) string {
result := ""
for _, chr := range cs {
if _, ok := set[uint8(chr)]; ok {
result += string(chr)
}
}
if result == "" && !allowEmpty {
return cs
}
return result
}
func PronouncablePasswordExt(rng io.Reader, pwlen int) (string, float64) {
// kinda pseudo markov-chain - with a few extra rules and no weights...
if pwlen <= 0 {
return "", 0
}
vowelCount := 0
consoCount := 0
entropy := float64(0)
startChar := ppRand(rng, ppStartChar, &entropy)
result := string(startChar)
currentChar := startChar
isVowel, isConsonant := ppCharType(currentChar)
if isVowel {
vowelCount = 1
}
if isConsonant {
consoCount = ppMaxRepeatedConsonant
}
segmentLen := 1
segmentLenTarget := ppSegmentLenMin + ppRandInt(rng, ppSegmentLenMax-ppSegmentLenMin)
for len(result) < pwlen {
charset := ppContinuation[currentChar]
if vowelCount >= ppMaxRepeatedVowel {
charset = ppCharsetRemove(charset, ppVowelMap, false)
}
if consoCount >= ppMaxRepeatedConsonant {
charset = ppCharsetRemove(charset, ppConsonantMap, false)
}
lastOfSegment := false
newSegment := false
if len(result)+1 == pwlen {
// last of result
charset = ppCharsetFilter(charset, ppEndCharMap, false)
} else if segmentLen+1 == segmentLenTarget {
// last of segment
charsetNew := ppCharsetFilter(charset, ppEndCharMap, true)
if charsetNew != "" {
charset = charsetNew
lastOfSegment = true
}
} else if segmentLen >= segmentLenTarget {
// (perhaps) start of new segment
if _, ok := ppEndCharMap[currentChar]; ok {
charset = ppStartChar
newSegment = true
} else {
// continue segment for one more char to (hopefully) find an end-char
charsetNew := ppCharsetFilter(charset, ppEndCharMap, true)
if charsetNew != "" {
charset = charsetNew
lastOfSegment = true
}
}
} else {
// normal continuation
}
newChar := ppRand(rng, charset, &entropy)
if lastOfSegment {
currentChar = newChar
segmentLen++
result += strings.ToLower(string(newChar))
} else if newSegment {
currentChar = newChar
segmentLen = 1
result += strings.ToUpper(string(newChar))
segmentLenTarget = ppSegmentLenMin + ppRandInt(rng, ppSegmentLenMax-ppSegmentLenMin)
vowelCount = 0
consoCount = 0
} else {
currentChar = newChar
segmentLen++
result += strings.ToLower(string(newChar))
}
isVowel, isConsonant := ppCharType(currentChar)
if isVowel {
vowelCount++
consoCount = 0
}
if isConsonant {
vowelCount = 0
if newSegment {
consoCount = ppMaxRepeatedConsonant
} else {
consoCount++
}
}
}
return result, entropy
}
func PronouncablePassword(len int) string {
v, _ := PronouncablePasswordExt(rand.Reader, len)
return v
}
func PronouncablePasswordSeeded(seed int64, len int) string {
v, _ := PronouncablePasswordExt(mathrand.New(mathrand.NewSource(seed)), len)
return v
}
+35
View File
@@ -0,0 +1,35 @@
package cryptext
import (
"fmt"
"math/rand"
"testing"
)
func TestPronouncablePasswordExt(t *testing.T) {
for i := 0; i < 20; i++ {
pw, entropy := PronouncablePasswordExt(rand.New(rand.NewSource(int64(i))), 16)
fmt.Printf("[%.2f] => %s\n", entropy, pw)
}
}
func TestPronouncablePasswordSeeded(t *testing.T) {
for i := 0; i < 20; i++ {
pw := PronouncablePasswordSeeded(int64(i), 8)
fmt.Printf("%s\n", pw)
}
}
func TestPronouncablePassword(t *testing.T) {
for i := 0; i < 20; i++ {
pw := PronouncablePassword(i + 1)
fmt.Printf("%s\n", pw)
}
}
func TestPronouncablePasswordWrongLen(t *testing.T) {
PronouncablePassword(0)
PronouncablePassword(-1)
PronouncablePassword(-2)
PronouncablePassword(-3)
}
+27
View File
@@ -0,0 +1,27 @@
package ctxext
import "context"
func Value[T any](ctx context.Context, key any) (T, bool) {
v := ctx.Value(key)
if v == nil {
return *new(T), false
}
if tv, ok := v.(T); !ok {
return *new(T), false
} else {
return tv, true
}
}
func ValueOrDefault[T any](ctx context.Context, key any, def T) T {
v := ctx.Value(key)
if v == nil {
return def
}
if tv, ok := v.(T); !ok {
return def
} else {
return tv
}
}
+10
View File
@@ -6,3 +6,13 @@ const (
SortASC SortDirection = "ASC" SortASC SortDirection = "ASC"
SortDESC SortDirection = "DESC" SortDESC SortDirection = "DESC"
) )
func (sd SortDirection) ToMongo() int {
if sd == SortASC {
return 1
} else if sd == SortDESC {
return -1
} else {
return 0
}
}
+8 -3
View File
@@ -1,10 +1,15 @@
package cursortoken package cursortoken
import ( import (
"context"
"go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo"
) )
type Filter interface { type RawFilter interface {
FilterQuery() mongo.Pipeline FilterQuery(ctx context.Context) mongo.Pipeline
Pagination() (string, SortDirection, string, SortDirection) }
type Filter interface {
FilterQuery(ctx context.Context) mongo.Pipeline
Pagination(ctx context.Context) (string, SortDirection, string, SortDirection)
} }
+50 -139
View File
@@ -3,12 +3,18 @@ package cursortoken
import ( import (
"encoding/base32" "encoding/base32"
"encoding/json" "encoding/json"
"errors" "git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"go.mongodb.org/mongo-driver/bson/primitive" "strconv"
"strings" "strings"
"time" "time"
) )
type CursorToken interface {
Token() string
IsStart() bool
IsEnd() bool
}
type Mode string type Mode string
const ( const (
@@ -24,97 +30,6 @@ type Extra struct {
PageSize *int PageSize *int
} }
type CursorToken struct {
Mode Mode
ValuePrimary string
ValueSecondary string
Direction SortDirection
DirectionSecondary SortDirection
PageSize int
Extra Extra
}
type cursorTokenSerialize struct {
ValuePrimary *string `json:"v1,omitempty"`
ValueSecondary *string `json:"v2,omitempty"`
Direction *SortDirection `json:"dir,omitempty"`
DirectionSecondary *SortDirection `json:"dir2,omitempty"`
PageSize *int `json:"size,omitempty"`
ExtraTimestamp *time.Time `json:"ts,omitempty"`
ExtraId *string `json:"id,omitempty"`
ExtraPage *int `json:"pg,omitempty"`
ExtraPageSize *int `json:"sz,omitempty"`
}
func Start() CursorToken {
return CursorToken{
Mode: CTMStart,
ValuePrimary: "",
ValueSecondary: "",
Direction: "",
DirectionSecondary: "",
PageSize: 0,
Extra: Extra{},
}
}
func End() CursorToken {
return CursorToken{
Mode: CTMEnd,
ValuePrimary: "",
ValueSecondary: "",
Direction: "",
DirectionSecondary: "",
PageSize: 0,
Extra: Extra{},
}
}
func (c *CursorToken) Token() string {
if c.Mode == CTMStart {
return "@start"
}
if c.Mode == CTMEnd {
return "@end"
}
// We kinda manually implement omitempty for the CursorToken here
// because omitempty does not work for time.Time and otherwise we would always
// get weird time values when decoding a token that initially didn't have an Timestamp set
// For this usecase we treat Unix=0 as an empty timestamp
sertok := cursorTokenSerialize{}
if c.ValuePrimary != "" {
sertok.ValuePrimary = &c.ValuePrimary
}
if c.ValueSecondary != "" {
sertok.ValueSecondary = &c.ValueSecondary
}
if c.Direction != "" {
sertok.Direction = &c.Direction
}
if c.DirectionSecondary != "" {
sertok.DirectionSecondary = &c.DirectionSecondary
}
if c.PageSize != 0 {
sertok.PageSize = &c.PageSize
}
sertok.ExtraTimestamp = c.Extra.Timestamp
sertok.ExtraId = c.Extra.Id
sertok.ExtraPage = c.Extra.Page
sertok.ExtraPageSize = c.Extra.PageSize
body, err := json.Marshal(sertok)
if err != nil {
panic(err)
}
return "tok_" + base32.StdEncoding.EncodeToString(body)
}
func Decode(tok string) (CursorToken, error) { func Decode(tok string) (CursorToken, error) {
if tok == "" { if tok == "" {
return Start(), nil return Start(), nil
@@ -125,60 +40,56 @@ func Decode(tok string) (CursorToken, error) {
if strings.ToLower(tok) == "@end" { if strings.ToLower(tok) == "@end" {
return End(), nil return End(), nil
} }
if strings.ToLower(tok) == "$end" {
if !strings.HasPrefix(tok, "tok_") { return PageEnd(), nil
return CursorToken{}, errors.New("could not decode token, missing prefix") }
if strings.HasPrefix(tok, "$") && len(tok) > 1 {
n, err := strconv.ParseInt(tok[1:], 10, 64)
if err != nil {
return nil, exerr.Wrap(err, "failed to deserialize token").Str("token", tok).WithType(exerr.TypeCursorTokenDecode).Build()
}
return Page(int(n)), nil
} }
body, err := base32.StdEncoding.DecodeString(tok[len("tok_"):]) if strings.HasPrefix(tok, "tok_") {
if err != nil {
return CursorToken{}, err
}
var tokenDeserialize cursorTokenSerialize body, err := base32.StdEncoding.DecodeString(tok[len("tok_"):])
err = json.Unmarshal(body, &tokenDeserialize) if err != nil {
if err != nil { return nil, err
return CursorToken{}, err }
}
token := CursorToken{Mode: CTMNormal} var tokenDeserialize cursorTokenKeySortSerialize
err = json.Unmarshal(body, &tokenDeserialize)
if err != nil {
return nil, exerr.Wrap(err, "failed to deserialize token").Str("token", tok).WithType(exerr.TypeCursorTokenDecode).Build()
}
if tokenDeserialize.ValuePrimary != nil { token := CTKeySort{Mode: CTMNormal}
token.ValuePrimary = *tokenDeserialize.ValuePrimary
}
if tokenDeserialize.ValueSecondary != nil {
token.ValueSecondary = *tokenDeserialize.ValueSecondary
}
if tokenDeserialize.Direction != nil {
token.Direction = *tokenDeserialize.Direction
}
if tokenDeserialize.DirectionSecondary != nil {
token.DirectionSecondary = *tokenDeserialize.DirectionSecondary
}
if tokenDeserialize.PageSize != nil {
token.PageSize = *tokenDeserialize.PageSize
}
token.Extra.Timestamp = tokenDeserialize.ExtraTimestamp if tokenDeserialize.ValuePrimary != nil {
token.Extra.Id = tokenDeserialize.ExtraId token.ValuePrimary = *tokenDeserialize.ValuePrimary
token.Extra.Page = tokenDeserialize.ExtraPage }
token.Extra.PageSize = tokenDeserialize.ExtraPageSize if tokenDeserialize.ValueSecondary != nil {
token.ValueSecondary = *tokenDeserialize.ValueSecondary
}
if tokenDeserialize.Direction != nil {
token.Direction = *tokenDeserialize.Direction
}
if tokenDeserialize.DirectionSecondary != nil {
token.DirectionSecondary = *tokenDeserialize.DirectionSecondary
}
if tokenDeserialize.PageSize != nil {
token.PageSize = *tokenDeserialize.PageSize
}
return token, nil token.Extra.Timestamp = tokenDeserialize.ExtraTimestamp
} token.Extra.Id = tokenDeserialize.ExtraId
token.Extra.Page = tokenDeserialize.ExtraPage
token.Extra.PageSize = tokenDeserialize.ExtraPageSize
return token, nil
func (c *CursorToken) ValuePrimaryObjectId() (primitive.ObjectID, bool) {
if oid, err := primitive.ObjectIDFromHex(c.ValuePrimary); err == nil {
return oid, true
} else { } else {
return primitive.ObjectID{}, false return nil, exerr.New(exerr.TypeCursorTokenDecode, "could not decode token, missing/unknown prefix").Str("token", tok).Build()
}
}
func (c *CursorToken) ValueSecondaryObjectId() (primitive.ObjectID, bool) {
if oid, err := primitive.ObjectIDFromHex(c.ValueSecondary); err == nil {
return oid, true
} else {
return primitive.ObjectID{}, false
} }
} }
+136
View File
@@ -0,0 +1,136 @@
package cursortoken
import (
"encoding/base32"
"encoding/json"
"go.mongodb.org/mongo-driver/bson/primitive"
"time"
)
type CTKeySort struct {
Mode Mode
ValuePrimary string
ValueSecondary string
Direction SortDirection
DirectionSecondary SortDirection
PageSize int
Extra Extra
}
type cursorTokenKeySortSerialize struct {
ValuePrimary *string `json:"v1,omitempty"`
ValueSecondary *string `json:"v2,omitempty"`
Direction *SortDirection `json:"dir,omitempty"`
DirectionSecondary *SortDirection `json:"dir2,omitempty"`
PageSize *int `json:"size,omitempty"`
ExtraTimestamp *time.Time `json:"ts,omitempty"`
ExtraId *string `json:"id,omitempty"`
ExtraPage *int `json:"pg,omitempty"`
ExtraPageSize *int `json:"sz,omitempty"`
}
func NewKeySortToken(valuePrimary string, valueSecondary string, direction SortDirection, directionSecondary SortDirection, pageSize int, extra Extra) CursorToken {
return CTKeySort{
Mode: CTMNormal,
ValuePrimary: valuePrimary,
ValueSecondary: valueSecondary,
Direction: direction,
DirectionSecondary: directionSecondary,
PageSize: pageSize,
Extra: extra,
}
}
func Start() CursorToken {
return CTKeySort{
Mode: CTMStart,
ValuePrimary: "",
ValueSecondary: "",
Direction: "",
DirectionSecondary: "",
PageSize: 0,
Extra: Extra{},
}
}
func End() CursorToken {
return CTKeySort{
Mode: CTMEnd,
ValuePrimary: "",
ValueSecondary: "",
Direction: "",
DirectionSecondary: "",
PageSize: 0,
Extra: Extra{},
}
}
func (c CTKeySort) Token() string {
if c.Mode == CTMStart {
return "@start"
}
if c.Mode == CTMEnd {
return "@end"
}
// We kinda manually implement omitempty for the CursorToken here
// because omitempty does not work for time.Time and otherwise we would always
// get weird time values when decoding a token that initially didn't have an Timestamp set
// For this usecase we treat Unix=0 as an empty timestamp
sertok := cursorTokenKeySortSerialize{}
if c.ValuePrimary != "" {
sertok.ValuePrimary = &c.ValuePrimary
}
if c.ValueSecondary != "" {
sertok.ValueSecondary = &c.ValueSecondary
}
if c.Direction != "" {
sertok.Direction = &c.Direction
}
if c.DirectionSecondary != "" {
sertok.DirectionSecondary = &c.DirectionSecondary
}
if c.PageSize != 0 {
sertok.PageSize = &c.PageSize
}
sertok.ExtraTimestamp = c.Extra.Timestamp
sertok.ExtraId = c.Extra.Id
sertok.ExtraPage = c.Extra.Page
sertok.ExtraPageSize = c.Extra.PageSize
body, err := json.Marshal(sertok)
if err != nil {
panic(err)
}
return "tok_" + base32.StdEncoding.EncodeToString(body)
}
func (c CTKeySort) IsEnd() bool {
return c.Mode == CTMEnd
}
func (c CTKeySort) IsStart() bool {
return c.Mode == CTMStart
}
func (c CTKeySort) valuePrimaryObjectId() (primitive.ObjectID, bool) {
if oid, err := primitive.ObjectIDFromHex(c.ValuePrimary); err == nil {
return oid, true
} else {
return primitive.ObjectID{}, false
}
}
func (c CTKeySort) valueSecondaryObjectId() (primitive.ObjectID, bool) {
if oid, err := primitive.ObjectIDFromHex(c.ValueSecondary); err == nil {
return oid, true
} else {
return primitive.ObjectID{}, false
}
}
+41
View File
@@ -0,0 +1,41 @@
package cursortoken
import "strconv"
type CTPaginated struct {
Mode Mode
Page int
}
func Page(p int) CursorToken {
return CTPaginated{
Mode: CTMNormal,
Page: p,
}
}
func PageEnd() CursorToken {
return CTPaginated{
Mode: CTMEnd,
Page: 0,
}
}
func (c CTPaginated) Token() string {
if c.Mode == CTMStart {
return "$1"
}
if c.Mode == CTMEnd {
return "$end"
}
return "$" + strconv.Itoa(c.Page)
}
func (c CTPaginated) IsEnd() bool {
return c.Mode == CTMEnd
}
func (c CTPaginated) IsStart() bool {
return c.Mode == CTMStart || c.Page == 1
}
+230
View File
@@ -0,0 +1,230 @@
package dataext
import (
"context"
"iter"
"sync"
"time"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/syncext"
"github.com/rs/xid"
)
// Broadcaster is a simple Broadcaster channel
// This is a simpler interface over Broadcaster - which does not have distinct namespaces
type Broadcaster[TData any] struct {
masterLock *sync.Mutex
subscriptions []*broadcastSubscription[TData]
}
type BroadcastSubscription interface {
Unsubscribe()
}
type broadcastSubscription[TData any] struct {
ID string
parent *Broadcaster[TData]
subLock *sync.Mutex
Func func(TData)
Chan chan TData
UnsubChan chan bool
}
func (p *broadcastSubscription[TData]) Unsubscribe() {
p.parent.unsubscribe(p)
}
func NewBroadcaster[TData any](capacity int) *Broadcaster[TData] {
return &Broadcaster[TData]{
masterLock: &sync.Mutex{},
subscriptions: make([]*broadcastSubscription[TData], 0, capacity),
}
}
func (bb *Broadcaster[TData]) SubscriberCount() int {
bb.masterLock.Lock()
defer bb.masterLock.Unlock()
return len(bb.subscriptions)
}
// Publish sends `data` to all subscriber
// But unbuffered - if one is currently not listening, we skip (the actualReceiver < subscriber)
func (bb *Broadcaster[TData]) Publish(data TData) (subscriber int, actualReceiver int) {
bb.masterLock.Lock()
subs := langext.ArrCopy(bb.subscriptions)
bb.masterLock.Unlock()
subscriber = len(subs)
actualReceiver = 0
for _, sub := range subs {
func() {
sub.subLock.Lock()
defer sub.subLock.Unlock()
if sub.Func != nil {
go func() { sub.Func(data) }()
actualReceiver++
} else if sub.Chan != nil {
msgSent := syncext.WriteNonBlocking(sub.Chan, data)
if msgSent {
actualReceiver++
}
}
}()
}
return subscriber, actualReceiver
}
// PublishWithContext sends `data` to all subscriber
// buffered - if one is currently not listening, we wait (but error out when the context runs out)
func (bb *Broadcaster[TData]) PublishWithContext(ctx context.Context, data TData) (subscriber int, actualReceiver int, err error) {
bb.masterLock.Lock()
subs := langext.ArrCopy(bb.subscriptions)
bb.masterLock.Unlock()
subscriber = len(subs)
actualReceiver = 0
for _, sub := range subs {
err := func() error {
sub.subLock.Lock()
defer sub.subLock.Unlock()
if err := ctx.Err(); err != nil {
return err
}
if sub.Func != nil {
go func() { sub.Func(data) }()
actualReceiver++
} else if sub.Chan != nil {
err := syncext.WriteChannelWithContext(ctx, sub.Chan, data)
if err != nil {
return err
}
actualReceiver++
}
return nil
}()
if err != nil {
return subscriber, actualReceiver, err
}
}
return subscriber, actualReceiver, nil
}
// PublishWithTimeout sends `data` to all subscriber
// buffered - if one is currently not listening, we wait (but wait at most `timeout` - if the timeout is exceeded then actualReceiver < subscriber)
func (bb *Broadcaster[TData]) PublishWithTimeout(data TData, timeout time.Duration) (subscriber int, actualReceiver int) {
bb.masterLock.Lock()
subs := langext.ArrCopy(bb.subscriptions)
bb.masterLock.Unlock()
subscriber = len(subs)
actualReceiver = 0
for _, sub := range subs {
func() {
sub.subLock.Lock()
defer sub.subLock.Unlock()
if sub.Func != nil {
go func() { sub.Func(data) }()
actualReceiver++
} else if sub.Chan != nil {
ok := syncext.WriteChannelWithTimeout(sub.Chan, data, timeout)
if ok {
actualReceiver++
}
}
}()
}
return subscriber, actualReceiver
}
func (bb *Broadcaster[TData]) SubscribeByCallback(fn func(TData)) BroadcastSubscription {
bb.masterLock.Lock()
defer bb.masterLock.Unlock()
sub := &broadcastSubscription[TData]{ID: xid.New().String(), parent: bb, subLock: &sync.Mutex{}, Func: fn, UnsubChan: nil}
bb.subscriptions = append(bb.subscriptions, sub)
return sub
}
func (bb *Broadcaster[TData]) SubscribeByChan(chanBufferSize int) (chan TData, BroadcastSubscription) {
bb.masterLock.Lock()
defer bb.masterLock.Unlock()
msgCh := make(chan TData, chanBufferSize)
sub := &broadcastSubscription[TData]{ID: xid.New().String(), parent: bb, subLock: &sync.Mutex{}, Chan: msgCh, UnsubChan: nil}
bb.subscriptions = append(bb.subscriptions, sub)
return msgCh, sub
}
func (bb *Broadcaster[TData]) SubscribeByIter(chanBufferSize int) (iter.Seq[TData], BroadcastSubscription) {
bb.masterLock.Lock()
defer bb.masterLock.Unlock()
msgCh := make(chan TData, chanBufferSize)
unsubChan := make(chan bool, 8)
sub := &broadcastSubscription[TData]{ID: xid.New().String(), parent: bb, subLock: &sync.Mutex{}, Chan: msgCh, UnsubChan: unsubChan}
bb.subscriptions = append(bb.subscriptions, sub)
iterFun := func(yield func(TData) bool) {
for {
select {
case msg := <-msgCh:
if !yield(msg) {
sub.Unsubscribe()
return
}
case <-sub.UnsubChan:
sub.Unsubscribe()
return
}
}
}
return iterFun, sub
}
func (bb *Broadcaster[TData]) unsubscribe(p *broadcastSubscription[TData]) {
bb.masterLock.Lock()
defer bb.masterLock.Unlock()
p.subLock.Lock()
defer p.subLock.Unlock()
if p.Chan != nil {
close(p.Chan)
p.Chan = nil
}
if p.UnsubChan != nil {
syncext.WriteNonBlocking(p.UnsubChan, true)
close(p.UnsubChan)
p.UnsubChan = nil
}
bb.subscriptions = langext.ArrFilter(bb.subscriptions, func(v *broadcastSubscription[TData]) bool {
return v.ID != p.ID
})
}
+332
View File
@@ -0,0 +1,332 @@
package dataext
import (
"context"
"sync"
"testing"
"time"
)
func TestNewBroadcast(t *testing.T) {
bb := NewBroadcaster[string](10)
if bb == nil {
t.Fatal("NewBroadcaster returned nil")
}
if bb.masterLock == nil {
t.Fatal("masterLock is nil")
}
if bb.subscriptions == nil {
t.Fatal("subscriptions is nil")
}
}
func TestBroadcast_SubscribeByCallback(t *testing.T) {
bb := NewBroadcaster[string](10)
var received string
var wg sync.WaitGroup
wg.Add(1)
callback := func(msg string) {
received = msg
wg.Done()
}
sub := bb.SubscribeByCallback(callback)
defer sub.Unsubscribe()
// Publish a message
subs, receivers := bb.Publish("hello")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Wait for the callback to be executed
wg.Wait()
if received != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", received)
}
}
func TestBroadcast_SubscribeByChan(t *testing.T) {
bb := NewBroadcaster[string](10)
ch, sub := bb.SubscribeByChan(1)
defer sub.Unsubscribe()
// Publish a message
subs, receivers := bb.Publish("hello")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Read from the channel with a timeout to avoid blocking
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
}
func TestBroadcast_SubscribeByIter(t *testing.T) {
bb := NewBroadcaster[string](10)
iterSeq, sub := bb.SubscribeByIter(1)
defer sub.Unsubscribe()
// Channel to communicate when message is received
done := make(chan bool)
received := false
// Start a goroutine to use the iterator
go func() {
for msg := range iterSeq {
if msg == "hello" {
received = true
done <- true
return // Stop iteration
}
}
}()
// Give time for the iterator to start
time.Sleep(100 * time.Millisecond)
// Publish a message
bb.Publish("hello")
// Wait for the message to be received or timeout
select {
case <-done:
if !received {
t.Fatal("Message was received but not 'hello'")
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
subCount := bb.SubscriberCount()
if subCount != 0 {
t.Fatalf("Expected 0 receivers, got %d", subCount)
}
}
func TestBroadcast_Publish(t *testing.T) {
bb := NewBroadcaster[string](10)
// Test publishing with no subscribers
subs, receivers := bb.Publish("hello")
if subs != 0 {
t.Fatalf("Expected 0 subscribers, got %d", subs)
}
if receivers != 0 {
t.Fatalf("Expected 0 receivers, got %d", receivers)
}
// Add a subscriber
ch, sub := bb.SubscribeByChan(1)
defer sub.Unsubscribe()
// Publish a message
subs, receivers = bb.Publish("hello")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Verify the message was received
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Test non-blocking behavior with a full channel
// First fill the channel
bb.Publish("fill")
// Now publish again - this should not block but may skip the receiver
subs, receivers = bb.Publish("overflow")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
_ = receivers // may be 0 if channel is full
// Drain the channel
<-ch
}
func TestBroadcast_PublishWithTimeout(t *testing.T) {
bb := NewBroadcaster[string](10)
// Add a subscriber with a channel
ch, sub := bb.SubscribeByChan(1)
defer sub.Unsubscribe()
// Publish with a timeout
subs, receivers := bb.PublishWithTimeout("hello", 100*time.Millisecond)
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Verify the message was received
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Fill the channel
bb.Publish("fill")
// Test timeout behavior with a full channel
start := time.Now()
subs, receivers = bb.PublishWithTimeout("timeout-test", 50*time.Millisecond)
elapsed := time.Since(start)
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
// The receiver count should be 0 if the timeout occurred
if elapsed < 50*time.Millisecond {
t.Fatalf("Expected to wait at least 50ms, only waited %v", elapsed)
}
// Drain the channel
<-ch
}
func TestBroadcast_PublishWithContext(t *testing.T) {
bb := NewBroadcaster[string](10)
// Add a subscriber with a channel
ch, sub := bb.SubscribeByChan(1)
defer sub.Unsubscribe()
// Create a context
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
// Publish with context
subs, receivers, err := bb.PublishWithContext(ctx, "hello")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Verify the message was received
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Fill the channel
bb.Publish("fill")
// Test context cancellation with a full channel
ctx, cancel = context.WithCancel(context.Background())
// Cancel the context after a short delay
go func() {
time.Sleep(50 * time.Millisecond)
cancel()
}()
start := time.Now()
subs, receivers, err = bb.PublishWithContext(ctx, "context-test")
elapsed := time.Since(start)
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
// Should get a context canceled error
if err == nil {
t.Fatal("Expected context canceled error, got nil")
}
if elapsed < 50*time.Millisecond {
t.Fatalf("Expected to wait at least 50ms, only waited %v", elapsed)
}
// Drain the channel
<-ch
}
func TestBroadcast_Unsubscribe(t *testing.T) {
bb := NewBroadcaster[string](10)
// Add a subscriber
ch, sub := bb.SubscribeByChan(1)
// Publish a message
subs, receivers := bb.Publish("hello")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Verify the message was received
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Unsubscribe
sub.Unsubscribe()
// Publish again
subs, receivers = bb.Publish("after-unsub")
if subs != 0 {
t.Fatalf("Expected 0 subscribers after unsubscribe, got %d", subs)
}
if receivers != 0 {
t.Fatalf("Expected 0 receivers after unsubscribe, got %d", receivers)
}
// Check that the subscriber count is 0
if bb.SubscriberCount() != 0 {
t.Fatalf("Expected SubscriberCount() == 0, got %d", bb.SubscriberCount())
}
}
+16 -1
View File
@@ -115,6 +115,9 @@ func (b *bufferedReadCloser) BufferedAll() ([]byte, error) {
return nil, err return nil, err
} }
} }
if err := b.Reset(); err != nil {
return nil, err
}
return b.buffer, nil return b.buffer, nil
case modeSourceFinished: case modeSourceFinished:
@@ -131,10 +134,22 @@ func (b *bufferedReadCloser) BufferedAll() ([]byte, error) {
} }
} }
// Reset resets the buffer to the beginning of the buffer.
// If the original source is partially read, we will finish reading it and fill our buffer
func (b *bufferedReadCloser) Reset() error { func (b *bufferedReadCloser) Reset() error {
switch b.mode { switch b.mode {
case modeSourceReading: case modeSourceReading:
fallthrough if b.off == 0 {
return nil // nobody has read anything yet
}
err := b.Close()
if err != nil {
return err
}
b.mode = modeBufferReading
b.off = 0
return nil
case modeSourceFinished: case modeSourceFinished:
err := b.Close() err := b.Close()
if err != nil { if err != nil {
+254
View File
@@ -0,0 +1,254 @@
package dataext
import (
"context"
"golang.org/x/sync/semaphore"
"runtime"
"sync"
"sync/atomic"
"time"
"unsafe"
)
// from https://github.com/viney-shih/go-lock/blob/2f19fd8ce335e33e0ab9dccb1ff2ce820c3da332/cas.go
// CASMutex is the struct implementing RWMutex with CAS mechanism.
type CASMutex struct {
state casState
turnstile *semaphore.Weighted
broadcastChan chan struct{}
broadcastMut sync.RWMutex
}
func NewCASMutex() *CASMutex {
return &CASMutex{
state: casStateNoLock,
turnstile: semaphore.NewWeighted(1),
broadcastChan: make(chan struct{}),
}
}
type casState int32
const (
casStateUndefined casState = iota - 2 // -2
casStateWriteLock // -1
casStateNoLock // 0
casStateReadLock // >= 1
)
func (m *CASMutex) getState(n int32) casState {
switch st := casState(n); {
case st == casStateWriteLock:
fallthrough
case st == casStateNoLock:
return st
case st >= casStateReadLock:
return casStateReadLock
default:
// actually, it should not happened.
return casStateUndefined
}
}
func (m *CASMutex) listen() <-chan struct{} {
m.broadcastMut.RLock()
defer m.broadcastMut.RUnlock()
return m.broadcastChan
}
func (m *CASMutex) broadcast() {
newCh := make(chan struct{})
m.broadcastMut.Lock()
ch := m.broadcastChan
m.broadcastChan = newCh
m.broadcastMut.Unlock()
close(ch)
}
func (m *CASMutex) tryLock(ctx context.Context) bool {
for {
broker := m.listen()
if atomic.CompareAndSwapInt32(
(*int32)(unsafe.Pointer(&m.state)),
int32(casStateNoLock),
int32(casStateWriteLock),
) {
return true
}
if ctx == nil {
return false
}
select {
case <-ctx.Done():
// timeout or cancellation
return false
case <-broker:
// waiting for signal triggered by m.broadcast() and trying again.
}
}
}
// TryLockWithContext attempts to acquire the lock, blocking until resources
// are available or ctx is done (timeout or cancellation).
func (m *CASMutex) TryLockWithContext(ctx context.Context) bool {
if err := m.turnstile.Acquire(ctx, 1); err != nil {
// Acquire failed due to timeout or cancellation
return false
}
defer m.turnstile.Release(1)
return m.tryLock(ctx)
}
// Lock acquires the lock.
// If it is currently held by others, Lock will wait until it has a chance to acquire it.
func (m *CASMutex) Lock() {
ctx := context.Background()
m.TryLockWithContext(ctx)
}
// TryLock attempts to acquire the lock without blocking.
// Return false if someone is holding it now.
func (m *CASMutex) TryLock() bool {
if !m.turnstile.TryAcquire(1) {
return false
}
defer m.turnstile.Release(1)
return m.tryLock(nil)
}
// TryLockWithTimeout attempts to acquire the lock within a period of time.
// Return false if spending time is more than duration and no chance to acquire it.
func (m *CASMutex) TryLockWithTimeout(duration time.Duration) bool {
ctx, cancel := context.WithTimeout(context.Background(), duration)
defer cancel()
return m.TryLockWithContext(ctx)
}
// Unlock releases the lock.
func (m *CASMutex) Unlock() {
if ok := atomic.CompareAndSwapInt32(
(*int32)(unsafe.Pointer(&m.state)),
int32(casStateWriteLock),
int32(casStateNoLock),
); !ok {
panic("Unlock failed")
}
m.broadcast()
}
func (m *CASMutex) rTryLock(ctx context.Context) bool {
for {
broker := m.listen()
n := atomic.LoadInt32((*int32)(unsafe.Pointer(&m.state)))
st := m.getState(n)
switch st {
case casStateNoLock, casStateReadLock:
if atomic.CompareAndSwapInt32((*int32)(unsafe.Pointer(&m.state)), n, n+1) {
return true
}
}
if ctx == nil {
return false
}
select {
case <-ctx.Done():
// timeout or cancellation
return false
default:
switch st {
// read-lock failed due to concurrence issue, try again immediately
case casStateNoLock, casStateReadLock:
runtime.Gosched() // allow other goroutines to do stuff.
continue
}
}
select {
case <-ctx.Done():
// timeout or cancellation
return false
case <-broker:
// waiting for signal triggered by m.broadcast() and trying again.
}
}
}
// RTryLockWithContext attempts to acquire the read lock, blocking until resources
// are available or ctx is done (timeout or cancellation).
func (m *CASMutex) RTryLockWithContext(ctx context.Context) bool {
if err := m.turnstile.Acquire(ctx, 1); err != nil {
// Acquire failed due to timeout or cancellation
return false
}
m.turnstile.Release(1)
return m.rTryLock(ctx)
}
// RLock acquires the read lock.
// If it is currently held by others writing, RLock will wait until it has a chance to acquire it.
func (m *CASMutex) RLock() {
ctx := context.Background()
m.RTryLockWithContext(ctx)
}
// RTryLock attempts to acquire the read lock without blocking.
// Return false if someone is writing it now.
func (m *CASMutex) RTryLock() bool {
if !m.turnstile.TryAcquire(1) {
return false
}
m.turnstile.Release(1)
return m.rTryLock(nil)
}
// RTryLockWithTimeout attempts to acquire the read lock within a period of time.
// Return false if spending time is more than duration and no chance to acquire it.
func (m *CASMutex) RTryLockWithTimeout(duration time.Duration) bool {
ctx, cancel := context.WithTimeout(context.Background(), duration)
defer cancel()
return m.RTryLockWithContext(ctx)
}
// RUnlock releases the read lock.
func (m *CASMutex) RUnlock() {
n := atomic.AddInt32((*int32)(unsafe.Pointer(&m.state)), -1)
switch m.getState(n) {
case casStateUndefined, casStateWriteLock:
panic("RUnlock failed")
case casStateNoLock:
m.broadcast()
}
}
// RLocker returns a Locker interface that implements the Lock and Unlock methods
// by calling CASMutex.RLock and CASMutex.RUnlock.
func (m *CASMutex) RLocker() sync.Locker {
return (*rlocker)(m)
}
type rlocker CASMutex
func (r *rlocker) Lock() { (*CASMutex)(r).RLock() }
func (r *rlocker) Unlock() { (*CASMutex)(r).RUnlock() }
+202
View File
@@ -0,0 +1,202 @@
package dataext
import (
"context"
"sync"
"time"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/syncext"
)
// DelayedCombiningInvoker is a utility to combine multiple consecutive requests into a single execution
//
// Requests are made with Request(), and consecutive requests are combined during the `delay` period.
//
// Can be used, e.g., for search-controls, where we want to init the search when teh user stops typing
// Or generally to queue an execution once a burst of requests is over.
type DelayedCombiningInvoker struct {
syncLock sync.Mutex
triggerChan chan bool
cancelChan chan bool
execNowChan chan bool
action func()
delay time.Duration
maxDelay time.Duration
executorRunning *syncext.AtomicBool
pendingRequests *syncext.Atomic[int]
lastRequestTime time.Time
initialRequestTime time.Time
onExecutionStart []func(immediately bool) // listener ( actual execution of action starts )
onExecutionDone []func() // listener ( actual execution of action finished )
onRequest []func(pending int, initial bool) // listener ( a request came in, waiting for execution )
}
func NewDelayedCombiningInvoker(action func(), delay time.Duration, maxDelay time.Duration) *DelayedCombiningInvoker {
return &DelayedCombiningInvoker{
action: action,
delay: delay,
maxDelay: maxDelay,
executorRunning: syncext.NewAtomicBool(false),
pendingRequests: syncext.NewAtomic[int](0),
triggerChan: make(chan bool),
cancelChan: make(chan bool, 1),
execNowChan: make(chan bool, 1),
lastRequestTime: time.Now(),
initialRequestTime: time.Now(),
onExecutionStart: make([]func(bool), 0),
onExecutionDone: make([]func(), 0),
onRequest: make([]func(int, bool), 0),
}
}
func (d *DelayedCombiningInvoker) Request() {
now := time.Now()
d.syncLock.Lock()
defer d.syncLock.Unlock()
if d.executorRunning.Get() {
d.lastRequestTime = now
d.pendingRequests.Update(func(v int) int { return v + 1 })
for _, fn := range d.onRequest {
_ = langext.RunPanicSafe(func() { fn(d.pendingRequests.Get(), true) })
}
d.triggerChan <- true
} else {
d.initialRequestTime = now
d.lastRequestTime = now
d.executorRunning.Set(true)
d.pendingRequests.Set(1)
syncext.ReadNonBlocking(d.triggerChan) // clear the channel
syncext.ReadNonBlocking(d.cancelChan) // clear the channel
syncext.ReadNonBlocking(d.execNowChan) // clear the channel
for _, fn := range d.onRequest {
_ = langext.RunPanicSafe(func() { fn(d.pendingRequests.Get(), false) })
}
go d.run()
}
}
func (d *DelayedCombiningInvoker) run() {
needsExecutorRunningCleanup := true
defer func() {
if needsExecutorRunningCleanup {
d.syncLock.Lock()
d.executorRunning.Set(false)
d.syncLock.Unlock()
}
}()
for {
d.syncLock.Lock()
timeOut := min(d.maxDelay-time.Since(d.initialRequestTime), d.delay-time.Since(d.lastRequestTime))
if timeOut < 0 {
timeOut = 0
}
d.syncLock.Unlock()
immediately := false
select {
case <-d.execNowChan:
// run immediately
immediately = true
break
case <-d.triggerChan:
// external trigger - needs to re-evaluate
break
case <-d.cancelChan:
// cancel
return
case <-time.After(timeOut):
// time elapsed - check for execution
break
}
d.syncLock.Lock()
execute := immediately || time.Since(d.lastRequestTime) >= d.delay || time.Since(d.initialRequestTime) >= d.maxDelay
if !execute {
d.syncLock.Unlock()
continue
}
d.pendingRequests.Set(0)
for _, fn := range d.onExecutionStart {
_ = langext.RunPanicSafe(func() { fn(immediately) })
}
// =================================================
_ = langext.RunPanicSafe(d.action)
// =================================================
d.executorRunning.Set(false) // ensure HasPendingRequests returns fals ein onExecutionDone listener
needsExecutorRunningCleanup = false
for _, fn := range d.onExecutionDone {
_ = langext.RunPanicSafe(fn)
}
d.syncLock.Unlock()
return
}
}
func (d *DelayedCombiningInvoker) CancelPendingRequests() {
d.syncLock.Lock()
defer d.syncLock.Unlock()
syncext.WriteNonBlocking(d.cancelChan, true)
}
func (d *DelayedCombiningInvoker) HasPendingRequests() bool {
return d.executorRunning.Get()
}
func (d *DelayedCombiningInvoker) CountPendingRequests() int {
return d.pendingRequests.Get()
}
func (d *DelayedCombiningInvoker) ExecuteNow() bool {
d.syncLock.Lock()
defer d.syncLock.Unlock()
if d.executorRunning.Get() {
syncext.WriteNonBlocking(d.execNowChan, true)
return true
} else {
return false
}
}
func (d *DelayedCombiningInvoker) WaitForCompletion(ctx context.Context) error {
return d.executorRunning.WaitWithContext(ctx, false)
}
func (d *DelayedCombiningInvoker) RegisterOnExecutionStart(fn func(immediately bool)) {
d.syncLock.Lock()
defer d.syncLock.Unlock()
d.onExecutionStart = append(d.onExecutionStart, fn)
}
func (d *DelayedCombiningInvoker) RegisterOnExecutionDone(fn func()) {
d.syncLock.Lock()
defer d.syncLock.Unlock()
d.onExecutionDone = append(d.onExecutionDone, fn)
}
func (d *DelayedCombiningInvoker) RegisterOnRequest(fn func(pending int, initial bool)) {
d.syncLock.Lock()
defer d.syncLock.Unlock()
d.onRequest = append(d.onRequest, fn)
}
+1 -1
View File
@@ -1,7 +1,7 @@
package dataext package dataext
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"math/rand" "math/rand"
"strconv" "strconv"
"testing" "testing"
+2 -2
View File
@@ -1,8 +1,8 @@
package dataext package dataext
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/tst" "git.blackforestbytes.com/BlackForestBytes/goext/tst"
"testing" "testing"
) )
+105
View File
@@ -0,0 +1,105 @@
package dataext
import (
"context"
"sync"
"time"
)
// MultiMutex is a simple map[key -> mutex]
type MultiMutex[TKey comparable] struct {
mutextMap *SyncMap[TKey, *CASMutex]
}
func NewMultiMutex[TKey comparable]() *MultiMutex[TKey] {
return &MultiMutex[TKey]{
mutextMap: NewSyncMap[TKey, *CASMutex](),
}
}
// TryLockWithContext attempts to acquire the lock, blocking until resources
// are available or ctx is done (timeout or cancellation).
func (mm *MultiMutex[TKey]) TryLockWithContext(ctx context.Context, key TKey) bool {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
return lck.TryLockWithContext(ctx)
}
// Lock acquires the lock.
// If it is currently held by others, Lock will wait until it has a chance to acquire it.
func (mm *MultiMutex[TKey]) Lock(key TKey) {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
lck.Lock()
}
// TryLock attempts to acquire the lock without blocking.
// Return false if someone is holding it now.
func (mm *MultiMutex[TKey]) TryLock(key TKey) bool {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
return lck.TryLock()
}
// TryLockWithTimeout attempts to acquire the lock within a period of time.
// Return false if spending time is more than duration and no chance to acquire it.
func (mm *MultiMutex[TKey]) TryLockWithTimeout(key TKey, duration time.Duration) bool {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
return lck.TryLockWithTimeout(duration)
}
// Unlock releases the lock.
func (mm *MultiMutex[TKey]) Unlock(key TKey) {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
lck.Unlock()
}
// RTryLockWithContext attempts to acquire the read lock, blocking until resources
// are available or ctx is done (timeout or cancellation).
func (mm *MultiMutex[TKey]) RTryLockWithContext(ctx context.Context, key TKey) bool {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
return lck.RTryLockWithContext(ctx)
}
// RLock acquires the read lock.
// If it is currently held by others writing, RLock will wait until it has a chance to acquire it.
func (mm *MultiMutex[TKey]) RLock(key TKey) {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
lck.RLock()
}
// RTryLock attempts to acquire the read lock without blocking.
// Return false if someone is writing it now.
func (mm *MultiMutex[TKey]) RTryLock(key TKey) bool {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
return lck.RTryLock()
}
// RTryLockWithTimeout attempts to acquire the read lock within a period of time.
// Return false if spending time is more than duration and no chance to acquire it.
func (mm *MultiMutex[TKey]) RTryLockWithTimeout(duration time.Duration, key TKey) bool {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
return lck.RTryLockWithTimeout(duration)
}
// RUnlock releases the read lock.
func (mm *MultiMutex[TKey]) RUnlock(key TKey) {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
lck.RUnlock()
}
// RLocker returns a Locker interface that implements the Lock and Unlock methods
// by calling CASMutex.RLock and CASMutex.RUnlock.
func (mm *MultiMutex[TKey]) RLocker(key TKey) sync.Locker {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
return lck.RLocker()
}
// Get returns a Locker interface
func (mm *MultiMutex[TKey]) Get(key TKey) sync.Locker {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
return lck
}
// GetCAS returns the underlying CASMutex
func (mm *MultiMutex[TKey]) GetCAS(key TKey) *CASMutex {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
return lck
}
+53
View File
@@ -0,0 +1,53 @@
package dataext
import "sync"
type MutexSet[T comparable] struct {
master sync.RWMutex
locks map[T]*sync.RWMutex
}
func NewMutexSet[T comparable]() *MutexSet[T] {
return &MutexSet[T]{
master: sync.RWMutex{},
locks: make(map[T]*sync.RWMutex),
}
}
func (ms *MutexSet[T]) get(key T) *sync.RWMutex {
ms.master.RLock()
if v, ok := ms.locks[key]; ok {
ms.master.RUnlock()
return v
}
ms.master.RUnlock()
// ---------
ms.master.Lock()
defer ms.master.Unlock()
if v, ok := ms.locks[key]; ok {
return v
}
m := &sync.RWMutex{}
ms.locks[key] = m
return m
}
func (ms *MutexSet[T]) Lock(key T) {
ms.get(key).Lock()
}
func (ms *MutexSet[T]) Unlock(key T) {
ms.get(key).Unlock()
}
func (ms *MutexSet[T]) RLock(key T) {
ms.get(key).RLock()
}
func (ms *MutexSet[T]) RUnlock(key T) {
ms.get(key).RUnlock()
}
+83
View File
@@ -0,0 +1,83 @@
package dataext
import (
"encoding/json"
"errors"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
)
type JsonOpt[T any] struct {
isSet bool
value T
}
func NewJsonOpt[T any](v T) JsonOpt[T] {
return JsonOpt[T]{isSet: true, value: v}
}
func EmptyJsonOpt[T any]() JsonOpt[T] {
return JsonOpt[T]{isSet: false}
}
// MarshalJSON returns m as the JSON encoding of m.
func (m JsonOpt[T]) MarshalJSON() ([]byte, error) {
if !m.isSet {
return []byte("null"), nil // actually this would be undefined - but undefined is not valid JSON
}
return json.Marshal(m.value)
}
// UnmarshalJSON sets *m to a copy of data.
func (m *JsonOpt[T]) UnmarshalJSON(data []byte) error {
if m == nil {
return errors.New("JsonOpt: UnmarshalJSON on nil pointer")
}
m.isSet = true
return json.Unmarshal(data, &m.value)
}
func (m JsonOpt[T]) IsSet() bool {
return m.isSet
}
func (m JsonOpt[T]) IsUnset() bool {
return !m.isSet
}
func (m JsonOpt[T]) Value() (T, bool) {
if !m.isSet {
return *new(T), false
}
return m.value, true
}
func (m JsonOpt[T]) ValueOrNil() *T {
if !m.isSet {
return nil
}
return &m.value
}
func (m JsonOpt[T]) ValueDblPtrOrNil() **T {
if !m.isSet {
return nil
}
return langext.DblPtr(m.value)
}
func (m JsonOpt[T]) MustValue() T {
if !m.isSet {
panic("value not set")
}
return m.value
}
func (m JsonOpt[T]) IfSet(fn func(v T)) bool {
if !m.isSet {
return false
}
fn(m.value)
return true
}
+267
View File
@@ -0,0 +1,267 @@
package dataext
import (
"context"
"iter"
"sync"
"time"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/syncext"
"github.com/rs/xid"
)
// PubSub is a simple Pub/Sub Broker
// Clients can subscribe to a namespace and receive published messages on this namespace
// Messages are broadcast to all subscribers
type PubSub[TNamespace comparable, TData any] struct {
masterLock *sync.Mutex
subscriptions map[TNamespace][]*pubSubSubscription[TNamespace, TData]
}
type PubSubSubscription interface {
Unsubscribe()
}
type pubSubSubscription[TNamespace comparable, TData any] struct {
ID string
parent *PubSub[TNamespace, TData]
namespace TNamespace
subLock *sync.Mutex
Func func(TData)
Chan chan TData
UnsubChan chan bool
}
func (p *pubSubSubscription[TNamespace, TData]) Unsubscribe() {
p.parent.unsubscribe(p)
}
func NewPubSub[TNamespace comparable, TData any](capacity int) *PubSub[TNamespace, TData] {
return &PubSub[TNamespace, TData]{
masterLock: &sync.Mutex{},
subscriptions: make(map[TNamespace][]*pubSubSubscription[TNamespace, TData], capacity),
}
}
func (ps *PubSub[TNamespace, TData]) Namespaces() []TNamespace {
ps.masterLock.Lock()
defer ps.masterLock.Unlock()
return langext.MapKeyArr(ps.subscriptions)
}
func (ps *PubSub[TNamespace, TData]) SubscriberCount(ns TNamespace) int {
ps.masterLock.Lock()
defer ps.masterLock.Unlock()
return len(ps.subscriptions[ns])
}
// Publish sends `data` to all subscriber
// But unbuffered - if one is currently not listening, we skip (the actualReceiver < subscriber)
func (ps *PubSub[TNamespace, TData]) Publish(ns TNamespace, data TData) (subscriber int, actualReceiver int) {
ps.masterLock.Lock()
subs := langext.ArrCopy(ps.subscriptions[ns])
ps.masterLock.Unlock()
subscriber = len(subs)
actualReceiver = 0
for _, sub := range subs {
func() {
sub.subLock.Lock()
defer sub.subLock.Unlock()
if sub.Func != nil {
go func() { sub.Func(data) }()
actualReceiver++
} else if sub.Chan != nil {
msgSent := syncext.WriteNonBlocking(sub.Chan, data)
if msgSent {
actualReceiver++
}
}
}()
}
return subscriber, actualReceiver
}
// PublishWithContext sends `data` to all subscriber
// buffered - if one is currently not listening, we wait (but error out when the context runs out)
func (ps *PubSub[TNamespace, TData]) PublishWithContext(ctx context.Context, ns TNamespace, data TData) (subscriber int, actualReceiver int, err error) {
ps.masterLock.Lock()
subs := langext.ArrCopy(ps.subscriptions[ns])
ps.masterLock.Unlock()
subscriber = len(subs)
actualReceiver = 0
for _, sub := range subs {
err := func() error {
sub.subLock.Lock()
defer sub.subLock.Unlock()
if err := ctx.Err(); err != nil {
return err
}
if sub.Func != nil {
go func() { sub.Func(data) }()
actualReceiver++
} else if sub.Chan != nil {
err := syncext.WriteChannelWithContext(ctx, sub.Chan, data)
if err != nil {
return err
}
actualReceiver++
}
return nil
}()
if err != nil {
return subscriber, actualReceiver, err
}
}
return subscriber, actualReceiver, nil
}
// PublishWithTimeout sends `data` to all subscriber
// buffered - if one is currently not listening, we wait (but wait at most `timeout` - if the timeout is exceeded then actualReceiver < subscriber)
func (ps *PubSub[TNamespace, TData]) PublishWithTimeout(ns TNamespace, data TData, timeout time.Duration) (subscriber int, actualReceiver int) {
ps.masterLock.Lock()
subs := langext.ArrCopy(ps.subscriptions[ns])
ps.masterLock.Unlock()
subscriber = len(subs)
actualReceiver = 0
for _, sub := range subs {
func() {
sub.subLock.Lock()
defer sub.subLock.Unlock()
if sub.Func != nil {
go func() { sub.Func(data) }()
actualReceiver++
} else if sub.Chan != nil {
ok := syncext.WriteChannelWithTimeout(sub.Chan, data, timeout)
if ok {
actualReceiver++
}
}
}()
}
return subscriber, actualReceiver
}
// PublishAsync sends `data` to all subscriber
// does not wait for subscriber (this method returns immediately), waits at most {timeout} seconds on channels (async)
func (ps *PubSub[TNamespace, TData]) PublishAsync(ns TNamespace, data TData, timeout time.Duration) (subscriber int) {
ps.masterLock.Lock()
subs := langext.ArrCopy(ps.subscriptions[ns])
ps.masterLock.Unlock()
subscriber = len(subs)
for _, sub := range subs {
go func() {
sub.subLock.Lock()
defer sub.subLock.Unlock()
if sub.Func != nil {
go func() { sub.Func(data) }()
} else if sub.Chan != nil {
_ = syncext.WriteChannelWithTimeout(sub.Chan, data, timeout)
}
}()
}
return subscriber
}
func (ps *PubSub[TNamespace, TData]) SubscribeByCallback(ns TNamespace, fn func(TData)) PubSubSubscription {
ps.masterLock.Lock()
defer ps.masterLock.Unlock()
sub := &pubSubSubscription[TNamespace, TData]{ID: xid.New().String(), namespace: ns, parent: ps, subLock: &sync.Mutex{}, Func: fn, UnsubChan: nil}
ps.subscriptions[ns] = append(ps.subscriptions[ns], sub)
return sub
}
func (ps *PubSub[TNamespace, TData]) SubscribeByChan(ns TNamespace, chanBufferSize int) (chan TData, PubSubSubscription) {
ps.masterLock.Lock()
defer ps.masterLock.Unlock()
msgCh := make(chan TData, chanBufferSize)
sub := &pubSubSubscription[TNamespace, TData]{ID: xid.New().String(), namespace: ns, parent: ps, subLock: &sync.Mutex{}, Chan: msgCh, UnsubChan: nil}
ps.subscriptions[ns] = append(ps.subscriptions[ns], sub)
return msgCh, sub
}
func (ps *PubSub[TNamespace, TData]) SubscribeByIter(ns TNamespace, chanBufferSize int) (iter.Seq[TData], PubSubSubscription) {
ps.masterLock.Lock()
defer ps.masterLock.Unlock()
msgCh := make(chan TData, chanBufferSize)
unsubChan := make(chan bool, 8)
sub := &pubSubSubscription[TNamespace, TData]{ID: xid.New().String(), namespace: ns, parent: ps, subLock: &sync.Mutex{}, Chan: msgCh, UnsubChan: unsubChan}
ps.subscriptions[ns] = append(ps.subscriptions[ns], sub)
iterFun := func(yield func(TData) bool) {
for {
select {
case msg := <-msgCh:
if !yield(msg) {
sub.Unsubscribe()
return
}
case <-sub.UnsubChan:
sub.Unsubscribe()
return
}
}
}
return iterFun, sub
}
func (ps *PubSub[TNamespace, TData]) unsubscribe(p *pubSubSubscription[TNamespace, TData]) {
ps.masterLock.Lock()
defer ps.masterLock.Unlock()
p.subLock.Lock()
defer p.subLock.Unlock()
if p.Chan != nil {
close(p.Chan)
p.Chan = nil
}
if p.UnsubChan != nil {
syncext.WriteNonBlocking(p.UnsubChan, true)
close(p.UnsubChan)
p.UnsubChan = nil
}
ps.subscriptions[p.namespace] = langext.ArrFilter(ps.subscriptions[p.namespace], func(v *pubSubSubscription[TNamespace, TData]) bool {
return v.ID != p.ID
})
if len(ps.subscriptions[p.namespace]) == 0 {
delete(ps.subscriptions, p.namespace)
}
}
+428
View File
@@ -0,0 +1,428 @@
package dataext
import (
"context"
"sync"
"testing"
"time"
)
func TestNewPubSub(t *testing.T) {
ps := NewPubSub[string, string](10)
if ps == nil {
t.Fatal("NewPubSub returned nil")
}
if ps.masterLock == nil {
t.Fatal("masterLock is nil")
}
if ps.subscriptions == nil {
t.Fatal("subscriptions is nil")
}
}
func TestPubSub_Namespaces(t *testing.T) {
ps := NewPubSub[string, string](10)
// Initially no namespaces
namespaces := ps.Namespaces()
if len(namespaces) != 0 {
t.Fatalf("Expected 0 namespaces, got %d", len(namespaces))
}
// Add a subscription to create a namespace
_, sub1 := ps.SubscribeByChan("test-ns1", 1)
defer sub1.Unsubscribe()
// Add another subscription to a different namespace
_, sub2 := ps.SubscribeByChan("test-ns2", 1)
defer sub2.Unsubscribe()
// Check namespaces
namespaces = ps.Namespaces()
if len(namespaces) != 2 {
t.Fatalf("Expected 2 namespaces, got %d", len(namespaces))
}
// Check if namespaces contain the expected values
found1, found2 := false, false
for _, ns := range namespaces {
if ns == "test-ns1" {
found1 = true
}
if ns == "test-ns2" {
found2 = true
}
}
if !found1 || !found2 {
t.Fatalf("Expected to find both namespaces, found ns1: %v, ns2: %v", found1, found2)
}
}
func TestPubSub_SubscribeByCallback(t *testing.T) {
ps := NewPubSub[string, string](10)
var received string
var wg sync.WaitGroup
wg.Add(1)
callback := func(msg string) {
received = msg
wg.Done()
}
sub := ps.SubscribeByCallback("test-ns", callback)
defer sub.Unsubscribe()
// Publish a message
subs, receivers := ps.Publish("test-ns", "hello")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Wait for the callback to be executed
wg.Wait()
if received != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", received)
}
}
func TestPubSub_SubscribeByChan(t *testing.T) {
ps := NewPubSub[string, string](10)
ch, sub := ps.SubscribeByChan("test-ns", 1)
defer sub.Unsubscribe()
// Publish a message
subs, receivers := ps.Publish("test-ns", "hello")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Read from the channel with a timeout to avoid blocking
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
}
func TestPubSub_SubscribeByIter(t *testing.T) {
ps := NewPubSub[string, string](10)
iterSeq, sub := ps.SubscribeByIter("test-ns", 1)
defer sub.Unsubscribe()
// Channel to communicate when message is received
done := make(chan bool)
received := false
// Start a goroutine to use the iterator
go func() {
for msg := range iterSeq {
if msg == "hello" {
received = true
done <- true
return // Stop iteration
}
}
}()
// Give time for the iterator to start
time.Sleep(100 * time.Millisecond)
// Publish a message
ps.Publish("test-ns", "hello")
// Wait for the message to be received or timeout
select {
case <-done:
if !received {
t.Fatal("Message was received but not 'hello'")
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
subCount := ps.SubscriberCount("test-ns")
if subCount != 0 {
t.Fatalf("Expected 0 receivers, got %d", subCount)
}
}
func TestPubSub_Publish(t *testing.T) {
ps := NewPubSub[string, string](10)
// Test publishing to a namespace with no subscribers
subs, receivers := ps.Publish("empty-ns", "hello")
if subs != 0 {
t.Fatalf("Expected 0 subscribers, got %d", subs)
}
if receivers != 0 {
t.Fatalf("Expected 0 receivers, got %d", receivers)
}
// Add a subscriber
ch, sub := ps.SubscribeByChan("test-ns", 1)
defer sub.Unsubscribe()
// Publish a message
subs, receivers = ps.Publish("test-ns", "hello")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Verify the message was received
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Test non-blocking behavior with a full channel
// First fill the channel
ps.Publish("test-ns", "fill")
// Now publish again - this should not block but skip the receiver
subs, receivers = ps.Publish("test-ns", "overflow")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
// The receiver count might be 0 if the channel is full
// Drain the channel
<-ch
}
func TestPubSub_PublishWithTimeout(t *testing.T) {
ps := NewPubSub[string, string](10)
// Add a subscriber with a channel
ch, sub := ps.SubscribeByChan("test-ns", 1)
defer sub.Unsubscribe()
// Publish with a timeout
subs, receivers := ps.PublishWithTimeout("test-ns", "hello", 100*time.Millisecond)
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Verify the message was received
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Fill the channel
ps.Publish("test-ns", "fill")
// Test timeout behavior with a full channel
start := time.Now()
subs, receivers = ps.PublishWithTimeout("test-ns", "timeout-test", 50*time.Millisecond)
elapsed := time.Since(start)
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
// The receiver count should be 0 if the timeout occurred
if elapsed < 50*time.Millisecond {
t.Fatalf("Expected to wait at least 50ms, only waited %v", elapsed)
}
// Drain the channel
<-ch
}
func TestPubSub_PublishWithContext(t *testing.T) {
ps := NewPubSub[string, string](10)
// Add a subscriber with a channel
ch, sub := ps.SubscribeByChan("test-ns", 1)
defer sub.Unsubscribe()
// Create a context
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
// Publish with context
subs, receivers, err := ps.PublishWithContext(ctx, "test-ns", "hello")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Verify the message was received
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Fill the channel
ps.Publish("test-ns", "fill")
// Test context cancellation with a full channel
ctx, cancel = context.WithCancel(context.Background())
// Cancel the context after a short delay
go func() {
time.Sleep(50 * time.Millisecond)
cancel()
}()
start := time.Now()
subs, receivers, err = ps.PublishWithContext(ctx, "test-ns", "context-test")
elapsed := time.Since(start)
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
// Should get a context canceled error
if err == nil {
t.Fatal("Expected context canceled error, got nil")
}
if elapsed < 50*time.Millisecond {
t.Fatalf("Expected to wait at least 50ms, only waited %v", elapsed)
}
// Drain the channel
<-ch
}
func TestPubSub_Unsubscribe(t *testing.T) {
ps := NewPubSub[string, string](10)
// Add a subscriber
ch, sub := ps.SubscribeByChan("test-ns", 1)
// Publish a message
subs, receivers := ps.Publish("test-ns", "hello")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Verify the message was received
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Unsubscribe
sub.Unsubscribe()
// Publish again
subs, receivers = ps.Publish("test-ns", "after-unsub")
if subs != 0 {
t.Fatalf("Expected 0 subscribers after unsubscribe, got %d", subs)
}
if receivers != 0 {
t.Fatalf("Expected 0 receivers after unsubscribe, got %d", receivers)
}
// Check that the namespace is removed
namespaces := ps.Namespaces()
if len(namespaces) != 0 {
t.Fatalf("Expected 0 namespaces after unsubscribe, got %d", len(namespaces))
}
}
func TestPubSub_MultipleSubscribers(t *testing.T) {
ps := NewPubSub[string, string](10)
// Add multiple subscribers
ch1, sub1 := ps.SubscribeByChan("test-ns", 1)
defer sub1.Unsubscribe()
ch2, sub2 := ps.SubscribeByChan("test-ns", 1)
defer sub2.Unsubscribe()
var received string
var wg sync.WaitGroup
wg.Add(1)
sub3 := ps.SubscribeByCallback("test-ns", func(msg string) {
received = msg
wg.Done()
})
defer sub3.Unsubscribe()
// Publish a message
subs, receivers := ps.Publish("test-ns", "hello")
if subs != 3 {
t.Fatalf("Expected 3 subscribers, got %d", subs)
}
if receivers != 3 {
t.Fatalf("Expected 3 receivers, got %d", receivers)
}
// Verify the message was received by all subscribers
select {
case msg := <-ch1:
if msg != "hello" {
t.Fatalf("Expected ch1 to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message on ch1")
}
select {
case msg := <-ch2:
if msg != "hello" {
t.Fatalf("Expected ch2 to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message on ch2")
}
// Wait for the callback
wg.Wait()
if received != "hello" {
t.Fatalf("Expected callback to receive 'hello', got '%s'", received)
}
}
+144
View File
@@ -0,0 +1,144 @@
package dataext
import "iter"
type RingBuffer[T any] struct {
items []T //
capacity int // max number of items the buffer can hold
size int // how many items are in the buffer
head int // ptr to next item
}
func NewRingBuffer[T any](capacity int) *RingBuffer[T] {
return &RingBuffer[T]{
items: make([]T, capacity),
capacity: capacity,
size: 0,
head: 0,
}
}
func (rb *RingBuffer[T]) Push(item T) {
if rb.size < rb.capacity {
rb.size++
}
rb.items[rb.head] = item
rb.head = (rb.head + 1) % rb.capacity
}
func (rb *RingBuffer[T]) PushPop(item T) *T {
if rb.size < rb.capacity {
rb.size++
rb.items[rb.head] = item
rb.head = (rb.head + 1) % rb.capacity
return nil
} else {
prev := rb.items[rb.head]
rb.items[rb.head] = item
rb.head = (rb.head + 1) % rb.capacity
return &prev
}
}
func (rb *RingBuffer[T]) Peek() (T, bool) {
if rb.size == 0 {
return *new(T), false
}
return rb.items[(rb.head-1+rb.capacity)%rb.capacity], true
}
func (rb *RingBuffer[T]) Items() []T {
if rb.size < rb.capacity {
return rb.items[:rb.size]
}
return append(rb.items[rb.head:], rb.items[:rb.head]...)
}
func (rb *RingBuffer[T]) Size() int {
return rb.size
}
func (rb *RingBuffer[T]) Capacity() int {
return rb.capacity
}
func (rb *RingBuffer[T]) Clear() {
rb.size = 0
rb.head = 0
}
func (rb *RingBuffer[T]) IsFull() bool {
return rb.size == rb.capacity
}
func (rb *RingBuffer[T]) At(i int) T {
if i < 0 || i >= rb.size {
panic("Index out of bounds")
}
if rb.size < rb.capacity {
return rb.items[i]
}
return rb.items[(rb.head+i)%rb.capacity]
}
func (rb *RingBuffer[T]) Get(i int) (T, bool) {
if i < 0 || i >= rb.size {
return *new(T), false
}
if rb.size < rb.capacity {
return rb.items[i], true
}
return rb.items[(rb.head+i)%rb.capacity], true
}
func (rb *RingBuffer[T]) Iter() iter.Seq[T] {
return func(yield func(T) bool) {
for i := 0; i < rb.size; i++ {
if !yield(rb.At(i)) {
return
}
}
}
}
func (rb *RingBuffer[T]) Iter2() iter.Seq2[int, T] {
return func(yield func(int, T) bool) {
for i := 0; i < rb.size; i++ {
if !yield(i, rb.At(i)) {
return
}
}
}
}
func (rb *RingBuffer[T]) Remove(fnEqual func(v T) bool) int {
// Mike [2024-11-13]: I *really* tried to write an in-place algorithm to remove elements
// But after carful consideration, I left that as an exercise for future readers
// It is, suprisingly, non-trivial, especially because the head-ptr must be weirdly updated
// And out At() method does not work correctly with {head<>0 && size<capacity}
dc := 0
b := make([]T, rb.capacity)
bsize := 0
for i := 0; i < rb.size; i++ {
comp := rb.At(i)
if fnEqual(comp) {
dc++
} else {
b[bsize] = comp
bsize++
}
}
if dc == 0 {
return 0
}
rb.items = b
rb.size = bsize
rb.head = bsize % rb.capacity
return dc
}
+447
View File
@@ -0,0 +1,447 @@
package dataext
import "testing"
func TestRingBufferPushAddsItem(t *testing.T) {
rb := NewRingBuffer[int](3)
rb.Push(1)
if rb.Size() != 1 {
t.Errorf("Expected size 1, got %d", rb.Size())
}
if item, _ := rb.Peek(); item != 1 {
t.Errorf("Expected item 1, got %d", item)
}
}
func TestRingBufferPushPopReturnsOldestItem(t *testing.T) {
rb := NewRingBuffer[int](3)
rb.Push(1)
rb.Push(2)
rb.Push(3)
if item := rb.PushPop(4); item == nil || *item != 1 {
t.Errorf("Expected item 1, got %v", item)
}
}
func TestRingBufferPeekReturnsLastPushedItem(t *testing.T) {
rb := NewRingBuffer[int](3)
rb.Push(1)
rb.Push(2)
if item, _ := rb.Peek(); item != 2 {
t.Errorf("Expected item 2, got %d", item)
}
}
func TestRingBufferOverflow1(t *testing.T) {
rb := NewRingBuffer[int](5)
rb.Push(1) // overriden
rb.Push(2) // overriden
rb.Push(3)
rb.Push(9)
rb.Push(4)
rb.Push(5)
rb.Push(7)
if rb.Size() != 5 {
t.Errorf("Expected size 4, got %d", rb.Size())
}
expected := []int{3, 9, 4, 5, 7}
items := rb.Items()
for i, item := range items {
if item != expected[i] {
t.Errorf("Expected item %d, got %d", expected[i], item)
}
}
}
func TestRingBufferItemsReturnsAllItems(t *testing.T) {
rb := NewRingBuffer[int](3)
rb.Push(1)
rb.Push(2)
rb.Push(3)
items := rb.Items()
expected := []int{1, 2, 3}
for i, item := range items {
if item != expected[i] {
t.Errorf("Expected item %d, got %d", expected[i], item)
}
}
}
func TestRingBufferClearEmptiesBuffer(t *testing.T) {
rb := NewRingBuffer[int](3)
rb.Push(1)
rb.Clear()
if rb.Size() != 0 {
t.Errorf("Expected size 0, got %d", rb.Size())
}
}
func TestRingBufferIsFullReturnsTrueWhenFull(t *testing.T) {
rb := NewRingBuffer[int](3)
rb.Push(1)
rb.Push(2)
rb.Push(3)
if !rb.IsFull() {
t.Errorf("Expected buffer to be full")
}
}
func TestRingBufferAtReturnsCorrectItem(t *testing.T) {
rb := NewRingBuffer[int](3)
rb.Push(1)
rb.Push(2)
rb.Push(3)
if item := rb.At(1); item != 2 {
t.Errorf("Expected item 2, got %d", item)
}
}
func TestRingBufferGetReturnsCorrectItem(t *testing.T) {
rb := NewRingBuffer[int](3)
rb.Push(1)
rb.Push(2)
rb.Push(3)
if item, ok := rb.Get(1); !ok || item != 2 {
t.Errorf("Expected item 2, got %d", item)
}
}
func TestRingBufferRemoveDeletesMatchingItems(t *testing.T) {
rb := NewRingBuffer[int](5)
rb.Push(1)
rb.Push(2)
rb.Push(3)
rb.Push(2)
rb.Push(4)
removed := rb.Remove(func(v int) bool { return v == 2 })
if removed != 2 {
t.Errorf("Expected 2 items removed, got %d", removed)
}
if rb.Size() != 3 {
t.Errorf("Expected size 3, got %d", rb.Size())
}
expected := []int{1, 3, 4}
items := rb.Items()
for i, item := range items {
if item != expected[i] {
t.Errorf("Expected item %d, got %d", expected[i], item)
}
}
}
func TestRingBufferRemoveDeletesMatchingItems2(t *testing.T) {
rb := NewRingBuffer[int](5)
rb.Push(1)
rb.Push(2)
rb.Push(3)
rb.Push(2)
rb.Push(4)
removed := rb.Remove(func(v int) bool { return v == 3 })
if removed != 1 {
t.Errorf("Expected 2 items removed, got %d", removed)
}
if rb.Size() != 4 {
t.Errorf("Expected size 3, got %d", rb.Size())
}
expected := []int{1, 2, 2, 4}
items := rb.Items()
for i, item := range items {
if item != expected[i] {
t.Errorf("Expected item %d, got %d", expected[i], item)
}
}
}
func TestRingBufferRemoveDeletesMatchingItems3(t *testing.T) {
rb := NewRingBuffer[int](5)
rb.Push(1)
rb.Push(2)
rb.Push(3)
rb.Push(9)
rb.Push(4)
removed := rb.Remove(func(v int) bool { return v == 3 })
if removed != 1 {
t.Errorf("Expected 2 items removed, got %d", removed)
}
if rb.Size() != 4 {
t.Errorf("Expected size 3, got %d", rb.Size())
}
expected := []int{1, 2, 9, 4}
items := rb.Items()
for i, item := range items {
if item != expected[i] {
t.Errorf("Expected item %d, got %d", expected[i], item)
}
}
}
func TestRingBufferRemoveDeletesMatchingItems4(t *testing.T) {
rb := NewRingBuffer[int](5)
rb.Push(1) // overriden
rb.Push(2) // overriden
rb.Push(3)
rb.Push(9)
rb.Push(4)
rb.Push(5)
rb.Push(7)
removed := rb.Remove(func(v int) bool { return v == 7 })
if removed != 1 {
t.Errorf("Expected 1 items removed, got %d", removed)
}
if rb.Size() != 4 {
t.Errorf("Expected size 4, got %d", rb.Size())
}
expected := []int{3, 9, 4, 5}
items := rb.Items()
for i, item := range items {
if item != expected[i] {
t.Errorf("Expected item %d, got %d", expected[i], item)
}
}
}
func TestRingBufferRemoveDeletesMatchingItems5(t *testing.T) {
rb := NewRingBuffer[int](5)
rb.Push(1) // overriden
rb.Push(2) // overriden
rb.Push(3)
rb.Push(9)
rb.Push(4)
rb.Push(5)
rb.Push(7)
removed := rb.Remove(func(v int) bool { return v == 3 })
if removed != 1 {
t.Errorf("Expected 1 items removed, got %d", removed)
}
if rb.Size() != 4 {
t.Errorf("Expected size 4, got %d", rb.Size())
}
expected := []int{9, 4, 5, 7}
items := rb.Items()
for i, item := range items {
if item != expected[i] {
t.Errorf("Expected item %d, got %d", expected[i], item)
}
}
}
func TestRingBufferRemoveDeletesMatchingItems6(t *testing.T) {
rb := NewRingBuffer[int](5)
rb.Push(1) // overriden
rb.Push(2) // overriden
rb.Push(3)
rb.Push(9)
rb.Push(4)
rb.Push(5)
rb.Push(7)
removed := rb.Remove(func(v int) bool { return v == 1 })
if removed != 0 {
t.Errorf("Expected 0 items removed, got %d", removed)
}
if rb.Size() != 5 {
t.Errorf("Expected size 5, got %d", rb.Size())
}
expected := []int{3, 9, 4, 5, 7}
items := rb.Items()
for i, item := range items {
if item != expected[i] {
t.Errorf("Expected item %d, got %d", expected[i], item)
}
}
if !rb.IsFull() {
t.Errorf("Expected buffer to not be full")
}
}
func TestRingBufferRemoveDeletesMatchingItems7(t *testing.T) {
rb := NewRingBuffer[int](5)
rb.Push(1) // overriden
rb.Push(2) // overriden
rb.Push(3)
rb.Push(9)
rb.Push(4)
rb.Push(5)
rb.Push(7)
removed := rb.Remove(func(v int) bool { return v == 9 })
if removed != 1 {
t.Errorf("Expected 1 items removed, got %d", removed)
}
if rb.Size() != 4 {
t.Errorf("Expected size 4, got %d", rb.Size())
}
expected := []int{3, 4, 5, 7}
items := rb.Items()
for i, item := range items {
if item != expected[i] {
t.Errorf("Expected item %d, got %d", expected[i], item)
}
}
if rb.IsFull() {
t.Errorf("Expected buffer to not be full")
}
}
func TestRingBufferAddItemsToFullRingBuffer(t *testing.T) {
rb := NewRingBuffer[int](3)
rb.Push(1)
rb.Push(2)
rb.Push(3)
rb.Push(4)
if rb.Size() != 3 {
t.Errorf("Expected size 3, got %d", rb.Size())
}
expected := []int{2, 3, 4}
items := rb.Items()
for i, item := range items {
if item != expected[i] {
t.Errorf("Expected item %d, got %d", expected[i], item)
}
}
}
func TestRingBufferAddItemsToNonFullRingBuffer(t *testing.T) {
rb := NewRingBuffer[int](3)
rb.Push(1)
rb.Push(2)
if rb.Size() != 2 {
t.Errorf("Expected size 2, got %d", rb.Size())
}
expected := []int{1, 2}
items := rb.Items()
for i, item := range items {
if item != expected[i] {
t.Errorf("Expected item %d, got %d", expected[i], item)
}
}
}
func TestRingBufferRemoveItemsFromNonFullRingBuffer(t *testing.T) {
rb := NewRingBuffer[int](3)
rb.Push(1)
rb.Push(2)
removed := rb.Remove(func(v int) bool { return v == 1 })
if removed != 1 {
t.Errorf("Expected 1 item removed, got %d", removed)
}
if rb.Size() != 1 {
t.Errorf("Expected size 1, got %d", rb.Size())
}
expected := []int{2}
items := rb.Items()
for i, item := range items {
if item != expected[i] {
t.Errorf("Expected item %d, got %d", expected[i], item)
}
}
}
func TestRingBufferRemoveItemsFromFullRingBuffer(t *testing.T) {
rb := NewRingBuffer[int](3)
rb.Push(1)
rb.Push(2)
rb.Push(3)
removed := rb.Remove(func(v int) bool { return v == 2 })
if removed != 1 {
t.Errorf("Expected 1 item removed, got %d", removed)
}
if rb.Size() != 2 {
t.Errorf("Expected size 2, got %d", rb.Size())
}
expected := []int{1, 3}
items := rb.Items()
for i, item := range items {
if item != expected[i] {
t.Errorf("Expected item %d, got %d", expected[i], item)
}
}
}
func TestRingBufferRemoveMultipleItemsFromRingBuffer(t *testing.T) {
rb := NewRingBuffer[int](5)
rb.Push(1)
rb.Push(2)
rb.Push(3)
rb.Push(2)
rb.Push(4)
removed := rb.Remove(func(v int) bool { return v == 2 })
if removed != 2 {
t.Errorf("Expected 2 items removed, got %d", removed)
}
if rb.Size() != 3 {
t.Errorf("Expected size 3, got %d", rb.Size())
}
expected := []int{1, 3, 4}
items := rb.Items()
for i, item := range items {
if item != expected[i] {
t.Errorf("Expected item %d, got %d", expected[i], item)
}
}
}
func TestRingBufferRemoveAllItemsFromRingBuffer(t *testing.T) {
rb := NewRingBuffer[int](3)
rb.Push(1)
rb.Push(2)
rb.Push(3)
removed := rb.Remove(func(v int) bool { return true })
if removed != 3 {
t.Errorf("Expected 3 items removed, got %d", removed)
}
if rb.Size() != 0 {
t.Errorf("Expected size 0, got %d", rb.Size())
}
}
func TestRingBufferRemoveNoItemsFromRingBuffer(t *testing.T) {
rb := NewRingBuffer[int](3)
rb.Push(1)
rb.Push(2)
rb.Push(3)
removed := rb.Remove(func(v int) bool { return false })
if removed != 0 {
t.Errorf("Expected 0 items removed, got %d", removed)
}
if rb.Size() != 3 {
t.Errorf("Expected size 3, got %d", rb.Size())
}
}
func TestRingBufferIteratesOverAllItems(t *testing.T) {
rb := NewRingBuffer[int](3)
rb.Push(1)
rb.Push(2)
rb.Push(3)
expected := []int{1, 2, 3}
i := 0
for item := range rb.Iter() {
if item != expected[i] {
t.Errorf("Expected item %d, got %d", expected[i], item)
}
i++
}
if i != len(expected) {
t.Errorf("Expected to iterate over %d items, but iterated over %d", len(expected), i)
}
}
func TestRingBufferIter2IteratesOverAllItemsWithIndices(t *testing.T) {
rb := NewRingBuffer[int](3)
rb.Push(1)
rb.Push(2)
rb.Push(3)
expected := []int{1, 2, 3}
i := 0
for index, item := range rb.Iter2() {
if index != i {
t.Errorf("Expected index %d, got %d", i, index)
}
if item != expected[i] {
t.Errorf("Expected item %d, got %d", expected[i], item)
}
i++
}
if i != len(expected) {
t.Errorf("Expected to iterate over %d items, but iterated over %d", len(expected), i)
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ package dataext
import ( import (
"errors" "errors"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"sync" "sync"
) )
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"hash" "hash"
"io" "io"
"reflect" "reflect"
+2 -2
View File
@@ -1,8 +1,8 @@
package dataext package dataext
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/tst" "git.blackforestbytes.com/BlackForestBytes/goext/tst"
"testing" "testing"
) )
+256
View File
@@ -0,0 +1,256 @@
package dataext
import "sync"
// SyncMap is a thread-safe map implementation for generic key-value pairs.
// All functions aresafe to be called in parallel.
type SyncMap[TKey comparable, TData any] struct {
data map[TKey]TData
lock sync.Mutex
}
func NewSyncMap[TKey comparable, TData any]() *SyncMap[TKey, TData] {
return &SyncMap[TKey, TData]{data: make(map[TKey]TData), lock: sync.Mutex{}}
}
// Set sets the value for the provided key
func (s *SyncMap[TKey, TData]) Set(key TKey, data TData) {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TKey]TData)
}
s.data[key] = data
}
// SetIfNotContains sets the value for the provided key if it does not already exist
func (s *SyncMap[TKey, TData]) SetIfNotContains(key TKey, data TData) bool {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TKey]TData)
}
if _, existsInPreState := s.data[key]; existsInPreState {
return false
}
s.data[key] = data
return true
}
// SetIfNotContainsFunc sets the value for the provided key using the provided function
func (s *SyncMap[TKey, TData]) SetIfNotContainsFunc(key TKey, data func() TData) bool {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TKey]TData)
}
if _, existsInPreState := s.data[key]; existsInPreState {
return false
}
s.data[key] = data()
return true
}
// Get retrieves the value for the provided key
func (s *SyncMap[TKey, TData]) Get(key TKey) (TData, bool) {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TKey]TData)
}
if v, ok := s.data[key]; ok {
return v, true
} else {
return *new(TData), false
}
}
// GetAndSetIfNotContains returns the existing value if the key exists.
// Otherwise, it sets the provided value and returns it.
func (s *SyncMap[TKey, TData]) GetAndSetIfNotContains(key TKey, data TData) TData {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TKey]TData)
}
if v, ok := s.data[key]; ok {
return v
} else {
s.data[key] = data
return data
}
}
// GetAndSetIfNotContainsFunc returns the existing value if the key exists.
// Otherwise, it calls the provided function to generate the value, sets it, and returns it.
func (s *SyncMap[TKey, TData]) GetAndSetIfNotContainsFunc(key TKey, data func() TData) TData {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TKey]TData)
}
if v, ok := s.data[key]; ok {
return v
} else {
dataObj := data()
s.data[key] = dataObj
return dataObj
}
}
// Delete removes the entry with the provided key and returns true if the key existed before.
func (s *SyncMap[TKey, TData]) Delete(key TKey) bool {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TKey]TData)
}
_, ok := s.data[key]
delete(s.data, key)
return ok
}
// DeleteIf deletes all entries that match the provided function and returns the number of removed entries.
func (s *SyncMap[TKey, TData]) DeleteIf(fn func(key TKey, data TData) bool) int {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TKey]TData)
}
rm := 0
for k, v := range s.data {
if fn(k, v) {
delete(s.data, k)
rm++
}
}
return rm
}
// UpdateIfExists updates the value if the key exists, otherwise it does nothing.
func (s *SyncMap[TKey, TData]) UpdateIfExists(key TKey, fn func(data TData) TData) bool {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TKey]TData)
}
if v, ok := s.data[key]; ok {
s.data[key] = fn(v)
return true
} else {
return false
}
}
// UpdateOrInsert updates the value if the key exists, otherwise it inserts the provided `insertValue`.
func (s *SyncMap[TKey, TData]) UpdateOrInsert(key TKey, fn func(data TData) TData, insertValue TData) bool {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TKey]TData)
}
if v, ok := s.data[key]; ok {
s.data[key] = fn(v)
return true
} else {
s.data[key] = insertValue
return false
}
}
// Clear removes all entries from the map.
func (s *SyncMap[TKey, TData]) Clear() {
s.lock.Lock()
defer s.lock.Unlock()
s.data = make(map[TKey]TData)
}
// Contains checks if the map contains the provided key.
func (s *SyncMap[TKey, TData]) Contains(key TKey) bool {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TKey]TData)
}
_, ok := s.data[key]
return ok
}
// GetAllKeys returns a copy (!) of all keys in the map.
func (s *SyncMap[TKey, TData]) GetAllKeys() []TKey {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TKey]TData)
}
r := make([]TKey, 0, len(s.data))
for k := range s.data {
r = append(r, k)
}
return r
}
// GetAllValues returns a copy (!) of all values in the map.
func (s *SyncMap[TKey, TData]) GetAllValues() []TData {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TKey]TData)
}
r := make([]TData, 0, len(s.data))
for _, v := range s.data {
r = append(r, v)
}
return r
}
// Count returns the number of entries in the map.
func (s *SyncMap[TKey, TData]) Count() int {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TKey]TData)
}
return len(s.data)
}
+143
View File
@@ -0,0 +1,143 @@
package dataext
import "sync"
type SyncRingSet[TData comparable] struct {
data map[TData]bool
lock sync.Mutex
ring *RingBuffer[TData]
}
func NewSyncRingSet[TData comparable](capacity int) *SyncRingSet[TData] {
return &SyncRingSet[TData]{
data: make(map[TData]bool, capacity+1),
lock: sync.Mutex{},
ring: NewRingBuffer[TData](capacity),
}
}
// Add adds `value` to the set
// returns true if the value was actually inserted (value did not exist beforehand)
// returns false if the value already existed
func (s *SyncRingSet[TData]) Add(value TData) bool {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TData]bool)
}
_, existsInPreState := s.data[value]
if existsInPreState {
return false
}
prev := s.ring.PushPop(value)
s.data[value] = true
if prev != nil {
delete(s.data, *prev)
}
return true
}
func (s *SyncRingSet[TData]) AddAll(values []TData) {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TData]bool)
}
for _, value := range values {
_, existsInPreState := s.data[value]
if existsInPreState {
continue
}
prev := s.ring.PushPop(value)
s.data[value] = true
if prev != nil {
delete(s.data, *prev)
}
}
}
func (s *SyncRingSet[TData]) Remove(value TData) bool {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TData]bool)
}
_, existsInPreState := s.data[value]
if !existsInPreState {
return false
}
delete(s.data, value)
s.ring.Remove(func(v TData) bool { return value == v })
return true
}
func (s *SyncRingSet[TData]) RemoveAll(values []TData) {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TData]bool)
}
for _, value := range values {
delete(s.data, value)
s.ring.Remove(func(v TData) bool { return value == v })
}
}
func (s *SyncRingSet[TData]) Contains(value TData) bool {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TData]bool)
}
_, ok := s.data[value]
return ok
}
func (s *SyncRingSet[TData]) Get() []TData {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TData]bool)
}
r := make([]TData, 0, len(s.data))
for k := range s.data {
r = append(r, k)
}
return r
}
// AddIfNotContains
// returns true if the value was actually added (value did not exist beforehand)
// returns false if the value already existed
func (s *SyncRingSet[TData]) AddIfNotContains(key TData) bool {
return s.Add(key)
}
// RemoveIfContains
// returns true if the value was actually removed (value did exist beforehand)
// returns false if the value did not exist in the set
func (s *SyncRingSet[TData]) RemoveIfContains(key TData) bool {
return s.Remove(key)
}
+54 -3
View File
@@ -7,8 +7,12 @@ type SyncSet[TData comparable] struct {
lock sync.Mutex lock sync.Mutex
} }
func NewSyncSet[TData comparable]() *SyncSet[TData] {
return &SyncSet[TData]{data: make(map[TData]bool), lock: sync.Mutex{}}
}
// Add adds `value` to the set // Add adds `value` to the set
// returns true if the value was actually inserted // returns true if the value was actually inserted (value did not exist beforehand)
// returns false if the value already existed // returns false if the value already existed
func (s *SyncSet[TData]) Add(value TData) bool { func (s *SyncSet[TData]) Add(value TData) bool {
s.lock.Lock() s.lock.Lock()
@@ -19,9 +23,12 @@ func (s *SyncSet[TData]) Add(value TData) bool {
} }
_, existsInPreState := s.data[value] _, existsInPreState := s.data[value]
s.data[value] = true if existsInPreState {
return false
}
return !existsInPreState s.data[value] = true
return true
} }
func (s *SyncSet[TData]) AddAll(values []TData) { func (s *SyncSet[TData]) AddAll(values []TData) {
@@ -37,6 +44,36 @@ func (s *SyncSet[TData]) AddAll(values []TData) {
} }
} }
func (s *SyncSet[TData]) Remove(value TData) bool {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TData]bool)
}
_, existsInPreState := s.data[value]
if !existsInPreState {
return false
}
delete(s.data, value)
return true
}
func (s *SyncSet[TData]) RemoveAll(values []TData) {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TData]bool)
}
for _, value := range values {
delete(s.data, value)
}
}
func (s *SyncSet[TData]) Contains(value TData) bool { func (s *SyncSet[TData]) Contains(value TData) bool {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@@ -66,3 +103,17 @@ func (s *SyncSet[TData]) Get() []TData {
return r return r
} }
// AddIfNotContains
// returns true if the value was actually added (value did not exist beforehand)
// returns false if the value already existed
func (s *SyncSet[TData]) AddIfNotContains(key TData) bool {
return s.Add(key)
}
// RemoveIfContains
// returns true if the value was actually removed (value did exist beforehand)
// returns false if the value did not exist in the set
func (s *SyncSet[TData]) RemoveIfContains(key TData) bool {
return s.Remove(key)
}
+241
View File
@@ -0,0 +1,241 @@
package dataext
type ValueGroup interface {
TupleLength() int
TupleValues() []any
}
// ----------------------------------------------------------------------------
type Single[T1 any] struct {
V1 T1
}
func (s Single[T1]) TupleLength() int {
return 1
}
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 {
V1 T1
V2 T2
}
func (t Tuple[T1, T2]) TupleLength() int {
return 2
}
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 {
V1 T1
V2 T2
V3 T3
}
func (t Triple[T1, T2, T3]) TupleLength() int {
return 3
}
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 {
V1 T1
V2 T2
V3 T3
V4 T4
}
func (t Quadruple[T1, T2, T3, T4]) TupleLength() int {
return 4
}
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 {
V1 T1
V2 T2
V3 T3
V4 T4
V5 T5
}
func (t Quintuple[T1, T2, T3, T4, T5]) TupleLength() int {
return 5
}
func (t Quintuple[T1, T2, T3, T4, T5]) TupleValues() []any {
return []any{t.V1, t.V2, t.V3, t.V4, t.V5}
}
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 {
V1 T1
V2 T2
V3 T3
V4 T4
V5 T5
V6 T6
}
func (t Sextuple[T1, T2, T3, T4, T5, T6]) TupleLength() int {
return 6
}
func (t Sextuple[T1, T2, T3, T4, T5, T6]) TupleValues() []any {
return []any{t.V1, t.V2, t.V3, t.V4, t.V5, t.V6}
}
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 {
V1 T1
V2 T2
V3 T3
V4 T4
V5 T5
V6 T6
V7 T7
}
func (t Septuple[T1, T2, T3, T4, T5, T6, T7]) TupleLength() int {
return 7
}
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 {
V1 T1
V2 T2
V3 T3
V4 T4
V5 T5
V6 T6
V7 T7
V8 T8
}
func (t Octuple[T1, T2, T3, T4, T5, T6, T7, T8]) TupleLength() int {
return 8
}
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 {
V1 T1
V2 T2
V3 T3
V4 T4
V5 T5
V6 T6
V7 T7
V8 T8
V9 T9
}
func (t Nonuple[T1, T2, T3, T4, T5, T6, T7, T8, T9]) TupleLength() int {
return 9
}
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}
}
+57
View File
@@ -0,0 +1,57 @@
package enums
import "encoding/json"
type Enum interface {
Valid() bool
ValuesAny() []any
ValuesMeta() []EnumMetaValue
VarName() string
TypeName() string
PackageName() string
}
type StringEnum interface {
Enum
String() string
}
type DescriptionEnum interface {
Enum
Description() string
DescriptionMeta() EnumDescriptionMetaValue
}
type EnumMetaValue struct {
VarName string `json:"varName"`
Value Enum `json:"value"`
Description *string `json:"description"`
}
type EnumDescriptionMetaValue struct {
VarName string `json:"varName"`
Value Enum `json:"value"`
Description string `json:"description"`
}
type EnumDataMetaValue struct {
VarName string `json:"varName"`
Value Enum `json:"value"`
Description *string `json:"description"`
Data map[string]any `json:"-"` //handled by MarshalJSON
}
func (v EnumDataMetaValue) MarshalJSON() ([]byte, error) {
m := make(map[string]any, 8)
for k, dv := range v.Data {
m[k] = dv
}
m["varName"] = v.VarName
m["value"] = v.Value
m["description"] = v.Description
return json.Marshal(m)
}
+300
View File
@@ -0,0 +1,300 @@
package excelext
import (
"reflect"
"git.blackforestbytes.com/BlackForestBytes/goext/dataext"
"git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
excelize360 "github.com/360EntSecGroup-Skylar/excelize"
"github.com/xuri/excelize/v2"
)
type excelMapperColDefinition[T any] struct {
style *int
header string
width *float64
fn func(T) (any, error)
}
type ExcelMapper[T any] struct {
StyleDate *int
StyleDatetime *int
StyleEUR *int
StylePercentage *int
StyleHeader *int
StyleWSHeader *int
SkipColumnHeader bool
sheetName string
wsHeader []dataext.Tuple[string, *int]
colDefinitions []excelMapperColDefinition[T]
colFilter []func(v T) bool
}
func NewExcelMapper[T any]() (*ExcelMapper[T], error) {
em := &ExcelMapper[T]{
StyleDate: nil,
StyleDatetime: nil,
StyleEUR: nil,
StylePercentage: nil,
StyleHeader: nil,
StyleWSHeader: nil,
sheetName: "",
SkipColumnHeader: false,
wsHeader: make([]dataext.Tuple[string, *int], 0),
colDefinitions: make([]excelMapperColDefinition[T], 0),
}
return em, nil
}
func (em *ExcelMapper[T]) InitNewFile(sheetName string) (*excelize.File, error) {
f := excelize.NewFile()
defSheet := f.GetSheetList()[0]
sheet1 := sheetName
sheetIdx, err := f.NewSheet(sheet1)
if err != nil {
return nil, err
}
f.SetActiveSheet(sheetIdx)
err = f.DeleteSheet(defSheet)
err = em.InitStyles(f)
if err != nil {
return nil, err
}
return f, nil
}
func (em *ExcelMapper[T]) InitStyles(f *excelize.File) error {
styleDate, err := f.NewStyle(&excelize.Style{
CustomNumFmt: langext.Ptr("dd.mm.yyyy"),
})
if err != nil {
return err
}
styleDatetime, err := f.NewStyle(&excelize.Style{
NumFmt: 22,
})
if err != nil {
return err
}
styleEUR, err := f.NewStyle(&excelize.Style{
NumFmt: 218,
})
if err != nil {
return err
}
stylePercentage, err := f.NewStyle(&excelize.Style{
NumFmt: 10,
})
if err != nil {
return err
}
styleHeader, err := f.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true, Size: 11},
})
if err != nil {
return err
}
styleWSHeader, err := f.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true, Size: 24},
})
if err != nil {
return err
}
em.StyleDate = &styleDate
em.StyleDatetime = &styleDatetime
em.StyleEUR = &styleEUR
em.StylePercentage = &stylePercentage
em.StyleHeader = &styleHeader
em.StyleWSHeader = &styleWSHeader
return nil
}
func (em *ExcelMapper[T]) AddWorksheetHeader(header string, style *int) {
em.wsHeader = append(em.wsHeader, dataext.NewTuple(header, style))
}
func (em *ExcelMapper[T]) AddColumn(header string, style *int, width *float64, fn func(T) any) {
em.colDefinitions = append(em.colDefinitions, excelMapperColDefinition[T]{
style: style,
header: header,
width: width,
fn: func(t T) (any, error) { return fn(t), nil },
})
}
func (em *ExcelMapper[T]) AddColumnErr(header string, style *int, width *float64, fn func(T) (any, error)) {
em.colDefinitions = append(em.colDefinitions, excelMapperColDefinition[T]{
style: style,
header: header,
width: width,
fn: fn,
})
}
func (em *ExcelMapper[T]) Build(sheetName string, data []T) ([]byte, error) {
f, err := em.InitNewFile(sheetName)
if err != nil {
return nil, exerr.Wrap(err, "failed to init new file").Build()
}
err = em.BuildSingleSheet(f, sheetName, data)
if err != nil {
return nil, exerr.Wrap(err, "").Build()
}
buffer, err := f.WriteToBuffer()
if err != nil {
return nil, exerr.Wrap(err, "failed to build xls").Build()
}
return buffer.Bytes(), nil
}
func (em *ExcelMapper[T]) BuildSingleSheet(f *excelize.File, sheetName string, data []T) error {
if em.StyleHeader == nil || em.StyleDate == nil || em.StyleDatetime == nil || em.StyleEUR == nil || em.StylePercentage == nil || em.StyleWSHeader == nil {
err := em.InitStyles(f)
if err != nil {
return exerr.Wrap(err, "failed to init styles").Build()
}
}
rowOffset := 0
if len(em.wsHeader) > 0 {
for range em.wsHeader {
rowOffset += 1
}
rowOffset += 1
}
if !em.SkipColumnHeader {
for i, col := range em.colDefinitions {
err := f.SetCellValue(sheetName, c(rowOffset+1, i), col.header)
if err != nil {
return err
}
}
}
for i, col := range em.colDefinitions {
if col.style != nil {
err := f.SetColStyle(sheetName, excelize360.ToAlphaString(i), *col.style)
if err != nil {
return err
}
}
}
for i, col := range em.colDefinitions {
if col.width != nil {
err := f.SetColWidth(sheetName, excelize360.ToAlphaString(i), excelize360.ToAlphaString(i), *col.width)
if err != nil {
return err
}
}
}
err := f.SetRowStyle(sheetName, rowOffset+1, rowOffset+1, *em.StyleHeader)
if err != nil {
return err
}
if len(em.wsHeader) > 0 {
for i, hdr := range em.wsHeader {
style := *langext.CoalesceOpt(hdr.V2, em.StyleWSHeader)
err = f.SetCellValue(sheetName, c(i+1, 0), hdr.V1)
if err != nil {
return err
}
err = f.MergeCell(sheetName, c(i+1, 0), c(i+1, len(em.colDefinitions)-1))
if err != nil {
return err
}
err = f.SetRowStyle(sheetName, 1, 1, style)
if err != nil {
return err
}
}
}
iRow := rowOffset + 1
if !em.SkipColumnHeader {
iRow += 1
}
for _, dat := range data {
skip := false
for _, filter := range em.colFilter {
if !filter(dat) {
skip = true
break
}
}
if skip {
continue
}
for iCol, col := range em.colDefinitions {
cellVal, err := col.fn(dat)
if err != nil {
return err
}
for reflect.ValueOf(cellVal).Kind() == reflect.Pointer && !reflect.ValueOf(cellVal).IsNil() {
cellVal = reflect.ValueOf(cellVal).Elem().Interface()
}
if langext.IsNil(cellVal) {
err = f.SetCellValue(sheetName, c(iRow, iCol), "")
if err != nil {
return err
}
} else {
err = f.SetCellValue(sheetName, c(iRow, iCol), cellVal)
if err != nil {
return err
}
}
}
iRow++
}
//for i, col := range em.colDefinitions {
// if col.width == nil {
// //TODO https://github.com/qax-os/excelize/pull/1386
// }
//}
return nil
}
func (em *ExcelMapper[T]) AddFilter(f func(v T) bool) {
em.colFilter = append(em.colFilter, f)
}
+26
View File
@@ -0,0 +1,26 @@
package excelext
import (
"strconv"
"git.blackforestbytes.com/BlackForestBytes/goext/rfctime"
"github.com/360EntSecGroup-Skylar/excelize"
)
func c(row int, col int) string {
return excelize.ToAlphaString(col) + strconv.Itoa(row)
}
func excelizeOptTime(t *rfctime.RFC3339NanoTime) any {
if t == nil {
return ""
}
return t.Time()
}
func excelizeOptDate(t *rfctime.Date) any {
if t == nil {
return ""
}
return t.TimeUTC()
}
+220 -79
View File
@@ -5,16 +5,18 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog"
"go.mongodb.org/mongo-driver/bson/primitive"
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http" "net/http"
"os" "os"
"runtime/debug" "runtime/debug"
"strings" "strings"
"time" "time"
"git.blackforestbytes.com/BlackForestBytes/goext/dataext"
"git.blackforestbytes.com/BlackForestBytes/goext/enums"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog"
"go.mongodb.org/mongo-driver/bson/primitive"
) )
// //
@@ -29,6 +31,10 @@ import (
// If possible add metadata to the error (eg the id that was not found, ...), the methods are the same as in zerolog // 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() // 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 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 also manually set the statuscode with `.WithStatuscode(http.NotFound)`
// You can set the type with `WithType(..)` // You can set the type with `WithType(..)`
@@ -54,21 +60,12 @@ import (
// => Wrap/New + Fatal // => Wrap/New + Fatal
// //
var stackSkipLogger zerolog.Logger
func init() {
cw := zerolog.ConsoleWriter{
Out: os.Stdout,
TimeFormat: "2006-01-02 15:04:05 Z07:00",
}
multi := zerolog.MultiLevelWriter(cw)
stackSkipLogger = zerolog.New(multi).With().Timestamp().CallerWithSkipFrameCount(4).Logger()
}
type Builder struct { type Builder struct {
errorData *ExErr wrappedErr error
containsGinData bool errorData *ExErr
containsGinData bool
containsContextData bool
noLog bool
} }
func Get(err error) *Builder { func Get(err error) *Builder {
@@ -80,12 +77,18 @@ func New(t ErrorType, msg string) *Builder {
} }
func Wrap(err error, msg string) *Builder { func Wrap(err error, msg string) *Builder {
if !pkgconfig.RecursiveErrors { if err == nil {
v := FromError(err) return &Builder{errorData: newExErr(CatSystem, TypeInternal, msg)} // prevent NPE if we call Wrap with err==nil
v.Message = msg }
return &Builder{errorData: v}
v := FromError(err)
if !pkgconfig.RecursiveErrors {
v.Message = msg
return &Builder{wrappedErr: err, errorData: v}
} else {
return &Builder{wrappedErr: err, errorData: wrapExErr(v, msg, CatWrap, 1)}
} }
return &Builder{errorData: wrapExErr(FromError(err), msg, CatWrap, 1)}
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@@ -105,6 +108,16 @@ func (b *Builder) WithMessage(msg string) *Builder {
return b return b
} }
func (b *Builder) WithSeverity(v ErrorSeverity) *Builder {
b.errorData.Severity = v
return b
}
func (b *Builder) WithCategory(v ErrorCategory) *Builder {
b.errorData.Category = v
return b
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Err changes the Severity to ERROR (default) // Err changes the Severity to ERROR (default)
@@ -185,6 +198,13 @@ func (b *Builder) System() *Builder {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
func (b *Builder) NoLog() *Builder {
b.noLog = true
return b
}
// ----------------------------------------------------------------------------
func (b *Builder) Id(key string, val fmt.Stringer) *Builder { func (b *Builder) Id(key string, val fmt.Stringer) *Builder {
return b.addMeta(key, MDTID, newIDWrap(val)) return b.addMeta(key, MDTID, newIDWrap(val))
} }
@@ -270,13 +290,17 @@ func (b *Builder) Any(key string, val any) *Builder {
} }
func (b *Builder) Stringer(key string, val fmt.Stringer) *Builder { func (b *Builder) Stringer(key string, val fmt.Stringer) *Builder {
if val == nil { if langext.IsNil(val) {
return b.addMeta(key, MDTString, "(!nil)") return b.addMeta(key, MDTString, "(!nil)")
} else { } else {
return b.addMeta(key, MDTString, val.String()) return b.addMeta(key, MDTString, val.String())
} }
} }
func (b *Builder) Enum(key string, val enums.Enum) *Builder {
return b.addMeta(key, MDTEnum, newEnumWrap(val))
}
func (b *Builder) Stack() *Builder { func (b *Builder) Stack() *Builder {
return b.addMeta("@Stack", MDTString, string(debug.Stack())) return b.addMeta("@Stack", MDTString, string(debug.Stack()))
} }
@@ -291,68 +315,86 @@ func (b *Builder) Errs(key string, val []error) *Builder {
func (b *Builder) GinReq(ctx context.Context, g *gin.Context, req *http.Request) *Builder { func (b *Builder) GinReq(ctx context.Context, g *gin.Context, req *http.Request) *Builder {
if v := ctx.Value("start_timestamp"); v != nil { if v := ctx.Value("start_timestamp"); v != nil {
if t, ok := v.(time.Time); ok { if t, ok := v.(time.Time); ok {
b.Time("ctx.startTimestamp", t) b.Time("ctx_startTimestamp", t)
b.Time("ctx.endTimestamp", time.Now()) b.Time("ctx_endTimestamp", time.Now())
} }
} }
b.Str("gin.method", req.Method) b.Str("gin_method", req.Method)
b.Str("gin.path", g.FullPath()) b.Str("gin_host", req.Host)
b.Str("gin.header", formatHeader(g.Request.Header)) b.Str("gin_path", g.FullPath())
b.Strs("gin_header", extractHeader(g.Request.Header))
if req.URL != nil { if req.URL != nil {
b.Str("gin.url", req.URL.String()) b.Str("gin_url", req.URL.String())
} }
if ctxVal := g.GetString("apiversion"); ctxVal != "" { if ctxVal := g.GetString("apiversion"); ctxVal != "" {
b.Str("gin.context.apiversion", ctxVal) b.Str("gin_context_apiversion", ctxVal)
} }
if ctxVal := g.GetString("uid"); ctxVal != "" { if ctxVal := g.GetString("uid"); ctxVal != "" {
b.Str("gin.context.uid", ctxVal) b.Str("gin_context_uid", ctxVal)
} }
if ctxVal := g.GetString("fcmId"); ctxVal != "" { if ctxVal := g.GetString("fcmId"); ctxVal != "" {
b.Str("gin.context.fcmid", ctxVal) b.Str("gin_context_fcmid", ctxVal)
} }
if ctxVal := g.GetString("reqid"); ctxVal != "" { if ctxVal := g.GetString("reqid"); ctxVal != "" {
b.Str("gin.context.reqid", ctxVal) b.Str("gin_context_reqid", ctxVal)
} }
if req.Method != "GET" && req.Body != nil && req.Header.Get("Content-Type") == "application/json" { if req.Method != "GET" && req.Body != nil {
if brc, ok := req.Body.(dataext.BufferedReadCloser); ok {
if bin, err := brc.BufferedAll(); err == nil { if req.Header.Get("Content-Type") == "application/json" {
if len(bin) < 16*1024 { if brc, ok := req.Body.(dataext.BufferedReadCloser); ok {
var prettyJSON bytes.Buffer if bin, err := brc.BufferedAll(); err == nil {
err = json.Indent(&prettyJSON, bin, "", " ") if len(bin) < 16*1024 {
if err == nil { var prettyJSON bytes.Buffer
b.Str("gin.body", string(prettyJSON.Bytes())) err = json.Indent(&prettyJSON, bin, "", " ")
if err == nil {
b.Str("gin_body", string(prettyJSON.Bytes()))
} else {
b.Bytes("gin_body", bin)
}
} else { } else {
b.Bytes("gin.body", bin) b.Str("gin_body", fmt.Sprintf("[[%v bytes | %s]]", len(bin), req.Header.Get("Content-Type")))
} }
} else {
b.Str("gin.body", fmt.Sprintf("[[%v bytes]]", len(bin)))
} }
} }
} }
if req.Header.Get("Content-Type") == "multipart/form-data" || req.Header.Get("Content-Type") == "x-www-form-urlencoded" {
if brc, ok := req.Body.(dataext.BufferedReadCloser); ok {
if bin, err := brc.BufferedAll(); err == nil {
if len(bin) < 16*1024 {
b.Bytes("gin_body", bin)
} else {
b.Str("gin_body", fmt.Sprintf("[[%v bytes | %s]]", len(bin), req.Header.Get("Content-Type")))
}
}
}
}
} }
pkgconfig.ExtendGinMeta(ctx, b, g, req)
b.containsGinData = true b.containsGinData = true
return b return b
} }
func formatHeader(header map[string][]string) string { func (b *Builder) CtxData(method Method, ctx context.Context) *Builder {
ml := 1 pkgconfig.ExtendContextMeta(b, method, ctx)
for k, _ := range header {
if len(k) > ml { b.containsContextData = true
ml = len(k)
} return b
} }
r := ""
func extractHeader(header map[string][]string) []string {
r := make([]string, 0, len(header))
for k, v := range header { for k, v := range header {
if r != "" {
r += "\n"
}
for _, hval := range v { for _, hval := range v {
value := hval value := hval
value = strings.ReplaceAll(value, "\n", "\\n") value = strings.ReplaceAll(value, "\n", "\\n")
value = strings.ReplaceAll(value, "\r", "\\r") value = strings.ReplaceAll(value, "\r", "\\r")
value = strings.ReplaceAll(value, "\t", "\\t") value = strings.ReplaceAll(value, "\t", "\\t")
r += langext.StrPadRight(k, " ", ml) + " := " + value r = append(r, k+": "+value)
} }
} }
return r return r
@@ -360,18 +402,52 @@ func formatHeader(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 // 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 // If the errors is not SevWarn or SevInfo it gets also logged (in short form, without stacktrace) onto stdout
func (b *Builder) Build() error { // Can be gloablly configured with ZeroLogErrTraces and ZeroLogAllTraces
// Can be locally suppressed with Builder.NoLog()
func (b *Builder) Build(ctxs ...context.Context) error {
return b.BuildAsExerr(ctxs...)
}
func (b *Builder) BuildAsExerr(ctxs ...context.Context) *ExErr {
warnOnPkgConfigNotInitialized() warnOnPkgConfigNotInitialized()
if pkgconfig.ZeroLogErrTraces && (b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal) { for _, dctx := range ctxs {
b.errorData.ShortLog(stackSkipLogger.Error()) b.CtxData(MethodBuild, dctx)
} else if pkgconfig.ZeroLogAllTraces {
b.errorData.ShortLog(stackSkipLogger.Error())
} }
b.CallListener(MethodBuild) if pkgconfig.DisableErrorWrapping && b.wrappedErr != nil {
return FromError(b.wrappedErr)
}
if pkgconfig.ZeroLogErrTraces && !b.noLog {
if b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal {
b.errorData.ShortLog(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 if b.errorData.Severity == SevDebug {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Debug())
} else if b.errorData.Severity == SevTrace {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Trace())
} else {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Error()) // ?!? unknown severity
}
}
b.errorData.CallListener(MethodBuild, ListenerOpt{NoLog: b.noLog})
return b.errorData return b.errorData
} }
@@ -380,32 +456,85 @@ func (b *Builder) Build() error {
// The error also gets printed to stdout/stderr // The error also gets printed to stdout/stderr
// If the error is SevErr|SevFatal we also send it to the error-service // If the error is SevErr|SevFatal we also send it to the error-service
func (b *Builder) Output(ctx context.Context, g *gin.Context) { func (b *Builder) Output(ctx context.Context, g *gin.Context) {
warnOnPkgConfigNotInitialized()
if !b.containsGinData && g.Request != nil { if !b.containsGinData && g.Request != nil {
// Auto-Add gin metadata if the caller hasn't already done it // Auto-Add gin metadata if the caller hasn't already done it
b.GinReq(ctx, g, g.Request) b.GinReq(ctx, g, g.Request)
} }
b.CtxData(MethodOutput, ctx)
// this is only here to add one level to the trace
// so that .Build() and .Output() and .Print() have the same depth and our stack-skip logger can have the same skip-count
b.doGinOutput(ctx, g)
}
// OutputRaw works teh same as Output() - but does not depend on gin and works with a raw http.ResponseWriter
func (b *Builder) OutputRaw(w http.ResponseWriter) {
warnOnPkgConfigNotInitialized()
// this is only here to add one level to the trace
// so that .Build() and .Output() and .Print() have the same depth and our stack-skip logger can have the same skip-count
b.doRawOutput(w)
}
func (b *Builder) doGinOutput(ctx context.Context, g *gin.Context) {
b.errorData.Output(g) b.errorData.Output(g)
if b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal { if (b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal) && (pkgconfig.ZeroLogErrGinOutput || pkgconfig.ZeroLogAllGinOutput) {
b.errorData.Log(stackSkipLogger.Error()) b.errorData.Log(pkgconfig.ZeroLogger.Error())
} else if b.errorData.Severity == SevWarn { } else if (b.errorData.Severity == SevWarn) && (pkgconfig.ZeroLogAllGinOutput) {
b.errorData.Log(stackSkipLogger.Warn()) b.errorData.Log(pkgconfig.ZeroLogger.Warn())
} }
b.CallListener(MethodOutput) b.errorData.CallListener(MethodOutput, ListenerOpt{NoLog: b.noLog})
}
func (b *Builder) doRawOutput(w http.ResponseWriter) {
b.errorData.OutputRaw(w)
if (b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal) && (pkgconfig.ZeroLogErrGinOutput || pkgconfig.ZeroLogAllGinOutput) {
b.errorData.Log(pkgconfig.ZeroLogger.Error())
} else if (b.errorData.Severity == SevWarn) && (pkgconfig.ZeroLogAllGinOutput) {
b.errorData.Log(pkgconfig.ZeroLogger.Warn())
}
b.errorData.CallListener(MethodOutput, ListenerOpt{NoLog: b.noLog})
} }
// Print prints the error // Print prints the error
// If the error is SevErr we also send it to the error-service // If the error is SevErr we also send it to the error-service
func (b *Builder) Print() { func (b *Builder) Print(ctxs ...context.Context) Proxy {
if b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal { warnOnPkgConfigNotInitialized()
b.errorData.Log(stackSkipLogger.Error())
} else if b.errorData.Severity == SevWarn { for _, dctx := range ctxs {
b.errorData.ShortLog(stackSkipLogger.Warn()) b.CtxData(MethodPrint, dctx)
} }
b.CallListener(MethodPrint) // this is only here to add one level to the trace
// so that .Build() and .Output() and .Print() have the same depth and our stack-skip logger can have the same skip-count
return b.doPrint()
}
func (b *Builder) doPrint() Proxy {
if b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal {
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 if b.errorData.Severity == SevDebug {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Debug())
} else if b.errorData.Severity == SevTrace {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Trace())
} else {
b.errorData.Log(pkgconfig.ZeroLogger.Error()) // ?!? unknown severity
}
b.errorData.CallListener(MethodPrint, ListenerOpt{NoLog: b.noLog})
return Proxy{v: *b.errorData} // we return Proxy<Exerr> here instead of Exerr to prevent warnings on ignored err-returns
} }
func (b *Builder) Format(level LogPrintLevel) string { func (b *Builder) Format(level LogPrintLevel) string {
@@ -414,15 +543,27 @@ func (b *Builder) Format(level LogPrintLevel) string {
// Fatal prints the error and terminates the program // Fatal prints the error and terminates the program
// If the error is SevErr we also send it to the error-service // If the error is SevErr we also send it to the error-service
func (b *Builder) Fatal() { func (b *Builder) Fatal(ctxs ...context.Context) {
b.errorData.Severity = SevFatal
b.errorData.Log(stackSkipLogger.WithLevel(zerolog.FatalLevel))
b.CallListener(MethodFatal) b.errorData.Severity = SevFatal
for _, dctx := range ctxs {
b.CtxData(MethodFatal, dctx)
}
// this is only here to add one level to the trace
// so that .Build() and .Output() and .Print() have the same depth and our stack-skip logger can have the same skip-count
b.doLogFatal()
b.errorData.CallListener(MethodFatal, ListenerOpt{NoLog: b.noLog})
os.Exit(1) os.Exit(1)
} }
func (b *Builder) doLogFatal() {
b.errorData.Log(pkgconfig.ZeroLogger.WithLevel(zerolog.FatalLevel))
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
func (b *Builder) addMeta(key string, mdtype metaDataType, val interface{}) *Builder { func (b *Builder) addMeta(key string, mdtype metaDataType, val interface{}) *Builder {
+73 -3
View File
@@ -4,7 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/bson/primitive"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"reflect" "reflect"
"time" "time"
) )
@@ -12,11 +12,78 @@ import (
var reflectTypeStr = reflect.TypeOf("") var reflectTypeStr = reflect.TypeOf("")
func FromError(err error) *ExErr { func FromError(err error) *ExErr {
if err == nil {
// prevent NPE if we call FromError with err==nil
return &ExErr{
UniqueID: newID(),
Category: CatForeign,
Type: TypeInternal,
Severity: SevErr,
Timestamp: time.Time{},
StatusCode: nil,
Message: "",
WrappedErrType: "nil",
WrappedErr: err,
Caller: "",
OriginalError: nil,
Meta: make(MetaMap),
Extra: make(map[string]any),
}
}
//goland:noinspection GoTypeAssertionOnErrors
if verr, ok := err.(*ExErr); ok { if verr, ok := err.(*ExErr); ok {
// A simple ExErr // A simple ExErr
return verr return verr
} }
//goland:noinspection GoTypeAssertionOnErrors
if verr, ok := err.(langext.PanicWrappedErr); ok {
return &ExErr{
UniqueID: newID(),
Category: CatForeign,
Type: TypePanic,
Severity: SevErr,
Timestamp: time.Time{},
StatusCode: nil,
Message: "A panic occured",
WrappedErrType: fmt.Sprintf("%T", verr),
WrappedErr: err,
Caller: "",
OriginalError: nil,
Meta: MetaMap{
"panic_object": {DataType: MDTString, Value: fmt.Sprintf("%+v", verr.RecoveredObj())},
"panic_type": {DataType: MDTString, Value: fmt.Sprintf("%T", verr.RecoveredObj())},
"stack": {DataType: MDTString, Value: verr.Stack},
},
Extra: make(map[string]any),
}
}
//goland:noinspection GoTypeAssertionOnErrors
if verr, ok := err.(*langext.PanicWrappedErr); ok && verr != nil {
return &ExErr{
UniqueID: newID(),
Category: CatForeign,
Type: TypePanic,
Severity: SevErr,
Timestamp: time.Time{},
StatusCode: nil,
Message: "A panic occured",
WrappedErrType: fmt.Sprintf("%T", verr),
WrappedErr: err,
Caller: "",
OriginalError: nil,
Meta: MetaMap{
"panic_object": {DataType: MDTString, Value: fmt.Sprintf("%+v", verr.RecoveredObj())},
"panic_type": {DataType: MDTString, Value: fmt.Sprintf("%T", verr.RecoveredObj())},
"stack": {DataType: MDTString, Value: verr.Stack},
},
Extra: make(map[string]any),
}
}
// A foreign error (eg a MongoDB exception) // A foreign error (eg a MongoDB exception)
return &ExErr{ return &ExErr{
UniqueID: newID(), UniqueID: newID(),
@@ -31,6 +98,7 @@ func FromError(err error) *ExErr {
Caller: "", Caller: "",
OriginalError: nil, OriginalError: nil,
Meta: getForeignMeta(err), Meta: getForeignMeta(err),
Extra: make(map[string]any),
} }
} }
@@ -48,6 +116,7 @@ func newExErr(cat ErrorCategory, errtype ErrorType, msg string) *ExErr {
Caller: callername(2), Caller: callername(2),
OriginalError: nil, OriginalError: nil,
Meta: make(map[string]MetaValue), Meta: make(map[string]MetaValue),
Extra: make(map[string]any),
} }
} }
@@ -56,7 +125,7 @@ func wrapExErr(e *ExErr, msg string, cat ErrorCategory, stacktraceskip int) *ExE
UniqueID: newID(), UniqueID: newID(),
Category: cat, Category: cat,
Type: TypeWrap, Type: TypeWrap,
Severity: SevErr, Severity: e.Severity,
Timestamp: time.Now(), Timestamp: time.Now(),
StatusCode: e.StatusCode, StatusCode: e.StatusCode,
Message: msg, Message: msg,
@@ -65,6 +134,7 @@ func wrapExErr(e *ExErr, msg string, cat ErrorCategory, stacktraceskip int) *ExE
Caller: callername(1 + stacktraceskip), Caller: callername(1 + stacktraceskip),
OriginalError: e, OriginalError: e,
Meta: make(map[string]MetaValue), Meta: make(map[string]MetaValue),
Extra: langext.CopyMap(langext.ForceMap(e.Extra)),
} }
} }
@@ -181,7 +251,7 @@ func getReflectedMetaValues(value interface{}, remainingDepth int) map[string]Me
jsonval, err := json.Marshal(value) jsonval, err := json.Marshal(value)
if err != nil { if err != nil {
panic(err) // gets recovered later up return map[string]MetaValue{"": {DataType: MDTString, Value: fmt.Sprintf("Failed to Marshal %T:\n%+v", value, value)}}
} }
return map[string]MetaValue{"": {DataType: MDTString, Value: string(jsonval)}} return map[string]MetaValue{"": {DataType: MDTString, Value: string(jsonval)}}
+7 -65
View File
@@ -1,72 +1,14 @@
package exerr package exerr
import ( type Method string
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/langext" const (
MethodOutput Method = "OUTPUT"
MethodPrint Method = "PRINT"
MethodBuild Method = "BUILD"
MethodFatal Method = "FATAL"
) )
type ErrorCategory struct{ Category string }
var (
CatWrap = ErrorCategory{"Wrap"} // The error is simply wrapping another error (e.g. when a grpc call returns an error)
CatSystem = ErrorCategory{"System"} // An internal system error (e.g. connection to db failed)
CatUser = ErrorCategory{"User"} // The user (the API caller) did something wrong (e.g. he has no permissions to do this)
CatForeign = ErrorCategory{"Foreign"} // A foreign error that some component threw (e.g. an unknown mongodb error), happens if we call Wrap(..) on an non-bmerror value
)
//goland:noinspection GoUnusedGlobalVariable
var AllCategories = []ErrorCategory{CatWrap, CatSystem, CatUser, CatForeign}
type ErrorSeverity struct{ Severity string }
var (
SevTrace = ErrorSeverity{"Trace"}
SevDebug = ErrorSeverity{"Debug"}
SevInfo = ErrorSeverity{"Info"}
SevWarn = ErrorSeverity{"Warn"}
SevErr = ErrorSeverity{"Err"}
SevFatal = ErrorSeverity{"Fatal"}
)
//goland:noinspection GoUnusedGlobalVariable
var AllSeverities = []ErrorSeverity{SevTrace, SevDebug, SevInfo, SevWarn, SevErr, SevFatal}
type ErrorType struct {
Key string
DefaultStatusCode *int
}
//goland:noinspection GoUnusedGlobalVariable
var (
TypeInternal = NewType("INTERNAL_ERROR", langext.Ptr(500))
TypePanic = NewType("PANIC", langext.Ptr(500))
TypeNotImplemented = NewType("NOT_IMPLEMENTED", langext.Ptr(500))
TypeWrap = NewType("Wrap", nil)
TypeBindFailURI = NewType("BINDFAIL_URI", langext.Ptr(400))
TypeBindFailQuery = NewType("BINDFAIL_QUERY", langext.Ptr(400))
TypeBindFailJSON = NewType("BINDFAIL_JSON", langext.Ptr(400))
TypeBindFailFormData = NewType("BINDFAIL_FORMDATA", langext.Ptr(400))
TypeBindFailHeader = NewType("BINDFAIL_HEADER", langext.Ptr(400))
TypeUnauthorized = NewType("UNAUTHORIZED", langext.Ptr(401))
TypeAuthFailed = NewType("AUTH_FAILED", langext.Ptr(401))
// other values come from pkgconfig
)
var registeredTypes = dataext.SyncSet[string]{}
func NewType(key string, defStatusCode *int) ErrorType {
insertOkay := registeredTypes.Add(key)
if !insertOkay {
panic("Cannot register same ErrType ('" + key + "') more than once")
}
return ErrorType{key, defStatusCode}
}
type LogPrintLevel string type LogPrintLevel string
const ( const (
+89
View File
@@ -0,0 +1,89 @@
package exerr
import (
"encoding/json"
"errors"
"fmt"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsoncodec"
"go.mongodb.org/mongo-driver/bson/bsonrw"
"go.mongodb.org/mongo-driver/bson/bsontype"
"reflect"
)
type ErrorCategory struct{ Category string }
var (
CatWrap = ErrorCategory{"Wrap"} // The error is simply wrapping another error (e.g. when a grpc call returns an error)
CatSystem = ErrorCategory{"System"} // An internal system error (e.g. connection to db failed)
CatUser = ErrorCategory{"User"} // The user (the API caller) did something wrong (e.g. he has no permissions to do this)
CatForeign = ErrorCategory{"Foreign"} // A foreign error that some component threw (e.g. an unknown mongodb error), happens if we call Wrap(..) on an non-bmerror value
)
func (e *ErrorCategory) UnmarshalJSON(bytes []byte) error {
return json.Unmarshal(bytes, &e.Category)
}
func (e ErrorCategory) MarshalJSON() ([]byte, error) {
return json.Marshal(e.Category)
}
func (e *ErrorCategory) UnmarshalBSONValue(bt bsontype.Type, data []byte) error {
if bt == bson.TypeNull {
// we can't set nil in UnmarshalBSONValue (so we use default(struct))
// Use mongoext.CreateGoExtBsonRegistry if you need to unmarsh pointer values
// https://stackoverflow.com/questions/75167597
// https://jira.mongodb.org/browse/GODRIVER-2252
*e = ErrorCategory{}
return nil
}
if bt != bson.TypeString {
return errors.New(fmt.Sprintf("cannot unmarshal %v into String", bt))
}
var tt string
err := bson.RawValue{Type: bt, Value: data}.Unmarshal(&tt)
if err != nil {
return err
}
*e = ErrorCategory{tt}
return nil
}
func (e ErrorCategory) MarshalBSONValue() (bsontype.Type, []byte, error) {
return bson.MarshalValue(e.Category)
}
func (e ErrorCategory) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
if val.Kind() == reflect.Ptr && val.IsNil() {
if !val.CanSet() {
return errors.New("ValueUnmarshalerDecodeValue")
}
val.Set(reflect.New(val.Type().Elem()))
}
tp, src, err := bsonrw.Copier{}.CopyValueToBytes(vr)
if err != nil {
return err
}
if val.Kind() == reflect.Ptr && len(src) == 0 {
val.Set(reflect.Zero(val.Type()))
return nil
}
err = e.UnmarshalBSONValue(tp, src)
if err != nil {
return err
}
if val.Kind() == reflect.Ptr {
val.Set(reflect.ValueOf(&e))
} else {
val.Set(reflect.ValueOf(e))
}
return nil
}
//goland:noinspection GoUnusedGlobalVariable
var AllCategories = []ErrorCategory{CatWrap, CatSystem, CatUser, CatForeign}
+91
View File
@@ -0,0 +1,91 @@
package exerr
import (
"encoding/json"
"errors"
"fmt"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsoncodec"
"go.mongodb.org/mongo-driver/bson/bsonrw"
"go.mongodb.org/mongo-driver/bson/bsontype"
"reflect"
)
type ErrorSeverity struct{ Severity string }
var (
SevTrace = ErrorSeverity{"Trace"}
SevDebug = ErrorSeverity{"Debug"}
SevInfo = ErrorSeverity{"Info"}
SevWarn = ErrorSeverity{"Warn"}
SevErr = ErrorSeverity{"Err"}
SevFatal = ErrorSeverity{"Fatal"}
)
func (e *ErrorSeverity) UnmarshalJSON(bytes []byte) error {
return json.Unmarshal(bytes, &e.Severity)
}
func (e ErrorSeverity) MarshalJSON() ([]byte, error) {
return json.Marshal(e.Severity)
}
func (e *ErrorSeverity) UnmarshalBSONValue(bt bsontype.Type, data []byte) error {
if bt == bson.TypeNull {
// we can't set nil in UnmarshalBSONValue (so we use default(struct))
// Use mongoext.CreateGoExtBsonRegistry if you need to unmarsh pointer values
// https://stackoverflow.com/questions/75167597
// https://jira.mongodb.org/browse/GODRIVER-2252
*e = ErrorSeverity{}
return nil
}
if bt != bson.TypeString {
return errors.New(fmt.Sprintf("cannot unmarshal %v into String", bt))
}
var tt string
err := bson.RawValue{Type: bt, Value: data}.Unmarshal(&tt)
if err != nil {
return err
}
*e = ErrorSeverity{tt}
return nil
}
func (e ErrorSeverity) MarshalBSONValue() (bsontype.Type, []byte, error) {
return bson.MarshalValue(e.Severity)
}
func (e ErrorSeverity) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
if val.Kind() == reflect.Ptr && val.IsNil() {
if !val.CanSet() {
return errors.New("ValueUnmarshalerDecodeValue")
}
val.Set(reflect.New(val.Type().Elem()))
}
tp, src, err := bsonrw.Copier{}.CopyValueToBytes(vr)
if err != nil {
return err
}
if val.Kind() == reflect.Ptr && len(src) == 0 {
val.Set(reflect.Zero(val.Type()))
return nil
}
err = e.UnmarshalBSONValue(tp, src)
if err != nil {
return err
}
if val.Kind() == reflect.Ptr {
val.Set(reflect.ValueOf(&e))
} else {
val.Set(reflect.ValueOf(e))
}
return nil
}
//goland:noinspection GoUnusedGlobalVariable
var AllSeverities = []ErrorSeverity{SevTrace, SevDebug, SevInfo, SevWarn, SevErr, SevFatal}
+159
View File
@@ -0,0 +1,159 @@
package exerr
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"git.blackforestbytes.com/BlackForestBytes/goext/dataext"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsoncodec"
"go.mongodb.org/mongo-driver/bson/bsonrw"
"go.mongodb.org/mongo-driver/bson/bsontype"
)
type ErrorType struct {
Key string
DefaultStatusCode *int
}
//goland:noinspection GoUnusedGlobalVariable
var (
TypeInternal = NewType("INTERNAL_ERROR", langext.Ptr(500))
TypePanic = NewType("PANIC", langext.Ptr(500))
TypeNotImplemented = NewType("NOT_IMPLEMENTED", langext.Ptr(500))
TypeAssert = NewType("ASSERT", langext.Ptr(500))
TypeMongoQuery = NewType("MONGO_QUERY", langext.Ptr(500))
TypeCursorTokenDecode = NewType("CURSOR_TOKEN_DECODE", langext.Ptr(500))
TypeMongoFilter = NewType("MONGO_FILTER", langext.Ptr(500))
TypeMongoReflection = NewType("MONGO_REFLECTION", langext.Ptr(500))
TypeMongoInvalidOpt = NewType("MONGO_INVALIDOPT", langext.Ptr(500))
TypeSQLQuery = NewType("SQL_QUERY", langext.Ptr(500))
TypeSQLBuild = NewType("SQL_BUILD", langext.Ptr(500))
TypeSQLDecode = NewType("SQL_DECODE", langext.Ptr(500))
TypeWrap = NewType("Wrap", nil)
TypeBindFailURI = NewType("BINDFAIL_URI", langext.Ptr(400))
TypeBindFailQuery = NewType("BINDFAIL_QUERY", langext.Ptr(400))
TypeBindFailJSON = NewType("BINDFAIL_JSON", langext.Ptr(400))
TypeBindFailFormData = NewType("BINDFAIL_FORMDATA", langext.Ptr(400))
TypeBindFailHeader = NewType("BINDFAIL_HEADER", langext.Ptr(400))
TypeMarshalEntityID = NewType("MARSHAL_ENTITY_ID", langext.Ptr(400))
TypeInvalidCSID = NewType("INVALID_CSID", langext.Ptr(400))
TypeGoogleStatuscode = NewType("GOOGLE_STATUSCODE", langext.Ptr(400))
TypeGoogleResponse = NewType("GOOGLE_RESPONSE", langext.Ptr(400))
TypeUnauthorized = NewType("UNAUTHORIZED", langext.Ptr(401))
TypeAuthFailed = NewType("AUTH_FAILED", langext.Ptr(401))
TypeInvalidImage = NewType("IMAGEEXT_INVALID_IMAGE", langext.Ptr(400))
TypeInvalidMimeType = NewType("IMAGEEXT_INVALID_MIMETYPE", langext.Ptr(400))
TypeWebsocket = NewType("WEBSOCKET", langext.Ptr(500))
// other values come from the downstream application that uses goext
)
func (e *ErrorType) UnmarshalJSON(bytes []byte) error {
var k string
err := json.Unmarshal(bytes, &k)
if err != nil {
return err
}
if d, ok := registeredTypes.Get(k); ok {
*e = d
return nil
} else {
*e = ErrorType{k, nil}
return nil
}
}
func (e ErrorType) MarshalJSON() ([]byte, error) {
return json.Marshal(e.Key)
}
func (e *ErrorType) UnmarshalBSONValue(bt bsontype.Type, data []byte) error {
if bt == bson.TypeNull {
// we can't set nil in UnmarshalBSONValue (so we use default(struct))
// Use mongoext.CreateGoExtBsonRegistry if you need to unmarsh pointer values
// https://stackoverflow.com/questions/75167597
// https://jira.mongodb.org/browse/GODRIVER-2252
*e = ErrorType{}
return nil
}
if bt != bson.TypeString {
return errors.New(fmt.Sprintf("cannot unmarshal %v into String", bt))
}
var tt string
err := bson.RawValue{Type: bt, Value: data}.Unmarshal(&tt)
if err != nil {
return err
}
if d, ok := registeredTypes.Get(tt); ok {
*e = d
return nil
} else {
*e = ErrorType{tt, nil}
return nil
}
}
func (e ErrorType) MarshalBSONValue() (bsontype.Type, []byte, error) {
return bson.MarshalValue(e.Key)
}
func (e ErrorType) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
if val.Kind() == reflect.Ptr && val.IsNil() {
if !val.CanSet() {
return errors.New("ValueUnmarshalerDecodeValue")
}
val.Set(reflect.New(val.Type().Elem()))
}
tp, src, err := bsonrw.Copier{}.CopyValueToBytes(vr)
if err != nil {
return err
}
if val.Kind() == reflect.Ptr && len(src) == 0 {
val.Set(reflect.Zero(val.Type()))
return nil
}
err = e.UnmarshalBSONValue(tp, src)
if err != nil {
return err
}
if val.Kind() == reflect.Ptr {
val.Set(reflect.ValueOf(&e))
} else {
val.Set(reflect.ValueOf(e))
}
return nil
}
var registeredTypes = dataext.SyncMap[string, ErrorType]{}
func NewType(key string, defStatusCode *int) ErrorType {
et := ErrorType{key, defStatusCode}
registeredTypes.Set(key, et)
return et
}
func ListRegisteredTypes() []ErrorType {
return registeredTypes.GetAllValues()
}
+153
View File
@@ -0,0 +1,153 @@
package exerr
import (
"context"
"encoding/json"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
"testing"
"time"
)
func TestJSONMarshalErrorCategory(t *testing.T) {
c1 := CatSystem
jsonbin := tst.Must(json.Marshal(c1))(t)
var c2 ErrorCategory
tst.AssertNoErr(t, json.Unmarshal(jsonbin, &c2))
tst.AssertEqual(t, c1, c2)
tst.AssertEqual(t, string(jsonbin), "\"System\"")
}
func TestJSONMarshalErrorSeverity(t *testing.T) {
c1 := SevErr
jsonbin := tst.Must(json.Marshal(c1))(t)
var c2 ErrorSeverity
tst.AssertNoErr(t, json.Unmarshal(jsonbin, &c2))
tst.AssertEqual(t, c1, c2)
tst.AssertEqual(t, string(jsonbin), "\"Err\"")
}
func TestJSONMarshalErrorType(t *testing.T) {
c1 := TypeNotImplemented
jsonbin := tst.Must(json.Marshal(c1))(t)
var c2 ErrorType
tst.AssertNoErr(t, json.Unmarshal(jsonbin, &c2))
tst.AssertEqual(t, c1, c2)
tst.AssertEqual(t, string(jsonbin), "\"NOT_IMPLEMENTED\"")
}
func TestBSONMarshalErrorCategory(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 350*time.Millisecond)
defer cancel()
client, err := mongo.Connect(ctx)
if err != nil {
t.Skip("Skip test - no local mongo found")
return
}
err = client.Ping(ctx, nil)
if err != nil {
t.Skip("Skip test - no local mongo found")
return
}
primimd := primitive.NewObjectID()
_, err = client.Database("_test").Collection("goext-cicd").InsertOne(ctx, bson.M{"_id": primimd, "val": CatSystem})
tst.AssertNoErr(t, err)
cursor := client.Database("_test").Collection("goext-cicd").FindOne(ctx, bson.M{"_id": primimd, "val": bson.M{"$type": "string"}})
var c1 struct {
ID primitive.ObjectID `bson:"_id"`
Val ErrorCategory `bson:"val"`
}
err = cursor.Decode(&c1)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, c1.Val, CatSystem)
}
func TestBSONMarshalErrorSeverity(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 350*time.Millisecond)
defer cancel()
client, err := mongo.Connect(ctx)
if err != nil {
t.Skip("Skip test - no local mongo found")
return
}
err = client.Ping(ctx, nil)
if err != nil {
t.Skip("Skip test - no local mongo found")
return
}
primimd := primitive.NewObjectID()
_, err = client.Database("_test").Collection("goext-cicd").InsertOne(ctx, bson.M{"_id": primimd, "val": SevErr})
tst.AssertNoErr(t, err)
cursor := client.Database("_test").Collection("goext-cicd").FindOne(ctx, bson.M{"_id": primimd, "val": bson.M{"$type": "string"}})
var c1 struct {
ID primitive.ObjectID `bson:"_id"`
Val ErrorSeverity `bson:"val"`
}
err = cursor.Decode(&c1)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, c1.Val, SevErr)
}
func TestBSONMarshalErrorType(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 350*time.Millisecond)
defer cancel()
client, err := mongo.Connect(ctx)
if err != nil {
t.Skip("Skip test - no local mongo found")
return
}
err = client.Ping(ctx, nil)
if err != nil {
t.Skip("Skip test - no local mongo found")
return
}
primimd := primitive.NewObjectID()
_, err = client.Database("_test").Collection("goext-cicd").InsertOne(ctx, bson.M{"_id": primimd, "val": TypeNotImplemented})
tst.AssertNoErr(t, err)
cursor := client.Database("_test").Collection("goext-cicd").FindOne(ctx, bson.M{"_id": primimd, "val": bson.M{"$type": "string"}})
var c1 struct {
ID primitive.ObjectID `bson:"_id"`
Val ErrorType `bson:"val"`
}
err = cursor.Decode(&c1)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, c1.Val, TypeNotImplemented)
}
+87 -25
View File
@@ -1,37 +1,62 @@
package exerr package exerr
import ( import (
"context"
"fmt" "fmt"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog"
"net/http"
"os"
) )
type ErrorPackageConfig struct { type ErrorPackageConfig struct {
ZeroLogErrTraces bool // autom print zerolog logs on .Build() (for SevErr and SevFatal) ZeroLogErrTraces bool // autom print zerolog logs on .Build() (for SevErr and SevFatal)
ZeroLogAllTraces bool // autom print zerolog logs on .Build() (for all Severities) ZeroLogAllTraces bool // autom print zerolog logs on .Build() (for all Severities)
RecursiveErrors bool // errors contains their Origin-Error RecursiveErrors bool // errors contains their Origin-Error
ExtendedGinOutput bool // Log extended data (trace, meta, ...) to gin in err.Output() ExtendedGinOutput bool // Log extended data (trace, meta, ...) to gin in err.Output()
ExtendGinOutput func(err *ExErr, json map[string]any) // (Optionally) extend the gin output with more fields IncludeMetaInGinOutput bool // Log meta fields ( from e.g. `.Str(key, val).Build()` ) to gin in err.Output()
ExtendGinDataOutput func(err *ExErr, depth int, json map[string]any) // (Optionally) extend the gin `__data` output with more fields ExtendGinOutput func(err *ExErr, json map[string]any) // (Optionally) extend the gin output with more fields
ExtendGinDataOutput func(err *ExErr, depth int, json map[string]any) // (Optionally) extend the gin `__data` output with more fields
DisableErrorWrapping bool // Disables the exerr.Wrap()...Build() function - will always return the original error
ZeroLogErrGinOutput bool // autom print zerolog logs on ginext.Error() / .Output(gin) (for SevErr and SevFatal)
ZeroLogAllGinOutput bool // autom print zerolog logs on ginext.Error() / .Output(gin) (for all Severities)
ExtendGinMeta func(ctx context.Context, b *Builder, g *gin.Context, req *http.Request) // (Optionally) extend the final error meta values with additional data from the gin context (a few are automatically added, here more can be included)
ExtendContextMeta func(b *Builder, method Method, dctx context.Context) // (Optionally) extend the final error meta values with additional data from the context (a few are automatically added, here more can be included)
ZeroLogger zerolog.Logger // The logger used to print exerr log messages
} }
type ErrorPackageConfigInit struct { type ErrorPackageConfigInit struct {
ZeroLogErrTraces bool ZeroLogErrTraces *bool
ZeroLogAllTraces bool ZeroLogAllTraces *bool
RecursiveErrors bool RecursiveErrors *bool
ExtendedGinOutput bool ExtendedGinOutput *bool
ExtendGinOutput func(err *ExErr, json map[string]any) IncludeMetaInGinOutput *bool
ExtendGinDataOutput func(err *ExErr, depth int, json map[string]any) ExtendGinOutput func(err *ExErr, json map[string]any)
ExtendGinDataOutput func(err *ExErr, depth int, json map[string]any)
DisableErrorWrapping *bool
ZeroLogErrGinOutput *bool
ZeroLogAllGinOutput *bool
ExtendGinMeta func(ctx context.Context, b *Builder, g *gin.Context, req *http.Request)
ExtendContextMeta func(b *Builder, method Method, dctx context.Context)
ZeroLogger *zerolog.Logger
} }
var initialized = false var initialized = false
var pkgconfig = ErrorPackageConfig{ var pkgconfig = ErrorPackageConfig{
ZeroLogErrTraces: true, ZeroLogErrTraces: true,
ZeroLogAllTraces: false, ZeroLogAllTraces: false,
RecursiveErrors: true, RecursiveErrors: true,
ExtendedGinOutput: false, ExtendedGinOutput: false,
ExtendGinOutput: func(err *ExErr, json map[string]any) {}, IncludeMetaInGinOutput: true,
ExtendGinDataOutput: func(err *ExErr, depth int, json map[string]any) {}, ExtendGinOutput: func(err *ExErr, json map[string]any) {},
ExtendGinDataOutput: func(err *ExErr, depth int, json map[string]any) {},
DisableErrorWrapping: false,
ZeroLogErrGinOutput: true,
ZeroLogAllGinOutput: false,
ExtendGinMeta: func(ctx context.Context, b *Builder, g *gin.Context, req *http.Request) {},
ExtendContextMeta: func(b *Builder, method Method, dctx context.Context) {},
} }
// Init initializes the exerr packages // Init initializes the exerr packages
@@ -44,6 +69,8 @@ func Init(cfg ErrorPackageConfigInit) {
ego := func(err *ExErr, json map[string]any) {} ego := func(err *ExErr, json map[string]any) {}
egdo := func(err *ExErr, depth int, json map[string]any) {} egdo := func(err *ExErr, depth int, json map[string]any) {}
egm := func(ctx context.Context, b *Builder, g *gin.Context, req *http.Request) {}
egcm := func(b *Builder, method Method, dctx context.Context) {}
if cfg.ExtendGinOutput != nil { if cfg.ExtendGinOutput != nil {
ego = cfg.ExtendGinOutput ego = cfg.ExtendGinOutput
@@ -51,19 +78,54 @@ func Init(cfg ErrorPackageConfigInit) {
if cfg.ExtendGinDataOutput != nil { if cfg.ExtendGinDataOutput != nil {
egdo = cfg.ExtendGinDataOutput egdo = cfg.ExtendGinDataOutput
} }
if cfg.ExtendGinMeta != nil {
egm = cfg.ExtendGinMeta
}
if cfg.ExtendContextMeta != nil {
egcm = cfg.ExtendContextMeta
}
var logger zerolog.Logger
if cfg.ZeroLogger != nil {
logger = *cfg.ZeroLogger
} else {
logger = newDefaultLogger()
}
pkgconfig = ErrorPackageConfig{ pkgconfig = ErrorPackageConfig{
ZeroLogErrTraces: cfg.ZeroLogErrTraces, ZeroLogErrTraces: langext.Coalesce(cfg.ZeroLogErrTraces, pkgconfig.ZeroLogErrTraces),
ZeroLogAllTraces: cfg.ZeroLogAllTraces, ZeroLogAllTraces: langext.Coalesce(cfg.ZeroLogAllTraces, pkgconfig.ZeroLogAllTraces),
RecursiveErrors: cfg.RecursiveErrors, RecursiveErrors: langext.Coalesce(cfg.RecursiveErrors, pkgconfig.RecursiveErrors),
ExtendedGinOutput: cfg.ExtendedGinOutput, ExtendedGinOutput: langext.Coalesce(cfg.ExtendedGinOutput, pkgconfig.ExtendedGinOutput),
ExtendGinOutput: ego, IncludeMetaInGinOutput: langext.Coalesce(cfg.IncludeMetaInGinOutput, pkgconfig.IncludeMetaInGinOutput),
ExtendGinDataOutput: egdo, ExtendGinOutput: ego,
ExtendGinDataOutput: egdo,
DisableErrorWrapping: langext.Coalesce(cfg.DisableErrorWrapping, pkgconfig.DisableErrorWrapping),
ZeroLogAllGinOutput: langext.Coalesce(cfg.ZeroLogAllGinOutput, pkgconfig.ZeroLogAllGinOutput),
ZeroLogErrGinOutput: langext.Coalesce(cfg.ZeroLogErrGinOutput, pkgconfig.ZeroLogErrGinOutput),
ExtendGinMeta: egm,
ExtendContextMeta: egcm,
ZeroLogger: logger,
} }
initialized = true initialized = true
} }
func newDefaultLogger() zerolog.Logger {
cw := zerolog.ConsoleWriter{
Out: os.Stdout,
TimeFormat: "2006-01-02 15:04:05 Z07:00",
}
multi := zerolog.MultiLevelWriter(cw)
return zerolog.New(multi).With().Timestamp().CallerWithSkipFrameCount(5).Logger()
}
func Initialized() bool {
return initialized
}
func warnOnPkgConfigNotInitialized() { func warnOnPkgConfigNotInitialized() {
if !initialized { if !initialized {
fmt.Printf("\n") fmt.Printf("\n")
+147 -9
View File
@@ -1,9 +1,10 @@
package exerr package exerr
import ( import (
"fmt"
"github.com/rs/xid" "github.com/rs/xid"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"reflect" "reflect"
"strings" "strings"
"time" "time"
@@ -26,16 +27,24 @@ type ExErr struct {
OriginalError *ExErr `json:"originalError"` OriginalError *ExErr `json:"originalError"`
Meta MetaMap `json:"meta"` Extra map[string]any `json:"extra"`
Meta MetaMap `json:"meta"`
} }
func (ee *ExErr) Error() string { func (ee *ExErr) Error() string {
return ee.Message return ee.RecursiveMessage()
} }
// Unwrap must be implemented so that some error.XXX methods work // Unwrap must be implemented so that some error.XXX methods work
func (ee *ExErr) Unwrap() error { func (ee *ExErr) Unwrap() error {
if ee.OriginalError == nil { 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 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 return ee.OriginalError
@@ -81,9 +90,29 @@ func (ee *ExErr) Log(evt *zerolog.Event) {
} }
func (ee *ExErr) FormatLog(lvl LogPrintLevel) string { 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 { if lvl == LogPrintShort {
msg := ee.Message msg := ee.Message
if msg == "" {
msg = ee.RecursiveMessage()
}
if ee.OriginalError != nil && ee.OriginalError.Category == CatForeign { if ee.OriginalError != nil && ee.OriginalError.Category == CatForeign {
msg = msg + " (" + strings.ReplaceAll(ee.OriginalError.Message, "\n", " ") + ")" msg = msg + " (" + strings.ReplaceAll(ee.OriginalError.Message, "\n", " ") + ")"
} }
@@ -98,6 +127,10 @@ func (ee *ExErr) FormatLog(lvl LogPrintLevel) string {
str := "[" + ee.RecursiveType().Key + "] <" + ee.UniqueID + "> " + strings.ReplaceAll(ee.RecursiveMessage(), "\n", " ") + "\n" 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 := "" indent := ""
for curr := ee; curr != nil; curr = curr.OriginalError { for curr := ee; curr != nil; curr = curr.OriginalError {
indent += " " indent += " "
@@ -119,12 +152,16 @@ func (ee *ExErr) FormatLog(lvl LogPrintLevel) string {
str := "[" + ee.RecursiveType().Key + "] <" + ee.UniqueID + "> " + strings.ReplaceAll(ee.RecursiveMessage(), "\n", " ") + "\n" 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 := "" indent := ""
for curr := ee; curr != nil; curr = curr.OriginalError { for curr := ee; curr != nil; curr = curr.OriginalError {
indent += " " indent += " "
etype := ee.Type.Key etype := curr.Type.Key
if ee.Type == TypeWrap { if curr.Type == TypeWrap {
etype = "~" etype = "~"
} }
@@ -164,19 +201,37 @@ func (ee *ExErr) FormatLog(lvl LogPrintLevel) string {
} }
func (ee *ExErr) ShortLog(evt *zerolog.Event) { func (ee *ExErr) ShortLog(evt *zerolog.Event) {
ee.Meta.Apply(evt).Msg(ee.FormatLog(LogPrintShort)) ee.Meta.Apply(evt, langext.Ptr(240)).Msg(ee.FormatLog(LogPrintShort))
} }
// RecursiveMessage returns the message to show // RecursiveMessage returns the message to show
// = first error (top-down) that is not wrapping/foreign/empty // = first error (top-down) that is not foreign/empty
// = lowest level error (that is not empty)
// = fallback to self.message
func (ee *ExErr) RecursiveMessage() string { func (ee *ExErr) RecursiveMessage() string {
// ==== [1] ==== first error (top-down) that is not wrapping/foreign/empty
for curr := ee; curr != nil; curr = curr.OriginalError { for curr := ee; curr != nil; curr = curr.OriginalError {
if curr.Message != "" && curr.Category != CatWrap && curr.Category != CatForeign { if curr.Message != "" && curr.Category != CatForeign {
return curr.Message return curr.Message
} }
} }
// fallback to self // ==== [2] ==== lowest level error (that is not empty)
deepestMsg := ""
for curr := ee; curr != nil; curr = curr.OriginalError {
if curr.Message != "" {
deepestMsg = curr.Message
}
}
if deepestMsg != "" {
return deepestMsg
}
// ==== [3] ==== fallback to self.message
return ee.Message return ee.Message
} }
@@ -240,6 +295,89 @@ func (ee *ExErr) Depth() int {
} }
} }
// GetMeta returns the meta value with the specified key
// this method recurses through all wrapped errors and returns the first matching meta value
func (ee *ExErr) GetMeta(key string) (any, bool) {
for curr := ee; curr != nil; curr = curr.OriginalError {
if v, ok := curr.Meta[key]; ok {
return v.Value, true
}
}
return nil, false
}
// GetMetaString functions the same as GetMeta, but returns false if the type does not match
func (ee *ExErr) GetMetaString(key string) (string, bool) {
if v1, ok := ee.GetMeta(key); ok {
if v2, ok := v1.(string); ok {
return v2, true
}
}
return "", false
}
func (ee *ExErr) GetMetaBool(key string) (bool, bool) {
if v1, ok := ee.GetMeta(key); ok {
if v2, ok := v1.(bool); ok {
return v2, true
}
}
return false, false
}
func (ee *ExErr) GetMetaInt(key string) (int, bool) {
if v1, ok := ee.GetMeta(key); ok {
if v2, ok := v1.(int); ok {
return v2, true
}
}
return 0, false
}
func (ee *ExErr) GetMetaFloat32(key string) (float32, bool) {
if v1, ok := ee.GetMeta(key); ok {
if v2, ok := v1.(float32); ok {
return v2, true
}
}
return 0, false
}
func (ee *ExErr) GetMetaFloat64(key string) (float64, bool) {
if v1, ok := ee.GetMeta(key); ok {
if v2, ok := v1.(float64); ok {
return v2, true
}
}
return 0, false
}
func (ee *ExErr) GetMetaTime(key string) (time.Time, bool) {
if v1, ok := ee.GetMeta(key); ok {
if v2, ok := v1.(time.Time); ok {
return v2, true
}
}
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
}
func (ee *ExErr) UniqueIDs() []string {
ids := make([]string, 0, 1)
for curr := ee; curr != nil; curr = curr.OriginalError {
ids = append(ids, curr.UniqueID)
}
return ids
}
// contains test if the supplied error is contained in this error (anywhere in the chain) // contains test if the supplied error is contained in this error (anywhere in the chain)
func (ee *ExErr) contains(original *ExErr) (*ExErr, bool) { func (ee *ExErr) contains(original *ExErr) (*ExErr, bool) {
if original == nil { if original == nil {
+10 -1
View File
@@ -2,10 +2,19 @@ package exerr
import ( import (
"errors" "errors"
"gogs.mikescher.com/BlackForestBytes/goext/tst" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
"os"
"testing" "testing"
) )
func TestMain(m *testing.M) {
if !Initialized() {
Init(ErrorPackageConfigInit{ZeroLogErrTraces: langext.PFalse, ZeroLogAllTraces: langext.PFalse})
}
os.Exit(m.Run())
}
type golangErr struct { type golangErr struct {
Message string Message string
} }
+82 -10
View File
@@ -1,24 +1,24 @@
package exerr package exerr
import ( import (
json "git.blackforestbytes.com/BlackForestBytes/goext/gojson"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
json "gogs.mikescher.com/BlackForestBytes/goext/gojson"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http" "net/http"
"time" "time"
) )
func (ee *ExErr) toJson(depth int, applyExtendListener bool) langext.H { func (ee *ExErr) toJson(depth int, applyExtendListener bool, outputMeta bool) langext.H {
ginJson := langext.H{} ginJson := langext.H{}
if ee.UniqueID != "" { if ee.UniqueID != "" {
ginJson["id"] = ee.UniqueID ginJson["id"] = ee.UniqueID
} }
if ee.Category != CatWrap { if ee.Category != CatWrap {
ginJson["category"] = ee.Category ginJson["category"] = ee.Category.Category
} }
if ee.Type != TypeWrap { if ee.Type != TypeWrap {
ginJson["type"] = ee.Type ginJson["type"] = ee.Type.Key
} }
if ee.StatusCode != nil { if ee.StatusCode != nil {
ginJson["statuscode"] = ee.StatusCode ginJson["statuscode"] = ee.StatusCode
@@ -30,7 +30,7 @@ func (ee *ExErr) toJson(depth int, applyExtendListener bool) langext.H {
ginJson["caller"] = ee.Caller ginJson["caller"] = ee.Caller
} }
if ee.Severity != SevErr { if ee.Severity != SevErr {
ginJson["severity"] = ee.Severity ginJson["severity"] = ee.Severity.Severity
} }
if ee.Timestamp != (time.Time{}) { if ee.Timestamp != (time.Time{}) {
ginJson["time"] = ee.Timestamp.Format(time.RFC3339) ginJson["time"] = ee.Timestamp.Format(time.RFC3339)
@@ -39,7 +39,21 @@ func (ee *ExErr) toJson(depth int, applyExtendListener bool) langext.H {
ginJson["wrappedErrType"] = ee.WrappedErrType ginJson["wrappedErrType"] = ee.WrappedErrType
} }
if ee.OriginalError != nil { if ee.OriginalError != nil {
ginJson["original"] = ee.OriginalError.toJson(depth+1, applyExtendListener) ginJson["original"] = ee.OriginalError.toJson(depth+1, applyExtendListener, outputMeta)
}
if outputMeta {
metaJson := langext.H{}
for metaKey, metaVal := range ee.Meta {
metaJson[metaKey] = metaVal.rawValueForJson()
}
ginJson["meta"] = metaJson
extraJson := langext.H{}
for extraKey, extraVal := range ee.Extra {
extraJson[extraKey] = extraVal
}
ginJson["extra"] = extraJson
} }
if applyExtendListener { if applyExtendListener {
@@ -49,13 +63,26 @@ func (ee *ExErr) toJson(depth int, applyExtendListener bool) langext.H {
return ginJson return ginJson
} }
func (ee *ExErr) ToDefaultAPIJson() (string, error) {
gjr := json.GoJsonRender{Data: ee.ToAPIJson(true, pkgconfig.ExtendedGinOutput, pkgconfig.IncludeMetaInGinOutput), NilSafeSlices: true, NilSafeMaps: true}
r, err := gjr.RenderString()
if err != nil {
return "", err
}
return r, nil
}
// ToAPIJson converts the ExError to a json object // ToAPIJson converts the ExError to a json object
// (the same object as used in the Output(gin) method) // (the same object as used in the Output(gin) method)
// //
// Parameters: // Parameters:
// - [applyExtendListener]: if false the pkgconfig.ExtendGinOutput / pkgconfig.ExtendGinDataOutput will not be applied // - [applyExtendListener]: if false the pkgconfig.ExtendGinOutput / pkgconfig.ExtendGinDataOutput will not be applied
// - [includeWrappedErrors]: if false we do not include the recursive/wrapped errors in `__data` // - [includeWrappedErrors]: if false we do not include the recursive/wrapped errors in `__data`
func (ee *ExErr) ToAPIJson(applyExtendListener bool, includeWrappedErrors bool) langext.H { // - [includeMetaFields]: if true we also include meta-values (aka from `.Str(key, value).Build()`), needs includeWrappedErrors=true
func (ee *ExErr) ToAPIJson(applyExtendListener bool, includeWrappedErrors bool, includeMetaFields bool) langext.H {
apiOutput := langext.H{ apiOutput := langext.H{
"errorid": ee.UniqueID, "errorid": ee.UniqueID,
@@ -65,7 +92,21 @@ func (ee *ExErr) ToAPIJson(applyExtendListener bool, includeWrappedErrors bool)
} }
if includeWrappedErrors { if includeWrappedErrors {
apiOutput["__data"] = ee.toJson(0, applyExtendListener) 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 { if applyExtendListener {
@@ -97,7 +138,38 @@ func (ee *ExErr) Output(g *gin.Context) {
statuscode = *baseType.DefaultStatusCode statuscode = *baseType.DefaultStatusCode
} }
ginOutput := ee.ToAPIJson(true, pkgconfig.ExtendedGinOutput) ginOutput := ee.ToAPIJson(true, pkgconfig.ExtendedGinOutput, pkgconfig.IncludeMetaInGinOutput)
g.Render(statuscode, json.GoJsonRender{Data: ginOutput, NilSafeSlices: true, NilSafeMaps: true}) g.Render(statuscode, json.GoJsonRender{Data: ginOutput, NilSafeSlices: true, NilSafeMaps: true})
} }
func (ee *ExErr) OutputRaw(w http.ResponseWriter) {
warnOnPkgConfigNotInitialized()
var statuscode = http.StatusInternalServerError
var baseCat = ee.RecursiveCategory()
var baseType = ee.RecursiveType()
var baseStatuscode = ee.RecursiveStatuscode()
if baseCat == CatUser {
statuscode = http.StatusBadRequest
} else if baseCat == CatSystem {
statuscode = http.StatusInternalServerError
}
if baseStatuscode != nil {
statuscode = *ee.StatusCode
} else if baseType.DefaultStatusCode != nil {
statuscode = *baseType.DefaultStatusCode
}
ginOutput, err := ee.ToDefaultAPIJson()
if err != nil {
panic(err) // cannot happen
}
w.WriteHeader(statuscode)
_, _ = w.Write([]byte(ginOutput))
}
+38
View File
@@ -86,3 +86,41 @@ func MessageMatch(e error, matcher func(string) bool) bool {
return false 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
}
func UniqueID(v error) *string {
if v == nil {
return nil
}
//goland:noinspection GoTypeAssertionOnErrors
if verr, ok := v.(*ExErr); ok {
return &verr.UniqueID
}
return nil
}
+6 -13
View File
@@ -4,16 +4,11 @@ import (
"sync" "sync"
) )
type Method string type ListenerOpt struct {
NoLog bool
}
const ( type Listener = func(method Method, v *ExErr, opt ListenerOpt)
MethodOutput Method = "OUTPUT"
MethodPrint Method = "PRINT"
MethodBuild Method = "BUILD"
MethodFatal Method = "FATAL"
)
type Listener = func(method Method, v *ExErr)
var listenerLock = sync.Mutex{} var listenerLock = sync.Mutex{}
var listener = make([]Listener, 0) var listener = make([]Listener, 0)
@@ -25,13 +20,11 @@ func RegisterListener(l Listener) {
listener = append(listener, l) listener = append(listener, l)
} }
func (b *Builder) CallListener(m Method) { func (ee *ExErr) CallListener(m Method, opt ListenerOpt) {
valErr := b.errorData
listenerLock.Lock() listenerLock.Lock()
defer listenerLock.Unlock() defer listenerLock.Unlock()
for _, v := range listener { for _, v := range listener {
v(m, valErr) v(m, ee, opt)
} }
} }
+118 -7
View File
@@ -5,10 +5,11 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/bson/primitive"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "math"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -43,6 +44,7 @@ const (
MDTID metaDataType = "ID" MDTID metaDataType = "ID"
MDTAny metaDataType = "Interface" MDTAny metaDataType = "Interface"
MDTNil metaDataType = "Nil" MDTNil metaDataType = "Nil"
MDTEnum metaDataType = "Enum"
) )
type MetaValue struct { type MetaValue struct {
@@ -131,6 +133,8 @@ func (v MetaValue) SerializeValue() (string, error) {
return string(r), nil return string(r), nil
case MDTNil: case MDTNil:
return "", nil return "", nil
case MDTEnum:
return v.Value.(EnumWrap).Serialize(), nil
} }
return "", errors.New("Unknown type: " + string(v.DataType)) return "", errors.New("Unknown type: " + string(v.DataType))
} }
@@ -208,27 +212,41 @@ func (v MetaValue) ShortString(lim int) string {
return langext.StrLimit(string(r), lim, "...") return langext.StrLimit(string(r), lim, "...")
case MDTNil: case MDTNil:
return "<<null>>" return "<<null>>"
case MDTEnum:
return v.Value.(EnumWrap).String()
} }
return "(err)" return "(err)"
} }
func (v MetaValue) Apply(key string, evt *zerolog.Event) *zerolog.Event { func (v MetaValue) Apply(key string, evt *zerolog.Event, limitLen *int) *zerolog.Event {
switch v.DataType { switch v.DataType {
case MDTString: case MDTString:
return evt.Str(key, v.Value.(string)) if limitLen == nil {
return evt.Str(key, v.Value.(string))
} else {
return evt.Str(key, langext.StrLimit(v.Value.(string), *limitLen, "..."))
}
case MDTID: case MDTID:
return evt.Str(key, v.Value.(IDWrap).Value) return evt.Str(key, v.Value.(IDWrap).Value)
case MDTAny: case MDTAny:
if v.Value.(AnyWrap).IsError { if v.Value.(AnyWrap).IsError {
return evt.Str(key, "(err)") return evt.Str(key, "(err)")
} else { } else {
return evt.Str(key, v.Value.(AnyWrap).Json) if limitLen == nil {
return evt.Str(key, v.Value.(AnyWrap).Json)
} else {
return evt.Str(key, langext.StrLimit(v.Value.(AnyWrap).Json, *limitLen, "..."))
}
} }
case MDTStringPtr: case MDTStringPtr:
if langext.IsNil(v.Value) { if langext.IsNil(v.Value) {
return evt.Str(key, "<<null>>") return evt.Str(key, "<<null>>")
} }
return evt.Str(key, langext.CoalesceString(v.Value.(*string), "<<null>>")) if limitLen == nil {
return evt.Str(key, langext.CoalesceString(v.Value.(*string), "<<null>>"))
} else {
return evt.Str(key, langext.StrLimit(langext.CoalesceString(v.Value.(*string), "<<null>>"), *limitLen, "..."))
}
case MDTInt: case MDTInt:
return evt.Int(key, v.Value.(int)) return evt.Int(key, v.Value.(int))
case MDTInt8: case MDTInt8:
@@ -270,6 +288,14 @@ func (v MetaValue) Apply(key string, evt *zerolog.Event) *zerolog.Event {
return evt.Ints32(key, v.Value.([]int32)) return evt.Ints32(key, v.Value.([]int32))
case MDTNil: case MDTNil:
return evt.Str(key, "<<null>>") return evt.Str(key, "<<null>>")
case MDTEnum:
if v.Value.(EnumWrap).IsNil {
return evt.Any(key, nil)
} else if v.Value.(EnumWrap).ValueRaw != nil {
return evt.Any(key, v.Value.(EnumWrap).ValueRaw)
} else {
return evt.Str(key, v.Value.(EnumWrap).ValueString)
}
} }
return evt.Str(key, "(err)") return evt.Str(key, "(err)")
} }
@@ -511,6 +537,10 @@ func (v *MetaValue) Deserialize(value string, datatype metaDataType) error {
v.Value = nil v.Value = nil
v.DataType = datatype v.DataType = datatype
return nil return nil
case MDTEnum:
v.Value = deserializeEnumWrap(value)
v.DataType = datatype
return nil
} }
return errors.New("Unknown type: " + string(datatype)) return errors.New("Unknown type: " + string(datatype))
} }
@@ -581,10 +611,88 @@ func (v MetaValue) ValueString() string {
return string(r) return string(r)
case MDTNil: case MDTNil:
return "<<null>>" return "<<null>>"
case MDTEnum:
return v.Value.(EnumWrap).String()
} }
return "(err)" return "(err)"
} }
// rawValueForJson returns most-of-the-time the `Value` field
// but for some datatyes we do special processing
// all, so we can pluck the output value in json.Marshal without any suprises
func (v MetaValue) rawValueForJson() any {
if v.DataType == MDTAny {
if v.Value.(AnyWrap).IsNil {
return nil
}
if v.Value.(AnyWrap).IsError {
return bson.M{"@error": true}
}
jsonobj := primitive.M{}
jsonarr := primitive.A{}
if err := json.Unmarshal([]byte(v.Value.(AnyWrap).Json), &jsonobj); err == nil {
return jsonobj
} else if err := json.Unmarshal([]byte(v.Value.(AnyWrap).Json), &jsonarr); err == nil {
return jsonarr
} else {
return bson.M{"type": v.Value.(AnyWrap).Type, "data": v.Value.(AnyWrap).Json}
}
}
if v.DataType == MDTID {
if v.Value.(IDWrap).IsNil {
return nil
}
return v.Value.(IDWrap).Value
}
if v.DataType == MDTBytes {
return hex.EncodeToString(v.Value.([]byte))
}
if v.DataType == MDTDuration {
return v.Value.(time.Duration).String()
}
if v.DataType == MDTTime {
return v.Value.(time.Time).Format(time.RFC3339Nano)
}
if v.DataType == MDTObjectID {
return v.Value.(primitive.ObjectID).Hex()
}
if v.DataType == MDTNil {
return nil
}
if v.DataType == MDTEnum {
if v.Value.(EnumWrap).IsNil {
return nil
}
if v.Value.(EnumWrap).ValueRaw != nil {
return v.Value.(EnumWrap).ValueRaw
}
return v.Value.(EnumWrap).ValueString
}
if v.DataType == MDTFloat32 {
if math.IsNaN(float64(v.Value.(float32))) {
return "float64::NaN"
} else if math.IsInf(float64(v.Value.(float32)), +1) {
return "float64::+inf"
} else if math.IsInf(float64(v.Value.(float32)), -1) {
return "float64::-inf"
} else {
return v.Value
}
}
if v.DataType == MDTFloat64 {
if math.IsNaN(v.Value.(float64)) {
return "float64::NaN"
} else if math.IsInf(v.Value.(float64), +1) {
return "float64::+inf"
} else if math.IsInf(v.Value.(float64), -1) {
return "float64::-inf"
} else {
return v.Value
}
}
return v.Value
}
func (mm MetaMap) FormatOneLine(singleMaxLen int) string { func (mm MetaMap) FormatOneLine(singleMaxLen int) string {
r := "" r := ""
@@ -612,6 +720,9 @@ func (mm MetaMap) FormatMultiLine(indentFront string, indentKeys string, maxLenV
if key == "gin.body" { if key == "gin.body" {
continue continue
} }
if key == "gin_body" {
continue
}
r += indentFront r += indentFront
r += indentKeys r += indentKeys
@@ -629,9 +740,9 @@ func (mm MetaMap) Any() bool {
return len(mm) > 0 return len(mm) > 0
} }
func (mm MetaMap) Apply(evt *zerolog.Event) *zerolog.Event { func (mm MetaMap) Apply(evt *zerolog.Event, limitLen *int) *zerolog.Event {
for key, val := range mm { for key, val := range mm {
evt = val.Apply(key, evt) evt = val.Apply(key, evt, limitLen)
} }
return evt return evt
} }
+13
View File
@@ -0,0 +1,13 @@
package exerr
type Proxy struct {
v ExErr
}
func (p *Proxy) UniqueID() string {
return p.v.UniqueID
}
func (p *Proxy) Get() ExErr {
return p.v
}
+57 -1
View File
@@ -4,7 +4,8 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/enums"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"strings" "strings"
) )
@@ -131,3 +132,58 @@ func deserializeAnyWrap(v string) AnyWrap {
} }
} }
} }
type EnumWrap struct {
Type string
ValueString string
ValueRaw enums.Enum // `ValueRaw` is lost during serialization roundtrip
IsNil bool
}
func newEnumWrap(val enums.Enum) EnumWrap {
t := fmt.Sprintf("%T", val)
arr := strings.Split(t, ".")
if len(arr) > 0 {
t = arr[len(arr)-1]
}
if langext.IsNil(val) {
return EnumWrap{Type: t, ValueString: "", ValueRaw: val, IsNil: true}
}
if enumstr, ok := val.(enums.StringEnum); ok {
return EnumWrap{Type: t, ValueString: enumstr.String(), ValueRaw: val, IsNil: false}
}
return EnumWrap{Type: t, ValueString: fmt.Sprintf("%v", val), ValueRaw: val, IsNil: false}
}
func (w EnumWrap) Serialize() string {
if w.IsNil {
return "!nil" + ":" + w.Type
}
return w.Type + ":" + w.ValueString
}
func (w EnumWrap) String() string {
if w.IsNil {
return w.Type + "<<nil>>"
}
return "[" + w.Type + "] " + w.ValueString
}
func deserializeEnumWrap(v string) EnumWrap {
r := strings.SplitN(v, ":", 2)
if len(r) == 2 && r[0] == "!nil" {
return EnumWrap{Type: r[1], ValueString: v, ValueRaw: nil, IsNil: true}
}
if len(r) == 0 {
return EnumWrap{}
} else if len(r) == 1 {
return EnumWrap{Type: "", ValueString: v, ValueRaw: nil, IsNil: false}
} else {
return EnumWrap{Type: r[0], ValueString: r[1], ValueRaw: nil, IsNil: false}
}
}
+36
View File
@@ -0,0 +1,36 @@
package fsext
import "os"
func PathExists(fp string) (bool, error) {
_, err := os.Stat(fp)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
func FileExists(fp string) (bool, error) {
stat, err := os.Stat(fp)
if err == nil {
return !stat.IsDir(), nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
func DirectoryExists(fp string) (bool, error) {
stat, err := os.Stat(fp)
if err == nil {
return stat.IsDir(), nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
+9
View File
@@ -25,6 +25,15 @@ func CreateAppContext(g *gin.Context, innerCtx context.Context, cancelFn context
} }
} }
func CreateBackgroundAppContext() *AppContext {
return &AppContext{
inner: context.Background(),
cancelFunc: nil,
cancelled: false,
GinContext: nil,
}
}
func (ac *AppContext) Deadline() (deadline time.Time, ok bool) { func (ac *AppContext) Deadline() (deadline time.Time, ok bool) {
return ac.inner.Deadline() return ac.inner.Deadline()
} }
+4 -6
View File
@@ -2,13 +2,11 @@ package ginext
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/dataext" "git.blackforestbytes.com/BlackForestBytes/goext/dataext"
) )
func BodyBuffer() gin.HandlerFunc { func BodyBuffer(g *gin.Context) {
return func(g *gin.Context) { if g.Request.Body != nil {
if g.Request.Body != nil { g.Request.Body = dataext.NewBufferedReadCloser(g.Request.Body)
g.Request.Body = dataext.NewBufferedReadCloser(g.Request.Body)
}
} }
} }
+6 -2
View File
@@ -3,13 +3,17 @@ package ginext
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/http" "net/http"
"strings"
) )
func CorsMiddleware() gin.HandlerFunc { func CorsMiddleware(allowheader []string, exposeheader []string) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*") c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") c.Writer.Header().Set("Access-Control-Allow-Headers", strings.Join(allowheader, ", "))
if len(exposeheader) > 0 {
c.Writer.Header().Set("Access-Control-Expose-Headers", strings.Join(exposeheader, ", "))
}
c.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, PATCH, DELETE, COUNT") c.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, PATCH, DELETE, COUNT")
if c.Request.Method == "OPTIONS" { if c.Request.Method == "OPTIONS" {
+127 -38
View File
@@ -4,22 +4,32 @@ import (
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/mathext" "git.blackforestbytes.com/BlackForestBytes/goext/mathext"
"git.blackforestbytes.com/BlackForestBytes/goext/rext"
"net" "net"
"net/http" "net/http"
"net/http/httptest"
"regexp"
"strings" "strings"
"time" "time"
) )
type GinWrapper struct { type GinWrapper struct {
engine *gin.Engine engine *gin.Engine
SuppressGinLogs bool suppressGinLogs bool
allowCors bool opt Options
ginDebug bool allowCors bool
bufferBody bool corsAllowHeader []string
requestTimeout time.Duration corsExposeHeader []string
ginDebug bool
bufferBody bool
requestTimeout time.Duration
listenerBeforeRequest []func(g *gin.Context)
listenerAfterRequest []func(g *gin.Context, resp HTTPResponse)
buildRequestBindError func(g *gin.Context, fieldtype string, err error) HTTPResponse
routeSpecs []ginRouteSpec routeSpecs []ginRouteSpec
} }
@@ -31,47 +41,65 @@ type ginRouteSpec struct {
Handler string Handler string
} }
type Options struct {
AllowCors *bool // Add cors handler to allow all CORS requests on the default http methods
CorsAllowHeader *[]string // override the default values of Access-Control-Allow-Headers (AllowCors must be true)
CorsExposeHeader *[]string // return Access-Control-Expose-Headers (AllowCors must be true)
GinDebug *bool // Set gin.debug to true (adds more logs)
SuppressGinLogs *bool // Suppress our custom gin logs (even if GinDebug == true)
BufferBody *bool // Buffers the input body stream, this way the ginext error handler can later include the whole request body
Timeout *time.Duration // The default handler timeout
ListenerBeforeRequest []func(g *gin.Context) // Register listener that are called before the handler method
ListenerAfterRequest []func(g *gin.Context, resp HTTPResponse) // Register listener that are called after the handler method
DebugTrimHandlerPrefixes []string // Trim these prefixes from the handler names in the debug print
DebugReplaceHandlerNames map[string]string // Replace handler names in debug output
BuildRequestBindError func(g *gin.Context, fieldtype string, err error) HTTPResponse // Override function which generates the HTTPResponse errors that are returned by the preContext..Start() methids
}
// NewEngine creates a new (wrapped) ginEngine // NewEngine creates a new (wrapped) ginEngine
// Parameters are: func NewEngine(opt Options) *GinWrapper {
// - [allowCors] Add cors handler to allow all CORS requests on the default http methods ginDebug := langext.Coalesce(opt.GinDebug, true)
// - [ginDebug] Set gin.debug to true (adds more logs) if ginDebug {
// - [bufferBody] Buffers the input body stream, this way the ginext error handler can later include the whole request body gin.SetMode(gin.DebugMode)
// - [timeout] The default handler timeout
func NewEngine(allowCors bool, ginDebug bool, bufferBody bool, timeout time.Duration) *GinWrapper { // do not debug-print routes
gin.DebugPrintRouteFunc = func(_, _, _ string, _ int) {}
} else {
gin.SetMode(gin.ReleaseMode)
// do not debug-print routes
gin.DebugPrintRouteFunc = func(_, _, _ string, _ int) {}
}
engine := gin.New() engine := gin.New()
wrapper := &GinWrapper{ wrapper := &GinWrapper{
engine: engine, engine: engine,
SuppressGinLogs: false, opt: opt,
allowCors: allowCors, suppressGinLogs: langext.Coalesce(opt.SuppressGinLogs, false),
ginDebug: ginDebug, allowCors: langext.Coalesce(opt.AllowCors, false),
bufferBody: bufferBody, corsAllowHeader: langext.Coalesce(opt.CorsAllowHeader, []string{"Content-Type", "Content-Length", "Accept-Encoding", "X-CSRF-Token", "Authorization", "accept", "origin", "Cache-Control", "X-Requested-With"}),
requestTimeout: timeout, corsExposeHeader: langext.Coalesce(opt.CorsExposeHeader, []string{}),
ginDebug: ginDebug,
bufferBody: langext.Coalesce(opt.BufferBody, false),
requestTimeout: langext.Coalesce(opt.Timeout, 24*time.Hour),
listenerBeforeRequest: opt.ListenerBeforeRequest,
listenerAfterRequest: opt.ListenerAfterRequest,
buildRequestBindError: langext.Conditional(opt.BuildRequestBindError == nil, defaultBuildRequestBindError, opt.BuildRequestBindError),
} }
engine.RedirectFixedPath = false engine.RedirectFixedPath = false
engine.RedirectTrailingSlash = false engine.RedirectTrailingSlash = false
if allowCors { if wrapper.allowCors {
engine.Use(CorsMiddleware()) engine.Use(CorsMiddleware(wrapper.corsAllowHeader, wrapper.corsExposeHeader))
} }
// do not debug-print routes if ginDebug && !wrapper.suppressGinLogs {
gin.DebugPrintRouteFunc = func(_, _, _ string, _ int) {}
if ginDebug {
gin.SetMode(gin.ReleaseMode)
ginlogger := gin.Logger() ginlogger := gin.Logger()
engine.Use(func(context *gin.Context) { engine.Use(func(context *gin.Context) { ginlogger(context) })
if !wrapper.SuppressGinLogs {
ginlogger(context)
}
})
} else {
gin.SetMode(gin.DebugMode)
} }
return wrapper return wrapper
} }
@@ -126,8 +154,8 @@ func (w *GinWrapper) DebugPrintRoutes() {
line := [4]string{ line := [4]string{
spec.Method, spec.Method,
spec.URL, spec.URL,
strings.Join(spec.Middlewares, " -> "), strings.Join(langext.ArrMap(spec.Middlewares, w.cleanMiddlewareName), " -> "),
spec.Handler, w.cleanMiddlewareName(spec.Handler),
} }
lines = append(lines, line) lines = append(lines, line)
@@ -138,12 +166,73 @@ func (w *GinWrapper) DebugPrintRoutes() {
pad[3] = mathext.Max(pad[3], len(line[3])) pad[3] = mathext.Max(pad[3], len(line[3]))
} }
fmt.Printf("Gin-Routes:\n")
fmt.Printf("{\n")
for _, line := range lines { for _, line := range lines {
fmt.Printf("Gin-Route: %s %s --> %s --> %s\n", fmt.Printf(" %s %s --> %s --> %s\n",
langext.StrPadRight("["+line[0]+"]", " ", pad[0]+2), langext.StrPadRight("["+line[0]+"]", " ", pad[0]+2),
langext.StrPadRight(line[1], " ", pad[1]), langext.StrPadRight(line[1], " ", pad[1]),
langext.StrPadRight(line[2], " ", pad[2]), langext.StrPadRight(line[2], " ", pad[2]),
langext.StrPadRight(line[3], " ", pad[3])) langext.StrPadRight(line[3], " ", pad[3]))
} }
fmt.Printf("}\n")
}
func (w *GinWrapper) cleanMiddlewareName(fname string) string {
funcSuffix := rext.W(regexp.MustCompile(`\.func[0-9]+(?:\.[0-9]+)*$`))
if match, ok := funcSuffix.MatchFirst(fname); ok {
fname = fname[:len(fname)-match.FullMatch().Length()]
}
if strings.HasSuffix(fname, ".(*GinRoutesWrapper).WithJSONFilter") {
fname = "[JSONFilter]"
}
if fname == "ginext.BodyBuffer" {
fname = "[BodyBuffer]"
}
skipPrefixes := []string{"api.(*Handler).", "api.", "ginext.", "handler.", "admin-app.", "employee-app.", "employer-app."}
for _, pfx := range skipPrefixes {
if strings.HasPrefix(fname, pfx) {
fname = fname[len(pfx):]
}
}
for _, pfx := range w.opt.DebugTrimHandlerPrefixes {
if strings.HasPrefix(fname, pfx) {
fname = fname[len(pfx):]
}
}
for k, v := range langext.ForceMap(w.opt.DebugReplaceHandlerNames) {
if strings.EqualFold(fname, k) {
fname = v
}
}
return fname
}
// ServeHTTP only used for unit tests
func (w *GinWrapper) ServeHTTP(req *http.Request) *httptest.ResponseRecorder {
respRec := httptest.NewRecorder()
w.engine.ServeHTTP(respRec, req)
return respRec
}
// ForwardRequest manually inserts a request into this router
// = behaves as if the request came from the outside (and writes the response to `writer`)
func (w *GinWrapper) ForwardRequest(writer http.ResponseWriter, req *http.Request) {
w.engine.ServeHTTP(writer, req)
}
func (w *GinWrapper) ListRoutes() []gin.RouteInfo {
return w.engine.Routes()
}
func defaultBuildRequestBindError(g *gin.Context, fieldtype string, err error) HTTPResponse {
return Error(err)
} }
+83 -2
View File
@@ -2,8 +2,10 @@ package ginext
import ( import (
"fmt" "fmt"
"net/http"
"git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
) )
type WHandlerFunc func(PreContext) HTTPResponse type WHandlerFunc func(PreContext) HTTPResponse
@@ -14,7 +16,17 @@ func Wrap(w *GinWrapper, fn WHandlerFunc) gin.HandlerFunc {
reqctx := g.Request.Context() reqctx := g.Request.Context()
wrap, stackTrace, panicObj := callPanicSafe(fn, PreContext{wrapper: w, ginCtx: g}) pctx := PreContext{
wrapper: w,
ginCtx: g,
persistantData: &preContextData{},
}
for _, lstr := range w.listenerBeforeRequest {
lstr(g)
}
wrap, stackTrace, panicObj := callPanicSafe(fn, pctx)
if panicObj != nil { if panicObj != nil {
fmt.Printf("\n======== ======== STACKTRACE ======== ========\n%s\n======== ======== ======== ========\n\n", stackTrace) fmt.Printf("\n======== ======== STACKTRACE ======== ========\n%s\n======== ======== ======== ========\n\n", stackTrace)
@@ -29,11 +41,80 @@ func Wrap(w *GinWrapper, fn WHandlerFunc) gin.HandlerFunc {
} }
if g.Writer.Written() { if g.Writer.Written() {
// (!) This panic is not preoperly cached - it will kill the whole process
// but that only means you should NOT write in WrapperFunc...
panic("Writing in WrapperFunc is not supported") panic("Writing in WrapperFunc is not supported")
} }
if pctx.persistantData.sessionObj != nil {
err := pctx.persistantData.sessionObj.Finish(reqctx, wrap)
if err != nil {
wrap = Error(exerr.Wrap(err, "Failed to finish session").Any("originalResponse", wrap).Build())
}
}
for _, lstr := range w.listenerAfterRequest {
lstr(g, wrap)
}
if g.IsAborted() {
return
}
if reqctx.Err() == nil { if reqctx.Err() == nil {
wrap.Write(g) wrap.Write(g)
if g.IsAborted() {
// can happen if we try to marshal an invalid json object (e.g. containing Inf)
exerr.
New(exerr.TypePanic, "Failed to write response to renderer").
Strs("errors", g.Errors.Errors()).
Stack().
Print(g)
g.Status(500)
}
}
}
}
func WrapHTTPHandler(w *GinWrapper, fn http.Handler) gin.HandlerFunc {
return func(g *gin.Context) {
for _, lstr := range w.listenerBeforeRequest {
lstr(g)
}
fn.ServeHTTP(g.Writer, g.Request)
for _, lstr := range w.listenerAfterRequest {
lstr(g, nil)
}
}
}
func WrapHTTPHandlerFunc(w *GinWrapper, fn http.HandlerFunc) gin.HandlerFunc {
return func(g *gin.Context) {
for _, lstr := range w.listenerBeforeRequest {
lstr(g)
}
fn(g.Writer, g.Request)
for _, lstr := range w.listenerAfterRequest {
lstr(g, nil)
}
}
}
func WrapExtHTTPHandlerFunc(w *GinWrapper, fn func(*gin.Context, http.ResponseWriter, *http.Request)) gin.HandlerFunc {
return func(g *gin.Context) {
for _, lstr := range w.listenerBeforeRequest {
lstr(g)
}
fn(g, g.Writer, g.Request)
for _, lstr := range w.listenerAfterRequest {
lstr(g, nil)
} }
} }
} }
+9
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)
}
+105 -24
View File
@@ -1,23 +1,35 @@
package ginext package ginext
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"gogs.mikescher.com/BlackForestBytes/goext/exerr" "git.blackforestbytes.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"io"
"runtime/debug" "runtime/debug"
"time"
) )
type PreContext struct { type PreContext struct {
ginCtx *gin.Context ginCtx *gin.Context
wrapper *GinWrapper wrapper *GinWrapper
uri any uri any
query any query any
body any body any
form any rawbody *[]byte
header any form any
header any
timeout *time.Duration
persistantData *preContextData // must be a ptr, so that we can get the values back in out Wrap func
ignoreWrongContentType bool
}
type preContextData struct {
sessionObj SessionObject
} }
func (pctx *PreContext) URI(uri any) *PreContext { func (pctx *PreContext) URI(uri any) *PreContext {
@@ -35,6 +47,11 @@ func (pctx *PreContext) Body(body any) *PreContext {
return pctx return pctx
} }
func (pctx *PreContext) RawBody(rawbody *[]byte) *PreContext {
pctx.rawbody = rawbody
return pctx
}
func (pctx *PreContext) Form(form any) *PreContext { func (pctx *PreContext) Form(form any) *PreContext {
pctx.form = form pctx.form = form
return pctx return pctx
@@ -45,6 +62,21 @@ func (pctx *PreContext) Header(header any) *PreContext {
return pctx return pctx
} }
func (pctx *PreContext) WithTimeout(to time.Duration) *PreContext {
pctx.timeout = &to
return pctx
}
func (pctx *PreContext) WithSession(sessionObj SessionObject) *PreContext {
pctx.persistantData.sessionObj = sessionObj
return pctx
}
func (pctx *PreContext) IgnoreWrongContentType() *PreContext {
pctx.ignoreWrongContentType = true
return pctx
}
func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) { func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) {
if pctx.uri != nil { if pctx.uri != nil {
if err := pctx.ginCtx.ShouldBindUri(pctx.uri); err != nil { if err := pctx.ginCtx.ShouldBindUri(pctx.uri); err != nil {
@@ -52,7 +84,7 @@ func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) {
WithType(exerr.TypeBindFailURI). WithType(exerr.TypeBindFailURI).
Str("struct_type", fmt.Sprintf("%T", pctx.uri)). Str("struct_type", fmt.Sprintf("%T", pctx.uri)).
Build() Build()
return nil, nil, langext.Ptr(Error(err)) return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "URI", err))
} }
} }
@@ -62,24 +94,54 @@ func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) {
WithType(exerr.TypeBindFailQuery). WithType(exerr.TypeBindFailQuery).
Str("struct_type", fmt.Sprintf("%T", pctx.query)). Str("struct_type", fmt.Sprintf("%T", pctx.query)).
Build() Build()
return nil, nil, langext.Ptr(Error(err)) return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "QUERY", err))
} }
} }
if pctx.body != nil { if pctx.body != nil {
if pctx.ginCtx.ContentType() == "application/json" { if pctx.ginCtx.ContentType() == "application/json" {
if brc, ok := pctx.body.(dataext.BufferedReadCloser); ok {
// Ensures a fully reset (offset=0) buffer before parsing
err := brc.Reset()
if err != nil {
err = exerr.Wrap(err, "Failed to read (brc.reset) json-body").
WithType(exerr.TypeBindFailJSON).
Str("struct_type", fmt.Sprintf("%T", pctx.body)).
Build()
return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "JSON", err))
}
}
if err := pctx.ginCtx.ShouldBindJSON(pctx.body); err != nil { if err := pctx.ginCtx.ShouldBindJSON(pctx.body); err != nil {
err = exerr.Wrap(err, "Failed to read json-body"). err = exerr.Wrap(err, "Failed to read json-body").
WithType(exerr.TypeBindFailJSON). WithType(exerr.TypeBindFailJSON).
Str("struct_type", fmt.Sprintf("%T", pctx.body)). Str("struct_type", fmt.Sprintf("%T", pctx.body)).
Build() Build()
return nil, nil, langext.Ptr(Error(err)) return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "JSON", err))
} }
} else { } else {
err := exerr.New(exerr.TypeBindFailJSON, "missing JSON body"). if !pctx.ignoreWrongContentType {
Str("struct_type", fmt.Sprintf("%T", pctx.body)). err := exerr.New(exerr.TypeBindFailJSON, "missing JSON body").
Build() Str("struct_type", fmt.Sprintf("%T", pctx.body)).
return nil, nil, langext.Ptr(Error(err)) Build()
return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "JSON", err))
}
}
}
if pctx.rawbody != nil {
if brc, ok := pctx.ginCtx.Request.Body.(dataext.BufferedReadCloser); ok {
v, err := brc.BufferedAll()
if err != nil {
return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "BODY", err))
}
*pctx.rawbody = v
} else {
buf := &bytes.Buffer{}
_, err := io.Copy(buf, pctx.ginCtx.Request.Body)
if err != nil {
return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "BODY", err))
}
*pctx.rawbody = buf.Bytes()
} }
} }
@@ -90,29 +152,48 @@ func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) {
WithType(exerr.TypeBindFailFormData). WithType(exerr.TypeBindFailFormData).
Str("struct_type", fmt.Sprintf("%T", pctx.form)). Str("struct_type", fmt.Sprintf("%T", pctx.form)).
Build() Build()
return nil, nil, langext.Ptr(Error(err)) return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "FORM", err))
}
} else if pctx.ginCtx.ContentType() == "application/x-www-form-urlencoded" {
if err := pctx.ginCtx.ShouldBindWith(pctx.form, binding.Form); err != nil {
err = exerr.Wrap(err, "Failed to read urlencoded-form").
WithType(exerr.TypeBindFailFormData).
Str("struct_type", fmt.Sprintf("%T", pctx.form)).
Build()
return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "FORM", err))
} }
} else { } else {
err := exerr.New(exerr.TypeBindFailFormData, "missing form body"). if !pctx.ignoreWrongContentType {
Str("struct_type", fmt.Sprintf("%T", pctx.form)). err := exerr.New(exerr.TypeBindFailFormData, "missing form body").
Build() Str("struct_type", fmt.Sprintf("%T", pctx.form)).
return nil, nil, langext.Ptr(Error(err)) Build()
return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "FORM", err))
}
} }
} }
if pctx.header != nil { if pctx.header != nil {
if err := pctx.ginCtx.ShouldBindHeader(pctx.query); err != nil { if err := pctx.ginCtx.ShouldBindHeader(pctx.header); err != nil {
err = exerr.Wrap(err, "Failed to read header"). err = exerr.Wrap(err, "Failed to read header").
WithType(exerr.TypeBindFailHeader). WithType(exerr.TypeBindFailHeader).
Str("struct_type", fmt.Sprintf("%T", pctx.query)). Str("struct_type", fmt.Sprintf("%T", pctx.query)).
Build() Build()
return nil, nil, langext.Ptr(Error(err)) return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "HEADER", err))
} }
} }
ictx, cancel := context.WithTimeout(context.Background(), pctx.wrapper.requestTimeout) ictx, cancel := context.WithTimeout(context.Background(), langext.Coalesce(pctx.timeout, pctx.wrapper.requestTimeout))
actx := CreateAppContext(pctx.ginCtx, ictx, cancel) actx := CreateAppContext(pctx.ginCtx, ictx, cancel)
if pctx.persistantData.sessionObj != nil {
err := pctx.persistantData.sessionObj.Init(pctx.ginCtx, actx)
if err != nil {
actx.Cancel()
return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "INIT", err))
}
}
return actx, pctx.ginCtx, nil return actx, pctx.ginCtx, nil
} }
+23 -192
View File
@@ -1,12 +1,20 @@
package ginext package ginext
import ( import (
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/exerr" "git.blackforestbytes.com/BlackForestBytes/goext/exerr"
json "gogs.mikescher.com/BlackForestBytes/goext/gojson"
) )
type cookieval struct {
name string
value string
maxAge int
path string
domain string
secure bool
httpOnly bool
}
type headerval struct { type headerval struct {
Key string Key string
Val string Val string
@@ -15,200 +23,23 @@ type headerval struct {
type HTTPResponse interface { type HTTPResponse interface {
Write(g *gin.Context) Write(g *gin.Context)
WithHeader(k string, v string) HTTPResponse WithHeader(k string, v string) HTTPResponse
WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse
IsSuccess() bool
} }
type jsonHTTPResponse struct { type InspectableHTTPResponse interface {
statusCode int HTTPResponse
data any
headers []headerval Statuscode() int
BodyString(g *gin.Context) *string
ContentType() string
Headers() []string
} }
func (j jsonHTTPResponse) Write(g *gin.Context) { type HTTPErrorResponse interface {
for _, v := range j.headers { HTTPResponse
g.Header(v.Key, v.Val)
}
g.Render(j.statusCode, json.GoJsonRender{Data: j.data, NilSafeSlices: true, NilSafeMaps: true})
}
func (j jsonHTTPResponse) WithHeader(k string, v string) HTTPResponse { Error() error
j.headers = append(j.headers, headerval{k, v})
return j
}
type emptyHTTPResponse struct {
statusCode int
headers []headerval
}
func (j emptyHTTPResponse) Write(g *gin.Context) {
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
g.Status(j.statusCode)
}
func (j emptyHTTPResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
type textHTTPResponse struct {
statusCode int
data string
headers []headerval
}
func (j textHTTPResponse) Write(g *gin.Context) {
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
g.String(j.statusCode, "%s", j.data)
}
func (j textHTTPResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
type dataHTTPResponse struct {
statusCode int
data []byte
contentType string
headers []headerval
}
func (j dataHTTPResponse) Write(g *gin.Context) {
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
g.Data(j.statusCode, j.contentType, j.data)
}
func (j dataHTTPResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
type fileHTTPResponse struct {
mimetype string
filepath string
filename *string
headers []headerval
}
func (j fileHTTPResponse) Write(g *gin.Context) {
g.Header("Content-Type", j.mimetype) // if we don't set it here gin does weird file-sniffing later...
if j.filename != nil {
g.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", *j.filename))
}
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
g.File(j.filepath)
}
func (j fileHTTPResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
type downloadDataHTTPResponse struct {
statusCode int
mimetype string
data []byte
filename *string
headers []headerval
}
func (j downloadDataHTTPResponse) Write(g *gin.Context) {
g.Header("Content-Type", j.mimetype) // if we don't set it here gin does weird file-sniffing later...
if j.filename != nil {
g.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", *j.filename))
}
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
g.Data(j.statusCode, j.mimetype, j.data)
}
func (j downloadDataHTTPResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
type redirectHTTPResponse struct {
statusCode int
url string
headers []headerval
}
func (j redirectHTTPResponse) Write(g *gin.Context) {
g.Redirect(j.statusCode, j.url)
}
func (j redirectHTTPResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
type jsonAPIErrResponse struct {
err *exerr.ExErr
headers []headerval
}
func (j jsonAPIErrResponse) Write(g *gin.Context) {
j.err.Output(g)
}
func (j jsonAPIErrResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
func Status(sc int) HTTPResponse {
return &emptyHTTPResponse{statusCode: sc}
}
func JSON(sc int, data any) HTTPResponse {
return &jsonHTTPResponse{statusCode: sc, data: data}
}
func Data(sc int, contentType string, data []byte) HTTPResponse {
return &dataHTTPResponse{statusCode: sc, contentType: contentType, data: data}
}
func Text(sc int, data string) HTTPResponse {
return &textHTTPResponse{statusCode: sc, data: data}
}
func File(mimetype string, filepath string) HTTPResponse {
return &fileHTTPResponse{mimetype: mimetype, filepath: filepath}
}
func Download(mimetype string, filepath string, filename string) HTTPResponse {
return &fileHTTPResponse{mimetype: mimetype, filepath: filepath, filename: &filename}
}
func DownloadData(status int, mimetype string, filename string, data []byte) HTTPResponse {
return &downloadDataHTTPResponse{statusCode: status, mimetype: mimetype, data: data, filename: &filename}
}
func Redirect(sc int, newURL string) HTTPResponse {
return &redirectHTTPResponse{statusCode: sc, url: newURL}
}
func Error(e error) HTTPResponse {
return &jsonAPIErrResponse{
err: exerr.FromError(e),
}
}
func ErrWrap(e error, errorType exerr.ErrorType, msg string) HTTPResponse {
return &jsonAPIErrResponse{
err: exerr.FromError(exerr.Wrap(e, msg).WithType(errorType).Build()),
}
} }
func NotImplemented() HTTPResponse { func NotImplemented() HTTPResponse {
+58
View File
@@ -0,0 +1,58 @@
package ginext
import (
"github.com/gin-gonic/gin"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
)
type dataHTTPResponse struct {
statusCode int
data []byte
contentType string
headers []headerval
cookies []cookieval
}
func (j dataHTTPResponse) Write(g *gin.Context) {
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
for _, v := range j.cookies {
g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly)
}
g.Data(j.statusCode, j.contentType, j.data)
}
func (j dataHTTPResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
func (j dataHTTPResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse {
j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly})
return j
}
func (j dataHTTPResponse) IsSuccess() bool {
return j.statusCode >= 200 && j.statusCode <= 399
}
func (j dataHTTPResponse) Statuscode() int {
return j.statusCode
}
func (j dataHTTPResponse) BodyString(*gin.Context) *string {
return langext.Ptr(string(j.data))
}
func (j dataHTTPResponse) ContentType() string {
return j.contentType
}
func (j dataHTTPResponse) Headers() []string {
return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val })
}
func Data(sc int, contentType string, data []byte) HTTPResponse {
return &dataHTTPResponse{statusCode: sc, contentType: contentType, data: data}
}
+64
View File
@@ -0,0 +1,64 @@
package ginext
import (
"fmt"
"github.com/gin-gonic/gin"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
)
type downloadDataHTTPResponse struct {
statusCode int
mimetype string
data []byte
filename *string
headers []headerval
cookies []cookieval
}
func (j downloadDataHTTPResponse) Write(g *gin.Context) {
g.Header("Content-Type", j.mimetype) // if we don't set it here gin does weird file-sniffing later...
if j.filename != nil {
g.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", *j.filename))
}
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
for _, v := range j.cookies {
g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly)
}
g.Data(j.statusCode, j.mimetype, j.data)
}
func (j downloadDataHTTPResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
func (j downloadDataHTTPResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse {
j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly})
return j
}
func (j downloadDataHTTPResponse) IsSuccess() bool {
return j.statusCode >= 200 && j.statusCode <= 399
}
func (j downloadDataHTTPResponse) Statuscode() int {
return j.statusCode
}
func (j downloadDataHTTPResponse) BodyString(*gin.Context) *string {
return langext.Ptr(string(j.data))
}
func (j downloadDataHTTPResponse) ContentType() string {
return j.mimetype
}
func (j downloadDataHTTPResponse) Headers() []string {
return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val })
}
func DownloadData(status int, mimetype string, filename string, data []byte) HTTPResponse {
return &downloadDataHTTPResponse{statusCode: status, mimetype: mimetype, data: data, filename: &filename}
}
+56
View File
@@ -0,0 +1,56 @@
package ginext
import (
"github.com/gin-gonic/gin"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
)
type emptyHTTPResponse struct {
statusCode int
headers []headerval
cookies []cookieval
}
func (j emptyHTTPResponse) Write(g *gin.Context) {
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
for _, v := range j.cookies {
g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly)
}
g.Status(j.statusCode)
}
func (j emptyHTTPResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
func (j emptyHTTPResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse {
j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly})
return j
}
func (j emptyHTTPResponse) IsSuccess() bool {
return j.statusCode >= 200 && j.statusCode <= 399
}
func (j emptyHTTPResponse) Statuscode() int {
return j.statusCode
}
func (j emptyHTTPResponse) BodyString(*gin.Context) *string {
return nil
}
func (j emptyHTTPResponse) ContentType() string {
return ""
}
func (j emptyHTTPResponse) Headers() []string {
return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val })
}
func Status(sc int) HTTPResponse {
return &emptyHTTPResponse{statusCode: sc}
}
+73
View File
@@ -0,0 +1,73 @@
package ginext
import (
"fmt"
"github.com/gin-gonic/gin"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"os"
)
type fileHTTPResponse struct {
mimetype string
filepath string
filename *string
headers []headerval
cookies []cookieval
}
func (j fileHTTPResponse) Write(g *gin.Context) {
g.Header("Content-Type", j.mimetype) // if we don't set it here gin does weird file-sniffing later...
if j.filename != nil {
g.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", *j.filename))
}
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
for _, v := range j.cookies {
g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly)
}
g.File(j.filepath)
}
func (j fileHTTPResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
func (j fileHTTPResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse {
j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly})
return j
}
func (j fileHTTPResponse) IsSuccess() bool {
return true
}
func (j fileHTTPResponse) Statuscode() int {
return 200
}
func (j fileHTTPResponse) BodyString(*gin.Context) *string {
data, err := os.ReadFile(j.filepath)
if err != nil {
return nil
}
return langext.Ptr(string(data))
}
func (j fileHTTPResponse) ContentType() string {
return j.mimetype
}
func (j fileHTTPResponse) Headers() []string {
return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val })
}
func File(mimetype string, filepath string) HTTPResponse {
return &fileHTTPResponse{mimetype: mimetype, filepath: filepath}
}
func Download(mimetype string, filepath string, filename string) HTTPResponse {
return &fileHTTPResponse{mimetype: mimetype, filepath: filepath, filename: &filename}
}
+78
View File
@@ -0,0 +1,78 @@
package ginext
import (
"github.com/gin-gonic/gin"
json "git.blackforestbytes.com/BlackForestBytes/goext/gojson"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
)
type jsonHTTPResponse struct {
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(jsonFilterKey); jsonfilter != "" {
f = &jsonfilter
}
if j.filterOverride != nil {
f = j.filterOverride
}
return json.GoJsonRender{Data: j.data, NilSafeSlices: true, NilSafeMaps: true, Filter: f}
}
func (j jsonHTTPResponse) Write(g *gin.Context) {
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
for _, v := range j.cookies {
g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly)
}
g.Render(j.statusCode, j.jsonRenderer(g))
}
func (j jsonHTTPResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
func (j jsonHTTPResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse {
j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly})
return j
}
func (j jsonHTTPResponse) IsSuccess() bool {
return j.statusCode >= 200 && j.statusCode <= 399
}
func (j jsonHTTPResponse) Statuscode() int {
return j.statusCode
}
func (j jsonHTTPResponse) BodyString(g *gin.Context) *string {
if str, err := j.jsonRenderer(g).RenderString(); err == nil {
return &str
} else {
return nil
}
}
func (j jsonHTTPResponse) ContentType() string {
return "application/json"
}
func (j jsonHTTPResponse) Headers() []string {
return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val })
}
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}
}
+81
View File
@@ -0,0 +1,81 @@
package ginext
import (
"context"
"github.com/gin-gonic/gin"
"git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
)
type jsonAPIErrResponse struct {
err *exerr.ExErr
headers []headerval
cookies []cookieval
}
func (j jsonAPIErrResponse) Error() error {
return j.err
}
func (j jsonAPIErrResponse) Write(g *gin.Context) {
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
for _, v := range j.cookies {
g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly)
}
exerr.Get(j.err).Output(context.Background(), g)
j.err.CallListener(exerr.MethodOutput, exerr.ListenerOpt{NoLog: false})
}
func (j jsonAPIErrResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
func (j jsonAPIErrResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse {
j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly})
return j
}
func (j jsonAPIErrResponse) IsSuccess() bool {
return false
}
func (j jsonAPIErrResponse) Statuscode() int {
return langext.Coalesce(j.err.RecursiveStatuscode(), 0)
}
func (j jsonAPIErrResponse) BodyString(*gin.Context) *string {
if str, err := j.err.ToDefaultAPIJson(); err == nil {
return &str
} else {
return nil
}
}
func (j jsonAPIErrResponse) ContentType() string {
return "application/json"
}
func (j jsonAPIErrResponse) Headers() []string {
return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val })
}
func (j jsonAPIErrResponse) Unwrap() error {
return j.err
}
func Error(e error) HTTPResponse {
return &jsonAPIErrResponse{
err: exerr.FromError(e),
}
}
func ErrWrap(e error, errorType exerr.ErrorType, msg string) HTTPResponse {
return &jsonAPIErrResponse{
err: exerr.FromError(exerr.Wrap(e, msg).WithType(errorType).Build()),
}
}
+57
View File
@@ -0,0 +1,57 @@
package ginext
import (
"github.com/gin-gonic/gin"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
)
type redirectHTTPResponse struct {
statusCode int
url string
headers []headerval
cookies []cookieval
}
func (j redirectHTTPResponse) Write(g *gin.Context) {
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
for _, v := range j.cookies {
g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly)
}
g.Redirect(j.statusCode, j.url)
}
func (j redirectHTTPResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
func (j redirectHTTPResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse {
j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly})
return j
}
func (j redirectHTTPResponse) IsSuccess() bool {
return j.statusCode >= 200 && j.statusCode <= 399
}
func (j redirectHTTPResponse) Statuscode() int {
return j.statusCode
}
func (j redirectHTTPResponse) BodyString(*gin.Context) *string {
return nil
}
func (j redirectHTTPResponse) ContentType() string {
return ""
}
func (j redirectHTTPResponse) Headers() []string {
return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val })
}
func Redirect(sc int, newURL string) HTTPResponse {
return &redirectHTTPResponse{statusCode: sc, url: newURL}
}
+72
View File
@@ -0,0 +1,72 @@
package ginext
import (
"github.com/gin-gonic/gin"
"git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"io"
"net/http"
"time"
)
type seekableResponse struct {
data io.ReadSeeker
contentType string
filename string
headers []headerval
cookies []cookieval
}
func (j seekableResponse) Write(g *gin.Context) {
g.Header("Content-Type", j.contentType) // if we don't set it here http.ServeContent does weird sniffing later...
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
for _, v := range j.cookies {
g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly)
}
http.ServeContent(g.Writer, g.Request, j.filename, time.Unix(0, 0), j.data)
if clsr, ok := j.data.(io.ReadSeekCloser); ok {
err := clsr.Close()
if err != nil {
exerr.Wrap(err, "failed to close io.ReadSeerkClose in ginext.Seekable").Str("filename", j.filename).Print()
}
}
}
func (j seekableResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
func (j seekableResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse {
j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly})
return j
}
func (j seekableResponse) IsSuccess() bool {
return true
}
func (j seekableResponse) Statuscode() int {
return 200
}
func (j seekableResponse) BodyString(*gin.Context) *string {
return langext.Ptr("(seekable)")
}
func (j seekableResponse) ContentType() string {
return j.contentType
}
func (j seekableResponse) Headers() []string {
return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val })
}
func Seekable(filename string, contentType string, data io.ReadSeeker) HTTPResponse {
return &seekableResponse{filename: filename, contentType: contentType, data: data}
}
+57
View File
@@ -0,0 +1,57 @@
package ginext
import (
"github.com/gin-gonic/gin"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
)
type textHTTPResponse struct {
statusCode int
data string
headers []headerval
cookies []cookieval
}
func (j textHTTPResponse) Write(g *gin.Context) {
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
for _, v := range j.cookies {
g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly)
}
g.String(j.statusCode, "%s", j.data)
}
func (j textHTTPResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
func (j textHTTPResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse {
j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly})
return j
}
func (j textHTTPResponse) IsSuccess() bool {
return j.statusCode >= 200 && j.statusCode <= 399
}
func (j textHTTPResponse) Statuscode() int {
return j.statusCode
}
func (j textHTTPResponse) BodyString(*gin.Context) *string {
return langext.Ptr(j.data)
}
func (j textHTTPResponse) ContentType() string {
return "text/plain"
}
func (j textHTTPResponse) Headers() []string {
return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val })
}
func Text(sc int, data string) HTTPResponse {
return &textHTTPResponse{statusCode: sc, data: data}
}
+121 -15
View File
@@ -1,15 +1,14 @@
package ginext package ginext
import ( import (
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/rext"
"net/http" "net/http"
"path" "path"
"reflect" "reflect"
"regexp"
"runtime" "runtime"
"strings" "strings"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/gin-gonic/gin"
) )
var anyMethods = []string{ var anyMethods = []string{
@@ -55,7 +54,11 @@ func (w *GinRoutesWrapper) Group(relativePath string) *GinRoutesWrapper {
func (w *GinRoutesWrapper) Use(middleware ...gin.HandlerFunc) *GinRoutesWrapper { func (w *GinRoutesWrapper) Use(middleware ...gin.HandlerFunc) *GinRoutesWrapper {
defHandler := langext.ArrCopy(w.defaultHandler) defHandler := langext.ArrCopy(w.defaultHandler)
defHandler = append(defHandler, middleware...) defHandler = append(defHandler, middleware...)
return &GinRoutesWrapper{wrapper: w.wrapper, routes: w.routes, defaultHandler: defHandler} return &GinRoutesWrapper{wrapper: w.wrapper, routes: w.routes, defaultHandler: defHandler, absPath: w.absPath}
}
func (w *GinRoutesWrapper) WithJSONFilter(filter string) *GinRoutesWrapper {
return w.Use(func(g *gin.Context) { g.Set(jsonFilterKey, filter) })
} }
func (w *GinRoutesWrapper) GET(relativePath string) *GinRouteBuilder { func (w *GinRoutesWrapper) GET(relativePath string) *GinRouteBuilder {
@@ -109,11 +112,15 @@ func (w *GinRouteBuilder) Use(middleware ...gin.HandlerFunc) *GinRouteBuilder {
return w return w
} }
func (w *GinRouteBuilder) WithJSONFilter(filter string) *GinRouteBuilder {
return w.Use(func(g *gin.Context) { g.Set(jsonFilterKey, filter) })
}
func (w *GinRouteBuilder) Handle(handler WHandlerFunc) { func (w *GinRouteBuilder) Handle(handler WHandlerFunc) {
if w.routes.wrapper.bufferBody { if w.routes.wrapper.bufferBody {
arr := make([]gin.HandlerFunc, 0, len(w.handlers)+1) arr := make([]gin.HandlerFunc, 0, len(w.handlers)+1)
arr = append(arr, BodyBuffer()) arr = append(arr, BodyBuffer)
arr = append(arr, w.handlers...) arr = append(arr, w.handlers...)
w.handlers = arr w.handlers = arr
} }
@@ -142,14 +149,119 @@ func (w *GinRouteBuilder) Handle(handler WHandlerFunc) {
}) })
} }
func (w *GinRouteBuilder) HandleRawHTTPHandler(f http.Handler) {
if w.routes.wrapper.bufferBody {
arr := make([]gin.HandlerFunc, 0, len(w.handlers)+1)
arr = append(arr, BodyBuffer)
arr = append(arr, w.handlers...)
w.handlers = arr
}
middlewareNames := langext.ArrMap(w.handlers, func(v gin.HandlerFunc) string { return nameOfFunction(v) })
w.handlers = append(w.handlers, WrapHTTPHandler(w.routes.wrapper, f))
methodName := w.method
if w.method == "*" {
methodName = "ANY"
for _, method := range anyMethods {
w.routes.routes.Handle(method, w.relPath, w.handlers...)
}
} else {
w.routes.routes.Handle(w.method, w.relPath, w.handlers...)
}
w.routes.wrapper.routeSpecs = append(w.routes.wrapper.routeSpecs, ginRouteSpec{
Method: methodName,
URL: w.absPath,
Middlewares: middlewareNames,
Handler: "[HTTPHandler]",
})
}
func (w *GinRouteBuilder) HandleRawHTTPHandlerFunc(f http.HandlerFunc) {
if w.routes.wrapper.bufferBody {
arr := make([]gin.HandlerFunc, 0, len(w.handlers)+1)
arr = append(arr, BodyBuffer)
arr = append(arr, w.handlers...)
w.handlers = arr
}
middlewareNames := langext.ArrMap(w.handlers, func(v gin.HandlerFunc) string { return nameOfFunction(v) })
w.handlers = append(w.handlers, WrapHTTPHandlerFunc(w.routes.wrapper, f))
methodName := w.method
if w.method == "*" {
methodName = "ANY"
for _, method := range anyMethods {
w.routes.routes.Handle(method, w.relPath, w.handlers...)
}
} else {
w.routes.routes.Handle(w.method, w.relPath, w.handlers...)
}
w.routes.wrapper.routeSpecs = append(w.routes.wrapper.routeSpecs, ginRouteSpec{
Method: methodName,
URL: w.absPath,
Middlewares: middlewareNames,
Handler: "[HTTPHandlerFunc]",
})
}
func (w *GinRouteBuilder) HandleExtHTTPHandlerFunc(f func(*gin.Context, http.ResponseWriter, *http.Request)) {
if w.routes.wrapper.bufferBody {
arr := make([]gin.HandlerFunc, 0, len(w.handlers)+1)
arr = append(arr, BodyBuffer)
arr = append(arr, w.handlers...)
w.handlers = arr
}
middlewareNames := langext.ArrMap(w.handlers, func(v gin.HandlerFunc) string { return nameOfFunction(v) })
w.handlers = append(w.handlers, WrapExtHTTPHandlerFunc(w.routes.wrapper, f))
methodName := w.method
if w.method == "*" {
methodName = "ANY"
for _, method := range anyMethods {
w.routes.routes.Handle(method, w.relPath, w.handlers...)
}
} else {
w.routes.routes.Handle(w.method, w.relPath, w.handlers...)
}
w.routes.wrapper.routeSpecs = append(w.routes.wrapper.routeSpecs, ginRouteSpec{
Method: methodName,
URL: w.absPath,
Middlewares: middlewareNames,
Handler: "[HTTPHandlerFunc]",
})
}
func (w *GinWrapper) NoRoute(handler WHandlerFunc) { func (w *GinWrapper) NoRoute(handler WHandlerFunc) {
w.engine.NoRoute(Wrap(w, handler))
handlers := make([]gin.HandlerFunc, 0)
if w.bufferBody {
handlers = append(handlers, BodyBuffer)
}
middlewareNames := langext.ArrMap(handlers, func(v gin.HandlerFunc) string { return nameOfFunction(v) })
handlerName := nameOfFunction(handler)
handlers = append(handlers, Wrap(w, handler))
w.engine.NoRoute(handlers...)
w.routeSpecs = append(w.routeSpecs, ginRouteSpec{ w.routeSpecs = append(w.routeSpecs, ginRouteSpec{
Method: "ANY", Method: "ANY",
URL: "[NO_ROUTE]", URL: "[NO_ROUTE]",
Middlewares: nil, Middlewares: middlewareNames,
Handler: nameOfFunction(handler), Handler: handlerName,
}) })
} }
@@ -169,12 +281,6 @@ func nameOfFunction(f any) string {
fname = fname[:len(fname)-len("-fm")] fname = fname[:len(fname)-len("-fm")]
} }
suffix := rext.W(regexp.MustCompile(`\.func[0-9]+(?:\.[0-9]+)*$`))
if match, ok := suffix.MatchFirst(fname); ok {
fname = fname[:len(fname)-match.FullMatch().Length()]
}
return fname return fname
} }

Some files were not shown because too many files have changed in this diff Show More