Compare commits

...

452 Commits

Author SHA1 Message Date
49375e90f0 v0.0.547 allow calling ListWithCount with pageSize=0
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m55s
2024-12-08 18:04:04 +01:00
d8cf255c80 v0.0.546 Fix ginext json-parse error when the bufferedReader was read beforehand
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m55s
2024-11-28 12:06:57 +01:00
b520282ba0 v0.0.545
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m6s
2024-11-27 13:21:45 +01:00
27cc9366b5 v0.0.544
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m0s
2024-11-26 15:10:27 +01:00
d9517fe73c v0.0.543
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m7s
2024-11-13 15:03:51 +01:00
8a92a6cc52 v0.0.542
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m9s
2024-11-07 13:13:12 +01:00
9b2028ab54 v0.0.541
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m3s
2024-11-05 14:38:42 +01:00
207fd331d5 v0.0.540 handle ct=nil same as ct=Start
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m25s
2024-10-28 14:35:05 +01:00
54b0d6701d v0.0.539
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m49s
2024-10-27 02:21:35 +02:00
fc2657179b v0.0.538
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m7s
2024-10-27 02:18:45 +02:00
d4894e31fe Merge branch 'cursortoken-paginated'
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-10-27 02:18:25 +02:00
0ddfaf666b v0.0.537 fix goext error print always showing error-type of highest-level error
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m12s
2024-10-26 23:38:13 +02:00
e154137105 Trying out paginated cursortoken variant [UNTESTED]
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m11s
2024-10-25 09:45:42 +02:00
9b9a79b4ad v0.0.536 revert bfcodegen changes
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m24s
2024-10-22 09:57:06 +02:00
5a8d7110e4 v0.0.535
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m52s
2024-10-22 09:41:59 +02:00
d47c84cd47 v0.0.534
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-10-22 09:40:53 +02:00
c571f3f888 v0.0.533 Made String() functions in bfcodegen nil-ptr safe
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m9s
2024-10-22 09:36:40 +02:00
e884ba6b89 v0.0.532
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m53s
2024-10-13 16:33:39 +02:00
1a8e31e5ef v0.0.531
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m58s
2024-10-13 16:10:55 +02:00
eccc0fe9e5 v0.0.530
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m31s
2024-10-09 11:15:26 +02:00
c8dec24a0d v0.0.529 handle PanicWrappedErr in exerr.FromError()
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m13s
2024-10-08 19:22:17 +02:00
b8cb989e54 v0.0.528
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m38s
2024-10-07 17:20:40 +02:00
ec672fbd49 v0.0.527
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-10-07 17:19:30 +02:00
cfb0b53fc7 v0.0.526
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m17s
2024-10-05 23:59:23 +02:00
a7389f44fa v0.0.525 upgrad go1.22 -> go1.23
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m4s
2024-10-05 02:45:20 +02:00
69f0fedd66 v0.0.524
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m22s
2024-10-05 01:41:10 +02:00
335ef4d8e8 v0.0.523 ringbuffer
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m23s
2024-10-05 01:28:46 +02:00
61801ff20d v0.0.522
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m53s
2024-10-05 01:12:00 +02:00
361dca5c85 v0.0.521 ctxext
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m56s
2024-10-05 01:06:36 +02:00
9f85a243e8 v0.0.520
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m7s
2024-10-05 01:02:25 +02:00
dc6cb274ee v0.0.519
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-10-05 00:58:15 +02:00
f6b47792a4 v0.0.518 Improve sq db-listener interface (breaking)
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m11s
2024-10-05 00:45:55 +02:00
295b3ef793 v0.0.517 add constructor funcs for tuples
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m21s
2024-10-02 11:31:34 +02:00
721c176337 v0.0.516
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m25s
2024-09-25 21:43:41 +02:00
ebba6545a3 v0.0.515
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 5m35s
2024-09-16 17:39:51 +02:00
19c7e22ced v0.0.514 fix mongo filter where the primary sort key is null in db (fallback to secondary)
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-09-16 17:39:18 +02:00
9f883b458f v0.0.513
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m55s
2024-09-16 15:27:32 +02:00
1f456c5134 v0.0.512
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 5m6s
2024-09-15 21:25:21 +02:00
d7fbef37db v0.0.511
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m10s
2024-09-15 18:22:07 +02:00
a1668b6e5a v0.0.510
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m24s
2024-09-13 18:06:49 +02:00
3a17edfaf0 v0.0.509
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 6m2s
2024-08-26 14:35:49 +02:00
3320a9c19d v0.0.508
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m25s
2024-08-25 17:36:20 +02:00
8dcd8a270a v0.0.507 fix jsonfilter:"-" not working
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 7m7s
2024-08-25 15:41:17 +02:00
03a9b276d8 v0.0.506 allow empty-string as value for enum
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 7m36s
2024-08-22 11:45:02 +02:00
9c8cde384f v0.0.505
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 6m17s
2024-08-08 15:57:05 +02:00
99b000ecf4 v0.0.504
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m53s
2024-08-07 19:44:45 +02:00
a173e30090 v0.0.503
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m52s
2024-08-07 19:37:38 +02:00
a3481a7d2d v0.0.502
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-08-07 19:35:23 +02:00
a8e6f98a89 v0.0.501
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-08-07 19:31:36 +02:00
ab805403b9 v0.0.500
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-08-07 19:30:38 +02:00
1e98d351ce v0.0.499
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 5m24s
2024-08-07 18:34:22 +02:00
c40bdc8e9e v0.0.498
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m13s
2024-08-07 17:26:35 +02:00
7204562879 v0.0.497
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m33s
2024-08-07 17:04:59 +02:00
741611a2e1 v0.0.496 wpdf fixes and wpdf test.go
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m58s
2024-08-07 15:34:06 +02:00
133aeb8374 v0.0.495
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m44s
2024-08-07 14:00:02 +02:00
b78a468632 v0.0.494 add tables to wpdf
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-08-07 13:57:29 +02:00
f1b4480e0f v0.0.493 fix panic in RegisterImage for very short images
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 5m22s
2024-08-07 09:22:37 +02:00
ffffe4bf24 v0.0.492
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 5m32s
2024-08-02 16:19:21 +02:00
413bf3c848 v0.0.491 small optimization in Paginate method
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 8m31s
2024-07-31 00:15:09 +02:00
646990b549 v0.0.490 documentation and extra-params in exerr
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m2s
2024-07-27 23:44:18 +02:00
e5818146a8 v0.0.489
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m54s
2024-07-23 14:21:03 +02:00
1310054121 v0.0.488 fix wpdf with 16bpp images
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m9s
2024-07-22 15:16:28 +02:00
49d423915c v0.0.487
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m54s
2024-07-18 17:45:56 +02:00
1962cb3c52 v0.0.486 add ginext -> CorsAllowHeader
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m51s
2024-07-18 17:29:18 +02:00
84f124dd4d v0.0.485
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m44s
2024-07-16 15:22:18 +02:00
ff8e066135 v0.0.484
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m58s
2024-07-16 15:16:56 +02:00
bc5c61e43d v0.0.483
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m37s
2024-07-16 15:08:37 +02:00
6ded615723 v0.0.482 mathext.Percentile
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m46s
2024-07-12 16:33:42 +02:00
abc8af525a v0.0.481
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m50s
2024-07-04 16:24:49 +02:00
19d943361b v0.0.480
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m20s
2024-07-02 11:32:22 +02:00
b464afae01 v0.0.479 AccessStruct
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m36s
2024-07-02 11:29:47 +02:00
56bc5e8285 v0.0.478
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m41s
2024-07-01 17:23:00 +02:00
cb95bb561c v0.0.477 add langext.StrWrap
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m4s
2024-06-29 15:36:39 +02:00
dff8941bd3 v0.0.476 Ãproperly close cursor in wmo
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m44s
2024-06-28 18:37:02 +02:00
78e1c33e30 v0.0.475 ArrGroupBy
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m36s
2024-06-16 17:14:21 +02:00
d2f2a0558a v0.0.474 Add ZeroLogger config field to exerr.Init to override used zerolog instance
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m27s
2024-06-14 23:18:58 +02:00
fc4bed4b9f v0.0.473 add ctx to wmo.FilterQuery|Sort|Pagination
All checks were successful
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
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m34s
2024-06-14 14:56:41 +02:00
f6121a6961 v0.0.471 Revert "v0.0.470 Add GoextJsonMarshaller interface to call when marshalling json via gojson"
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m50s
2024-06-11 19:39:43 +02:00
7fc73f1e93 v0.0.470 Add GoextJsonMarshaller interface to call when marshalling json via gojson
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m51s
2024-06-11 19:34:48 +02:00
2504ef00a0 v0.0.469
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m26s
2024-06-11 12:10:49 +02:00
fc5803493c added DblPtrIfNotNil
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m31s
2024-06-05 17:53:57 +02:00
a9295bfabf added CoalesceDblPtr
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m37s
2024-06-05 15:10:31 +02:00
12fa53d848 v0.0.466 exerr.Wrap now inherits the Severity of the wrapped error
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m24s
2024-06-03 13:48:30 +02:00
d2bb362135 v0.0.465
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m39s
2024-06-03 09:39:57 +02:00
9dd81f6bd5 v0.0.464 improve ZeroLogErrTraces/ZeroLogAllTraces output for empty-message wrapped exerrs
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m36s
2024-06-01 02:40:48 +02:00
d2c04afcd5 v0.0.463 Fix SubtractYears
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 3m29s
2024-05-29 20:20:01 +02:00
62980e1489 v0.0.462
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m32s
2024-05-23 14:37:05 +02:00
59963adf74 v0.0.461
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m17s
2024-05-20 00:52:49 +02:00
194ea4ace5 v0.0.460
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m16s
2024-05-20 00:38:04 +02:00
73b80a66bc v0.0.459
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m16s
2024-05-20 00:20:31 +02:00
d8b2d01274 v0.0.458 revert 457 and fix ObjectFitImage
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m49s
2024-05-20 00:15:24 +02:00
bfa8457e95 v0.0.457 test
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m38s
2024-05-20 00:07:33 +02:00
70106733d9 v0.0.456
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m38s
2024-05-18 23:38:47 +02:00
ce7837b9ef v0.0.455 add proper json/bson marshalling to exerr [severity|type|category]
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m39s
2024-05-16 15:38:42 +02:00
d0d72167eb v0.0.454
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m17s
2024-05-14 15:10:27 +02:00
a55ee1a6ce v0.0.453
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m18s
2024-05-14 14:57:10 +02:00
dfc319573c v0.0.452
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m16s
2024-05-14 12:48:43 +02:00
246e555f3f v0.0.451 wpdf image processing
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-05-14 12:46:49 +02:00
c28bc086b2 v0.0.450 wpdf
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 3m33s
2024-05-14 11:52:56 +02:00
d44e971325 v0.0.449
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m15s
2024-05-12 16:51:52 +02:00
fe4cdc48af v0.0.448 wmo marshalHook
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 25s
2024-05-12 16:45:45 +02:00
631006a4e1 v0.0.447
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m18s
2024-05-10 21:33:01 +02:00
567ead8697 v0.0.446 syncMap.GetAndSetIfNotContains and CASMutex
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-05-10 21:31:36 +02:00
e4886b4a7d v0.0.445 added CtxData() and ExtendGinMeta/ExtendContextMeta to exerr
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m55s
2024-05-03 15:28:53 +02:00
dcb5d3d7cd v0.0.444 change gin values in exerr auto meta to not include dots in keys (fucks up mongo)
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 3m9s
2024-05-03 13:24:08 +02:00
15a639f85a v0.0.443 fix wmo.List with pageSize==0
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 3m9s
2024-05-03 11:56:29 +02:00
303bd04649 v0.0.442
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m0s
2024-04-29 17:24:10 +02:00
7bda674939 v0.0.441
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m29s
2024-04-29 17:19:55 +02:00
126d4fbd0b v0.0.440 improve exerr.toJson
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m54s
2024-04-29 16:03:58 +02:00
fed8bccaab v0.0.439
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m38s
2024-04-25 11:47:16 +02:00
47b6a6b508 v0.0.438
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m20s
2024-04-25 11:40:01 +02:00
764ce79a71 v0.0.437 properly handle $group in wmo extraModPipeline
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 3m23s
2024-04-23 16:12:17 +02:00
b876c64ba2 v0.0.436
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m56s
2024-04-18 14:09:26 +02:00
8d52b41f57 v0.0.435 add ConvertStructToMapOpt.MaxDepth
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m39s
2024-04-15 12:55:44 +02:00
f47e2a33fe v0.0.434
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m13s
2024-04-15 10:43:26 +02:00
9321938dad v0.0.433 fix exerr missing gindata when using ginext.Error and add config for Output logging to stderr
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m0s
2024-04-15 10:25:30 +02:00
3828d601a2 v0.0.432 better handling of unmarshall-able values in exerr meta
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m12s
2024-04-13 22:08:45 +02:00
2e713c808d v0.0.431
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m10s
2024-04-10 15:29:59 +02:00
6602f86b43 v0.0.430
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-04-10 15:27:41 +02:00
24d9f0fdc7 v0.0.429
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m23s
2024-04-08 16:33:44 +02:00
8446b2da22 v0.0.428
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-04-08 16:32:34 +02:00
758e5a67b5 v0.0.427
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m30s
2024-04-07 15:10:21 +02:00
678ddd7124 v0.0.426 fix JsonOpt
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m57s
2024-04-01 16:03:00 +02:00
36b71dfaf3 v0.0.425 ArrAppend
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m25s
2024-03-30 14:24:53 +01:00
9491b72b8d v0.0.424 timeext.SubtractYears
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m29s
2024-03-30 03:01:55 +01:00
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?)
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m32s
2024-03-24 15:25:52 +01:00
8bf3a337cf v0.0.422
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m29s
2024-03-23 20:29:46 +01:00
16146494dc v0.0.421
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 32s
2024-03-23 20:28:51 +01:00
b0e443ad99 v0.0.420
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 36s
2024-03-23 18:01:41 +01:00
9955eacf96 v0.0.419 JsonOpt
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 39s
2024-03-23 17:49:56 +01:00
f0347a9435 v0.0.418 fix tests?
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m1s
2024-03-20 09:42:06 +01:00
7c869c65f3 v0.0.417 add GinWrapper.ForwardRequest
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m26s
2024-03-20 08:58:59 +01:00
14f39a9162 v0.0.416
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m43s
2024-03-18 11:19:01 +01:00
dcd106c1cd v0.0.415 add 'tagkey' to gojson.Decoder
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m45s
2024-03-18 10:42:00 +01:00
b704e2a362 v0.0.414 fix rfctime.Date bson marshalling for zero value
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m28s
2024-03-16 19:42:59 +01:00
6b4bd5a6f8 v0.0.413 fix tests
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m33s
2024-03-11 21:00:30 +01:00
6df4f5f2a1 v0.0.412 fix GenerateIDSpecs accepting nil for opt
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m32s
2024-03-11 20:58:06 +01:00
780905ba35 v0.0.411
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m33s
2024-03-11 20:43:37 +01:00
c679797765 v0.0.410 add ginext.SuppressGinLogs
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-03-11 20:42:12 +01:00
401aad9fa4 v0.0.409
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m41s
2024-03-11 17:05:10 +01:00
645113d553 v0.0.408
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m43s
2024-03-11 16:41:47 +01:00
4a33986b6a v0.0.407 sq.Iterate
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-03-11 16:40:41 +01:00
c1c8c64c76 v0.0.406 bf
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m34s
2024-03-10 16:44:21 +01:00
0927fdc4d7 v0.0.405
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m36s
2024-03-10 15:28:26 +01:00
102a280dda v0.0.404
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m37s
2024-03-10 15:25:30 +01:00
f13384d794 v0.0.403 bf
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m37s
2024-03-10 12:58:59 +01:00
409d6e108d v0.0.402 add PackageName() and TypeName() to enums_codegen
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m12s
2024-03-10 12:49:31 +01:00
ed53f297bd v0.0.401 bf
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m27s
2024-03-09 15:07:03 +01:00
42424f4bc2 v0.0.400 added CommentTrimmer and DBOptions to sq
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m16s
2024-03-09 14:59:32 +01:00
9e5b8c5277 v0.0.399 added sq.NewAutoDBTypeConverter
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m25s
2024-03-09 14:16:35 +01:00
9abe28c490 v0.0.398 added As* version to sort functions
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m13s
2024-03-09 13:36:06 +01:00
422bbd8593 v0.0.397 added wmo.IColl inetrface for tim
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m2s
2024-03-04 12:17:10 +01:00
3956675e04 v0.0.396 sq.ConverterRFCSecondsF64ToString
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m2s
2024-02-28 14:28:48 +01:00
10c3780b52 v0.0.395 MapMerge
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m14s
2024-02-27 13:42:06 +01:00
8edc067a3b v0.0.394
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m31s
2024-02-21 18:40:42 +01:00
1007f2c834 v0.0.393
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m19s
2024-02-21 18:33:18 +01:00
c25da03217 v0.0.392
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-02-21 18:32:06 +01:00
4b55dbaacf v0.0.391
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m36s
2024-02-21 16:18:04 +01:00
c399fa42ae v0.0.390
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m26s
2024-02-21 16:11:15 +01:00
9e586f7706 v0.0.389
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-02-21 16:10:28 +01:00
3cc8dccc63 v0.0.388 fix ginext.Use loosing absPath information
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m26s
2024-02-20 09:19:06 +01:00
7fedfbca81 v0.0.387 bf
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m24s
2024-02-12 18:17:49 +01:00
3c439ba428 v0.0.386 InsertAndQuerySingle
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m27s
2024-02-09 15:58:21 +01:00
ad24f6db44 v0.0.385 UpdateAndQuerySingle
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m29s
2024-02-09 15:40:09 +01:00
1869ff3d75 v0.0.384 QuerySingleOpt
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m28s
2024-02-09 15:20:46 +01:00
30ce8c4b60 v0.0.383 sq.InsertMultiple
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m15s
2024-02-09 15:17:51 +01:00
885bb53244 add BuildUrl test
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m59s
2024-02-09 12:27:18 +01:00
1c7dc1820a v0.0.382 add build url method
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2024-02-09 12:25:01 +01:00
7e16e799e4 v0.0.381
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m28s
2024-01-23 17:51:52 +01:00
890e16241d v0.0.380 exerr properly handle inf and nan floats
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m8s
2024-01-22 12:33:41 +01:00
b9d0348735 v0.0.379
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m57s
2024-01-19 17:30:20 +01:00
b9e9575b9b v0.0.378
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m18s
2024-01-16 15:04:10 +01:00
295a098eb4 v0.0.377 fix sq.ConverterBoolToBit
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m12s
2024-01-14 17:06:42 +01:00
b69a082bb1 v0.0.376
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m22s
2024-01-14 01:52:52 +01:00
a4a8c83d17 v0.0.375
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m25s
2024-01-14 01:50:48 +01:00
e952176bb0 v0.0.374 ppwgen
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m47s
2024-01-14 01:37:38 +01:00
d99adb203b v0.0.373 BuildInsertStatement
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m24s
2024-01-14 00:07:01 +01:00
f1f91f4cfa v0.0.372 sq.Paginate
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m25s
2024-01-13 21:36:47 +01:00
2afb265ea4 v0.0.371
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m26s
2024-01-13 14:19:19 +01:00
be24f7a190 v0.0.370 improve sq errors
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m36s
2024-01-13 14:10:25 +01:00
aae8a706e9 v0.0.369 autom. allow usage of existing converter for pointer-types (sq)
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m29s
2024-01-13 02:01:30 +01:00
7d64f18f54 v0.0.368
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m9s
2024-01-13 01:29:40 +01:00
d08b2e565a v0.0.367
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m32s
2024-01-12 18:40:29 +01:00
d29e84894d v0.0.366 ginext: set cookies
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m28s
2024-01-12 15:10:48 +01:00
617298c366 v0.0.365
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m24s
2024-01-09 18:23:46 +01:00
668f308565 v0.0.364 add ServerHTTP() to GinWrapper for integration testing
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m59s
2024-01-09 18:17:55 +01:00
240a8ed7aa v0.0.363 wmo.extraModPipeline as func
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m9s
2024-01-09 08:51:46 +01:00
70de8e8d04 v0.0.362
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m27s
2024-01-07 04:18:03 +01:00
d38fa60fbc v0.0.361 call exerrListener in ginext.Error
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m37s
2024-01-07 04:01:13 +01:00
5fba7e0e2f v0.0.360 bf
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m26s
2024-01-06 01:31:07 +01:00
8757643399 v0.0.359 fix tests
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m18s
2024-01-05 16:55:53 +01:00
42bd4cf58d v0.0.358
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m22s
2024-01-05 16:53:14 +01:00
413178e2d3 v0.0.357
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m4s
2024-01-05 10:59:06 +01:00
9264a2e99b v0.0.356 exerr.GetMeta
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m9s
2024-01-05 10:43:39 +01:00
2a0471fb3d v0.0.355 sq.Json
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m27s
2024-01-05 10:25:05 +01:00
1497c013f9 v0.0.354
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m13s
2024-01-05 07:21:43 +01:00
ef78b7467b v0.0.353 add scn.sendmessage
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m16s
2024-01-04 12:38:03 +01:00
0eda32b725 v0.0.352
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m1s
2023-12-29 19:29:36 +01:00
f9ccafb976 v0.0.351 sq value converter
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m30s
2023-12-29 19:25:36 +01:00
6e90239fef v0.0.350
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m2s
2023-12-29 18:06:45 +01:00
05580c384a v0.0.349
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m51s
2023-12-28 01:36:21 +01:00
3188b951fb v0.0.348 added listener and options to goext
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m35s
2023-12-27 20:29:37 +01:00
6b211d1443 fix tests
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m37s
2023-12-17 14:04:35 +01:00
b2b9b40792 v0.0.347
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m39s
2023-12-16 17:57:42 +01:00
2f915cb6c1 v0.0.346
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m56s
2023-12-13 16:22:15 +01:00
b2b93f570a v0.0.345
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m48s
2023-12-07 19:39:31 +01:00
8247fc4524 v0.0.344
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m11s
2023-12-07 19:36:21 +01:00
5dad44ad09 v0.0.343
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m2s
2023-12-07 18:29:17 +01:00
f042183433 v0.0.342 support json data in enum comment
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m29s
2023-12-07 17:57:06 +01:00
b0be93a7a0 v0.0.341
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m6s
2023-12-07 14:43:12 +01:00
1c143921e6 v0.0.340
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2023-12-07 14:42:25 +01:00
68e63a9cf6 v0.0.339
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m5s
2023-12-07 10:54:36 +01:00
c3162fec95 v0.0.338
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 59s
2023-12-05 19:50:24 +01:00
1124aa781a v0.0.337
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m6s
2023-12-05 19:45:35 +01:00
eef0e9f2aa v0.0.336
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m3s
2023-12-05 19:42:37 +01:00
af38b06d22 v0.0.335 added DescriptionMeta to enum codegen
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 58s
2023-12-05 19:38:03 +01:00
2fad6340c7 v0.0.334 allow dot in enum-value
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m3s
2023-12-05 19:23:27 +01:00
03aa0a2282 Merge branch 'feature/gmail_api'
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m23s
2023-12-04 13:56:18 +01:00
358c238f3d google mail API [[FIN]]
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m28s
2023-12-04 13:55:41 +01:00
d65ac8ba2b v0.0.329
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m2s
2023-12-02 13:38:17 +01:00
55d02b8c65 v0.0.328
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 59s
2023-12-02 13:35:18 +01:00
8a3965f666 v0.0.327
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m44s
2023-12-02 13:15:19 +01:00
4aa2f494b1 v0.0.326 ginext::WithSession
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m38s
2023-12-02 13:07:36 +01:00
8f13eb2f16 google mail API [[[WIP]]] 2023-12-01 18:33:04 +01:00
8f15d42173 v0.0.325
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m48s
2023-11-27 14:14:58 +01:00
07fa21dcca v0.0.324
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m29s
2023-11-25 15:48:28 +01:00
e657de7f78 v0.0.323 fix langext.IsNil for reflect.Array
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m35s
2023-11-16 17:15:44 +01:00
c534e998e8 v0.0.322 bf SecondsF64
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m1s
2023-11-14 16:31:05 +01:00
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)
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m4s
2023-11-14 16:00:14 +01:00
8528b5cb66 v0.0.320 bugfix sort
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m14s
2023-11-14 14:50:27 +01:00
5ba84bd8ee v0.0.319 fix error when findoneÃ+pipeline fails
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m15s
2023-11-13 16:45:00 +01:00
1260b2dc77 v0.0.318 add failure mail to testx.yml
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m9s
2023-11-13 15:34:58 +01:00
7d18b913c6 v0.0.317 try fix tests on pipeline
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m38s
2023-11-13 15:28:37 +01:00
d1f9069f2f v0.0.316 bugfix sorting
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m14s
2023-11-13 15:19:48 +01:00
fa6d73301e v0.0.315 atomic
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m26s
2023-11-12 03:10:55 +01:00
bfe62799d3 v0.0.314
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m22s
2023-11-10 13:37:55 +01:00
ede912eb7b v0.0.313
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m22s
2023-11-10 13:26:30 +01:00
ff8f128fe8 v0.0.312 improve exerr.RecursiveMessage()
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m23s
2023-11-10 10:16:31 +01:00
1971f1396f v0.0.311 BF
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m17s
2023-11-09 11:48:45 +01:00
bf6c184d12 v0.0.310 debug
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m18s
2023-11-09 11:40:48 +01:00
770f5c5c64 v0.0.309
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m17s
2023-11-09 10:17:29 +01:00
623c021689 v0.0.308
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m20s
2023-11-09 10:02:31 +01:00
afcc89bf9e v0.0.307
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m20s
2023-11-09 10:00:01 +01:00
1672e8f8fd v0.0.306
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m12s
2023-11-09 09:36:41 +01:00
398ed56d32 v0.0.305
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m21s
2023-11-09 09:35:56 +01:00
f3ecba3883 v0.0.304 add support for WithModifyingPipeline to wmo
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m22s
2023-11-09 09:26:46 +01:00
45031b05cf v0.0.303
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m35s
2023-11-08 19:01:15 +01:00
7413ea045d v0.0.302
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m41s
2023-11-08 18:53:02 +01:00
62c9a4e734 v0.0.301 pagination (page+limit) support in wmo
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m11s
2023-11-08 18:30:30 +01:00
3a8baaa6d9 v0.0.300 add custom unmarshal-hooks to wmo
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m45s
2023-11-04 18:55:44 +01:00
498785e213 v0.0.299 pctx.RawBody( *[]byte )
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m41s
2023-11-03 16:53:41 +01:00
678f95642c v0.0.298 use go/format instead of manual command invocation
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m6s
2023-11-01 04:20:08 +01:00
dacc97e2ce v0.0.297
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m1s
2023-11-01 00:31:51 +01:00
f8c0c0afa0 v0.0.296 add csid.generateIDFromSeed
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m4s
2023-11-01 00:29:58 +01:00
2fbd5cf965 v0.0.295 added generic base-conversion algorithm
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m4s
2023-11-01 00:23:17 +01:00
75f71fe3db v0.0.294 migrate bfcodegen to templates
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 58s
2023-10-31 22:58:28 +01:00
ab1a1ab6f6 v0.0.293 fix NPE
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m52s
2023-10-30 13:37:31 +01:00
19ee5019ef v0.0.292
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m11s
2023-10-30 10:14:38 +01:00
42b68507f2 v0.0.291
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 56s
2023-10-26 13:02:45 +02:00
9d0047a11e v0.0.290 csid
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m38s
2023-10-26 13:01:58 +02:00
06d81f1682 v0.0.289 fsext
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m31s
2023-10-26 11:29:08 +02:00
7b8ab03779 v0.0.288 default to recursive-error-msg in exerr.Error()
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m54s
2023-10-19 14:16:01 +02:00
07cbcf5a0a v0.0.287 fix bug in confext::ApplyEnvOverrides if a struct env key exists in the os.env
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m37s
2023-10-12 10:02:42 +02:00
da41ec3e84 run CICD tests without doker workaround
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m37s
2023-10-11 15:50:09 +02:00
592fae25af v0.0.286 allow spaces in enum-keys
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m14s
2023-10-11 11:27:18 +02:00
7968460fa2 v0.0.285
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m0s
2023-10-09 15:25:30 +02:00
b808c5727c v0.0.284
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m14s
2023-10-09 15:22:57 +02:00
796f7956b8 v0.0.283
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m5s
2023-10-09 15:17:22 +02:00
1e6b92d1d9 v0.0.282 ginext bugfix
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 48s
2023-10-09 09:23:40 +02:00
0b85fa5af9 v0.0.281 typo fix
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 50s
2023-10-09 09:04:07 +02:00
c3318cc1de v0.0.280 DYN-166 ginext jsonfilter middleware
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 51s
2023-10-09 09:02:37 +02:00
fbf4d7b915 v0.0.279 DYN-166 ginext jsonfilter middleware
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m12s
2023-10-09 08:55:22 +02:00
9cc0abf9e0 v0.0.278 DYN-166 bugfix jsonfilter
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 52s
2023-10-05 12:54:07 +02:00
7c40bcfd3c v0.0.277 DYN-166 json marshal filter in ginext Write
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 54s
2023-10-05 12:00:51 +02:00
05636a1e4d v0.0.276
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m12s
2023-10-05 10:59:20 +02:00
0f52b860ea DYN-166 add jsonfilter to json library
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 56s
2023-10-05 10:57:34 +02:00
b5cd116219 DYN-166 add jsonfilter to json library
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 47s
2023-10-05 10:45:09 +02:00
98486842ae v0.0.275 fix missing returns in (v MetaValue) ShortString
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 54s
2023-09-29 16:00:40 +02:00
7577a2dd47 v0.0.274 limit exerr log meta values (shortlog) to 240 chars
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 50s
2023-09-27 16:18:21 +02:00
08681756b6 v0.0.273 add stack to PanicWrappedErr
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m9s
2023-09-27 14:15:59 +02:00
64772d0474 v0.0.272 WMO: fix FindOneAndReplace not using FindOneAndReplace
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 50s
2023-09-26 14:41:15 +02:00
127764556e Merge branch 'master' of ssh://gogs.mikescher.com:8022/BlackForestBytes/goext 2023-09-26 14:41:06 +02:00
170f43d806 WMO: fix FindOneAndReplace not using FindOneAndReplace 2023-09-26 14:40:56 +02:00
9dffc41274 v0.0.271 return old value in AtomicBool::Set
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 53s
2023-09-26 14:32:45 +02:00
c63cf442f8 try to fix test 'cmdext:TestFailOnStderr'
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 39s
2023-09-25 18:04:56 +02:00
a2ba283632 v0.0.270 fix inversion of AssertDeepEqual
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 54s
2023-09-25 11:35:03 +02:00
4a1fb1ae18 fix inversion of AssertDeepEqual 2023-09-25 11:34:51 +02:00
a127b24e62 v0.0.269 add AssertSetDeepEqual
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m8s
2023-09-25 09:18:22 +02:00
69d6290376 add AssertSetDeepEqual 2023-09-25 09:18:07 +02:00
c08a739158 v0.0.268 added WeekStart() and WeekEnd()
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 50s
2023-09-21 16:29:23 +02:00
5f5f0e44f0 v0.0.267 fix AssertDeepEqual
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 50s
2023-09-21 14:15:02 +02:00
6e6797eac5 fix AssertDeepEqual 2023-09-21 14:14:51 +02:00
cd9406900a v0.0.266 fix tst showing wrong file:line
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m22s
2023-09-21 13:08:13 +02:00
6c81f7f6bc fix tst showing wrong file:line, add DeepEqual 2023-09-21 13:07:55 +02:00
d56a0235af v0.0.265 add ListWithCount
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 49s
2023-09-18 12:57:27 +02:00
de2ca763c1 add function for ListWithCount 2023-09-18 12:56:56 +02:00
da52bb5c90 v0.0.264 added Valid() to id-gen
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 49s
2023-09-18 11:46:17 +02:00
3d4afe7b25 v0.0.263 re-add checksum guard to id-generate
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 50s
2023-09-18 10:43:29 +02:00
f5766d639c v0.0.262 ignore _gen files in bfcodegen checksum-calc
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 46s
2023-09-18 10:42:43 +02:00
cdf2a6e76b v0.0.261 added id-generate.go
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m7s
2023-09-18 10:38:25 +02:00
6d7cfb86f8 v0.0.260 wmo: fix endless recursion in wmo reflection
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 50s
2023-09-12 11:40:39 +02:00
1e9d663ffe fix endless recursion in wmo reflection 2023-09-12 11:39:51 +02:00
5b8d7ebf87 v0.0.259 wmo: allow fields to pointers to structs
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 49s
2023-09-12 10:48:57 +02:00
11dc6d2640 use type instead of value for Reflection in Coll.initFields 2023-09-12 10:47:41 +02:00
29a3f73f15 v0.0.258
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m22s
2023-09-11 11:28:34 +02:00
98105642fc removed default sort 2023-09-11 11:28:26 +02:00
0fd5f3b417 v0.0.257 better handling if pagination is faulty in wmo list
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m2s
2023-09-05 15:01:55 +02:00
43cac4b3bb v0.0.256 bind header
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m4s
2023-08-28 10:44:38 +02:00
cd68af8e66 v0.0.255 tuples
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m43s
2023-08-24 09:47:32 +02:00
113d838876 v0.0.254 revert back to 0.0.250
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m14s
2023-08-22 10:49:57 +02:00
9e5bc0d3ea v0.0.253
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m23s
2023-08-22 10:36:35 +02:00
6d3bd13f61 v0.0.252
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m5s
2023-08-22 10:23:04 +02:00
b5ca475b3f v0.0.251 exerr.WithStackSkip
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m9s
2023-08-22 10:21:13 +02:00
a75b1291cb v0.0.250
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 51s
2023-08-21 15:34:27 +02:00
21cd1ee066 v0.0.249 better MDTAny json serialization
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 54s
2023-08-21 15:19:40 +02:00
ae43cbb623 v0.0.248 exerr in wmo package
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 55s
2023-08-21 15:08:35 +02:00
9b752a911c v0.0.247 -.-
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m12s
2023-08-21 14:23:44 +02:00
ec9ac26a4c v0.0.246 timeext
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m11s
2023-08-21 14:15:06 +02:00
39a0b73d56 v0.0.245
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 40s
2023-08-21 13:27:36 +02:00
2e2e15d4d2 v0.0.244
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 48s
2023-08-18 13:27:02 +02:00
0d16946aba v0.0.243
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m18s
2023-08-18 13:25:18 +02:00
14441c2378 Adde gitea workflow: tests
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 57s
2023-08-14 18:39:22 +02:00
f6bcdc9903 Merge remote-tracking branch 'origin/master' 2023-08-14 16:33:03 +02:00
a95053211c Fix tests 2023-08-14 16:32:39 +02:00
813ce71e3e v0.0.242 forgot to return something 2023-08-14 16:05:12 +02:00
56ae0cfc6c v0.0.241 join string array 2023-08-14 15:54:50 +02:00
202afc9068 v0.0.240 2023-08-14 15:36:12 +02:00
56094b3cb6 v0.0.239 pctx.WithTImeout 2023-08-11 16:32:34 +02:00
0da098e9f9 v0.0.238 2023-08-09 19:51:41 +02:00
f0881c9fd6 v0.0.237 parse application/x-www-form-urlencoded in ginext 2023-08-09 19:35:01 +02:00
029b408749 v0.0.236 cmdext.FailOnStdErr 2023-08-09 17:48:06 +02:00
84b2be3169 v0.0.235 added .Enum(..) to exerr 2023-08-09 14:40:16 +02:00
c872cecc67 v0.0.234 2023-08-09 10:39:14 +02:00
99cd92729e v0.0.233 IncludeMetaInGinOutput 2023-08-09 10:37:59 +02:00
ac416f7b69 v0.0.232 2023-08-08 18:01:00 +02:00
e10140e143 v0.0.231 2023-08-08 16:10:31 +02:00
e165f0f62f v0.0.230 2023-08-08 16:09:02 +02:00
655d4daad9 v0.0.229 2023-08-08 16:05:44 +02:00
87a004e577 v0.0.228 bf 2023-08-08 15:33:52 +02:00
376c6cab50 v0.0.227 error on duplicate exerr.ErrorType 2023-08-08 15:28:29 +02:00
4a3f25baa0 v0.0.226 2023-08-08 14:28:09 +02:00
aa33bc8df3 v0.0.225 2023-08-08 13:09:15 +02:00
96b3718375 v0.0.224 implement error.As(x) for exerr 2023-08-08 12:38:22 +02:00
5f9b55933b v0.0.223 2023-08-08 11:52:40 +02:00
74d42637e7 v0.0.222 forgot status code 2023-08-06 19:11:59 +02:00
0c05bcf29b v0.0.221 download file data 2023-08-06 19:10:31 +02:00
9136143f2f v0.0.220 add ginext.bufferBody 2023-08-03 09:09:27 +02:00
2f1b784dc2 v0.0.219 implement error.Is(*) for exerr 2023-07-28 15:42:12 +02:00
190584e0e6 v0.0.218 bf 2023-07-27 17:16:30 +02:00
b7003b9ec9 v0.0.217 2023-07-27 17:12:41 +02:00
4f871271e8 v0.0.216 2023-07-27 17:00:53 +02:00
91f4793678 v0.0.215 Add (ee *ExErr) ToAPIJson 2023-07-27 14:37:11 +02:00
3b30bb049e v0.0.214 reassign innerctx 2023-07-27 09:58:10 +02:00
f0c5b36ea9 v0.0.213 inject gin key value pairs into context 2023-07-27 09:46:06 +02:00
647ec64c3b v0.0.212 2023-07-26 10:44:26 +02:00
b5f9b6b638 v0.0.211 2023-07-26 10:40:42 +02:00
c7949febf2 v0.0.210 fix ginext route dump 2023-07-25 11:16:11 +02:00
15a4b2a713 v0.0.209 removed g context from err func 2023-07-25 10:56:03 +02:00
493c6ebae8 v0.0.208 remove context from err functions because its not used 2023-07-25 10:51:14 +02:00
fb847b03af v0.0.207 renamed APIError to Error 2023-07-25 10:47:00 +02:00
f826633e6e v0.0.206 2023-07-24 18:50:14 +02:00
edeae23bf1 v0.0.205 2023-07-24 18:47:48 +02:00
a038b86147 v0.0.204 2023-07-24 18:42:33 +02:00
ede0b99d3a v0.0.203 2023-07-24 18:38:04 +02:00
d04ce18eb0 v0.0.202 2023-07-24 18:34:56 +02:00
8ae9a0f107 v0.0.201 2023-07-24 18:22:36 +02:00
a259bb6dbc v0.0.200 2023-07-24 17:42:18 +02:00
adf32568ee v0.0.199 2023-07-24 17:23:38 +02:00
0cfa159cb1 v0.0.198 2023-07-24 14:16:02 +02:00
0ead99608a v0.0.197 2023-07-24 12:27:06 +02:00
7fe3e66cad v0.0.196 2023-07-24 11:47:47 +02:00
a73d7d1654 v0.0.195 2023-07-24 11:42:52 +02:00
bbd7a7bc2c v0.0.194 2023-07-24 11:40:47 +02:00
f5151eb214 v0.0.193 2023-07-24 11:38:57 +02:00
eefb9ac9f5 v0.0.192 2023-07-24 11:30:07 +02:00
468a7d212d v0.0.191 2023-07-24 11:18:25 +02:00
a4def75d06 v0.0.190 2023-07-24 11:16:57 +02:00
16c66ee28c v0.0.189 2023-07-24 11:11:15 +02:00
2e6ca48d22 v0.0.188 exerr MVP 2023-07-24 10:42:39 +02:00
b1d6509294 v0.0.187 forget to use function 2023-07-24 09:16:37 +02:00
e909d656d9 v0.0.186 convert array to interface arr 2023-07-24 09:13:19 +02:00
0971f60c30 v0.0.185 add Meta() to enums 2023-07-19 19:34:39 +02:00
d8270e53ed v0.0.184 re-add missing array methods from merge commit 56684b2c0b 2023-07-19 19:29:59 +02:00
1ee127937a v0.0.183 2023-07-19 19:24:58 +02:00
56684b2c0b exerr [WIP]
(cherry picked from commit c0443af63b)
2023-07-19 19:24:43 +02:00
1ea6695f82 v0.0.182 added optional commit message 2023-07-19 11:26:23 +02:00
5273ff7600 v0.0.181 2023-07-19 11:26:06 +02:00
caa69c3629 v0.0.180 Super Test 2023-07-19 11:24:11 +02:00
0ff5f0aa28 v0.0.179 TestTest 2023-07-19 11:22:25 +02:00
d5cb1e48ed v0.0.178 2023-07-19 11:20:35 +02:00
1c2d3f541f v0.0.177 2023-07-18 16:08:24 +02:00
ec62ad436f v0.0.176 2023-07-18 16:01:34 +02:00
8d0ef0f002 v0.0.175 2023-07-18 15:59:12 +02:00
d78550672e v0.0.174 2023-07-18 15:23:32 +02:00
1d629f6db8 v0.0.173 2023-07-18 15:12:06 +02:00
f7d291056d v0.0.172 2023-07-18 14:40:10 +02:00
710c257c64 v0.0.171 2023-07-18 13:34:54 +02:00
c320bb3d90 v0.0.170 2023-07-17 12:42:49 +02:00
2f01a1d50f v0.0.169 2023-07-05 19:27:49 +02:00
ffc57b7e89 v0.0.168 2023-07-05 19:27:15 +02:00
d88cd3c22b v0.0.167 2023-06-22 17:33:56 +02:00
ac5ad640bd v0.0.166 2023-06-22 15:07:06 +02:00
21d241f9b1 v0.0.163 2023-06-18 01:16:52 +02:00
2569c165f8 v0.0.162 2023-06-11 16:38:47 +02:00
ee262a94fb v0.0.161 2023-06-11 16:35:20 +02:00
7977c0e59c Added rfctime.Date type 2023-06-10 19:13:15 +02:00
ceff0161c6 v0.0.159 2023-06-10 18:35:56 +02:00
a30da61419 v0.0.158 2023-06-10 16:28:50 +02:00
b613b122e3 v0.0.157 2023-06-10 16:22:14 +02:00
d017530444 v0.0.156 2023-06-10 00:19:17 +02:00
8de83cc290 v0.0.155 2023-06-08 16:26:06 +02:00
603ec82b83 v0.0.154 2023-06-08 16:24:53 +02:00
93c4cf31a8 v0.0.153 2023-06-08 16:24:15 +02:00
dc2d8a9103 v0.0.152 2023-06-08 16:17:01 +02:00
6589e8d5cd v0.0.151 2023-06-07 17:57:03 +02:00
0006c6859d v0.0.150 2023-06-07 17:48:36 +02:00
827b3fc1b7 v0.0.149 2023-06-07 17:45:45 +02:00
f7dce4a102 v0.0.148 2023-06-07 17:22:38 +02:00
45d4fd7101 v0.0.147 2023-06-07 16:58:17 +02:00
c7df9d2264 v0.0.146 2023-06-07 12:59:15 +02:00
d0954bf133 v0.0.145 2023-06-07 12:45:48 +02:00
8affa81bb9 v0.0.144 2023-06-07 12:39:21 +02:00
fe9ebf0bab v0.0.143 2023-06-07 12:36:41 +02:00
a4b5f33d15 v0.0.142 2023-06-07 11:28:07 +02:00
e89e2c18f2 v0.0.141 2023-06-07 10:56:11 +02:00
b16d5152c7 v0.0.140 2023-06-07 10:42:56 +02:00
5fb2f8a312 v0.0.139 2023-06-06 21:40:34 +02:00
2ad820be8d v0.0.138 2023-06-06 21:33:49 +02:00
555096102a v0.0.137 2023-06-06 21:30:22 +02:00
d76d7b5cb9 v0.0.136 2023-06-06 21:26:12 +02:00
6622c9003d v0.0.135 2023-06-06 21:24:13 +02:00
b02e1d2e85 v0.0.134 2023-06-06 21:22:44 +02:00
c338d23070 v0.0.133 2023-06-06 21:18:40 +02:00
1fbae343a4 Fix RFC3339 serialization 2023-06-06 11:26:46 +02:00
31418bf0e6 v0.0.130 2023-06-05 13:30:32 +02:00
6d45f6f667 v0.0.129 2023-06-05 13:24:52 +02:00
f610a2202c v0.0.128 2023-06-02 09:44:31 +02:00
2807299d46 v0.0.127 2023-05-28 22:55:06 +02:00
e872dbccec v0.0.126 2023-05-28 19:53:30 +02:00
9daf71e2ed v0.0.125 2023-05-28 19:41:24 +02:00
fe278f7772 v0.0.124 2023-05-28 18:21:02 +02:00
8ebda6fb3a v0.0.123 2023-05-25 18:20:31 +02:00
b0d3ce8c1c v0.0.122 2023-05-24 22:01:29 +02:00
021465e524 v0.0.121 2023-05-24 21:55:21 +02:00
cf9c73aa4a v0.0.120 2023-05-24 21:42:10 +02:00
0652bf22dc v0.0.119 2023-05-24 21:32:00 +02:00
b196adffc7 v0.0.118 2023-05-09 11:33:01 +02:00
717065e62d v0.0.117 2023-05-09 09:57:05 +02:00
e7b2b040b2 v0.0.116 2023-05-05 18:22:15 +02:00
05d0f9e469 v0.0.115 2023-05-05 18:18:20 +02:00
ccd03e50c8 v0.0.114 2023-05-05 18:17:15 +02:00
223 changed files with 21522 additions and 500 deletions

View File

@@ -0,0 +1,55 @@
# 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
- name: Send failure mail
if: failure()
uses: dawidd6/action-send-mail@v3
with:
server_address: smtp.fastmail.com
server_port: 465
secure: true
username: ${{secrets.MAIL_USERNAME}}
password: ${{secrets.MAIL_PASSWORD}}
subject: Pipeline on '${{ gitea.repository }}' failed
to: ${{ steps.commiter_info.outputs.MAIL }}
from: Gitea Actions <gitea_actions@blackforestbytes.de>
body: "Go to https://gogs.blackforestbytes.com/${{ gitea.repository }}/actions"

2
.idea/.gitignore generated vendored
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
.idea/goext.iml generated
View File

@@ -1,6 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4"> <module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" /> <component name="Go" enabled="true">
<buildTags>
<option name="goVersion" value="1.19" />
</buildTags>
</component>
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" /> <content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />

6
.idea/golinter.xml generated Normal file
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
.idea/sqldialects.xml generated Normal file
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>

View File

@@ -3,7 +3,15 @@ run:
echo "This is a library - can't be run" && false echo "This is a library - can't be run" && false
test: test:
go test ./... # go test ./...
which gotestsum || go install gotest.tools/gotestsum@latest
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
README.md
View File

@@ -5,4 +5,107 @@ 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="gogs.mikescher.com"`
## Packages:
| Name | Maintainer | Description |
|-------------|------------|---------------------------------------------------------------------------------------------------------------|
| langext | Mike | General uttility/helper functions, (everything thats missing from go standard library) |
| mathext | Mike | Utility/Helper functions for math |
| cryptext | Mike | Utility/Helper functions for encryption |
| syncext | Mike | Utility/Helper funtions for multi-threading / mutex / channels |
| dataext | Mike | Various useful data structures |
| zipext | Mike | Utility for zip/gzip/tar etc |
| reflectext | Mike | Utility for golang reflection |
| fsext | Mike | Utility for filesytem access |
| ctxext | Mike | Utility for context.Context |
| | | |
| mongoext | Mike | Utility/Helper functions for mongodb (kinda abandoned) |
| cursortoken | Mike | MongoDB cursortoken implementation |
| pagination | Mike | Pagination implementation |
| | | |
| totpext | Mike | Implementation of TOTP (2-Factor-Auth) |
| termext | Mike | Utilities for terminals (mostly color output) |
| confext | Mike | Parses environment configuration into structs |
| cmdext | Mike | Runner for external commands/processes |
| | | |
| sq | Mike | Utility functions for sql based databases (primarily sqlite) |
| tst | Mike | Utility functions for unit tests |
| | | |
| 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)

9
TODO.md Normal file
View File

@@ -0,0 +1,9 @@
- cronext
- rfctime.HMSTimeOnly
- rfctime.NanoTimeOnly
- remove sqlx dependency from sq (unmaintained, and mostly superseeded by our own stuff?)
- Move DBLogger and DBPreprocessor to sq

View File

@@ -21,8 +21,15 @@ if [ "$( git rev-parse --abbrev-ref HEAD )" != "master" ]; then
exit 1 exit 1
fi fi
echo ""
echo -n "Insert optional commit message: "
read commitMessage
echo ""
git pull --ff git pull --ff
go get -u ./...
curr_vers=$(git describe --tags --abbrev=0 | sed 's/v//g') curr_vers=$(git describe --tags --abbrev=0 | sed 's/v//g')
next_ver=$(echo "$curr_vers" | awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{if(length($NF+1)>length($NF))$(NF-1)++; $NF=sprintf("%0*d", length($NF), ($NF+1)%(10^length($NF))); print}') next_ver=$(echo "$curr_vers" | awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{if(length($NF+1)>length($NF))$(NF-1)++; $NF=sprintf("%0*d", length($NF), ($NF+1)%(10^length($NF))); print}')
@@ -32,10 +39,17 @@ echo "> Current Version: ${curr_vers}"
echo "> Next Version: ${next_ver}" echo "> Next Version: ${next_ver}"
echo "" echo ""
printf "package goext\n\nconst GoextVersion = \"%s\"\n\nconst GoextVersionTimestamp = \"%s\"\n" "${next_ver}" "$( date +"%Y-%m-%dT%H:%M:%S%z" )" > "goextVersion.go"
git add --verbose . git add --verbose .
msg="v${next_ver}" msg="v${next_ver}"
if [[ "$commitMessage" != "" ]]; then
msg="${msg} ${commitMessage}"
fi
if [ $# -gt 0 ]; then if [ $# -gt 0 ]; then
msg="$1" msg="$1"
fi fi

Binary file not shown.

Binary file not shown.

197
bfcodegen/csid-generate.go Normal file
View File

@@ -0,0 +1,197 @@
package bfcodegen
import (
"bytes"
_ "embed"
"errors"
"fmt"
"go/format"
"gogs.mikescher.com/BlackForestBytes/goext"
"gogs.mikescher.com/BlackForestBytes/goext/cryptext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.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()
}

View File

@@ -0,0 +1,190 @@
// 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 "gogs.mikescher.com/BlackForestBytes/goext/exerr"
import "gogs.mikescher.com/BlackForestBytes/goext/langext"
import "gogs.mikescher.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}}) Regex() rext.Regex {
return regex{{.Name}}
}
{{end}}

View File

@@ -0,0 +1,52 @@
package bfcodegen
import (
_ "embed"
"fmt"
"gogs.mikescher.com/BlackForestBytes/goext/cmdext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.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()
}

373
bfcodegen/enum-generate.go Normal file
View File

@@ -0,0 +1,373 @@
package bfcodegen
import (
"bytes"
_ "embed"
"encoding/json"
"errors"
"fmt"
"go/format"
"gogs.mikescher.com/BlackForestBytes/goext"
"gogs.mikescher.com/BlackForestBytes/goext/cryptext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/rext"
"io"
"os"
"path"
"path/filepath"
"reflect"
"regexp"
"strings"
"text/template"
)
type EnumDefVal struct {
VarName string
Value string
Description *string
Data *map[string]any
RawComment *string
}
type EnumDef struct {
File string
FileRelative string
EnumTypeName string
Type string
Values []EnumDefVal
}
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 rexEnumValueDef = rext.W(regexp.MustCompile(`^\s*(?P<name>[A-Za-z0-9_]+)\s+(?P<type>[A-Za-z0-9_]+)\s*=\s*(?P<value>("[A-Za-z0-9_:\s\-.]*"|[0-9]+))\s*(//(?P<comm>.*))?.*$`))
var rexEnumChecksumConst = rext.W(regexp.MustCompile(`const ChecksumEnumGenerator = "(?P<cs>[A-Za-z0-9_]*)"`))
//go:embed enum-generate.template
var templateEnumGenerateText string
func GenerateEnumSpecs(sourceDir string, destFile string, opt EnumGenOptions) error {
oldChecksum := "N/A"
if _, err := os.Stat(destFile); !os.IsNotExist(err) {
content, err := os.ReadFile(destFile)
if err != nil {
return err
}
if m, ok := rexEnumChecksumConst.MatchFirst(string(content)); ok {
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 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 "", "", false, err
}
newChecksumStr += "\n" + f.Name() + "\t" + cryptext.BytesSha256(content)
}
newChecksum := cryptext.BytesSha256([]byte(newChecksumStr))
if newChecksum != oldChecksum {
fmt.Printf("[EnumGenerate] Checksum has changed ( %s -> %s ), will generate new file\n\n", oldChecksum, newChecksum)
} else {
fmt.Printf("[EnumGenerate] Checksum unchanged ( %s ), nothing to do\n", oldChecksum)
return "", oldChecksum, false, nil
}
allEnums := make([]EnumDef, 0)
pkgname := ""
for _, f := range files {
if debugOutput {
fmt.Printf("========= %s =========\n\n", f.Name())
}
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...)
if pn != "" {
pkgname = pn
}
}
if pkgname == "" {
return "", "", false, errors.New("no package name found in any file")
}
rdata := fmtEnumOutput(newChecksum, allEnums, pkgname)
if !gofmt {
return rdata, newChecksum, true, nil
}
fdata, err := format.Source([]byte(rdata))
if err != nil {
return "", "", false, err
}
return string(fdata), newChecksum, true, nil
}
func processEnumFile(basedir string, fn string, debugOutput bool) ([]EnumDef, 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")
enums := make([]EnumDef, 0)
pkgname := ""
for i, line := range lines {
if i == 0 && strings.HasPrefix(line, "// Code generated by") {
break
}
if match, ok := rexEnumPackage.MatchFirst(line); i == 0 && ok {
pkgname = match.GroupByName("name").Value()
continue
}
if match, ok := rexEnumDef.MatchFirst(line); ok {
rfp, err := filepath.Rel(basedir, fn)
if err != nil {
return nil, "", err
}
def := EnumDef{
File: fn,
FileRelative: rfp,
EnumTypeName: match.GroupByName("name").Value(),
Type: match.GroupByName("type").Value(),
Values: make([]EnumDefVal, 0),
}
enums = append(enums, def)
if debugOutput {
fmt.Printf("Found enum definition { '%s' -> '%s' }\n", def.EnumTypeName, def.Type)
}
}
if match, ok := rexEnumValueDef.MatchFirst(line); ok {
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{
VarName: match.GroupByName("name").Value(),
Value: match.GroupByName("value").Value(),
RawComment: comment,
Description: descr,
Data: data,
}
found := false
for i, v := range enums {
if v.EnumTypeName == typename {
enums[i].Values = append(enums[i].Values, def)
found = true
if debugOutput {
if def.Description != nil {
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
}
}
if !found {
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)
}
}
}
}
return enums, pkgname, nil
}
func tryParseDataComment(s string) (map[string]any, bool) {
r := make(map[string]any)
err := json.Unmarshal([]byte(s), &r)
if err != nil {
return nil, false
}
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()
}

View File

@@ -0,0 +1,155 @@
// Code generated by enum-generate.go DO NOT EDIT.
package {{.PkgName}}
import "gogs.mikescher.com/BlackForestBytes/goext/langext"
import "gogs.mikescher.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}}
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 $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}}
}
}

View File

@@ -0,0 +1,91 @@
package bfcodegen
import (
_ "embed"
"fmt"
"gogs.mikescher.com/BlackForestBytes/goext/cmdext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/tst"
"os"
"path/filepath"
"testing"
"time"
)
//go:embed _test_example_1.tgz
var EnumExampleModels1 []byte
//go:embed _test_example_2.tgz
var EnumExampleModels2 []byte
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
bfcodegen/id-generate.go Normal file
View File

@@ -0,0 +1,198 @@
package bfcodegen
import (
"bytes"
_ "embed"
"errors"
"fmt"
"go/format"
"gogs.mikescher.com/BlackForestBytes/goext"
"gogs.mikescher.com/BlackForestBytes/goext/cryptext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.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()
}

View File

@@ -0,0 +1,47 @@
// 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 "gogs.mikescher.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)
}
{{end}}
func New{{.Name}}() {{.Name}} {
return {{.Name}}(primitive.NewObjectID().Hex())
}
{{end}}

View File

@@ -0,0 +1,52 @@
package bfcodegen
import (
_ "embed"
"fmt"
"gogs.mikescher.com/BlackForestBytes/goext/cmdext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.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()
}

View File

@@ -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

View File

@@ -11,6 +11,7 @@ import (
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)

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)
} }

View File

@@ -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 {

View File

@@ -8,6 +8,7 @@ import (
"os" "os"
"reflect" "reflect"
"strconv" "strconv"
"strings"
"time" "time"
) )
@@ -40,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 != "" {
@@ -56,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
} }
@@ -172,6 +170,20 @@ func parseEnvToValue(envval string, fullEnvKey string, rvtype reflect.Type) (ref
return envcvl, nil return envcvl, nil
} else if rvtype.ConvertibleTo(reflect.TypeOf(false)) {
if strings.TrimSpace(strings.ToLower(envval)) == "true" {
return reflect.ValueOf(true).Convert(rvtype), nil
} else if strings.TrimSpace(strings.ToLower(envval)) == "false" {
return reflect.ValueOf(false).Convert(rvtype), nil
} else if strings.TrimSpace(strings.ToLower(envval)) == "1" {
return reflect.ValueOf(true).Convert(rvtype), nil
} else if strings.TrimSpace(strings.ToLower(envval)) == "0" {
return reflect.ValueOf(false).Convert(rvtype), nil
} else {
return reflect.Value{}, errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to <%s, ,bool> (value := '%s')", rvtype.Name(), fullEnvKey, envval))
}
} else if rvtype.ConvertibleTo(reflect.TypeOf("")) { } else if rvtype.ConvertibleTo(reflect.TypeOf("")) {
envcvl := reflect.ValueOf(envval).Convert(rvtype) envcvl := reflect.ValueOf(envval).Convert(rvtype)

View File

@@ -68,6 +68,7 @@ func TestApplyEnvOverridesSimple(t *testing.T) {
V7 aliasstring `env:"TEST_V7"` V7 aliasstring `env:"TEST_V7"`
V8 time.Duration `env:"TEST_V8"` V8 time.Duration `env:"TEST_V8"`
V9 time.Time `env:"TEST_V9"` V9 time.Time `env:"TEST_V9"`
VA bool `env:"TEST_VA"`
} }
data := testdata{ data := testdata{
@@ -82,6 +83,7 @@ func TestApplyEnvOverridesSimple(t *testing.T) {
V7: "7", V7: "7",
V8: 9, V8: 9,
V9: time.Unix(1671102873, 0), V9: time.Unix(1671102873, 0),
VA: false,
} }
t.Setenv("TEST_V1", "846") t.Setenv("TEST_V1", "846")
@@ -93,6 +95,7 @@ func TestApplyEnvOverridesSimple(t *testing.T) {
t.Setenv("TEST_V7", "AAAAAA") t.Setenv("TEST_V7", "AAAAAA")
t.Setenv("TEST_V8", "1min4s") t.Setenv("TEST_V8", "1min4s")
t.Setenv("TEST_V9", "2009-11-10T23:00:00Z") t.Setenv("TEST_V9", "2009-11-10T23:00:00Z")
t.Setenv("TEST_VA", "true")
err := ApplyEnvOverrides("", &data, ".") err := ApplyEnvOverrides("", &data, ".")
if err != nil { if err != nil {
@@ -109,6 +112,7 @@ func TestApplyEnvOverridesSimple(t *testing.T) {
tst.AssertEqual(t, data.V7, "AAAAAA") tst.AssertEqual(t, data.V7, "AAAAAA")
tst.AssertEqual(t, data.V8, time.Second*64) tst.AssertEqual(t, data.V8, time.Second*64)
tst.AssertEqual(t, data.V9, time.Unix(1257894000, 0).UTC()) tst.AssertEqual(t, data.V9, time.Unix(1257894000, 0).UTC())
tst.AssertEqual(t, data.VA, true)
} }
func TestApplyEnvOverridesRecursive(t *testing.T) { func TestApplyEnvOverridesRecursive(t *testing.T) {

View File

@@ -3,6 +3,7 @@ package cryptext
import ( import (
"crypto/rand" "crypto/rand"
"crypto/sha256" "crypto/sha256"
"crypto/sha512"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"errors" "errors"
@@ -14,14 +15,15 @@ import (
"strings" "strings"
) )
const LatestPassHashVersion = 4 const LatestPassHashVersion = 5
// PassHash // PassHash
// - [v0]: plaintext password ( `0|...` ) // - [v0]: plaintext password ( `0|...` ) // simple, used to write PW's directly in DB
// - [v1]: sha256(plaintext) // - [v1]: sha256(plaintext) // simple hashing
// - [v2]: seed | sha256<seed>(plaintext) // - [v2]: seed | sha256<seed>(plaintext) // add seed
// - [v3]: seed | sha256<seed>(plaintext) | [hex(totp)] // - [v3]: seed | sha256<seed>(plaintext) | [hex(totp)] // add TOTP support
// - [v4]: bcrypt(plaintext) | [hex(totp)] // - [v4]: bcrypt(plaintext) | [hex(totp)] // use proper bcrypt
// - [v5]: bcrypt(sha512(plaintext)) | [hex(totp)] // hash pw before bcrypt (otherwise max pw-len = 72)
type PassHash string type PassHash string
func (ph PassHash) Valid() bool { func (ph PassHash) Valid() bool {
@@ -64,7 +66,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
@@ -109,7 +110,21 @@ func (ph PassHash) Data() (_version int, _seed []byte, _payload []byte, _totp bo
totp := false totp := false
totpsecret := make([]byte, 0) totpsecret := make([]byte, 0)
if split[2] != "0" { if split[2] != "0" {
totpsecret, err = hex.DecodeString(split[3]) totpsecret, err = hex.DecodeString(split[2])
totp = true
}
return int(version), nil, payload, totp, totpsecret, true
}
if version == 5 {
if len(split) != 3 {
return -1, nil, nil, false, nil, false
}
payload := []byte(split[1])
totp := false
totpsecret := make([]byte, 0)
if split[2] != "0" {
totpsecret, err = hex.DecodeString(split[2])
totp = true totp = true
} }
return int(version), nil, payload, totp, totpsecret, true return int(version), nil, payload, totp, totpsecret, true
@@ -156,6 +171,14 @@ func (ph PassHash) Verify(plainpass string, totp *string) bool {
} }
} }
if version == 5 {
if !hastotp {
return bcrypt.CompareHashAndPassword(payload, hash512(plainpass)) == nil
} else {
return bcrypt.CompareHashAndPassword(payload, hash512(plainpass)) == nil && totpext.Validate(totpsecret, *totp)
}
}
return false return false
} }
@@ -209,6 +232,12 @@ func (ph PassHash) ClearTOTP() (PassHash, error) {
return PassHash(strings.Join(split, "|")), nil return PassHash(strings.Join(split, "|")), nil
} }
if version == 5 {
split := strings.Split(string(ph), "|")
split[2] = "0"
return PassHash(strings.Join(split, "|")), nil
}
return "", errors.New("unknown version") return "", errors.New("unknown version")
} }
@@ -242,6 +271,12 @@ func (ph PassHash) WithTOTP(totpSecret []byte) (PassHash, error) {
return PassHash(strings.Join(split, "|")), nil return PassHash(strings.Join(split, "|")), nil
} }
if version == 5 {
split := strings.Split(string(ph), "|")
split[2] = hex.EncodeToString(totpSecret)
return PassHash(strings.Join(split, "|")), nil
}
return "", errors.New("unknown version") return "", errors.New("unknown version")
} }
@@ -271,6 +306,10 @@ func (ph PassHash) Change(newPlainPass string) (PassHash, error) {
return HashPasswordV4(newPlainPass, langext.Conditional(hastotp, totpsecret, nil)) return HashPasswordV4(newPlainPass, langext.Conditional(hastotp, totpsecret, nil))
} }
if version == 5 {
return HashPasswordV5(newPlainPass, langext.Conditional(hastotp, totpsecret, nil))
}
return "", errors.New("unknown version") return "", errors.New("unknown version")
} }
@@ -279,7 +318,24 @@ func (ph PassHash) String() string {
} }
func HashPassword(plainpass string, totpSecret []byte) (PassHash, error) { func HashPassword(plainpass string, totpSecret []byte) (PassHash, error) {
return HashPasswordV4(plainpass, totpSecret) return HashPasswordV5(plainpass, totpSecret)
}
func HashPasswordV5(plainpass string, totpSecret []byte) (PassHash, error) {
var strtotp string
if totpSecret == nil {
strtotp = "0"
} else {
strtotp = hex.EncodeToString(totpSecret)
}
payload, err := bcrypt.GenerateFromPassword(hash512(plainpass), bcrypt.MinCost)
if err != nil {
return "", err
}
return PassHash(fmt.Sprintf("5|%s|%s", string(payload), strtotp)), nil
} }
func HashPasswordV4(plainpass string, totpSecret []byte) (PassHash, error) { func HashPasswordV4(plainpass string, totpSecret []byte) (PassHash, error) {
@@ -340,6 +396,13 @@ func HashPasswordV0(plainpass string) (PassHash, error) {
return PassHash(fmt.Sprintf("0|%s", plainpass)), nil return PassHash(fmt.Sprintf("0|%s", plainpass)), nil
} }
func hash512(s string) []byte {
h := sha512.New()
h.Write([]byte(s))
bs := h.Sum(nil)
return bs
}
func hash256(s string) []byte { func hash256(s string) []byte {
h := sha256.New() h := sha256.New()
h.Write([]byte(s)) h.Write([]byte(s))

210
cryptext/passHash_test.go Normal file
View File

@@ -0,0 +1,210 @@
package cryptext
import (
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/totpext"
"gogs.mikescher.com/BlackForestBytes/goext/tst"
"testing"
)
func TestPassHash1(t *testing.T) {
ph, err := HashPassword("test123", nil)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
tst.AssertTrue(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
}
func TestPassHashTOTP(t *testing.T) {
sec, err := totpext.GenerateSecret()
tst.AssertNoErr(t, err)
ph, err := HashPassword("test123", sec)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertTrue(t, ph.HasTOTP())
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
tst.AssertFalse(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec))))
tst.AssertFalse(t, ph.Verify("test124", nil))
}
func TestPassHashUpgrade_V0(t *testing.T) {
ph, err := HashPasswordV0("test123")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertTrue(t, ph.NeedsPasswordUpgrade())
tst.AssertTrue(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
ph, err = ph.Upgrade("test123")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
tst.AssertTrue(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
}
func TestPassHashUpgrade_V1(t *testing.T) {
ph, err := HashPasswordV1("test123")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertTrue(t, ph.NeedsPasswordUpgrade())
tst.AssertTrue(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
ph, err = ph.Upgrade("test123")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
tst.AssertTrue(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
}
func TestPassHashUpgrade_V2(t *testing.T) {
ph, err := HashPasswordV2("test123")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertTrue(t, ph.NeedsPasswordUpgrade())
tst.AssertTrue(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
ph, err = ph.Upgrade("test123")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
tst.AssertTrue(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
}
func TestPassHashUpgrade_V3(t *testing.T) {
ph, err := HashPasswordV3("test123", nil)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertTrue(t, ph.NeedsPasswordUpgrade())
tst.AssertTrue(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
ph, err = ph.Upgrade("test123")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
tst.AssertTrue(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
}
func TestPassHashUpgrade_V3_TOTP(t *testing.T) {
sec, err := totpext.GenerateSecret()
tst.AssertNoErr(t, err)
ph, err := HashPasswordV3("test123", sec)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertTrue(t, ph.HasTOTP())
tst.AssertTrue(t, ph.NeedsPasswordUpgrade())
tst.AssertFalse(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec))))
tst.AssertFalse(t, ph.Verify("test124", nil))
ph, err = ph.Upgrade("test123")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertTrue(t, ph.HasTOTP())
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
tst.AssertFalse(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec))))
tst.AssertFalse(t, ph.Verify("test124", nil))
}
func TestPassHashUpgrade_V4(t *testing.T) {
ph, err := HashPasswordV4("test123", nil)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertTrue(t, ph.NeedsPasswordUpgrade())
tst.AssertTrue(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
ph, err = ph.Upgrade("test123")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
tst.AssertTrue(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
}
func TestPassHashUpgrade_V4_TOTP(t *testing.T) {
sec, err := totpext.GenerateSecret()
tst.AssertNoErr(t, err)
ph, err := HashPasswordV4("test123", sec)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertTrue(t, ph.HasTOTP())
tst.AssertTrue(t, ph.NeedsPasswordUpgrade())
tst.AssertFalse(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec))))
tst.AssertFalse(t, ph.Verify("test124", nil))
ph, err = ph.Upgrade("test123")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertTrue(t, ph.HasTOTP())
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
tst.AssertFalse(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec))))
tst.AssertFalse(t, ph.Verify("test124", nil))
}

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
}

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
ctxext/getter.go Normal file
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
}
}

8
cursortoken/direction.go Normal file
View File

@@ -0,0 +1,8 @@
package cursortoken
type SortDirection string //@enum:type
const (
SortASC SortDirection = "ASC"
SortDESC SortDirection = "DESC"
)

15
cursortoken/filter.go Normal file
View File

@@ -0,0 +1,15 @@
package cursortoken
import (
"context"
"go.mongodb.org/mongo-driver/mongo"
)
type RawFilter interface {
FilterQuery(ctx context.Context) mongo.Pipeline
}
type Filter interface {
FilterQuery(ctx context.Context) mongo.Pipeline
Pagination(ctx context.Context) (string, SortDirection, string, SortDirection)
}

95
cursortoken/token.go Normal file
View File

@@ -0,0 +1,95 @@
package cursortoken
import (
"encoding/base32"
"encoding/json"
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
"strconv"
"strings"
"time"
)
type CursorToken interface {
Token() string
IsStart() bool
IsEnd() bool
}
type Mode string
const (
CTMStart Mode = "START"
CTMNormal Mode = "NORMAL"
CTMEnd Mode = "END"
)
type Extra struct {
Timestamp *time.Time
Id *string
Page *int
PageSize *int
}
func Decode(tok string) (CursorToken, error) {
if tok == "" {
return Start(), nil
}
if strings.ToLower(tok) == "@start" {
return Start(), nil
}
if strings.ToLower(tok) == "@end" {
return End(), nil
}
if strings.ToLower(tok) == "$end" {
return PageEnd(), nil
}
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
}
if strings.HasPrefix(tok, "tok_") {
body, err := base32.StdEncoding.DecodeString(tok[len("tok_"):])
if err != nil {
return nil, err
}
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()
}
token := CTKeySort{Mode: CTMNormal}
if tokenDeserialize.ValuePrimary != nil {
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
token.Extra.Id = tokenDeserialize.ExtraId
token.Extra.Page = tokenDeserialize.ExtraPage
token.Extra.PageSize = tokenDeserialize.ExtraPageSize
return token, nil
} else {
return nil, exerr.New(exerr.TypeCursorTokenDecode, "could not decode token, missing/unknown prefix").Str("token", tok).Build()
}
}

136
cursortoken/tokenKeySort.go Normal file
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
}
}

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
}

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
dataext/casMutex.go Normal file
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() }

83
dataext/optional.go Normal file
View File

@@ -0,0 +1,83 @@
package dataext
import (
"encoding/json"
"errors"
"gogs.mikescher.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
}

144
dataext/ringBuffer.go Normal file
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
dataext/ringBuffer_test.go Normal file
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)
}
}

167
dataext/syncMap.go Normal file
View File

@@ -0,0 +1,167 @@
package dataext
import "sync"
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{}}
}
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
}
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
}
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
}
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
}
}
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
}
}
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
}
}
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
}
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
}
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
}
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
}

143
dataext/syncRingSet.go Normal file
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)
}

View File

@@ -7,6 +7,13 @@ 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
// returns true if the value was actually inserted (value did not exist beforehand)
// 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()
defer s.lock.Unlock() defer s.lock.Unlock()
@@ -15,10 +22,13 @@ func (s *SyncSet[TData]) Add(value TData) bool {
s.data = make(map[TData]bool) s.data = make(map[TData]bool)
} }
_, ok := s.data[value] _, existsInPreState := s.data[value]
s.data[value] = true if existsInPreState {
return false
}
return !ok s.data[value] = true
return true
} }
func (s *SyncSet[TData]) AddAll(values []TData) { func (s *SyncSet[TData]) AddAll(values []TData) {
@@ -34,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()
@@ -63,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
dataext/tuple.go Normal file
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}
}

33
enums/enum.go Normal file
View File

@@ -0,0 +1,33 @@
package enums
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"`
}

501
exerr/builder.go Normal file
View File

@@ -0,0 +1,501 @@
package exerr
import (
"bytes"
"context"
"encoding/json"
"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/enums"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http"
"os"
"runtime/debug"
"strings"
"time"
)
//
// ==== USAGE =====
//
// If some method returns an error _always wrap it into an exerror:
// value, err := do_something(..)
// if err != nil {
// return nil, exerror.Wrap(err, "do something failed").Build()
// }
//
// If possible add metadata to the error (eg the id that was not found, ...), the methods are the same as in zerolog
// return nil, exerror.Wrap(err, "do something failed").Str("someid", id).Int("count", in.Count).Build()
//
// You can also add extra-data to an error with Extra(..)
// in contrast to metadata is extradata always printed in the resulting error and is more intended for additional (programmatically readable) data in addition to the errortype
// (metadata is more internal debug info/help)
//
// You can change the errortype with `.User()` and `.System()` (User-errors are 400 and System-errors 500)
// You can also manually set the statuscode with `.WithStatuscode(http.NotFound)`
// You can set the type with `WithType(..)`
//
// New Errors (that don't wrap an existing err object) are created with New
// return nil, exerror.New(exerror.TypeInternal, "womethign wen horrible wrong").Build()
// You can eitehr use an existing ErrorType, the "catch-all" ErrInternal, or add you own ErrType in consts.go
//
// All errors should be handled one of the following four ways:
// - return the error to the caller and let him handle it:
// (also auto-prints the error to the log)
// => Wrap/New + Build
// - Print the error
// (also auto-sends it to the error-service)
// This is useful for errors that happen asynchron or are non-fatal for the current request
// => Wrap/New + Print
// - Return the error to the Rest-API caller
// (also auto-prints the error to the log)
// (also auto-sends it to the error-service)
// => Wrap/New + Output
// - Print and stop the service
// (also auto-sends it to the error-service)
// => Wrap/New + Fatal
//
type Builder struct {
wrappedErr error
errorData *ExErr
containsGinData bool
containsContextData bool
noLog bool
}
func Get(err error) *Builder {
return &Builder{errorData: FromError(err)}
}
func New(t ErrorType, msg string) *Builder {
return &Builder{errorData: newExErr(CatSystem, t, msg)}
}
func Wrap(err error, msg string) *Builder {
if err == nil {
return &Builder{errorData: newExErr(CatSystem, TypeInternal, msg)} // prevent NPE if we call Wrap with err==nil
}
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)}
}
}
// ----------------------------------------------------------------------------
func (b *Builder) WithType(t ErrorType) *Builder {
b.errorData.Type = t
return b
}
func (b *Builder) WithStatuscode(status int) *Builder {
b.errorData.StatusCode = &status
return b
}
func (b *Builder) WithMessage(msg string) *Builder {
b.errorData.Message = msg
return b
}
// ----------------------------------------------------------------------------
// Err changes the Severity to ERROR (default)
// The error will be:
//
// - On Build():
//
// - Short-Logged as Err
//
// - On Print():
//
// - Logged as Err
//
// - Send to the error-service
//
// - On Output():
//
// - Logged as Err
//
// - Send to the error-service
func (b *Builder) Err() *Builder {
b.errorData.Severity = SevErr
return b
}
// Warn changes the Severity to WARN
// The error will be:
//
// - On Build():
//
// - -(nothing)-
//
// - On Print():
//
// - Short-Logged as Warn
//
// - On Output():
//
// - Logged as Warn
func (b *Builder) Warn() *Builder {
b.errorData.Severity = SevWarn
return b
}
// Info changes the Severity to INFO
// The error will be:
//
// - On Build():
//
// - -(nothing)-
//
// - On Print():
//
// - -(nothing)-
//
// - On Output():
//
// - -(nothing)-
func (b *Builder) Info() *Builder {
b.errorData.Severity = SevInfo
return b
}
// ----------------------------------------------------------------------------
// User sets the Category to CatUser
//
// Errors with category
func (b *Builder) User() *Builder {
b.errorData.Category = CatUser
return b
}
func (b *Builder) System() *Builder {
b.errorData.Category = CatSystem
return b
}
// ----------------------------------------------------------------------------
func (b *Builder) NoLog() *Builder {
b.noLog = true
return b
}
// ----------------------------------------------------------------------------
func (b *Builder) Id(key string, val fmt.Stringer) *Builder {
return b.addMeta(key, MDTID, newIDWrap(val))
}
func (b *Builder) StrPtr(key string, val *string) *Builder {
return b.addMeta(key, MDTStringPtr, val)
}
func (b *Builder) Str(key string, val string) *Builder {
return b.addMeta(key, MDTString, val)
}
func (b *Builder) Int(key string, val int) *Builder {
return b.addMeta(key, MDTInt, val)
}
func (b *Builder) Int8(key string, val int8) *Builder {
return b.addMeta(key, MDTInt8, val)
}
func (b *Builder) Int16(key string, val int16) *Builder {
return b.addMeta(key, MDTInt16, val)
}
func (b *Builder) Int32(key string, val int32) *Builder {
return b.addMeta(key, MDTInt32, val)
}
func (b *Builder) Int64(key string, val int64) *Builder {
return b.addMeta(key, MDTInt64, val)
}
func (b *Builder) Float32(key string, val float32) *Builder {
return b.addMeta(key, MDTFloat32, val)
}
func (b *Builder) Float64(key string, val float64) *Builder {
return b.addMeta(key, MDTFloat64, val)
}
func (b *Builder) Bool(key string, val bool) *Builder {
return b.addMeta(key, MDTBool, val)
}
func (b *Builder) Bytes(key string, val []byte) *Builder {
return b.addMeta(key, MDTBytes, val)
}
func (b *Builder) ObjectID(key string, val primitive.ObjectID) *Builder {
return b.addMeta(key, MDTObjectID, val)
}
func (b *Builder) Time(key string, val time.Time) *Builder {
return b.addMeta(key, MDTTime, val)
}
func (b *Builder) Dur(key string, val time.Duration) *Builder {
return b.addMeta(key, MDTDuration, val)
}
func (b *Builder) Strs(key string, val []string) *Builder {
return b.addMeta(key, MDTStringArray, val)
}
func (b *Builder) Ints(key string, val []int) *Builder {
return b.addMeta(key, MDTIntArray, val)
}
func (b *Builder) Ints32(key string, val []int32) *Builder {
return b.addMeta(key, MDTInt32Array, val)
}
func (b *Builder) Type(key string, cls interface{}) *Builder {
return b.addMeta(key, MDTString, fmt.Sprintf("%T", cls))
}
func (b *Builder) Interface(key string, val interface{}) *Builder {
return b.addMeta(key, MDTAny, newAnyWrap(val))
}
func (b *Builder) Any(key string, val any) *Builder {
return b.addMeta(key, MDTAny, newAnyWrap(val))
}
func (b *Builder) Stringer(key string, val fmt.Stringer) *Builder {
if langext.IsNil(val) {
return b.addMeta(key, MDTString, "(!nil)")
} else {
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 {
return b.addMeta("@Stack", MDTString, string(debug.Stack()))
}
func (b *Builder) Errs(key string, val []error) *Builder {
for i, valerr := range val {
b.addMeta(fmt.Sprintf("%v[%v]", key, i), MDTString, Get(valerr).errorData.FormatLog(LogPrintFull))
}
return b
}
func (b *Builder) GinReq(ctx context.Context, g *gin.Context, req *http.Request) *Builder {
if v := ctx.Value("start_timestamp"); v != nil {
if t, ok := v.(time.Time); ok {
b.Time("ctx_startTimestamp", t)
b.Time("ctx_endTimestamp", time.Now())
}
}
b.Str("gin_method", req.Method)
b.Str("gin_path", g.FullPath())
b.Strs("gin_header", extractHeader(g.Request.Header))
if req.URL != nil {
b.Str("gin_url", req.URL.String())
}
if ctxVal := g.GetString("apiversion"); ctxVal != "" {
b.Str("gin_context_apiversion", ctxVal)
}
if ctxVal := g.GetString("uid"); ctxVal != "" {
b.Str("gin_context_uid", ctxVal)
}
if ctxVal := g.GetString("fcmId"); ctxVal != "" {
b.Str("gin_context_fcmid", ctxVal)
}
if ctxVal := g.GetString("reqid"); ctxVal != "" {
b.Str("gin_context_reqid", ctxVal)
}
if req.Method != "GET" && req.Body != nil {
if req.Header.Get("Content-Type") == "application/json" {
if brc, ok := req.Body.(dataext.BufferedReadCloser); ok {
if bin, err := brc.BufferedAll(); err == nil {
if len(bin) < 16*1024 {
var prettyJSON bytes.Buffer
err = json.Indent(&prettyJSON, bin, "", " ")
if err == nil {
b.Str("gin_body", string(prettyJSON.Bytes()))
} else {
b.Bytes("gin_body", bin)
}
} else {
b.Str("gin_body", fmt.Sprintf("[[%v bytes | %s]]", len(bin), req.Header.Get("Content-Type")))
}
}
}
}
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
return b
}
func (b *Builder) CtxData(method Method, ctx context.Context) *Builder {
pkgconfig.ExtendContextMeta(b, method, ctx)
b.containsContextData = true
return b
}
func extractHeader(header map[string][]string) []string {
r := make([]string, 0, len(header))
for k, v := range header {
for _, hval := range v {
value := hval
value = strings.ReplaceAll(value, "\n", "\\n")
value = strings.ReplaceAll(value, "\r", "\\r")
value = strings.ReplaceAll(value, "\t", "\\t")
r = append(r, k+": "+value)
}
}
return r
}
// ----------------------------------------------------------------------------
// Extra adds additional data to the error
// this is not like the other metadata (like Id(), Str(), etc)
// this data is public and will be printed/outputted
func (b *Builder) Extra(key string, val any) *Builder {
b.errorData.Extra[key] = val
return b
}
// ----------------------------------------------------------------------------
// Build creates a new error, ready to pass up the stack
// If the errors is not SevWarn or SevInfo it gets also logged (in short form, without stacktrace) onto stdout
// Can be gloablly configured with ZeroLogErrTraces and ZeroLogAllTraces
// Can be locally suppressed with Builder.NoLog()
func (b *Builder) Build(ctxs ...context.Context) error {
warnOnPkgConfigNotInitialized()
for _, dctx := range ctxs {
b.CtxData(MethodBuild, dctx)
}
if pkgconfig.DisableErrorWrapping && b.wrappedErr != nil {
return b.wrappedErr
}
if pkgconfig.ZeroLogErrTraces && !b.noLog && (b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal) {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Error())
} else if pkgconfig.ZeroLogAllTraces && !b.noLog {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Error())
}
b.errorData.CallListener(MethodBuild)
return b.errorData
}
// Output prints the error onto the gin stdout.
// The error also gets printed to stdout/stderr
// If the error is SevErr|SevFatal we also send it to the error-service
func (b *Builder) Output(ctx context.Context, g *gin.Context) {
if !b.containsGinData && g.Request != nil {
// Auto-Add gin metadata if the caller hasn't already done it
b.GinReq(ctx, g, g.Request)
}
b.CtxData(MethodOutput, ctx)
b.errorData.Output(g)
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)
}
// Print prints the error
// If the error is SevErr we also send it to the error-service
func (b *Builder) Print(ctxs ...context.Context) Proxy {
warnOnPkgConfigNotInitialized()
for _, dctx := range ctxs {
b.CtxData(MethodPrint, dctx)
}
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 {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Debug())
}
b.errorData.CallListener(MethodPrint)
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 {
return b.errorData.FormatLog(level)
}
// Fatal prints the error and terminates the program
// If the error is SevErr we also send it to the error-service
func (b *Builder) Fatal(ctxs ...context.Context) {
b.errorData.Severity = SevFatal
for _, dctx := range ctxs {
b.CtxData(MethodFatal, dctx)
}
b.errorData.Log(pkgconfig.ZeroLogger.WithLevel(zerolog.FatalLevel))
b.errorData.CallListener(MethodFatal)
os.Exit(1)
}
// ----------------------------------------------------------------------------
func (b *Builder) addMeta(key string, mdtype metaDataType, val interface{}) *Builder {
b.errorData.Meta.add(key, mdtype, val)
return b
}

274
exerr/constructor.go Normal file
View File

@@ -0,0 +1,274 @@
package exerr
import (
"encoding/json"
"fmt"
"go.mongodb.org/mongo-driver/bson/primitive"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"reflect"
"time"
)
var reflectTypeStr = reflect.TypeOf("")
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 {
// A simple ExErr
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)
return &ExErr{
UniqueID: newID(),
Category: CatForeign,
Type: TypeInternal,
Severity: SevErr,
Timestamp: time.Time{},
StatusCode: nil,
Message: err.Error(),
WrappedErrType: fmt.Sprintf("%T", err),
WrappedErr: err,
Caller: "",
OriginalError: nil,
Meta: getForeignMeta(err),
Extra: make(map[string]any),
}
}
func newExErr(cat ErrorCategory, errtype ErrorType, msg string) *ExErr {
return &ExErr{
UniqueID: newID(),
Category: cat,
Type: errtype,
Severity: SevErr,
Timestamp: time.Now(),
StatusCode: nil,
Message: msg,
WrappedErrType: "",
WrappedErr: nil,
Caller: callername(2),
OriginalError: nil,
Meta: make(map[string]MetaValue),
Extra: make(map[string]any),
}
}
func wrapExErr(e *ExErr, msg string, cat ErrorCategory, stacktraceskip int) *ExErr {
return &ExErr{
UniqueID: newID(),
Category: cat,
Type: TypeWrap,
Severity: e.Severity,
Timestamp: time.Now(),
StatusCode: e.StatusCode,
Message: msg,
WrappedErrType: "",
WrappedErr: nil,
Caller: callername(1 + stacktraceskip),
OriginalError: e,
Meta: make(map[string]MetaValue),
Extra: langext.CopyMap(langext.ForceMap(e.Extra)),
}
}
func getForeignMeta(err error) (mm MetaMap) {
mm = make(map[string]MetaValue)
defer func() {
if panicerr := recover(); panicerr != nil {
New(TypePanic, "Panic while trying to get foreign meta").
Str("source", err.Error()).
Interface("panic-object", panicerr).
Stack().
Print()
}
}()
rval := reflect.ValueOf(err)
if rval.Kind() == reflect.Interface || rval.Kind() == reflect.Ptr {
rval = reflect.ValueOf(err).Elem()
}
mm.add("foreign.errortype", MDTString, rval.Type().String())
for k, v := range addMetaPrefix("foreign", getReflectedMetaValues(err, 8)) {
mm[k] = v
}
return mm
}
func getReflectedMetaValues(value interface{}, remainingDepth int) map[string]MetaValue {
if remainingDepth <= 0 {
return map[string]MetaValue{}
}
if langext.IsNil(value) {
return map[string]MetaValue{"": {DataType: MDTNil, Value: nil}}
}
rval := reflect.ValueOf(value)
if rval.Type().Kind() == reflect.Ptr {
if rval.IsNil() {
return map[string]MetaValue{"*": {DataType: MDTNil, Value: nil}}
}
elem := rval.Elem()
return addMetaPrefix("*", getReflectedMetaValues(elem.Interface(), remainingDepth-1))
}
if !rval.CanInterface() {
return map[string]MetaValue{"": {DataType: MDTString, Value: "<<no-interface>>"}}
}
raw := rval.Interface()
switch ifraw := raw.(type) {
case time.Time:
return map[string]MetaValue{"": {DataType: MDTTime, Value: ifraw}}
case time.Duration:
return map[string]MetaValue{"": {DataType: MDTDuration, Value: ifraw}}
case int:
return map[string]MetaValue{"": {DataType: MDTInt, Value: ifraw}}
case int8:
return map[string]MetaValue{"": {DataType: MDTInt8, Value: ifraw}}
case int16:
return map[string]MetaValue{"": {DataType: MDTInt16, Value: ifraw}}
case int32:
return map[string]MetaValue{"": {DataType: MDTInt32, Value: ifraw}}
case int64:
return map[string]MetaValue{"": {DataType: MDTInt64, Value: ifraw}}
case string:
return map[string]MetaValue{"": {DataType: MDTString, Value: ifraw}}
case bool:
return map[string]MetaValue{"": {DataType: MDTBool, Value: ifraw}}
case []byte:
return map[string]MetaValue{"": {DataType: MDTBytes, Value: ifraw}}
case float32:
return map[string]MetaValue{"": {DataType: MDTFloat32, Value: ifraw}}
case float64:
return map[string]MetaValue{"": {DataType: MDTFloat64, Value: ifraw}}
case []int:
return map[string]MetaValue{"": {DataType: MDTIntArray, Value: ifraw}}
case []int32:
return map[string]MetaValue{"": {DataType: MDTInt32Array, Value: ifraw}}
case primitive.ObjectID:
return map[string]MetaValue{"": {DataType: MDTObjectID, Value: ifraw}}
case []string:
return map[string]MetaValue{"": {DataType: MDTStringArray, Value: ifraw}}
}
if rval.Type().Kind() == reflect.Struct {
m := make(map[string]MetaValue)
for i := 0; i < rval.NumField(); i++ {
fieldtype := rval.Type().Field(i)
fieldname := fieldtype.Name
if fieldtype.IsExported() {
for k, v := range addMetaPrefix(fieldname, getReflectedMetaValues(rval.Field(i).Interface(), remainingDepth-1)) {
m[k] = v
}
}
}
return m
}
if rval.Type().ConvertibleTo(reflectTypeStr) {
return map[string]MetaValue{"": {DataType: MDTString, Value: rval.Convert(reflectTypeStr).String()}}
}
jsonval, err := json.Marshal(value)
if err != nil {
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)}}
}
func addMetaPrefix(prefix string, m map[string]MetaValue) map[string]MetaValue {
if len(m) == 1 {
for k, v := range m {
if k == "" {
return map[string]MetaValue{prefix: v}
}
}
}
r := make(map[string]MetaValue, len(m))
for k, v := range m {
r[prefix+"."+k] = v
}
return r
}

18
exerr/data.go Normal file
View File

@@ -0,0 +1,18 @@
package exerr
type Method string
const (
MethodOutput Method = "OUTPUT"
MethodPrint Method = "PRINT"
MethodBuild Method = "BUILD"
MethodFatal Method = "FATAL"
)
type LogPrintLevel string
const (
LogPrintFull LogPrintLevel = "Full"
LogPrintOverview LogPrintLevel = "Overview"
LogPrintShort LogPrintLevel = "Short"
)

89
exerr/dataCategory.go Normal file
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
exerr/dataSeverity.go Normal file
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}

156
exerr/dataType.go Normal file
View File

@@ -0,0 +1,156 @@
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"
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"reflect"
)
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))
// 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
exerr/data_test.go Normal file
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"
"gogs.mikescher.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)
}

138
exerr/errinit.go Normal file
View File

@@ -0,0 +1,138 @@
package exerr
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http"
"os"
)
type ErrorPackageConfig struct {
ZeroLogErrTraces bool // autom print zerolog logs on .Build() (for SevErr and SevFatal)
ZeroLogAllTraces bool // autom print zerolog logs on .Build() (for all Severities)
RecursiveErrors bool // errors contains their Origin-Error
ExtendedGinOutput bool // Log extended data (trace, meta, ...) to gin in err.Output()
IncludeMetaInGinOutput bool // Log meta fields ( from e.g. `.Str(key, val).Build()` ) to gin in err.Output()
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 {
ZeroLogErrTraces *bool
ZeroLogAllTraces *bool
RecursiveErrors *bool
ExtendedGinOutput *bool
IncludeMetaInGinOutput *bool
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 pkgconfig = ErrorPackageConfig{
ZeroLogErrTraces: true,
ZeroLogAllTraces: false,
RecursiveErrors: true,
ExtendedGinOutput: false,
IncludeMetaInGinOutput: true,
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
// Must be called at the program start, before (!) any errors
// Is not thread-safe
func Init(cfg ErrorPackageConfigInit) {
if initialized {
panic("Cannot re-init error package")
}
ego := func(err *ExErr, 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 {
ego = cfg.ExtendGinOutput
}
if cfg.ExtendGinDataOutput != nil {
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{
ZeroLogErrTraces: langext.Coalesce(cfg.ZeroLogErrTraces, pkgconfig.ZeroLogErrTraces),
ZeroLogAllTraces: langext.Coalesce(cfg.ZeroLogAllTraces, pkgconfig.ZeroLogAllTraces),
RecursiveErrors: langext.Coalesce(cfg.RecursiveErrors, pkgconfig.RecursiveErrors),
ExtendedGinOutput: langext.Coalesce(cfg.ExtendedGinOutput, pkgconfig.ExtendedGinOutput),
IncludeMetaInGinOutput: langext.Coalesce(cfg.IncludeMetaInGinOutput, pkgconfig.IncludeMetaInGinOutput),
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
}
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(4).Logger()
}
func Initialized() bool {
return initialized
}
func warnOnPkgConfigNotInitialized() {
if !initialized {
fmt.Printf("\n")
fmt.Printf("%s\n", langext.StrRepeat("=", 80))
fmt.Printf("%s\n", "[WARNING] exerr package used without initializiation")
fmt.Printf("%s\n", " call exerr.Init() in your main() function")
fmt.Printf("%s\n", langext.StrRepeat("=", 80))
fmt.Printf("\n")
}
}

436
exerr/exerr.go Normal file
View File

@@ -0,0 +1,436 @@
package exerr
import (
"fmt"
"github.com/rs/xid"
"github.com/rs/zerolog"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"reflect"
"strings"
"time"
)
type ExErr struct {
UniqueID string `json:"uniqueID"`
Timestamp time.Time `json:"timestamp"`
Category ErrorCategory `json:"category"`
Severity ErrorSeverity `json:"severity"`
Type ErrorType `json:"type"`
StatusCode *int `json:"statusCode"`
Message string `json:"message"`
WrappedErrType string `json:"wrappedErrType"`
WrappedErr any `json:"-"`
Caller string `json:"caller"`
OriginalError *ExErr `json:"originalError"`
Extra map[string]any `json:"extra"`
Meta MetaMap `json:"meta"`
}
func (ee *ExErr) Error() string {
return ee.RecursiveMessage()
}
// Unwrap must be implemented so that some error.XXX methods work
func (ee *ExErr) Unwrap() error {
if ee.OriginalError == nil {
if ee.WrappedErr != nil {
if werr, ok := ee.WrappedErr.(error); ok {
return werr
}
}
return nil // this is neccessary - otherwise we return a wrapped nil and the `x == nil` comparison fails (= panic in errors.Is and other failures)
}
return ee.OriginalError
}
// Is must be implemented so that error.Is(x) works
func (ee *ExErr) Is(e error) bool {
return IsFrom(ee, e)
}
// As must be implemented so that error.As(x) works
//
//goland:noinspection GoTypeAssertionOnErrors
func (ee *ExErr) As(target any) bool {
if dstErr, ok := target.(*ExErr); ok {
if dst0, ok := ee.contains(dstErr); ok {
dstErr = dst0
return true
} else {
return false
}
} else {
val := reflect.ValueOf(target)
typStr := val.Type().Elem().String()
for curr := ee; curr != nil; curr = curr.OriginalError {
if curr.Category == CatForeign && curr.WrappedErrType == typStr && curr.WrappedErr != nil {
val.Elem().Set(reflect.ValueOf(curr.WrappedErr))
return true
}
}
return false
}
}
func (ee *ExErr) Log(evt *zerolog.Event) {
evt.Msg(ee.FormatLog(LogPrintFull))
}
func (ee *ExErr) FormatLog(lvl LogPrintLevel) string {
// [LogPrintShort]
//
// - Only print message and type
// - Used e.g. for logging to the console when Build is called
// - also used in Print() if level == Warn/Info
//
// [LogPrintOverview]
//
// - print message, extra and errortrace
//
// [LogPrintFull]
//
// - print full error, with meta and extra, and trace, etc
// - Used in Output() and Print()
//
if lvl == LogPrintShort {
msg := ee.Message
if msg == "" {
msg = ee.RecursiveMessage()
}
if ee.OriginalError != nil && ee.OriginalError.Category == CatForeign {
msg = msg + " (" + strings.ReplaceAll(ee.OriginalError.Message, "\n", " ") + ")"
}
if ee.Type != TypeWrap {
return "[" + ee.Type.Key + "] " + msg
} else {
return msg
}
} else if lvl == LogPrintOverview {
str := "[" + ee.RecursiveType().Key + "] <" + ee.UniqueID + "> " + strings.ReplaceAll(ee.RecursiveMessage(), "\n", " ") + "\n"
for exk, exv := range ee.Extra {
str += fmt.Sprintf(" # [[[ %s ==> %v ]]]\n", exk, exv)
}
indent := ""
for curr := ee; curr != nil; curr = curr.OriginalError {
indent += " "
str += indent
str += "-> "
strmsg := strings.Trim(curr.Message, " \r\n\t")
if lbidx := strings.Index(curr.Message, "\n"); lbidx >= 0 {
strmsg = strmsg[0:lbidx]
}
strmsg = langext.StrLimit(strmsg, 61, "...")
str += strmsg
str += "\n"
}
return str
} else if lvl == LogPrintFull {
str := "[" + ee.RecursiveType().Key + "] <" + ee.UniqueID + "> " + strings.ReplaceAll(ee.RecursiveMessage(), "\n", " ") + "\n"
for exk, exv := range ee.Extra {
str += fmt.Sprintf(" # [[[ %s ==> %v ]]]\n", exk, exv)
}
indent := ""
for curr := ee; curr != nil; curr = curr.OriginalError {
indent += " "
etype := curr.Type.Key
if curr.Type == TypeWrap {
etype = "~"
}
str += indent
str += "-> ["
str += etype
if curr.Category == CatForeign {
str += "|Foreign"
}
str += "] "
str += strings.ReplaceAll(curr.Message, "\n", " ")
if curr.Caller != "" {
str += " (@ "
str += curr.Caller
str += ")"
}
str += "\n"
if curr.Meta.Any() {
meta := indent + " {" + curr.Meta.FormatOneLine(240) + "}"
if len(meta) < 200 {
str += meta
str += "\n"
} else {
str += curr.Meta.FormatMultiLine(indent+" ", " ", 1024)
str += "\n"
}
}
}
return str
} else {
return "[?[" + ee.UniqueID + "]?]"
}
}
func (ee *ExErr) ShortLog(evt *zerolog.Event) {
ee.Meta.Apply(evt, langext.Ptr(240)).Msg(ee.FormatLog(LogPrintShort))
}
// RecursiveMessage returns the message to show
// = first error (top-down) that is not wrapping/foreign/empty
// = lowest level error (that is not empty)
// = fallback to self.message
func (ee *ExErr) RecursiveMessage() string {
// ==== [1] ==== first error (top-down) that is not wrapping/foreign/empty
for curr := ee; curr != nil; curr = curr.OriginalError {
if curr.Message != "" && curr.Category != CatWrap && curr.Category != CatForeign {
return curr.Message
}
}
// ==== [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
}
// RecursiveType returns the statuscode to use
// = first error (top-down) that is not wrapping/empty
func (ee *ExErr) RecursiveType() ErrorType {
for curr := ee; curr != nil; curr = curr.OriginalError {
if curr.Type != TypeWrap {
return curr.Type
}
}
// fallback to self
return ee.Type
}
// RecursiveStatuscode returns the HTTP Statuscode to use
// = first error (top-down) that has a statuscode set
func (ee *ExErr) RecursiveStatuscode() *int {
for curr := ee; curr != nil; curr = curr.OriginalError {
if curr.StatusCode != nil {
return langext.Ptr(*curr.StatusCode)
}
}
return nil
}
// RecursiveCategory returns the ErrorCategory to use
// = first error (top-down) that has a statuscode set
func (ee *ExErr) RecursiveCategory() ErrorCategory {
for curr := ee; curr != nil; curr = curr.OriginalError {
if curr.Category != CatWrap {
return curr.Category
}
}
// fallback to <empty>
return ee.Category
}
// RecursiveMeta searches (top-down) for teh first error that has a meta value with teh specified key
// and returns its value (or nil)
func (ee *ExErr) RecursiveMeta(key string) *MetaValue {
for curr := ee; curr != nil; curr = curr.OriginalError {
if metaval, ok := curr.Meta[key]; ok {
return langext.Ptr(metaval)
}
}
return nil
}
// Depth returns the depth of recursively contained errors
func (ee *ExErr) Depth() int {
if ee.OriginalError == nil {
return 1
} else {
return ee.OriginalError.Depth() + 1
}
}
// 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 := []string{ee.UniqueID}
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)
func (ee *ExErr) contains(original *ExErr) (*ExErr, bool) {
if original == nil {
return nil, false
}
if ee == original {
return ee, true
}
for curr := ee; curr != nil; curr = curr.OriginalError {
if curr.equalsDirectProperties(curr) {
return curr, true
}
}
return nil, false
}
// equalsDirectProperties tests if ee and other are equals, but only looks at primary properties (not `OriginalError` or `Meta`)
func (ee *ExErr) equalsDirectProperties(other *ExErr) bool {
if ee.UniqueID != other.UniqueID {
return false
}
if ee.Timestamp != other.Timestamp {
return false
}
if ee.Category != other.Category {
return false
}
if ee.Severity != other.Severity {
return false
}
if ee.Type != other.Type {
return false
}
if ee.StatusCode != other.StatusCode {
return false
}
if ee.Message != other.Message {
return false
}
if ee.WrappedErrType != other.WrappedErrType {
return false
}
if ee.Caller != other.Caller {
return false
}
return true
}
func newID() string {
return xid.New().String()
}

102
exerr/exerr_test.go Normal file
View File

@@ -0,0 +1,102 @@
package exerr
import (
"errors"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/tst"
"os"
"testing"
)
func TestMain(m *testing.M) {
if !Initialized() {
Init(ErrorPackageConfigInit{ZeroLogErrTraces: langext.PFalse, ZeroLogAllTraces: langext.PFalse})
}
os.Exit(m.Run())
}
type golangErr struct {
Message string
}
func (g golangErr) Error() string {
return g.Message
}
type golangErr2 struct {
Message string
}
func (g golangErr2) Error() string {
return g.Message
}
type simpleError struct {
}
func (g simpleError) Error() string {
return "Something simple went wroong"
}
type simpleError2 struct {
}
func (g simpleError2) Error() string {
return "Something simple went wroong"
}
func TestExErrIs1(t *testing.T) {
e0 := simpleError{}
wrap := Wrap(e0, "something went wrong").Str("test", "123").Build()
tst.AssertTrue(t, errors.Is(wrap, simpleError{}))
tst.AssertFalse(t, errors.Is(wrap, golangErr{}))
tst.AssertFalse(t, errors.Is(wrap, golangErr{"error1"}))
}
func TestExErrIs2(t *testing.T) {
e0 := golangErr{"error1"}
wrap := Wrap(e0, "something went wrong").Str("test", "123").Build()
tst.AssertTrue(t, errors.Is(wrap, e0))
tst.AssertTrue(t, errors.Is(wrap, golangErr{"error1"}))
tst.AssertFalse(t, errors.Is(wrap, golangErr{"error2"}))
tst.AssertFalse(t, errors.Is(wrap, simpleError{}))
}
func TestExErrAs(t *testing.T) {
e0 := golangErr{"error1"}
w0 := Wrap(e0, "something went wrong").Str("test", "123").Build()
{
out := golangErr{}
ok := errors.As(w0, &out)
tst.AssertTrue(t, ok)
tst.AssertEqual(t, out.Message, "error1")
}
w1 := Wrap(w0, "outher error").Build()
{
out := golangErr{}
ok := errors.As(w1, &out)
tst.AssertTrue(t, ok)
tst.AssertEqual(t, out.Message, "error1")
}
{
out := golangErr2{}
ok := errors.As(w1, &out)
tst.AssertFalse(t, ok)
}
{
out := simpleError2{}
ok := errors.As(w1, &out)
tst.AssertFalse(t, ok)
}
}

145
exerr/gin.go Normal file
View File

@@ -0,0 +1,145 @@
package exerr
import (
"github.com/gin-gonic/gin"
json "gogs.mikescher.com/BlackForestBytes/goext/gojson"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http"
"time"
)
func (ee *ExErr) toJson(depth int, applyExtendListener bool, outputMeta bool) langext.H {
ginJson := langext.H{}
if ee.UniqueID != "" {
ginJson["id"] = ee.UniqueID
}
if ee.Category != CatWrap {
ginJson["category"] = ee.Category.Category
}
if ee.Type != TypeWrap {
ginJson["type"] = ee.Type.Key
}
if ee.StatusCode != nil {
ginJson["statuscode"] = ee.StatusCode
}
if ee.Message != "" {
ginJson["message"] = ee.Message
}
if ee.Caller != "" {
ginJson["caller"] = ee.Caller
}
if ee.Severity != SevErr {
ginJson["severity"] = ee.Severity.Severity
}
if ee.Timestamp != (time.Time{}) {
ginJson["time"] = ee.Timestamp.Format(time.RFC3339)
}
if ee.WrappedErrType != "" {
ginJson["wrappedErrType"] = ee.WrappedErrType
}
if ee.OriginalError != nil {
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 {
pkgconfig.ExtendGinDataOutput(ee, depth, 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
// (the same object as used in the Output(gin) method)
//
// Parameters:
// - [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`
// - [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{
"errorid": ee.UniqueID,
"message": ee.RecursiveMessage(),
"errorcode": ee.RecursiveType().Key,
"category": ee.RecursiveCategory().Category,
}
if includeWrappedErrors {
apiOutput["__data"] = ee.toJson(0, applyExtendListener, includeMetaFields)
}
for exkey, exval := range ee.Extra {
// ensure we do not override existing values
for {
if _, ok := apiOutput[exkey]; ok {
exkey = "_" + exkey
} else {
break
}
}
apiOutput[exkey] = exval
}
if applyExtendListener {
pkgconfig.ExtendGinOutput(ee, apiOutput)
}
return apiOutput
}
func (ee *ExErr) Output(g *gin.Context) {
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 := ee.ToAPIJson(true, pkgconfig.ExtendedGinOutput, pkgconfig.IncludeMetaInGinOutput)
g.Render(statuscode, json.GoJsonRender{Data: ginOutput, NilSafeSlices: true, NilSafeMaps: true})
}

126
exerr/helper.go Normal file
View File

@@ -0,0 +1,126 @@
package exerr
import "fmt"
// IsType test if the supplied error is of the specified ErrorType.
func IsType(err error, errType ErrorType) bool {
if err == nil {
return false
}
bmerr := FromError(err)
for bmerr != nil {
if bmerr.Type == errType {
return true
}
bmerr = bmerr.OriginalError
}
return false
}
// IsFrom test if the supplied error stems originally from original
func IsFrom(e error, original error) bool {
if e == nil {
return false
}
//goland:noinspection GoDirectComparisonOfErrors
if e == original {
return true
}
bmerr := FromError(e)
for bmerr == nil {
return false
}
for curr := bmerr; curr != nil; curr = curr.OriginalError {
if curr.Category == CatForeign && curr.Message == original.Error() && curr.WrappedErrType == fmt.Sprintf("%T", original) {
return true
}
}
return false
}
// HasSourceMessage tests if the supplied error stems originally from an error with the message msg
func HasSourceMessage(e error, msg string) bool {
if e == nil {
return false
}
bmerr := FromError(e)
for bmerr == nil {
return false
}
for curr := bmerr; curr != nil; curr = curr.OriginalError {
if curr.OriginalError == nil && curr.Message == msg {
return true
}
}
return false
}
func MessageMatch(e error, matcher func(string) bool) bool {
if e == nil {
return false
}
if matcher(e.Error()) {
return true
}
bmerr := FromError(e)
for bmerr == nil {
return false
}
for curr := bmerr; curr != nil; curr = curr.OriginalError {
if matcher(curr.Message) {
return true
}
}
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
}

26
exerr/listener.go Normal file
View File

@@ -0,0 +1,26 @@
package exerr
import (
"sync"
)
type Listener = func(method Method, v *ExErr)
var listenerLock = sync.Mutex{}
var listener = make([]Listener, 0)
func RegisterListener(l Listener) {
listenerLock.Lock()
defer listenerLock.Unlock()
listener = append(listener, l)
}
func (ee *ExErr) CallListener(m Method) {
listenerLock.Lock()
defer listenerLock.Unlock()
for _, v := range listener {
v(m, ee)
}
}

759
exerr/meta.go Normal file
View File

@@ -0,0 +1,759 @@
package exerr
import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/rs/zerolog"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"math"
"strconv"
"strings"
"time"
)
// This is a buffed up map[string]any
// we also save type information of the map-values
// which allows us to deserialize them back into te correct types later
type MetaMap map[string]MetaValue
type metaDataType string
const (
MDTString metaDataType = "String"
MDTStringPtr metaDataType = "StringPtr"
MDTInt metaDataType = "Int"
MDTInt8 metaDataType = "Int8"
MDTInt16 metaDataType = "Int16"
MDTInt32 metaDataType = "Int32"
MDTInt64 metaDataType = "Int64"
MDTFloat32 metaDataType = "Float32"
MDTFloat64 metaDataType = "Float64"
MDTBool metaDataType = "Bool"
MDTBytes metaDataType = "Bytes"
MDTObjectID metaDataType = "ObjectID"
MDTTime metaDataType = "Time"
MDTDuration metaDataType = "Duration"
MDTStringArray metaDataType = "StringArr"
MDTIntArray metaDataType = "IntArr"
MDTInt32Array metaDataType = "Int32Arr"
MDTID metaDataType = "ID"
MDTAny metaDataType = "Interface"
MDTNil metaDataType = "Nil"
MDTEnum metaDataType = "Enum"
)
type MetaValue struct {
DataType metaDataType `json:"dataType"`
Value interface{} `json:"value"`
}
type metaValueSerialization struct {
DataType metaDataType `bson:"dataType"`
Value string `bson:"value"`
Raw interface{} `bson:"raw"`
}
func (v MetaValue) SerializeValue() (string, error) {
switch v.DataType {
case MDTString:
return v.Value.(string), nil
case MDTID:
return v.Value.(IDWrap).Serialize(), nil
case MDTAny:
return v.Value.(AnyWrap).Serialize(), nil
case MDTStringPtr:
if langext.IsNil(v.Value) {
return "#", nil
}
r := v.Value.(*string)
if r != nil {
return "*" + *r, nil
} else {
return "#", nil
}
case MDTInt:
return strconv.Itoa(v.Value.(int)), nil
case MDTInt8:
return strconv.FormatInt(int64(v.Value.(int8)), 10), nil
case MDTInt16:
return strconv.FormatInt(int64(v.Value.(int16)), 10), nil
case MDTInt32:
return strconv.FormatInt(int64(v.Value.(int32)), 10), nil
case MDTInt64:
return strconv.FormatInt(v.Value.(int64), 10), nil
case MDTFloat32:
return strconv.FormatFloat(float64(v.Value.(float32)), 'X', -1, 32), nil
case MDTFloat64:
return strconv.FormatFloat(v.Value.(float64), 'X', -1, 64), nil
case MDTBool:
if v.Value.(bool) {
return "true", nil
} else {
return "false", nil
}
case MDTBytes:
return hex.EncodeToString(v.Value.([]byte)), nil
case MDTObjectID:
return v.Value.(primitive.ObjectID).Hex(), nil
case MDTTime:
return strconv.FormatInt(v.Value.(time.Time).Unix(), 10) + "|" + strconv.FormatInt(int64(v.Value.(time.Time).Nanosecond()), 10), nil
case MDTDuration:
return v.Value.(time.Duration).String(), nil
case MDTStringArray:
if langext.IsNil(v.Value) {
return "#", nil
}
r, err := json.Marshal(v.Value.([]string))
if err != nil {
return "", err
}
return string(r), nil
case MDTIntArray:
if langext.IsNil(v.Value) {
return "#", nil
}
r, err := json.Marshal(v.Value.([]int))
if err != nil {
return "", err
}
return string(r), nil
case MDTInt32Array:
if langext.IsNil(v.Value) {
return "#", nil
}
r, err := json.Marshal(v.Value.([]int32))
if err != nil {
return "", err
}
return string(r), nil
case MDTNil:
return "", nil
case MDTEnum:
return v.Value.(EnumWrap).Serialize(), nil
}
return "", errors.New("Unknown type: " + string(v.DataType))
}
func (v MetaValue) ShortString(lim int) string {
switch v.DataType {
case MDTString:
r := strings.ReplaceAll(v.Value.(string), "\r", "")
r = strings.ReplaceAll(r, "\n", "\\n")
r = strings.ReplaceAll(r, "\t", "\\t")
return langext.StrLimit(r, lim, "...")
case MDTID:
return v.Value.(IDWrap).String()
case MDTAny:
return v.Value.(AnyWrap).String()
case MDTStringPtr:
if langext.IsNil(v.Value) {
return "<<null>>"
}
r := langext.CoalesceString(v.Value.(*string), "<<null>>")
r = strings.ReplaceAll(r, "\r", "")
r = strings.ReplaceAll(r, "\n", "\\n")
r = strings.ReplaceAll(r, "\t", "\\t")
return langext.StrLimit(r, lim, "...")
case MDTInt:
return strconv.Itoa(v.Value.(int))
case MDTInt8:
return strconv.FormatInt(int64(v.Value.(int8)), 10)
case MDTInt16:
return strconv.FormatInt(int64(v.Value.(int16)), 10)
case MDTInt32:
return strconv.FormatInt(int64(v.Value.(int32)), 10)
case MDTInt64:
return strconv.FormatInt(v.Value.(int64), 10)
case MDTFloat32:
return strconv.FormatFloat(float64(v.Value.(float32)), 'g', 4, 32)
case MDTFloat64:
return strconv.FormatFloat(v.Value.(float64), 'g', 4, 64)
case MDTBool:
return fmt.Sprintf("%v", v.Value.(bool))
case MDTBytes:
return langext.StrLimit(hex.EncodeToString(v.Value.([]byte)), lim, "...")
case MDTObjectID:
return v.Value.(primitive.ObjectID).Hex()
case MDTTime:
return v.Value.(time.Time).Format(time.RFC3339)
case MDTDuration:
return v.Value.(time.Duration).String()
case MDTStringArray:
if langext.IsNil(v.Value) {
return "<<null>>"
}
r, err := json.Marshal(v.Value.([]string))
if err != nil {
return "(err)"
}
return langext.StrLimit(string(r), lim, "...")
case MDTIntArray:
if langext.IsNil(v.Value) {
return "<<null>>"
}
r, err := json.Marshal(v.Value.([]int))
if err != nil {
return "(err)"
}
return langext.StrLimit(string(r), lim, "...")
case MDTInt32Array:
if langext.IsNil(v.Value) {
return "<<null>>"
}
r, err := json.Marshal(v.Value.([]int32))
if err != nil {
return "(err)"
}
return langext.StrLimit(string(r), lim, "...")
case MDTNil:
return "<<null>>"
case MDTEnum:
return v.Value.(EnumWrap).String()
}
return "(err)"
}
func (v MetaValue) Apply(key string, evt *zerolog.Event, limitLen *int) *zerolog.Event {
switch v.DataType {
case MDTString:
if limitLen == nil {
return evt.Str(key, v.Value.(string))
} else {
return evt.Str(key, langext.StrLimit(v.Value.(string), *limitLen, "..."))
}
case MDTID:
return evt.Str(key, v.Value.(IDWrap).Value)
case MDTAny:
if v.Value.(AnyWrap).IsError {
return evt.Str(key, "(err)")
} else {
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:
if langext.IsNil(v.Value) {
return evt.Str(key, "<<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:
return evt.Int(key, v.Value.(int))
case MDTInt8:
return evt.Int8(key, v.Value.(int8))
case MDTInt16:
return evt.Int16(key, v.Value.(int16))
case MDTInt32:
return evt.Int32(key, v.Value.(int32))
case MDTInt64:
return evt.Int64(key, v.Value.(int64))
case MDTFloat32:
return evt.Float32(key, v.Value.(float32))
case MDTFloat64:
return evt.Float64(key, v.Value.(float64))
case MDTBool:
return evt.Bool(key, v.Value.(bool))
case MDTBytes:
return evt.Bytes(key, v.Value.([]byte))
case MDTObjectID:
return evt.Str(key, v.Value.(primitive.ObjectID).Hex())
case MDTTime:
return evt.Time(key, v.Value.(time.Time))
case MDTDuration:
return evt.Dur(key, v.Value.(time.Duration))
case MDTStringArray:
if langext.IsNil(v.Value) {
return evt.Strs(key, nil)
}
return evt.Strs(key, v.Value.([]string))
case MDTIntArray:
if langext.IsNil(v.Value) {
return evt.Ints(key, nil)
}
return evt.Ints(key, v.Value.([]int))
case MDTInt32Array:
if langext.IsNil(v.Value) {
return evt.Ints32(key, nil)
}
return evt.Ints32(key, v.Value.([]int32))
case MDTNil:
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)")
}
func (v MetaValue) MarshalJSON() ([]byte, error) {
str, err := v.SerializeValue()
if err != nil {
return nil, err
}
return json.Marshal(string(v.DataType) + ":" + str)
}
func (v *MetaValue) UnmarshalJSON(data []byte) error {
var str = ""
err := json.Unmarshal(data, &str)
if err != nil {
return err
}
split := strings.SplitN(str, ":", 2)
if len(split) != 2 {
return errors.New("failed to decode MetaValue: '" + str + "'")
}
return v.Deserialize(split[1], metaDataType(split[0]))
}
func (v MetaValue) MarshalBSON() ([]byte, error) {
serval, err := v.SerializeValue()
if err != nil {
return nil, Wrap(err, "failed to bson-marshal MetaValue (serialize)").Build()
}
// this is an kinda ugly hack - but serialization to mongodb and back can loose the correct type information....
bin, err := bson.Marshal(metaValueSerialization{
DataType: v.DataType,
Value: serval,
Raw: v.Value,
})
if err != nil {
return nil, Wrap(err, "failed to bson-marshal MetaValue (marshal)").Build()
}
return bin, nil
}
func (v *MetaValue) UnmarshalBSON(bytes []byte) error {
var serval metaValueSerialization
err := bson.Unmarshal(bytes, &serval)
if err != nil {
return Wrap(err, "failed to bson-unmarshal MetaValue (unmarshal)").Build()
}
err = v.Deserialize(serval.Value, serval.DataType)
if err != nil {
return Wrap(err, "failed to deserialize MetaValue from bson").Str("raw", serval.Value).Build()
}
return nil
}
func (v *MetaValue) Deserialize(value string, datatype metaDataType) error {
switch datatype {
case MDTString:
v.Value = value
v.DataType = datatype
return nil
case MDTID:
v.Value = deserializeIDWrap(value)
v.DataType = datatype
return nil
case MDTAny:
v.Value = deserializeAnyWrap(value)
v.DataType = datatype
return nil
case MDTStringPtr:
if len(value) <= 0 || (value[0] != '*' && value[0] != '#') {
return errors.New("Invalid StringPtr: " + value)
} else if value == "#" {
v.Value = nil
v.DataType = datatype
return nil
} else {
v.Value = langext.Ptr(value[1:])
v.DataType = datatype
return nil
}
case MDTInt:
pv, err := strconv.ParseInt(value, 10, 0)
if err != nil {
return err
}
v.Value = int(pv)
v.DataType = datatype
return nil
case MDTInt8:
pv, err := strconv.ParseInt(value, 10, 8)
if err != nil {
return err
}
v.Value = int8(pv)
v.DataType = datatype
return nil
case MDTInt16:
pv, err := strconv.ParseInt(value, 10, 16)
if err != nil {
return err
}
v.Value = int16(pv)
v.DataType = datatype
return nil
case MDTInt32:
pv, err := strconv.ParseInt(value, 10, 32)
if err != nil {
return err
}
v.Value = int32(pv)
v.DataType = datatype
return nil
case MDTInt64:
pv, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return err
}
v.Value = pv
v.DataType = datatype
return nil
case MDTFloat32:
pv, err := strconv.ParseFloat(value, 64)
if err != nil {
return err
}
v.Value = float32(pv)
v.DataType = datatype
return nil
case MDTFloat64:
pv, err := strconv.ParseFloat(value, 64)
if err != nil {
return err
}
v.Value = pv
v.DataType = datatype
return nil
case MDTBool:
if value == "true" {
v.Value = true
v.DataType = datatype
return nil
}
if value == "false" {
v.Value = false
v.DataType = datatype
return nil
}
return errors.New("invalid bool value: " + value)
case MDTBytes:
r, err := hex.DecodeString(value)
if err != nil {
return err
}
v.Value = r
v.DataType = datatype
return nil
case MDTObjectID:
r, err := primitive.ObjectIDFromHex(value)
if err != nil {
return err
}
v.Value = r
v.DataType = datatype
return nil
case MDTTime:
ps := strings.Split(value, "|")
if len(ps) != 2 {
return errors.New("invalid time.time: " + value)
}
p1, err := strconv.ParseInt(ps[0], 10, 64)
if err != nil {
return err
}
p2, err := strconv.ParseInt(ps[1], 10, 32)
if err != nil {
return err
}
v.Value = time.Unix(p1, p2)
v.DataType = datatype
return nil
case MDTDuration:
r, err := time.ParseDuration(value)
if err != nil {
return err
}
v.Value = r
v.DataType = datatype
return nil
case MDTStringArray:
if value == "#" {
v.Value = nil
v.DataType = datatype
return nil
}
pj := make([]string, 0)
err := json.Unmarshal([]byte(value), &pj)
if err != nil {
return err
}
v.Value = pj
v.DataType = datatype
return nil
case MDTIntArray:
if value == "#" {
v.Value = nil
v.DataType = datatype
return nil
}
pj := make([]int, 0)
err := json.Unmarshal([]byte(value), &pj)
if err != nil {
return err
}
v.Value = pj
v.DataType = datatype
return nil
case MDTInt32Array:
if value == "#" {
v.Value = nil
v.DataType = datatype
return nil
}
pj := make([]int32, 0)
err := json.Unmarshal([]byte(value), &pj)
if err != nil {
return err
}
v.Value = pj
v.DataType = datatype
return nil
case MDTNil:
v.Value = nil
v.DataType = datatype
return nil
case MDTEnum:
v.Value = deserializeEnumWrap(value)
v.DataType = datatype
return nil
}
return errors.New("Unknown type: " + string(datatype))
}
func (v MetaValue) ValueString() string {
switch v.DataType {
case MDTString:
return v.Value.(string)
case MDTID:
return v.Value.(IDWrap).String()
case MDTAny:
return v.Value.(AnyWrap).String()
case MDTStringPtr:
if langext.IsNil(v.Value) {
return "<<null>>"
}
return langext.CoalesceString(v.Value.(*string), "<<null>>")
case MDTInt:
return strconv.Itoa(v.Value.(int))
case MDTInt8:
return strconv.FormatInt(int64(v.Value.(int8)), 10)
case MDTInt16:
return strconv.FormatInt(int64(v.Value.(int16)), 10)
case MDTInt32:
return strconv.FormatInt(int64(v.Value.(int32)), 10)
case MDTInt64:
return strconv.FormatInt(v.Value.(int64), 10)
case MDTFloat32:
return strconv.FormatFloat(float64(v.Value.(float32)), 'g', 4, 32)
case MDTFloat64:
return strconv.FormatFloat(v.Value.(float64), 'g', 4, 64)
case MDTBool:
return fmt.Sprintf("%v", v.Value.(bool))
case MDTBytes:
return hex.EncodeToString(v.Value.([]byte))
case MDTObjectID:
return v.Value.(primitive.ObjectID).Hex()
case MDTTime:
return v.Value.(time.Time).Format(time.RFC3339Nano)
case MDTDuration:
return v.Value.(time.Duration).String()
case MDTStringArray:
if langext.IsNil(v.Value) {
return "<<null>>"
}
r, err := json.MarshalIndent(v.Value.([]string), "", " ")
if err != nil {
return "(err)"
}
return string(r)
case MDTIntArray:
if langext.IsNil(v.Value) {
return "<<null>>"
}
r, err := json.MarshalIndent(v.Value.([]int), "", " ")
if err != nil {
return "(err)"
}
return string(r)
case MDTInt32Array:
if langext.IsNil(v.Value) {
return "<<null>>"
}
r, err := json.MarshalIndent(v.Value.([]int32), "", " ")
if err != nil {
return "(err)"
}
return string(r)
case MDTNil:
return "<<null>>"
case MDTEnum:
return v.Value.(EnumWrap).String()
}
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 {
r := ""
i := 0
for key, val := range mm {
if i > 0 {
r += ", "
}
r += "\"" + key + "\""
r += ": "
r += "\"" + val.ShortString(singleMaxLen) + "\""
i++
}
return r
}
func (mm MetaMap) FormatMultiLine(indentFront string, indentKeys string, maxLenValue int) string {
r := ""
r += indentFront + "{" + "\n"
for key, val := range mm {
if key == "gin.body" {
continue
}
r += indentFront
r += indentKeys
r += "\"" + key + "\""
r += ": "
r += "\"" + val.ShortString(maxLenValue) + "\""
r += ",\n"
}
r += indentFront + "}"
return r
}
func (mm MetaMap) Any() bool {
return len(mm) > 0
}
func (mm MetaMap) Apply(evt *zerolog.Event, limitLen *int) *zerolog.Event {
for key, val := range mm {
evt = val.Apply(key, evt, limitLen)
}
return evt
}
func (mm MetaMap) add(key string, mdtype metaDataType, val interface{}) {
if _, ok := mm[key]; !ok {
mm[key] = MetaValue{DataType: mdtype, Value: val}
return
}
for i := 2; ; i++ {
realkey := key + "-" + strconv.Itoa(i)
if _, ok := mm[realkey]; !ok {
mm[realkey] = MetaValue{DataType: mdtype, Value: val}
return
}
}
}

13
exerr/proxy.go Normal file
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
}

14
exerr/stacktrace.go Normal file
View File

@@ -0,0 +1,14 @@
package exerr
import (
"fmt"
"runtime"
)
func callername(skip int) string {
pc := make([]uintptr, 15)
n := runtime.Callers(skip+2, pc)
frames := runtime.CallersFrames(pc[:n])
frame, _ := frames.Next()
return fmt.Sprintf("%s:%d %s", frame.File, frame.Line, frame.Function)
}

189
exerr/typeWrapper.go Normal file
View File

@@ -0,0 +1,189 @@
package exerr
import (
"encoding/json"
"fmt"
"github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/enums"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"strings"
)
//
// These are wrapper objects, because for some metadata-types we need to serialize a bit more complex data
// (eg thy actual type for ID objects, or the json representation for any types)
//
type IDWrap struct {
Type string
Value string
IsNil bool
}
func newIDWrap(val fmt.Stringer) IDWrap {
t := fmt.Sprintf("%T", val)
arr := strings.Split(t, ".")
if len(arr) > 0 {
t = arr[len(arr)-1]
}
if langext.IsNil(val) {
return IDWrap{Type: t, Value: "", IsNil: true}
}
v := val.String()
return IDWrap{Type: t, Value: v, IsNil: false}
}
func (w IDWrap) Serialize() string {
if w.IsNil {
return "!nil" + ":" + w.Type
}
return w.Type + ":" + w.Value
}
func (w IDWrap) String() string {
if w.IsNil {
return w.Type + "<<nil>>"
}
return w.Type + "(" + w.Value + ")"
}
func deserializeIDWrap(v string) IDWrap {
r := strings.SplitN(v, ":", 2)
if len(r) == 2 && r[0] == "!nil" {
return IDWrap{Type: r[1], Value: v, IsNil: true}
}
if len(r) == 0 {
return IDWrap{}
} else if len(r) == 1 {
return IDWrap{Type: "", Value: v, IsNil: false}
} else {
return IDWrap{Type: r[0], Value: r[1], IsNil: false}
}
}
type AnyWrap struct {
Type string
Json string
IsError bool
IsNil bool
}
func newAnyWrap(val any) (result AnyWrap) {
result = AnyWrap{Type: "", Json: "", IsError: true, IsNil: false} // ensure a return in case of recover()
defer func() {
if err := recover(); err != nil {
// send error should never crash our program
log.Error().Interface("err", err).Msg("Panic while trying to marshal anywrap ( bmerror.Interface )")
}
}()
t := fmt.Sprintf("%T", val)
if langext.IsNil(val) {
return AnyWrap{Type: t, Json: "", IsError: false, IsNil: true}
}
j, err := json.Marshal(val)
if err == nil {
return AnyWrap{Type: t, Json: string(j), IsError: false, IsNil: false}
} else {
return AnyWrap{Type: t, Json: "", IsError: true, IsNil: false}
}
}
func (w AnyWrap) Serialize() string {
if w.IsError {
return "ERR" + ":" + w.Type + ":" + w.Json
} else if w.IsNil {
return "NIL" + ":" + w.Type + ":" + w.Json
} else {
return "OK" + ":" + w.Type + ":" + w.Json
}
}
func (w AnyWrap) String() string {
if w.IsError {
return "(error)"
} else if w.IsNil {
return "(nil)"
} else {
return w.Json
}
}
func deserializeAnyWrap(v string) AnyWrap {
r := strings.SplitN(v, ":", 3)
if len(r) != 3 {
return AnyWrap{IsError: true, Type: "", Json: "", IsNil: false}
} else {
if r[0] == "OK" {
return AnyWrap{IsError: false, Type: r[1], Json: r[2], IsNil: false}
} else if r[0] == "ERR" {
return AnyWrap{IsError: true, Type: r[1], Json: r[2], IsNil: false}
} else if r[0] == "NIL" {
return AnyWrap{IsError: false, Type: r[1], Json: "", IsNil: true}
} else {
return AnyWrap{IsError: true, Type: "", Json: "", IsNil: false}
}
}
}
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
fsext/exists.go Normal file
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
}

59
ginext/appContext.go Normal file
View File

@@ -0,0 +1,59 @@
package ginext
import (
"context"
"github.com/gin-gonic/gin"
"time"
)
type AppContext struct {
inner context.Context
cancelFunc context.CancelFunc
cancelled bool
GinContext *gin.Context
}
func CreateAppContext(g *gin.Context, innerCtx context.Context, cancelFn context.CancelFunc) *AppContext {
for key, value := range g.Keys {
innerCtx = context.WithValue(innerCtx, key, value)
}
return &AppContext{
inner: innerCtx,
cancelFunc: cancelFn,
cancelled: false,
GinContext: g,
}
}
func (ac *AppContext) Deadline() (deadline time.Time, ok bool) {
return ac.inner.Deadline()
}
func (ac *AppContext) Done() <-chan struct{} {
return ac.inner.Done()
}
func (ac *AppContext) Err() error {
return ac.inner.Err()
}
func (ac *AppContext) Value(key any) any {
return ac.inner.Value(key)
}
func (ac *AppContext) Set(key, value any) {
ac.inner = context.WithValue(ac.inner, key, value)
}
func (ac *AppContext) Cancel() {
ac.cancelled = true
ac.cancelFunc()
}
func (ac *AppContext) RequestURI() string {
if ac.GinContext != nil && ac.GinContext.Request != nil {
return ac.GinContext.Request.Method + " :: " + ac.GinContext.Request.RequestURI
} else {
return ""
}
}

23
ginext/commonHandler.go Normal file
View File

@@ -0,0 +1,23 @@
package ginext
import (
"net/http"
)
func RedirectFound(newuri string) WHandlerFunc {
return func(pctx PreContext) HTTPResponse {
return Redirect(http.StatusFound, newuri)
}
}
func RedirectTemporary(newuri string) WHandlerFunc {
return func(pctx PreContext) HTTPResponse {
return Redirect(http.StatusTemporaryRedirect, newuri)
}
}
func RedirectPermanent(newuri string) WHandlerFunc {
return func(pctx PreContext) HTTPResponse {
return Redirect(http.StatusPermanentRedirect, newuri)
}
}

View File

@@ -0,0 +1,12 @@
package ginext
import (
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
)
func BodyBuffer(g *gin.Context) {
if g.Request.Body != nil {
g.Request.Body = dataext.NewBufferedReadCloser(g.Request.Body)
}
}

25
ginext/cors.go Normal file
View File

@@ -0,0 +1,25 @@
package ginext
import (
"github.com/gin-gonic/gin"
"net/http"
"strings"
)
func CorsMiddleware(allowheader []string, exposeheader []string) gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
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")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusOK)
} else {
c.Next()
}
}
}

238
ginext/engine.go Normal file
View File

@@ -0,0 +1,238 @@
package ginext
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/mathext"
"gogs.mikescher.com/BlackForestBytes/goext/rext"
"net"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"time"
)
type GinWrapper struct {
engine *gin.Engine
suppressGinLogs bool
opt Options
allowCors bool
corsAllowHeader []string
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
}
type ginRouteSpec struct {
Method string
URL string
Middlewares []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
func NewEngine(opt Options) *GinWrapper {
ginDebug := langext.Coalesce(opt.GinDebug, true)
if ginDebug {
gin.SetMode(gin.DebugMode)
// 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()
wrapper := &GinWrapper{
engine: engine,
opt: opt,
suppressGinLogs: langext.Coalesce(opt.SuppressGinLogs, false),
allowCors: langext.Coalesce(opt.AllowCors, false),
corsAllowHeader: langext.Coalesce(opt.CorsAllowHeader, []string{"Content-Type", "Content-Length", "Accept-Encoding", "X-CSRF-Token", "Authorization", "accept", "origin", "Cache-Control", "X-Requested-With"}),
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.RedirectTrailingSlash = false
if wrapper.allowCors {
engine.Use(CorsMiddleware(wrapper.corsAllowHeader, wrapper.corsExposeHeader))
}
if ginDebug && !wrapper.suppressGinLogs {
ginlogger := gin.Logger()
engine.Use(func(context *gin.Context) { ginlogger(context) })
}
return wrapper
}
func (w *GinWrapper) ListenAndServeHTTP(addr string, postInit func(port string)) (chan error, *http.Server) {
w.DebugPrintRoutes()
httpserver := &http.Server{
Addr: addr,
Handler: w.engine,
}
errChan := make(chan error)
go func() {
ln, err := net.Listen("tcp", httpserver.Addr)
if err != nil {
errChan <- err
return
}
_, port, err := net.SplitHostPort(ln.Addr().String())
if err != nil {
errChan <- err
return
}
log.Info().Str("address", httpserver.Addr).Msg("HTTP-Server started on http://localhost:" + port)
if postInit != nil {
postInit(port) // the net.Listener a few lines above is at this point actually already buffering requests
}
errChan <- httpserver.Serve(ln)
}()
return errChan, httpserver
}
func (w *GinWrapper) DebugPrintRoutes() {
if !w.ginDebug {
return
}
lines := make([][4]string, 0)
pad := [4]int{0, 0, 0, 0}
for _, spec := range w.routeSpecs {
line := [4]string{
spec.Method,
spec.URL,
strings.Join(langext.ArrMap(spec.Middlewares, w.cleanMiddlewareName), " -> "),
w.cleanMiddlewareName(spec.Handler),
}
lines = append(lines, line)
pad[0] = mathext.Max(pad[0], len(line[0]))
pad[1] = mathext.Max(pad[1], len(line[1]))
pad[2] = mathext.Max(pad[2], len(line[2]))
pad[3] = mathext.Max(pad[3], len(line[3]))
}
fmt.Printf("Gin-Routes:\n")
fmt.Printf("{\n")
for _, line := range lines {
fmt.Printf(" %s %s --> %s --> %s\n",
langext.StrPadRight("["+line[0]+"]", " ", pad[0]+2),
langext.StrPadRight(line[1], " ", pad[1]),
langext.StrPadRight(line[2], " ", pad[2]),
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)
}

60
ginext/funcWrapper.go Normal file
View File

@@ -0,0 +1,60 @@
package ginext
import (
"fmt"
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
)
type WHandlerFunc func(PreContext) HTTPResponse
func Wrap(w *GinWrapper, fn WHandlerFunc) gin.HandlerFunc {
return func(g *gin.Context) {
reqctx := g.Request.Context()
pctx := PreContext{
wrapper: w,
ginCtx: g,
persistantData: &preContextData{},
}
for _, lstr := range w.listenerBeforeRequest {
lstr(g)
}
wrap, stackTrace, panicObj := callPanicSafe(fn, pctx)
if panicObj != nil {
fmt.Printf("\n======== ======== STACKTRACE ======== ========\n%s\n======== ======== ======== ========\n\n", stackTrace)
err := exerr.
New(exerr.TypePanic, "Panic occured (in gin handler)").
Any("panicObj", panicObj).
Str("trace", stackTrace).
Build()
wrap = Error(err)
}
if g.Writer.Written() {
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 reqctx.Err() == nil {
wrap.Write(g)
}
}
}

9
ginext/jsonFilter.go Normal file
View File

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

211
ginext/preContext.go Normal file
View File

@@ -0,0 +1,211 @@
package ginext
import (
"bytes"
"context"
"fmt"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"io"
"runtime/debug"
"time"
)
type PreContext struct {
ginCtx *gin.Context
wrapper *GinWrapper
uri any
query any
body any
rawbody *[]byte
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 {
pctx.uri = uri
return pctx
}
func (pctx *PreContext) Query(query any) *PreContext {
pctx.query = query
return pctx
}
func (pctx *PreContext) Body(body any) *PreContext {
pctx.body = body
return pctx
}
func (pctx *PreContext) RawBody(rawbody *[]byte) *PreContext {
pctx.rawbody = rawbody
return pctx
}
func (pctx *PreContext) Form(form any) *PreContext {
pctx.form = form
return pctx
}
func (pctx *PreContext) Header(header any) *PreContext {
pctx.header = header
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) {
if pctx.uri != nil {
if err := pctx.ginCtx.ShouldBindUri(pctx.uri); err != nil {
err = exerr.Wrap(err, "Failed to read uri").
WithType(exerr.TypeBindFailURI).
Str("struct_type", fmt.Sprintf("%T", pctx.uri)).
Build()
return nil, nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "URI", err))
}
}
if pctx.query != nil {
if err := pctx.ginCtx.ShouldBindQuery(pctx.query); err != nil {
err = exerr.Wrap(err, "Failed to read query").
WithType(exerr.TypeBindFailQuery).
Str("struct_type", fmt.Sprintf("%T", pctx.query)).
Build()
return nil, nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "QUERY", err))
}
}
if pctx.body != nil {
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 nil, nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "JSON", err))
}
}
if err := pctx.ginCtx.ShouldBindJSON(pctx.body); err != nil {
err = exerr.Wrap(err, "Failed to read json-body").
WithType(exerr.TypeBindFailJSON).
Str("struct_type", fmt.Sprintf("%T", pctx.body)).
Build()
return nil, nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "JSON", err))
}
} else {
if !pctx.ignoreWrongContentType {
err := exerr.New(exerr.TypeBindFailJSON, "missing JSON body").
Str("struct_type", fmt.Sprintf("%T", pctx.body)).
Build()
return nil, 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 nil, 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 nil, nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "BODY", err))
}
*pctx.rawbody = buf.Bytes()
}
}
if pctx.form != nil {
if pctx.ginCtx.ContentType() == "multipart/form-data" {
if err := pctx.ginCtx.ShouldBindWith(pctx.form, binding.Form); err != nil {
err = exerr.Wrap(err, "Failed to read multipart-form").
WithType(exerr.TypeBindFailFormData).
Str("struct_type", fmt.Sprintf("%T", pctx.form)).
Build()
return nil, 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 nil, nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "FORM", err))
}
} else {
if !pctx.ignoreWrongContentType {
err := exerr.New(exerr.TypeBindFailFormData, "missing form body").
Str("struct_type", fmt.Sprintf("%T", pctx.form)).
Build()
return nil, nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "FORM", err))
}
}
}
if pctx.header != nil {
if err := pctx.ginCtx.ShouldBindHeader(pctx.header); err != nil {
err = exerr.Wrap(err, "Failed to read header").
WithType(exerr.TypeBindFailHeader).
Str("struct_type", fmt.Sprintf("%T", pctx.query)).
Build()
return nil, nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "HEADER", err))
}
}
ictx, cancel := context.WithTimeout(context.Background(), langext.Coalesce(pctx.timeout, pctx.wrapper.requestTimeout))
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 nil, nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "INIT", err))
}
}
return actx, pctx.ginCtx, nil
}
func callPanicSafe(fn WHandlerFunc, pctx PreContext) (res HTTPResponse, stackTrace string, panicObj any) {
defer func() {
if rec := recover(); rec != nil {
res = nil
stackTrace = string(debug.Stack())
panicObj = rec
}
}()
res = fn(pctx)
return res, "", nil
}

47
ginext/response.go Normal file
View File

@@ -0,0 +1,47 @@
package ginext
import (
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
)
type cookieval struct {
name string
value string
maxAge int
path string
domain string
secure bool
httpOnly bool
}
type headerval struct {
Key string
Val string
}
type HTTPResponse interface {
Write(g *gin.Context)
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 InspectableHTTPResponse interface {
HTTPResponse
Statuscode() int
BodyString(g *gin.Context) *string
ContentType() string
Headers() []string
}
type HTTPErrorResponse interface {
HTTPResponse
Error() error
}
func NotImplemented() HTTPResponse {
return Error(exerr.New(exerr.TypeNotImplemented, "").Build())
}

58
ginext/responseData.go Normal file
View File

@@ -0,0 +1,58 @@
package ginext
import (
"github.com/gin-gonic/gin"
"gogs.mikescher.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}
}

View File

@@ -0,0 +1,64 @@
package ginext
import (
"fmt"
"github.com/gin-gonic/gin"
"gogs.mikescher.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
ginext/responseEmpty.go Normal file
View File

@@ -0,0 +1,56 @@
package ginext
import (
"github.com/gin-gonic/gin"
"gogs.mikescher.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
ginext/responseFile.go Normal file
View File

@@ -0,0 +1,73 @@
package ginext
import (
"fmt"
"github.com/gin-gonic/gin"
"gogs.mikescher.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
ginext/responseJson.go Normal file
View File

@@ -0,0 +1,78 @@
package ginext
import (
"github.com/gin-gonic/gin"
json "gogs.mikescher.com/BlackForestBytes/goext/gojson"
"gogs.mikescher.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
ginext/responseJsonAPI.go Normal file
View File

@@ -0,0 +1,81 @@
package ginext
import (
"context"
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
"gogs.mikescher.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)
}
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()),
}
}

View File

@@ -0,0 +1,57 @@
package ginext
import (
"github.com/gin-gonic/gin"
"gogs.mikescher.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}
}

View File

@@ -0,0 +1,72 @@
package ginext
import (
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
"gogs.mikescher.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
ginext/responseText.go Normal file
View File

@@ -0,0 +1,57 @@
package ginext
import (
"github.com/gin-gonic/gin"
"gogs.mikescher.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}
}

211
ginext/routes.go Normal file
View File

@@ -0,0 +1,211 @@
package ginext
import (
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http"
"path"
"reflect"
"runtime"
"strings"
)
var anyMethods = []string{
http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch,
http.MethodHead, http.MethodOptions, http.MethodDelete, http.MethodConnect,
http.MethodTrace,
}
type GinRoutesWrapper struct {
wrapper *GinWrapper
routes gin.IRouter
absPath string
defaultHandler []gin.HandlerFunc
}
type GinRouteBuilder struct {
routes *GinRoutesWrapper
method string
relPath string
absPath string
handlers []gin.HandlerFunc
}
func (w *GinWrapper) Routes() *GinRoutesWrapper {
return &GinRoutesWrapper{
wrapper: w,
routes: w.engine,
absPath: "",
defaultHandler: make([]gin.HandlerFunc, 0),
}
}
func (w *GinRoutesWrapper) Group(relativePath string) *GinRoutesWrapper {
return &GinRoutesWrapper{
wrapper: w.wrapper,
routes: w.routes.Group(relativePath),
defaultHandler: langext.ArrCopy(w.defaultHandler),
absPath: joinPaths(w.absPath, relativePath),
}
}
func (w *GinRoutesWrapper) Use(middleware ...gin.HandlerFunc) *GinRoutesWrapper {
defHandler := langext.ArrCopy(w.defaultHandler)
defHandler = append(defHandler, middleware...)
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 {
return w._route(http.MethodGet, relativePath)
}
func (w *GinRoutesWrapper) POST(relativePath string) *GinRouteBuilder {
return w._route(http.MethodPost, relativePath)
}
func (w *GinRoutesWrapper) DELETE(relativePath string) *GinRouteBuilder {
return w._route(http.MethodDelete, relativePath)
}
func (w *GinRoutesWrapper) PATCH(relativePath string) *GinRouteBuilder {
return w._route(http.MethodPatch, relativePath)
}
func (w *GinRoutesWrapper) PUT(relativePath string) *GinRouteBuilder {
return w._route(http.MethodPut, relativePath)
}
func (w *GinRoutesWrapper) OPTIONS(relativePath string) *GinRouteBuilder {
return w._route(http.MethodOptions, relativePath)
}
func (w *GinRoutesWrapper) HEAD(relativePath string) *GinRouteBuilder {
return w._route(http.MethodHead, relativePath)
}
func (w *GinRoutesWrapper) COUNT(relativePath string) *GinRouteBuilder {
return w._route("COUNT", relativePath)
}
func (w *GinRoutesWrapper) Any(relativePath string) *GinRouteBuilder {
return w._route("*", relativePath)
}
func (w *GinRoutesWrapper) _route(method string, relativePath string) *GinRouteBuilder {
return &GinRouteBuilder{
routes: w,
method: method,
relPath: relativePath,
absPath: joinPaths(w.absPath, relativePath),
handlers: langext.ArrCopy(w.defaultHandler),
}
}
func (w *GinRouteBuilder) Use(middleware ...gin.HandlerFunc) *GinRouteBuilder {
w.handlers = append(w.handlers, middleware...)
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) {
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) })
handlerName := nameOfFunction(handler)
w.handlers = append(w.handlers, Wrap(w.routes.wrapper, handler))
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: handlerName,
})
}
func (w *GinWrapper) NoRoute(handler WHandlerFunc) {
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{
Method: "ANY",
URL: "[NO_ROUTE]",
Middlewares: middlewareNames,
Handler: handlerName,
})
}
func nameOfFunction(f any) string {
fname := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
split := strings.Split(fname, "/")
if len(split) == 0 {
return ""
}
fname = split[len(split)-1]
// https://stackoverflow.com/a/32925345/1761622
if strings.HasSuffix(fname, "-fm") {
fname = fname[:len(fname)-len("-fm")]
}
return fname
}
// joinPaths is copied verbatim from gin@v1.9.1/gin.go
func joinPaths(absolutePath, relativePath string) string {
if relativePath == "" {
return absolutePath
}
finalPath := path.Join(absolutePath, relativePath)
if lastChar(relativePath) == '/' && lastChar(finalPath) != '/' {
return finalPath + "/"
}
return finalPath
}
func lastChar(str string) uint8 {
if str == "" {
panic("The length of the string can't be 0")
}
return str[len(str)-1]
}

11
ginext/session.go Normal file
View File

@@ -0,0 +1,11 @@
package ginext
import (
"context"
"github.com/gin-gonic/gin"
)
type SessionObject interface {
Init(g *gin.Context, ctx *AppContext) error
Finish(ctx context.Context, resp HTTPResponse) error
}

62
go.mod
View File

@@ -1,14 +1,64 @@
module gogs.mikescher.com/BlackForestBytes/goext module gogs.mikescher.com/BlackForestBytes/goext
go 1.19 go 1.23
require ( require (
golang.org/x/sys v0.3.0 github.com/gin-gonic/gin v1.10.0
golang.org/x/term v0.3.0 github.com/glebarez/go-sqlite v1.22.0 // only needed for tests -.-
github.com/jmoiron/sqlx v1.4.0
github.com/rs/xid v1.6.0
github.com/rs/zerolog v1.33.0
go.mongodb.org/mongo-driver v1.17.1
golang.org/x/crypto v0.30.0
golang.org/x/sys v0.28.0
golang.org/x/term v0.27.0
) )
require ( require (
github.com/jmoiron/sqlx v1.3.5 // indirect github.com/disintegration/imaging v1.6.2
go.mongodb.org/mongo-driver v1.11.1 // indirect github.com/jung-kurt/gofpdf v1.16.2
golang.org/x/crypto v0.4.0 // indirect golang.org/x/sync v0.10.0
)
require (
github.com/bytedance/sonic v1.12.5 // indirect
github.com/bytedance/sonic/loader v0.2.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.7 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.23.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
golang.org/x/arch v0.12.0 // indirect
golang.org/x/image v0.23.0 // indirect
golang.org/x/net v0.32.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.35.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.37.6 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/sqlite v1.28.0 // indirect
) )

256
go.sum
View File

@@ -1,50 +1,234 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU=
github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic v1.12.4 h1:9Csb3c9ZJhfUWeMtpCDCq6BUoH5ogfDFLUgQ/jG+R0k=
github.com/bytedance/sonic v1.12.4/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic v1.12.5 h1:hoZxY8uW+mT+OpkcUWw4k0fDINtOcVavEsGfzwzFU/w=
github.com/bytedance/sonic v1.12.5/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc=
github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0=
github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0=
github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
go.mongodb.org/mongo-driver v1.11.1 h1:QP0znIRTuL0jf1oBQoAoM0C6ZJfBK4kx0Uumtv1A7w8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
go.mongodb.org/mongo-driver v1.11.1/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g=
golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=
modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

5
goextVersion.go Normal file
View File

@@ -0,0 +1,5 @@
package goext
const GoextVersion = "0.0.547"
const GoextVersionTimestamp = "2024-12-08T18:04:04+0100"

View File

@@ -217,6 +217,7 @@ type decodeState struct {
savedError error savedError error
useNumber bool useNumber bool
disallowUnknownFields bool disallowUnknownFields bool
tagkey *string
} }
// readIndex returns the position of the last byte read. // readIndex returns the position of the last byte read.
@@ -652,7 +653,11 @@ func (d *decodeState) object(v reflect.Value) error {
v.Set(reflect.MakeMap(t)) v.Set(reflect.MakeMap(t))
} }
case reflect.Struct: case reflect.Struct:
fields = cachedTypeFields(t) tagkey := "json"
if d.tagkey != nil {
tagkey = *d.tagkey
}
fields = cachedTypeFields(t, tagkey)
// ok // ok
default: default:
d.saveError(&UnmarshalTypeError{Value: "object", Type: t, Offset: int64(d.off)}) d.saveError(&UnmarshalTypeError{Value: "object", Type: t, Offset: int64(d.off)})

View File

@@ -156,7 +156,6 @@ import (
// an error. // an error.
func Marshal(v any) ([]byte, error) { func Marshal(v any) ([]byte, error) {
e := newEncodeState() e := newEncodeState()
defer encodeStatePool.Put(e)
err := e.marshal(v, encOpts{escapeHTML: true}) err := e.marshal(v, encOpts{escapeHTML: true})
if err != nil { if err != nil {
@@ -164,6 +163,8 @@ func Marshal(v any) ([]byte, error) {
} }
buf := append([]byte(nil), e.Bytes()...) buf := append([]byte(nil), e.Bytes()...)
encodeStatePool.Put(e)
return buf, nil return buf, nil
} }
@@ -174,9 +175,9 @@ type IndentOpt struct {
// MarshalSafeCollections is like Marshal except it will marshal nil maps and // MarshalSafeCollections is like Marshal except it will marshal nil maps and
// slices as '{}' and '[]' respectfully instead of 'null' // slices as '{}' and '[]' respectfully instead of 'null'
func MarshalSafeCollections(v interface{}, nilSafeSlices bool, nilSafeMaps bool, indent *IndentOpt) ([]byte, error) { func MarshalSafeCollections(v interface{}, nilSafeSlices bool, nilSafeMaps bool, indent *IndentOpt, filter *string) ([]byte, error) {
e := &encodeState{} e := &encodeState{}
err := e.marshal(v, encOpts{escapeHTML: true, nilSafeSlices: nilSafeSlices, nilSafeMaps: nilSafeMaps}) err := e.marshal(v, encOpts{escapeHTML: true, nilSafeSlices: nilSafeSlices, nilSafeMaps: nilSafeMaps, filter: filter})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -381,7 +382,12 @@ func isEmptyValue(v reflect.Value) bool {
} }
func (e *encodeState) reflectValue(v reflect.Value, opts encOpts) { func (e *encodeState) reflectValue(v reflect.Value, opts encOpts) {
valueEncoder(v)(e, v, opts) tagkey := "json"
if opts.tagkey != nil {
tagkey = *opts.tagkey
}
valueEncoder(v, tagkey)(e, v, opts)
} }
type encOpts struct { type encOpts struct {
@@ -393,20 +399,25 @@ type encOpts struct {
nilSafeSlices bool nilSafeSlices bool
// nilSafeMaps marshals a nil maps '{}' instead of 'null' // nilSafeMaps marshals a nil maps '{}' instead of 'null'
nilSafeMaps bool nilSafeMaps bool
// filter matches jsonfilter tag of struct
// marshals if no jsonfilter is set or otherwise if jsonfilter has the filter value
filter *string
// use different tag instead of "json"
tagkey *string
} }
type encoderFunc func(e *encodeState, v reflect.Value, opts encOpts) type encoderFunc func(e *encodeState, v reflect.Value, opts encOpts)
var encoderCache sync.Map // map[reflect.Type]encoderFunc var encoderCache sync.Map // map[reflect.Type]encoderFunc
func valueEncoder(v reflect.Value) encoderFunc { func valueEncoder(v reflect.Value, tagkey string) encoderFunc {
if !v.IsValid() { if !v.IsValid() {
return invalidValueEncoder return invalidValueEncoder
} }
return typeEncoder(v.Type()) return typeEncoder(v.Type(), tagkey)
} }
func typeEncoder(t reflect.Type) encoderFunc { func typeEncoder(t reflect.Type, tagkey string) encoderFunc {
if fi, ok := encoderCache.Load(t); ok { if fi, ok := encoderCache.Load(t); ok {
return fi.(encoderFunc) return fi.(encoderFunc)
} }
@@ -429,7 +440,7 @@ func typeEncoder(t reflect.Type) encoderFunc {
} }
// Compute the real encoder and replace the indirect func with it. // Compute the real encoder and replace the indirect func with it.
f = newTypeEncoder(t, true) f = newTypeEncoder(t, true, tagkey)
wg.Done() wg.Done()
encoderCache.Store(t, f) encoderCache.Store(t, f)
return f return f
@@ -442,19 +453,19 @@ var (
// newTypeEncoder constructs an encoderFunc for a type. // newTypeEncoder constructs an encoderFunc for a type.
// The returned encoder only checks CanAddr when allowAddr is true. // The returned encoder only checks CanAddr when allowAddr is true.
func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc { func newTypeEncoder(t reflect.Type, allowAddr bool, tagkey string) encoderFunc {
// If we have a non-pointer value whose type implements // If we have a non-pointer value whose type implements
// Marshaler with a value receiver, then we're better off taking // Marshaler with a value receiver, then we're better off taking
// the address of the value - otherwise we end up with an // the address of the value - otherwise we end up with an
// allocation as we cast the value to an interface. // allocation as we cast the value to an interface.
if t.Kind() != reflect.Pointer && allowAddr && reflect.PointerTo(t).Implements(marshalerType) { if t.Kind() != reflect.Pointer && allowAddr && reflect.PointerTo(t).Implements(marshalerType) {
return newCondAddrEncoder(addrMarshalerEncoder, newTypeEncoder(t, false)) return newCondAddrEncoder(addrMarshalerEncoder, newTypeEncoder(t, false, tagkey))
} }
if t.Implements(marshalerType) { if t.Implements(marshalerType) {
return marshalerEncoder return marshalerEncoder
} }
if t.Kind() != reflect.Pointer && allowAddr && reflect.PointerTo(t).Implements(textMarshalerType) { if t.Kind() != reflect.Pointer && allowAddr && reflect.PointerTo(t).Implements(textMarshalerType) {
return newCondAddrEncoder(addrTextMarshalerEncoder, newTypeEncoder(t, false)) return newCondAddrEncoder(addrTextMarshalerEncoder, newTypeEncoder(t, false, tagkey))
} }
if t.Implements(textMarshalerType) { if t.Implements(textMarshalerType) {
return textMarshalerEncoder return textMarshalerEncoder
@@ -476,15 +487,15 @@ func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
case reflect.Interface: case reflect.Interface:
return interfaceEncoder return interfaceEncoder
case reflect.Struct: case reflect.Struct:
return newStructEncoder(t) return newStructEncoder(t, tagkey)
case reflect.Map: case reflect.Map:
return newMapEncoder(t) return newMapEncoder(t, tagkey)
case reflect.Slice: case reflect.Slice:
return newSliceEncoder(t) return newSliceEncoder(t, tagkey)
case reflect.Array: case reflect.Array:
return newArrayEncoder(t) return newArrayEncoder(t, tagkey)
case reflect.Pointer: case reflect.Pointer:
return newPtrEncoder(t) return newPtrEncoder(t, tagkey)
default: default:
return unsupportedTypeEncoder return unsupportedTypeEncoder
} }
@@ -777,6 +788,8 @@ FieldLoop:
if f.omitEmpty && isEmptyValue(fv) { if f.omitEmpty && isEmptyValue(fv) {
continue continue
} else if !matchesJSONFilter(f.jsonfilter, opts.filter) {
continue
} }
e.WriteByte(next) e.WriteByte(next)
next = ',' next = ','
@@ -795,8 +808,32 @@ FieldLoop:
} }
} }
func newStructEncoder(t reflect.Type) encoderFunc { func matchesJSONFilter(filter jsonfilter, value *string) bool {
se := structEncoder{fields: cachedTypeFields(t)} if len(filter) == 0 {
return true // no filter in struct
}
if value == nil || *value == "" {
return false // no filter set, but struct has filter, return false
}
if len(filter) == 1 && filter[0] == "-" {
return false
}
if filter.Contains(*value) {
return true
}
if filter.Contains("*") {
return true
}
return false
}
func newStructEncoder(t reflect.Type, tagkey string) encoderFunc {
se := structEncoder{fields: cachedTypeFields(t, tagkey)}
return se.encode return se.encode
} }
@@ -849,7 +886,7 @@ func (me mapEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
e.ptrLevel-- e.ptrLevel--
} }
func newMapEncoder(t reflect.Type) encoderFunc { func newMapEncoder(t reflect.Type, tagkey string) encoderFunc {
switch t.Key().Kind() { switch t.Key().Kind() {
case reflect.String, case reflect.String,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
@@ -859,7 +896,7 @@ func newMapEncoder(t reflect.Type) encoderFunc {
return unsupportedTypeEncoder return unsupportedTypeEncoder
} }
} }
me := mapEncoder{typeEncoder(t.Elem())} me := mapEncoder{typeEncoder(t.Elem(), tagkey)}
return me.encode return me.encode
} }
@@ -930,7 +967,7 @@ func (se sliceEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
e.ptrLevel-- e.ptrLevel--
} }
func newSliceEncoder(t reflect.Type) encoderFunc { func newSliceEncoder(t reflect.Type, tagkey string) encoderFunc {
// Byte slices get special treatment; arrays don't. // Byte slices get special treatment; arrays don't.
if t.Elem().Kind() == reflect.Uint8 { if t.Elem().Kind() == reflect.Uint8 {
p := reflect.PointerTo(t.Elem()) p := reflect.PointerTo(t.Elem())
@@ -938,7 +975,7 @@ func newSliceEncoder(t reflect.Type) encoderFunc {
return encodeByteSlice return encodeByteSlice
} }
} }
enc := sliceEncoder{newArrayEncoder(t)} enc := sliceEncoder{newArrayEncoder(t, tagkey)}
return enc.encode return enc.encode
} }
@@ -958,8 +995,8 @@ func (ae arrayEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
e.WriteByte(']') e.WriteByte(']')
} }
func newArrayEncoder(t reflect.Type) encoderFunc { func newArrayEncoder(t reflect.Type, tagkey string) encoderFunc {
enc := arrayEncoder{typeEncoder(t.Elem())} enc := arrayEncoder{typeEncoder(t.Elem(), tagkey)}
return enc.encode return enc.encode
} }
@@ -986,8 +1023,8 @@ func (pe ptrEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
e.ptrLevel-- e.ptrLevel--
} }
func newPtrEncoder(t reflect.Type) encoderFunc { func newPtrEncoder(t reflect.Type, tagkey string) encoderFunc {
enc := ptrEncoder{typeEncoder(t.Elem())} enc := ptrEncoder{typeEncoder(t.Elem(), tagkey)}
return enc.encode return enc.encode
} }
@@ -1220,15 +1257,28 @@ type field struct {
nameNonEsc string // `"` + name + `":` nameNonEsc string // `"` + name + `":`
nameEscHTML string // `"` + HTMLEscape(name) + `":` nameEscHTML string // `"` + HTMLEscape(name) + `":`
tag bool tag bool
index []int index []int
typ reflect.Type typ reflect.Type
omitEmpty bool omitEmpty bool
quoted bool jsonfilter jsonfilter
quoted bool
encoder encoderFunc encoder encoderFunc
} }
// jsonfilter stores the value of the jsonfilter struct tag
type jsonfilter []string
func (j jsonfilter) Contains(t string) bool {
for _, tag := range j {
if t == tag {
return true
}
}
return false
}
// byIndex sorts field by index sequence. // byIndex sorts field by index sequence.
type byIndex []field type byIndex []field
@@ -1251,7 +1301,7 @@ func (x byIndex) Less(i, j int) bool {
// typeFields returns a list of fields that JSON should recognize for the given type. // typeFields returns a list of fields that JSON should recognize for the given type.
// The algorithm is breadth-first search over the set of structs to include - the top struct // The algorithm is breadth-first search over the set of structs to include - the top struct
// and then any reachable anonymous structs. // and then any reachable anonymous structs.
func typeFields(t reflect.Type) structFields { func typeFields(t reflect.Type, tagkey string) structFields {
// Anonymous fields to explore at the current level and the next. // Anonymous fields to explore at the current level and the next.
current := []field{} current := []field{}
next := []field{{typ: t}} next := []field{{typ: t}}
@@ -1296,7 +1346,7 @@ func typeFields(t reflect.Type) structFields {
// Ignore unexported non-embedded fields. // Ignore unexported non-embedded fields.
continue continue
} }
tag := sf.Tag.Get("json") tag := sf.Tag.Get(tagkey)
if tag == "-" { if tag == "-" {
continue continue
} }
@@ -1304,6 +1354,13 @@ func typeFields(t reflect.Type) structFields {
if !isValidTag(name) { if !isValidTag(name) {
name = "" name = ""
} }
var jsonfilter []string
jsonfilterTag := sf.Tag.Get("jsonfilter")
if jsonfilterTag != "" {
jsonfilter = strings.Split(jsonfilterTag, ",")
}
index := make([]int, len(f.index)+1) index := make([]int, len(f.index)+1)
copy(index, f.index) copy(index, f.index)
index[len(f.index)] = i index[len(f.index)] = i
@@ -1334,12 +1391,13 @@ func typeFields(t reflect.Type) structFields {
name = sf.Name name = sf.Name
} }
field := field{ field := field{
name: name, name: name,
tag: tagged, tag: tagged,
index: index, index: index,
typ: ft, typ: ft,
omitEmpty: opts.Contains("omitempty"), omitEmpty: opts.Contains("omitempty"),
quoted: quoted, jsonfilter: jsonfilter,
quoted: quoted,
} }
field.nameBytes = []byte(field.name) field.nameBytes = []byte(field.name)
field.equalFold = foldFunc(field.nameBytes) field.equalFold = foldFunc(field.nameBytes)
@@ -1422,7 +1480,7 @@ func typeFields(t reflect.Type) structFields {
for i := range fields { for i := range fields {
f := &fields[i] f := &fields[i]
f.encoder = typeEncoder(typeByIndex(t, f.index)) f.encoder = typeEncoder(typeByIndex(t, f.index), tagkey)
} }
nameIndex := make(map[string]int, len(fields)) nameIndex := make(map[string]int, len(fields))
for i, field := range fields { for i, field := range fields {
@@ -1447,13 +1505,26 @@ func dominantField(fields []field) (field, bool) {
return fields[0], true return fields[0], true
} }
var fieldCache sync.Map // map[reflect.Type]structFields var fieldCache sync.Map // map[string]map[reflect.Type]structFields
// cachedTypeFields is like typeFields but uses a cache to avoid repeated work. // cachedTypeFields is like typeFields but uses a cache to avoid repeated work.
func cachedTypeFields(t reflect.Type) structFields { func cachedTypeFields(t reflect.Type, tagkey string) structFields {
if f, ok := fieldCache.Load(t); ok { if m0, ok := fieldCache.Load(tagkey); ok {
if f, ok := m0.(*sync.Map).Load(t); ok {
return f.(structFields)
}
f, _ := m0.(*sync.Map).LoadOrStore(t, typeFields(t, tagkey))
return f.(structFields)
} else {
m0 := &sync.Map{}
f, _ := m0.LoadOrStore(t, typeFields(t, tagkey))
fieldCache.Store(tagkey, m0)
return f.(structFields) return f.(structFields)
} }
f, _ := fieldCache.LoadOrStore(t, typeFields(t))
return f.(structFields)
} }

View File

@@ -1253,6 +1253,10 @@ func TestMarshalSafeCollections(t *testing.T) {
nilMapStruct struct { nilMapStruct struct {
NilMap map[string]interface{} `json:"nil_map"` NilMap map[string]interface{} `json:"nil_map"`
} }
testWithFilter struct {
Test1 string `json:"test1" jsonfilter:"FILTERONE"`
Test2 string `json:"test2" jsonfilter:"FILTERTWO"`
}
) )
tests := []struct { tests := []struct {
@@ -1271,10 +1275,12 @@ func TestMarshalSafeCollections(t *testing.T) {
{map[string]interface{}{"1": 1, "2": 2, "3": 3}, "{\"1\":1,\"2\":2,\"3\":3}"}, {map[string]interface{}{"1": 1, "2": 2, "3": 3}, "{\"1\":1,\"2\":2,\"3\":3}"},
{pNilMap, "null"}, {pNilMap, "null"},
{nilMapStruct{}, "{\"nil_map\":{}}"}, {nilMapStruct{}, "{\"nil_map\":{}}"},
{testWithFilter{}, "{\"test1\":\"\"}"},
} }
filter := "FILTERONE"
for i, tt := range tests { for i, tt := range tests {
b, err := MarshalSafeCollections(tt.in, true, true, nil) b, err := MarshalSafeCollections(tt.in, true, true, nil, &filter)
if err != nil { if err != nil {
t.Errorf("test %d, unexpected failure: %v", i, err) t.Errorf("test %d, unexpected failure: %v", i, err)
} }

View File

@@ -97,7 +97,10 @@ func equalFoldRight(s, t []byte) bool {
t = t[size:] t = t[size:]
} }
return len(t) == 0 if len(t) > 0 {
return false
}
return true
} }
// asciiEqualFold is a specialization of bytes.EqualFold for use when // asciiEqualFold is a specialization of bytes.EqualFold for use when

View File

@@ -52,7 +52,9 @@ func TestFold(t *testing.T) {
} }
func TestFoldAgainstUnicode(t *testing.T) { func TestFoldAgainstUnicode(t *testing.T) {
var buf1, buf2 []byte const bufSize = 5
buf1 := make([]byte, 0, bufSize)
buf2 := make([]byte, 0, bufSize)
var runes []rune var runes []rune
for i := 0x20; i <= 0x7f; i++ { for i := 0x20; i <= 0x7f; i++ {
runes = append(runes, rune(i)) runes = append(runes, rune(i))
@@ -94,8 +96,12 @@ func TestFoldAgainstUnicode(t *testing.T) {
continue continue
} }
for _, r2 := range runes { for _, r2 := range runes {
buf1 = append(utf8.AppendRune(append(buf1[:0], 'x'), r), 'x') buf1 := append(buf1[:0], 'x')
buf2 = append(utf8.AppendRune(append(buf2[:0], 'x'), r2), 'x') buf2 := append(buf2[:0], 'x')
buf1 = buf1[:1+utf8.EncodeRune(buf1[1:bufSize], r)]
buf2 = buf2[:1+utf8.EncodeRune(buf2[1:bufSize], r2)]
buf1 = append(buf1, 'x')
buf2 = append(buf2, 'x')
want := bytes.EqualFold(buf1, buf2) want := bytes.EqualFold(buf1, buf2)
if got := ff.fold(buf1, buf2); got != want { if got := ff.fold(buf1, buf2); got != want {
t.Errorf("%s(%q, %q) = %v; want %v", ff.name, buf1, buf2, got, want) t.Errorf("%s(%q, %q) = %v; want %v", ff.name, buf1, buf2, got, want)

View File

@@ -17,6 +17,7 @@ type GoJsonRender struct {
NilSafeSlices bool NilSafeSlices bool
NilSafeMaps bool NilSafeMaps bool
Indent *IndentOpt Indent *IndentOpt
Filter *string
} }
func (r GoJsonRender) Render(w http.ResponseWriter) error { func (r GoJsonRender) Render(w http.ResponseWriter) error {
@@ -25,7 +26,7 @@ func (r GoJsonRender) Render(w http.ResponseWriter) error {
header["Content-Type"] = []string{"application/json; charset=utf-8"} header["Content-Type"] = []string{"application/json; charset=utf-8"}
} }
jsonBytes, err := MarshalSafeCollections(r.Data, r.NilSafeSlices, r.NilSafeMaps, r.Indent) jsonBytes, err := MarshalSafeCollections(r.Data, r.NilSafeSlices, r.NilSafeMaps, r.Indent, r.Filter)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -36,6 +37,14 @@ func (r GoJsonRender) Render(w http.ResponseWriter) error {
return nil return nil
} }
func (r GoJsonRender) RenderString() (string, error) {
jsonBytes, err := MarshalSafeCollections(r.Data, r.NilSafeSlices, r.NilSafeMaps, r.Indent, r.Filter)
if err != nil {
panic(err)
}
return string(jsonBytes), nil
}
func (r GoJsonRender) WriteContentType(w http.ResponseWriter) { func (r GoJsonRender) WriteContentType(w http.ResponseWriter) {
header := w.Header() header := w.Header()
if val := header["Content-Type"]; len(val) == 0 { if val := header["Content-Type"]; len(val) == 0 {

View File

@@ -116,3 +116,18 @@ func TestNumberIsValid(t *testing.T) {
} }
} }
} }
func BenchmarkNumberIsValid(b *testing.B) {
s := "-61657.61667E+61673"
for i := 0; i < b.N; i++ {
isValidNumber(s)
}
}
func BenchmarkNumberIsValidRegexp(b *testing.B) {
var jsonNumberRegexp = regexp.MustCompile(`^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$`)
s := "-61657.61667E+61673"
for i := 0; i < b.N; i++ {
jsonNumberRegexp.MatchString(s)
}
}

View File

@@ -594,7 +594,7 @@ func (s *scanner) error(c byte, context string) int {
return scanError return scanError
} }
// quoteChar formats c as a quoted character literal. // quoteChar formats c as a quoted character literal
func quoteChar(c byte) string { func quoteChar(c byte) string {
// special cases - different from quoted strings // special cases - different from quoted strings
if c == '\'' { if c == '\'' {

View File

@@ -41,6 +41,9 @@ func (dec *Decoder) UseNumber() { dec.d.useNumber = true }
// non-ignored, exported fields in the destination. // non-ignored, exported fields in the destination.
func (dec *Decoder) DisallowUnknownFields() { dec.d.disallowUnknownFields = true } func (dec *Decoder) DisallowUnknownFields() { dec.d.disallowUnknownFields = true }
// TagKey sets a different TagKey (instead of "json")
func (dec *Decoder) TagKey(v string) { dec.d.tagkey = &v }
// Decode reads the next JSON-encoded value from its // Decode reads the next JSON-encoded value from its
// input and stores it in the value pointed to by v. // input and stores it in the value pointed to by v.
// //
@@ -179,11 +182,9 @@ func nonSpace(b []byte) bool {
// An Encoder writes JSON values to an output stream. // An Encoder writes JSON values to an output stream.
type Encoder struct { type Encoder struct {
w io.Writer w io.Writer
err error err error
escapeHTML bool escapeHTML bool
nilSafeSlices bool
nilSafeMaps bool
indentBuf *bytes.Buffer indentBuf *bytes.Buffer
indentPrefix string indentPrefix string
@@ -204,11 +205,8 @@ func (enc *Encoder) Encode(v any) error {
if enc.err != nil { if enc.err != nil {
return enc.err return enc.err
} }
e := newEncodeState() e := newEncodeState()
defer encodeStatePool.Put(e) err := e.marshal(v, encOpts{escapeHTML: enc.escapeHTML})
err := e.marshal(v, encOpts{escapeHTML: enc.escapeHTML, nilSafeMaps: enc.nilSafeMaps, nilSafeSlices: enc.nilSafeSlices})
if err != nil { if err != nil {
return err return err
} }
@@ -236,6 +234,7 @@ func (enc *Encoder) Encode(v any) error {
if _, err = enc.w.Write(b); err != nil { if _, err = enc.w.Write(b); err != nil {
enc.err = err enc.err = err
} }
encodeStatePool.Put(e)
return err return err
} }
@@ -247,13 +246,6 @@ func (enc *Encoder) SetIndent(prefix, indent string) {
enc.indentValue = indent enc.indentValue = indent
} }
// SetNilSafeCollection specifies whether to represent nil slices and maps as
// '[]' or '{}' respectfully (flag on) instead of 'null' (default) when marshaling json.
func (enc *Encoder) SetNilSafeCollection(nilSafeSlices bool, nilSafeMaps bool) {
enc.nilSafeSlices = nilSafeSlices
enc.nilSafeMaps = nilSafeMaps
}
// SetEscapeHTML specifies whether problematic HTML characters // SetEscapeHTML specifies whether problematic HTML characters
// should be escaped inside JSON quoted strings. // should be escaped inside JSON quoted strings.
// The default behavior is to escape &, <, and > to \u0026, \u003c, and \u003e // The default behavior is to escape &, <, and > to \u0026, \u003c, and \u003e

View File

@@ -12,7 +12,6 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect" "reflect"
"runtime/debug"
"strings" "strings"
"testing" "testing"
) )
@@ -42,7 +41,7 @@ false
func TestEncoder(t *testing.T) { func TestEncoder(t *testing.T) {
for i := 0; i <= len(streamTest); i++ { for i := 0; i <= len(streamTest); i++ {
var buf strings.Builder var buf bytes.Buffer
enc := NewEncoder(&buf) enc := NewEncoder(&buf)
// Check that enc.SetIndent("", "") turns off indentation. // Check that enc.SetIndent("", "") turns off indentation.
enc.SetIndent(">", ".") enc.SetIndent(">", ".")
@@ -60,43 +59,6 @@ func TestEncoder(t *testing.T) {
} }
} }
func TestEncoderErrorAndReuseEncodeState(t *testing.T) {
// Disable the GC temporarily to prevent encodeState's in Pool being cleaned away during the test.
percent := debug.SetGCPercent(-1)
defer debug.SetGCPercent(percent)
// Trigger an error in Marshal with cyclic data.
type Dummy struct {
Name string
Next *Dummy
}
dummy := Dummy{Name: "Dummy"}
dummy.Next = &dummy
var buf bytes.Buffer
enc := NewEncoder(&buf)
if err := enc.Encode(dummy); err == nil {
t.Errorf("Encode(dummy) == nil; want error")
}
type Data struct {
A string
I int
}
data := Data{A: "a", I: 1}
if err := enc.Encode(data); err != nil {
t.Errorf("Marshal(%v) = %v", data, err)
}
var data2 Data
if err := Unmarshal(buf.Bytes(), &data2); err != nil {
t.Errorf("Unmarshal(%v) = %v", data2, err)
}
if data2 != data {
t.Errorf("expect: %v, but get: %v", data, data2)
}
}
var streamEncodedIndent = `0.1 var streamEncodedIndent = `0.1
"hello" "hello"
null null
@@ -115,7 +77,7 @@ false
` `
func TestEncoderIndent(t *testing.T) { func TestEncoderIndent(t *testing.T) {
var buf strings.Builder var buf bytes.Buffer
enc := NewEncoder(&buf) enc := NewEncoder(&buf)
enc.SetIndent(">", ".") enc.SetIndent(">", ".")
for _, v := range streamTest { for _, v := range streamTest {
@@ -185,7 +147,7 @@ func TestEncoderSetEscapeHTML(t *testing.T) {
`{"bar":"\"<html>foobar</html>\""}`, `{"bar":"\"<html>foobar</html>\""}`,
}, },
} { } {
var buf strings.Builder var buf bytes.Buffer
enc := NewEncoder(&buf) enc := NewEncoder(&buf)
if err := enc.Encode(tt.v); err != nil { if err := enc.Encode(tt.v); err != nil {
t.Errorf("Encode(%s): %s", tt.name, err) t.Errorf("Encode(%s): %s", tt.name, err)
@@ -347,6 +309,21 @@ func TestBlocking(t *testing.T) {
} }
} }
func BenchmarkEncoderEncode(b *testing.B) {
b.ReportAllocs()
type T struct {
X, Y string
}
v := &T{"foo", "bar"}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if err := NewEncoder(io.Discard).Encode(v); err != nil {
b.Fatal(err)
}
}
})
}
type tokenStreamCase struct { type tokenStreamCase struct {
json string json string
expTokens []any expTokens []any
@@ -495,45 +472,3 @@ func TestHTTPDecoding(t *testing.T) {
t.Errorf("err = %v; want io.EOF", err) t.Errorf("err = %v; want io.EOF", err)
} }
} }
func TestEncoderSetNilSafeCollection(t *testing.T) {
var (
nilSlice []interface{}
pNilSlice *[]interface{}
nilMap map[string]interface{}
pNilMap *map[string]interface{}
)
for _, tt := range []struct {
name string
v interface{}
want string
rescuedWant string
}{
{"nilSlice", nilSlice, "null", "[]"},
{"nonNilSlice", []interface{}{}, "[]", "[]"},
{"sliceWithValues", []interface{}{1, 2, 3}, "[1,2,3]", "[1,2,3]"},
{"pNilSlice", pNilSlice, "null", "null"},
{"nilMap", nilMap, "null", "{}"},
{"nonNilMap", map[string]interface{}{}, "{}", "{}"},
{"mapWithValues", map[string]interface{}{"1": 1, "2": 2, "3": 3}, "{\"1\":1,\"2\":2,\"3\":3}", "{\"1\":1,\"2\":2,\"3\":3}"},
{"pNilMap", pNilMap, "null", "null"},
} {
var buf bytes.Buffer
enc := NewEncoder(&buf)
if err := enc.Encode(tt.v); err != nil {
t.Fatalf("Encode(%s): %s", tt.name, err)
}
if got := strings.TrimSpace(buf.String()); got != tt.want {
t.Errorf("Encode(%s) = %#q, want %#q", tt.name, got, tt.want)
}
buf.Reset()
enc.SetNilSafeCollection(true, true)
if err := enc.Encode(tt.v); err != nil {
t.Fatalf("SetNilSafeCollection(true) Encode(%s): %s", tt.name, err)
}
if got := strings.TrimSpace(buf.String()); got != tt.rescuedWant {
t.Errorf("SetNilSafeCollection(true) Encode(%s) = %#q, want %#q",
tt.name, got, tt.want)
}
}
}

54
googleapi/README.md Normal file
View File

@@ -0,0 +1,54 @@
Google OAuth Setup (to send mails)
==================================
- Login @ https://console.cloud.google.com
- GMail API akivieren: https://console.cloud.google.com/apis/library/gmail.googleapis.com?
- Create new Project (aka 'BackendMailAPI') @ https://console.cloud.google.com/projectcreate
User Type: Intern
Anwendungsname: 'BackendMailAPI'
Support-Email: ...
Authorisierte Domains: 'heydyno.de' (or project domain)
Kontakt-Email: ...
- Unter "Anmeldedaten" neuer OAuth Client erstellen @ https://console.cloud.google.com/apis/credentials
Anwendungstyp: Web
Name: 'BackendMailOAuth'
Redirect-Uri: 'http://localhost/oauth'
Client-ID und Client-Key merken
- Open in Browser:
https://accounts.google.com/o/oauth2/v2/auth?redirect_uri=http://localhost/oauth&prompt=consent&response_type=code&client_id={...}&scope=https://www.googleapis.com/auth/gmail.send&access_type=offline
Code aus redirected URI merken
- Code via request einlösen (und refresh_roken merken):
```
curl --request POST \
--url https://oauth2.googleapis.com/token \
--data code={...} \
--data redirect_uri=http://localhost/oauth \
--data client_id={...} \
--data client_secret={...} \
--data grant_type=authorization_code \
--data scope=https://www.googleapis.com/auth/gmail.send
```
- Fertig, mit `client_id`, `client_secret` und `refresh_token` kann das package benutzt werden

46
googleapi/attachment.go Normal file
View File

@@ -0,0 +1,46 @@
package googleapi
import (
"encoding/base64"
"fmt"
)
type MailAttachment struct {
IsInline bool
ContentType string
Filename string
Data []byte
}
func (a MailAttachment) dump() []string {
res := make([]string, 0, 4)
if a.ContentType != "" {
res = append(res, "Content-Type: "+a.ContentType+"; charset=UTF-8")
}
res = append(res, "Content-Transfer-Encoding: base64")
if a.IsInline {
if a.Filename != "" {
res = append(res, fmt.Sprintf("Content-Disposition: inline;filename=\"%s\"", a.Filename))
} else {
res = append(res, "Content-Disposition: inline")
}
} else {
if a.Filename != "" {
res = append(res, fmt.Sprintf("Content-Disposition: attachment;filename=\"%s\"", a.Filename))
} else {
res = append(res, "Content-Disposition: attachment")
}
}
b64 := base64.StdEncoding.EncodeToString(a.Data)
for i := 0; i < len(b64); i += 80 {
res = append(res, b64[i:min(i+80, len(b64))])
}
res = append(res)
return res
}

6
googleapi/body.go Normal file
View File

@@ -0,0 +1,6 @@
package googleapi
type MailBody struct {
Plain string
HTML string
}

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