chore: bump reva to latest main
This commit is contained in:
committed by
Ralf Haferkamp
parent
4c86d2a289
commit
b8c4f581fb
@@ -65,7 +65,7 @@ require (
|
||||
github.com/open-policy-agent/opa v1.15.1
|
||||
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d
|
||||
github.com/opencloud-eu/reva/v2 v2.42.6
|
||||
github.com/opencloud-eu/reva/v2 v2.42.7-0.20260408072824-411780d0b756
|
||||
github.com/opensearch-project/opensearch-go/v4 v4.6.0
|
||||
github.com/orcaman/concurrent-map v1.0.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
@@ -75,7 +75,7 @@ require (
|
||||
github.com/riandyrn/otelchi v0.12.2
|
||||
github.com/rogpeppe/go-internal v1.14.1
|
||||
github.com/rs/cors v1.11.1
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/rs/zerolog v1.35.0
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
@@ -95,14 +95,14 @@ require (
|
||||
github.com/xhit/go-simple-mail/v2 v2.16.0
|
||||
go-micro.dev/v4 v4.11.0
|
||||
go.etcd.io/bbolt v1.4.3
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0
|
||||
go.opentelemetry.io/contrib/zpages v0.67.0
|
||||
go.opentelemetry.io/otel v1.42.0
|
||||
go.opentelemetry.io/otel v1.43.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0
|
||||
go.opentelemetry.io/otel/sdk v1.42.0
|
||||
go.opentelemetry.io/otel/trace v1.42.0
|
||||
go.opentelemetry.io/otel/sdk v1.43.0
|
||||
go.opentelemetry.io/otel/trace v1.43.0
|
||||
golang.org/x/crypto v0.49.0
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
|
||||
golang.org/x/image v0.38.0
|
||||
@@ -112,7 +112,7 @@ require (
|
||||
golang.org/x/term v0.41.0
|
||||
golang.org/x/text v0.35.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57
|
||||
google.golang.org/grpc v1.79.3
|
||||
google.golang.org/grpc v1.80.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
@@ -174,7 +174,7 @@ require (
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v1.0.0-rc.2 // indirect
|
||||
github.com/coreos/go-semver v0.3.1 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.7.0 // indirect
|
||||
github.com/cornelk/hashmap v1.0.8 // indirect
|
||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
@@ -206,10 +206,10 @@ require (
|
||||
github.com/go-acme/lego/v4 v4.4.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||
github.com/go-git/go-git/v5 v5.16.5 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.8.0 // indirect
|
||||
github.com/go-git/go-git/v5 v5.17.1 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/go-kit/log v0.2.1 // indirect
|
||||
github.com/go-logfmt/logfmt v0.5.1 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
@@ -336,9 +336,9 @@ require (
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/samber/lo v1.52.0 // indirect
|
||||
github.com/samber/slog-common v0.20.0 // indirect
|
||||
github.com/samber/slog-zerolog/v2 v2.9.1 // indirect
|
||||
github.com/samber/lo v1.53.0 // indirect
|
||||
github.com/samber/slog-common v0.21.0 // indirect
|
||||
github.com/samber/slog-zerolog/v2 v2.9.2 // indirect
|
||||
github.com/segmentio/asm v1.2.1 // indirect
|
||||
github.com/segmentio/kafka-go v0.4.50 // indirect
|
||||
github.com/segmentio/ksuid v1.0.4 // indirect
|
||||
@@ -382,7 +382,7 @@ require (
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
@@ -393,7 +393,7 @@ require (
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect
|
||||
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
|
||||
@@ -245,9 +245,8 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
|
||||
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
|
||||
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
|
||||
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
|
||||
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
|
||||
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cornelk/hashmap v1.0.8 h1:nv0AWgw02n+iDcawr5It4CjQIAcdMMKRrs10HOJYlrc=
|
||||
github.com/cornelk/hashmap v1.0.8/go.mod h1:RfZb7JO3RviW/rT6emczVuC/oxpdz4UsSB2LJSclR1k=
|
||||
@@ -385,12 +384,12 @@ github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
||||
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||
github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
|
||||
github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=
|
||||
github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M=
|
||||
github.com/go-git/go-git/v5 v5.17.1 h1:WnljyxIzSj9BRRUlnmAU35ohDsjRK0EKmL0evDqi5Jk=
|
||||
github.com/go-git/go-git/v5 v5.17.1/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
@@ -399,8 +398,8 @@ github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3I
|
||||
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
@@ -484,7 +483,6 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
@@ -809,7 +807,6 @@ github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
@@ -818,8 +815,6 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
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-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
@@ -959,8 +954,8 @@ github.com/opencloud-eu/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9 h1:dIft
|
||||
github.com/opencloud-eu/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9/go.mod h1:JWyDC6H+5oZRdUJUgKuaye+8Ph5hEs6HVzVoPKzWSGI=
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d h1:JcqGDiyrcaQwVyV861TUyQgO7uEmsjkhfm7aQd84dOw=
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d/go.mod h1:pzatilMEHZFT3qV7C/X3MqOa3NlRQuYhlRhZTL+hN6Q=
|
||||
github.com/opencloud-eu/reva/v2 v2.42.6 h1:GjGPa1lNrhpkBfj7No1vM9idcurG57Ax4qzf1dqnPTk=
|
||||
github.com/opencloud-eu/reva/v2 v2.42.6/go.mod h1:Ki3c/BKVg/5aHvAyOJIx+iZ50vvlhFvCf8wDZacF5qw=
|
||||
github.com/opencloud-eu/reva/v2 v2.42.7-0.20260408072824-411780d0b756 h1:Jbftg+S89x2PD7NVWhQB0+vlI/Qo0wv9rBwnM1N50yc=
|
||||
github.com/opencloud-eu/reva/v2 v2.42.7-0.20260408072824-411780d0b756/go.mod h1:hjMR/IerRm9xX4bthVRE9ZO/vhvrXVMbuvnnzwLjzK4=
|
||||
github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4 h1:l2oB/RctH+t8r7QBj5p8thfEHCM/jF35aAY3WQ3hADI=
|
||||
github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
@@ -1082,8 +1077,8 @@ github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
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.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
|
||||
github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||
github.com/russellhaering/goxmldsig v1.6.0 h1:8fdWXEPh2k/NZNQBPFNoVfS3JmzS4ZprY/sAOpKQLks=
|
||||
github.com/russellhaering/goxmldsig v1.6.0/go.mod h1:TrnaquDcYxWXfJrOjeMBTX4mLBeYAqaHEyUeWPxZlBM=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
@@ -1095,12 +1090,12 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
|
||||
github.com/sacloud/libsacloud v1.36.2/go.mod h1:P7YAOVmnIn3DKHqCZcUKYUXmSwGBm3yS7IBEjKVSrjg=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/samber/slog-common v0.20.0 h1:WaLnm/aCvBJSk5nR5aXZTFBaV0B47A+AEaEOiZDeUnc=
|
||||
github.com/samber/slog-common v0.20.0/go.mod h1:+Ozat1jgnnE59UAlmNX1IF3IByHsODnnwf9jUcBZ+m8=
|
||||
github.com/samber/slog-zerolog/v2 v2.9.1 h1:RMOq8XqzfuGx1X0TEIlS9OXbbFmqLY2/wJppghz66YY=
|
||||
github.com/samber/slog-zerolog/v2 v2.9.1/go.mod h1:DQYYve14WgCRN/XnKeHl4266jXK0DgYkYXkfZ4Fp98k=
|
||||
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
|
||||
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/samber/slog-common v0.21.0 h1:Wo2hTly1Br5RjYqX/BTWJJeDnTE85oWk/7vqlpZuAUc=
|
||||
github.com/samber/slog-common v0.21.0/go.mod h1:d/6OaSlzdkl9PFpfRLgn8FwY1OW6EFmPtBpsHX4MrU0=
|
||||
github.com/samber/slog-zerolog/v2 v2.9.2 h1:DIFzfzDTxHeRyGlfg/D7b2by7VVzcsBTybRPrzjWF4c=
|
||||
github.com/samber/slog-zerolog/v2 v2.9.2/go.mod h1:2q6cYK2OcN6YfQE/WyCnUtigc+yYf3ozqGsGmRwZR6I=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||
@@ -1303,14 +1298,14 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
|
||||
go.opentelemetry.io/contrib/zpages v0.67.0 h1:cIUwWSVDovuLEbDIKreptjdxMuIhGiqwq0uL8YNaq1c=
|
||||
go.opentelemetry.io/contrib/zpages v0.67.0/go.mod h1:vK8fsYHgPYg4Z/XDbFSEvItSGZDbjWTvjBOu8+AiDhc=
|
||||
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
|
||||
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=
|
||||
@@ -1319,14 +1314,14 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 h1:inYW9
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0/go.mod h1:Izur+Wt8gClgMJqO/cZ8wdeeMryJ/xxiOVgFSSfpDTY=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs=
|
||||
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
|
||||
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
|
||||
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
|
||||
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
@@ -1558,11 +1553,9 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
@@ -1661,8 +1654,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
@@ -1720,8 +1713,8 @@ google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH8
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
@@ -1737,8 +1730,8 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/grpc/examples v0.0.0-20211102180624-670c133e568e h1:m7aQHHqd0q89mRwhwS9Bx2rjyl/hsFAeta+uGrHsQaU=
|
||||
google.golang.org/grpc/examples v0.0.0-20211102180624-670c133e568e/go.mod h1:gID3PKrg7pWKntu9Ss6zTLJ0ttC0X9IHgREOCZwbCVU=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
|
||||
+1
-1
@@ -106,7 +106,7 @@ func fdIsJournalStream(fd int) (bool, error) {
|
||||
var expectedStat syscall.Stat_t
|
||||
_, err := fmt.Sscanf(journalStream, "%d:%d", &expectedStat.Dev, &expectedStat.Ino)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to parse JOURNAL_STREAM=%q: %v", journalStream, err)
|
||||
return false, fmt.Errorf("failed to parse JOURNAL_STREAM=%q: %w", journalStream, err)
|
||||
}
|
||||
|
||||
var stat syscall.Stat_t
|
||||
|
||||
+9
-3
@@ -128,12 +128,18 @@ type Symlink interface {
|
||||
Readlink(link string) (string, error)
|
||||
}
|
||||
|
||||
// Change abstract the FileInfo change related operations in a storage-agnostic
|
||||
// interface as an extension to the Basic interface
|
||||
type Change interface {
|
||||
// Chmod abstracts the logic around changing file modes.
|
||||
type Chmod interface {
|
||||
// Chmod changes the mode of the named file to mode. If the file is a
|
||||
// symbolic link, it changes the mode of the link's target.
|
||||
Chmod(name string, mode os.FileMode) error
|
||||
}
|
||||
|
||||
// Change abstract the FileInfo change related operations in a storage-agnostic
|
||||
// interface as an extension to the Basic interface
|
||||
type Change interface {
|
||||
Chmod
|
||||
|
||||
// Lchown changes the numeric uid and gid of the named file. If the file is
|
||||
// a symbolic link, it changes the uid and gid of the link itself.
|
||||
Lchown(name string, uid, gid int) error
|
||||
|
||||
+14
@@ -1,6 +1,7 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -200,6 +201,19 @@ func (fs *ChrootHelper) Readlink(link string) (string, error) {
|
||||
return string(os.PathSeparator) + target, nil
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) Chmod(path string, mode os.FileMode) error {
|
||||
fullpath, err := fs.underlyingPath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, ok := fs.underlying.(billy.Chmod)
|
||||
if !ok {
|
||||
return errors.New("underlying fs does not implement billy.Chmod")
|
||||
}
|
||||
return c.Chmod(fullpath, mode)
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) Chroot(path string) (billy.Filesystem, error) {
|
||||
fullpath, err := fs.underlyingPath(path)
|
||||
if err != nil {
|
||||
|
||||
+10
-1
@@ -13,7 +13,7 @@ type Polyfill struct {
|
||||
c capabilities
|
||||
}
|
||||
|
||||
type capabilities struct{ tempfile, dir, symlink, chroot bool }
|
||||
type capabilities struct{ tempfile, dir, symlink, chroot, chmod bool }
|
||||
|
||||
// New creates a new filesystem wrapping up 'fs' the intercepts all the calls
|
||||
// made and errors if fs doesn't implement any of the billy interfaces.
|
||||
@@ -28,6 +28,7 @@ func New(fs billy.Basic) billy.Filesystem {
|
||||
_, h.c.dir = h.Basic.(billy.Dir)
|
||||
_, h.c.symlink = h.Basic.(billy.Symlink)
|
||||
_, h.c.chroot = h.Basic.(billy.Chroot)
|
||||
_, h.c.chmod = h.Basic.(billy.Chmod)
|
||||
return h
|
||||
}
|
||||
|
||||
@@ -87,6 +88,14 @@ func (h *Polyfill) Chroot(path string) (billy.Filesystem, error) {
|
||||
return h.Basic.(billy.Chroot).Chroot(path)
|
||||
}
|
||||
|
||||
func (h *Polyfill) Chmod(path string, mode os.FileMode) error {
|
||||
if !h.c.chmod {
|
||||
return billy.ErrNotSupported
|
||||
}
|
||||
|
||||
return h.Basic.(billy.Chmod).Chmod(path, mode)
|
||||
}
|
||||
|
||||
func (h *Polyfill) Root() string {
|
||||
if !h.c.chroot {
|
||||
return string(filepath.Separator)
|
||||
|
||||
+4
@@ -177,6 +177,10 @@ func (fs *Memory) Remove(filename string) error {
|
||||
return fs.s.Remove(filename)
|
||||
}
|
||||
|
||||
func (fs *Memory) Chmod(path string, mode os.FileMode) error {
|
||||
return fs.s.Chmod(path, mode)
|
||||
}
|
||||
|
||||
// Falls back to Go's filepath.Join, which works differently depending on the
|
||||
// OS where the code is being executed.
|
||||
func (fs *Memory) Join(elem ...string) string {
|
||||
|
||||
+12
@@ -169,6 +169,18 @@ func (s *storage) Remove(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *storage) Chmod(path string, mode os.FileMode) error {
|
||||
path = clean(path)
|
||||
|
||||
f, has := s.Get(path)
|
||||
if !has {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
f.mode = mode
|
||||
return nil
|
||||
}
|
||||
|
||||
func clean(path string) string {
|
||||
return filepath.Clean(filepath.FromSlash(path))
|
||||
}
|
||||
|
||||
+16
@@ -126,6 +126,14 @@ func (fs *BoundOS) TempFile(dir, prefix string) (billy.File, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = os.Stat(dir)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
err = os.MkdirAll(dir, defaultDirectoryMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tempFile(dir, prefix)
|
||||
@@ -176,6 +184,14 @@ func (fs *BoundOS) Readlink(link string) (string, error) {
|
||||
return os.Readlink(link)
|
||||
}
|
||||
|
||||
func (fs *BoundOS) Chmod(path string, mode os.FileMode) error {
|
||||
abspath, err := fs.abs(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chmod(abspath, mode)
|
||||
}
|
||||
|
||||
// Chroot returns a new OS filesystem, with the base dir set to the
|
||||
// result of joining the provided path with the underlying base dir.
|
||||
func (fs *BoundOS) Chroot(path string) (billy.Filesystem, error) {
|
||||
|
||||
+4
@@ -74,6 +74,10 @@ func (fs *ChrootOS) Remove(filename string) error {
|
||||
return os.Remove(filename)
|
||||
}
|
||||
|
||||
func (fs *ChrootOS) Chmod(path string, mode os.FileMode) error {
|
||||
return os.Chmod(path, mode)
|
||||
}
|
||||
|
||||
func (fs *ChrootOS) TempFile(dir, prefix string) (billy.File, error) {
|
||||
if err := fs.createDir(dir + string(os.PathSeparator)); err != nil {
|
||||
return nil, err
|
||||
|
||||
+7
-3
@@ -91,8 +91,8 @@ func readVersion(idx *MemoryIndex, r io.Reader) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if v > VersionSupported {
|
||||
return ErrUnsupportedVersion
|
||||
if v != VersionSupported {
|
||||
return fmt.Errorf("%w: v%d", ErrUnsupportedVersion, v)
|
||||
}
|
||||
|
||||
idx.Version = v
|
||||
@@ -106,6 +106,10 @@ func readFanout(idx *MemoryIndex, r io.Reader) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if k > 0 && n < idx.Fanout[k-1] {
|
||||
return fmt.Errorf("%w: fanout table is not monotonically non-decreasing at entry %d", ErrMalformedIdxFile, k)
|
||||
}
|
||||
|
||||
idx.Fanout[k] = n
|
||||
idx.FanoutMapping[k] = noMapping
|
||||
}
|
||||
@@ -155,7 +159,7 @@ func readCRC32(idx *MemoryIndex, r io.Reader) error {
|
||||
}
|
||||
|
||||
func readOffsets(idx *MemoryIndex, r io.Reader) error {
|
||||
var o64cnt int
|
||||
var o64cnt int64
|
||||
for k := 0; k < fanout; k++ {
|
||||
if pos := idx.FanoutMapping[k]; pos != noMapping {
|
||||
if _, err := io.ReadFull(r, idx.Offset32[pos]); err != nil {
|
||||
|
||||
+71
-39
@@ -4,8 +4,8 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -26,12 +26,14 @@ var (
|
||||
ErrInvalidChecksum = errors.New("invalid checksum")
|
||||
// ErrUnknownExtension is returned when an index extension is encountered that is considered mandatory
|
||||
ErrUnknownExtension = errors.New("unknown extension")
|
||||
// ErrMalformedIndexFile is returned when the index file contents are
|
||||
// structurally invalid.
|
||||
ErrMalformedIndexFile = errors.New("index decoder: malformed index file")
|
||||
)
|
||||
|
||||
const (
|
||||
entryHeaderLength = 62
|
||||
entryExtended = 0x4000
|
||||
entryValid = 0x8000
|
||||
nameMask = 0xfff
|
||||
intentToAddMask = 1 << 13
|
||||
skipWorkTreeMask = 1 << 14
|
||||
@@ -140,33 +142,55 @@ func (d *Decoder) readEntry(idx *Index) (*Entry, error) {
|
||||
e.SkipWorktree = extended&skipWorkTreeMask != 0
|
||||
}
|
||||
|
||||
if err := d.readEntryName(idx, e, flags); err != nil {
|
||||
nameConsumed, err := d.readEntryName(idx, e, flags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return e, d.padEntry(idx, e, read)
|
||||
return e, d.padEntry(idx, e, read, nameConsumed)
|
||||
}
|
||||
|
||||
func (d *Decoder) readEntryName(idx *Index, e *Entry, flags uint16) error {
|
||||
var name string
|
||||
var err error
|
||||
|
||||
// readEntryName reads the entry path and sets e.Name. It returns the
|
||||
// number of bytes consumed from the stream for the name portion.
|
||||
func (d *Decoder) readEntryName(idx *Index, e *Entry, flags uint16) (int, error) {
|
||||
switch idx.Version {
|
||||
case 2, 3:
|
||||
len := flags & nameMask
|
||||
name, err = d.doReadEntryName(len)
|
||||
nameLen := flags & nameMask
|
||||
name, consumed, err := d.doReadEntryName(nameLen)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
e.Name = name
|
||||
return consumed, nil
|
||||
case 4:
|
||||
name, err = d.doReadEntryNameV4()
|
||||
name, err := d.doReadEntryNameV4()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
e.Name = name
|
||||
return 0, nil // V4 has no padding; consumed count unused
|
||||
default:
|
||||
return ErrUnsupportedVersion
|
||||
return 0, ErrUnsupportedVersion
|
||||
}
|
||||
}
|
||||
|
||||
// doReadEntryName reads the entry path for V2/V3 indexes. It returns the
|
||||
// name, the number of bytes consumed from the stream, and any error.
|
||||
// When nameLen equals nameMask (0xFFF), the name was too long to fit in
|
||||
// the 12-bit field and the real length is found by scanning for the NUL
|
||||
// terminator — matching C Git's strlen(name) fallback in create_from_disk.
|
||||
func (d *Decoder) doReadEntryName(nameLen uint16) (string, int, error) {
|
||||
if nameLen == nameMask {
|
||||
name, err := binary.ReadUntil(d.r, '\x00')
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
return string(name), len(name) + 1, nil // +1 for the consumed NUL delimiter
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.Name = name
|
||||
return nil
|
||||
name := make([]byte, nameLen)
|
||||
_, err := io.ReadFull(d.r, name)
|
||||
return string(name), int(nameLen), err
|
||||
}
|
||||
|
||||
func (d *Decoder) doReadEntryNameV4() (string, error) {
|
||||
@@ -177,7 +201,14 @@ func (d *Decoder) doReadEntryNameV4() (string, error) {
|
||||
|
||||
var base string
|
||||
if d.lastEntry != nil {
|
||||
if l < 0 || int(l) > len(d.lastEntry.Name) {
|
||||
return "", fmt.Errorf("%w: invalid V4 entry name strip length %d (previous name length: %d)",
|
||||
ErrMalformedIndexFile, l, len(d.lastEntry.Name))
|
||||
}
|
||||
base = d.lastEntry.Name[:len(d.lastEntry.Name)-int(l)]
|
||||
} else if l > 0 {
|
||||
return "", fmt.Errorf("%w: non-zero strip length %d on first V4 entry",
|
||||
ErrMalformedIndexFile, l)
|
||||
}
|
||||
|
||||
name, err := binary.ReadUntil(d.r, '\x00')
|
||||
@@ -188,24 +219,23 @@ func (d *Decoder) doReadEntryNameV4() (string, error) {
|
||||
return base + string(name), nil
|
||||
}
|
||||
|
||||
func (d *Decoder) doReadEntryName(len uint16) (string, error) {
|
||||
name := make([]byte, len)
|
||||
_, err := io.ReadFull(d.r, name)
|
||||
|
||||
return string(name), err
|
||||
}
|
||||
|
||||
// Index entries are padded out to the next 8 byte alignment
|
||||
// for historical reasons related to how C Git read the files.
|
||||
func (d *Decoder) padEntry(idx *Index, e *Entry, read int) error {
|
||||
// padEntry discards NUL padding bytes that follow each V2/V3 entry on
|
||||
// disk. nameConsumed is the number of stream bytes consumed while reading
|
||||
// the entry name (which may exceed len(e.Name) when a NUL terminator was
|
||||
// consumed for long names where the 12-bit length field overflowed).
|
||||
func (d *Decoder) padEntry(idx *Index, e *Entry, read, nameConsumed int) error {
|
||||
if idx.Version == 4 {
|
||||
return nil
|
||||
}
|
||||
|
||||
entrySize := read + len(e.Name)
|
||||
padLen := 8 - entrySize%8
|
||||
_, err := io.CopyN(io.Discard, d.r, int64(padLen))
|
||||
return err
|
||||
padLen -= nameConsumed - len(e.Name)
|
||||
if padLen > 0 {
|
||||
_, err := io.CopyN(io.Discard, d.r, int64(padLen))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Decoder) readExtensions(idx *Index) error {
|
||||
@@ -312,7 +342,7 @@ func (d *Decoder) readChecksum(expected []byte) error {
|
||||
}
|
||||
|
||||
func validateHeader(r io.Reader) (version uint32, err error) {
|
||||
var s = make([]byte, 4)
|
||||
s := make([]byte, 4)
|
||||
if _, err := io.ReadFull(r, s); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -376,24 +406,26 @@ func (d *treeExtensionDecoder) readEntry() (*TreeEntry, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// An entry can be in an invalidated state and is represented by having a
|
||||
// negative number in the entry_count field.
|
||||
if i == -1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
e.Entries = i
|
||||
trees, err := binary.ReadUntil(d.r, '\n')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
i, err = strconv.Atoi(string(trees))
|
||||
subtrees, err := strconv.Atoi(string(trees))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e.Trees = i
|
||||
e.Trees = subtrees
|
||||
|
||||
// An entry can be in an invalidated state and is represented by having a
|
||||
// negative number in the entry_count field. In this case, there is no
|
||||
// object name and the next entry starts immediately after the newline.
|
||||
if i < 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
_, err = io.ReadFull(d.r, e.Hash[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+25
-14
@@ -5,9 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing/hash"
|
||||
@@ -160,26 +158,39 @@ func (e *Encoder) encodeEntryName(entry *Entry) error {
|
||||
}
|
||||
|
||||
func (e *Encoder) encodeEntryNameV4(entry *Entry) error {
|
||||
name := entry.Name
|
||||
l := 0
|
||||
// V4 prefix compression: find the longest common prefix between the
|
||||
// previous entry's name and the current one. The strip length tells
|
||||
// the decoder how many bytes to remove from the end of the previous
|
||||
// name, and the suffix is the remainder of the current name.
|
||||
prefix := 0
|
||||
if e.lastEntry != nil {
|
||||
dir := path.Dir(e.lastEntry.Name) + "/"
|
||||
if strings.HasPrefix(entry.Name, dir) {
|
||||
l = len(e.lastEntry.Name) - len(dir)
|
||||
name = strings.TrimPrefix(entry.Name, dir)
|
||||
} else {
|
||||
l = len(e.lastEntry.Name)
|
||||
}
|
||||
prefix = commonPrefixLen(e.lastEntry.Name, entry.Name)
|
||||
}
|
||||
stripLen := 0
|
||||
if e.lastEntry != nil {
|
||||
stripLen = len(e.lastEntry.Name) - prefix
|
||||
}
|
||||
|
||||
e.lastEntry = entry
|
||||
|
||||
err := binary.WriteVariableWidthInt(e.w, int64(l))
|
||||
if err != nil {
|
||||
if err := binary.WriteVariableWidthInt(e.w, int64(stripLen)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return binary.Write(e.w, []byte(name+string('\x00')))
|
||||
suffix := entry.Name[prefix:]
|
||||
return binary.Write(e.w, append([]byte(suffix), '\x00'))
|
||||
}
|
||||
|
||||
// commonPrefixLen returns the length of the longest common byte prefix
|
||||
// between a and b.
|
||||
func commonPrefixLen(a, b string) int {
|
||||
n := min(len(b), len(a))
|
||||
for i := range n {
|
||||
if a[i] != b[i] {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (e *Encoder) encodeRawExtension(signature string, data []byte) error {
|
||||
|
||||
+2
@@ -54,6 +54,8 @@ type Index struct {
|
||||
ResolveUndo *ResolveUndo
|
||||
// EndOfIndexEntry represents the 'End of Index Entry' extension
|
||||
EndOfIndexEntry *EndOfIndexEntry
|
||||
// ModTime is the modification time of the index file
|
||||
ModTime time.Time
|
||||
}
|
||||
|
||||
// Add creates a new Entry and returns it. The caller should first check that
|
||||
|
||||
+6
@@ -208,6 +208,12 @@ func Open(s storage.Storer, worktree billy.Filesystem) (*Repository, error) {
|
||||
return nil, ErrRepositoryNotExists
|
||||
}
|
||||
|
||||
cfg, err := s.Config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = verifyExtensions(s, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-git/v5/config"
|
||||
cfgformat "github.com/go-git/go-git/v5/plumbing/format/config"
|
||||
"github.com/go-git/go-git/v5/storage"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrUnsupportedExtensionRepositoryFormatVersion represents when an
|
||||
// extension being used is not compatible with the repository's
|
||||
// core.repositoryFormatVersion.
|
||||
ErrUnsupportedExtensionRepositoryFormatVersion = errors.New("core.repositoryformatversion does not support extension")
|
||||
|
||||
// ErrUnsupportedRepositoryFormatVersion represents when an repository
|
||||
// is using a format version that is not supported.
|
||||
ErrUnsupportedRepositoryFormatVersion = errors.New("core.repositoryformatversion not supported")
|
||||
|
||||
// ErrUnknownExtension represents when a repository has an extension
|
||||
// which is unknown or unsupported by go-git.
|
||||
ErrUnknownExtension = errors.New("unknown extension")
|
||||
|
||||
// builtinExtensions defines the Git extensions that are supported by
|
||||
// the core go-git implementation.
|
||||
//
|
||||
// Some extensions are storage-specific, those are defined by the Storers
|
||||
// themselves by implementing the ExtensionChecker interface.
|
||||
builtinExtensions = map[string]struct{}{
|
||||
// noop does not change git’s behavior at all.
|
||||
// It is useful only for testing format-1 compatibility.
|
||||
//
|
||||
// This extension is respected regardless of the
|
||||
// core.repositoryFormatVersion setting.
|
||||
"noop": {},
|
||||
|
||||
// noop-v1 does not change git’s behavior at all.
|
||||
// It is useful only for testing format-1 compatibility.
|
||||
"noop-v1": {},
|
||||
}
|
||||
|
||||
// Some Git extensions were supported upstream before the introduction
|
||||
// of repositoryformatversion. These are the only extensions that can be
|
||||
// enabled while core.repositoryformatversion is unset or set to 0.
|
||||
extensionsValidForV0 = map[string]struct{}{
|
||||
"noop": {},
|
||||
"partialClone": {},
|
||||
"preciousObjects": {},
|
||||
"worktreeConfig": {},
|
||||
}
|
||||
)
|
||||
|
||||
type extension struct {
|
||||
name string
|
||||
value string
|
||||
}
|
||||
|
||||
func extensions(cfg *config.Config) []extension {
|
||||
if cfg == nil || cfg.Raw == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !cfg.Raw.HasSection("extensions") {
|
||||
return nil
|
||||
}
|
||||
|
||||
section := cfg.Raw.Section("extensions")
|
||||
out := make([]extension, 0, len(section.Options))
|
||||
for _, opt := range section.Options {
|
||||
out = append(out, extension{name: strings.ToLower(opt.Key), value: strings.ToLower(opt.Value)})
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func verifyExtensions(st storage.Storer, cfg *config.Config) error {
|
||||
needed := extensions(cfg)
|
||||
|
||||
switch cfg.Core.RepositoryFormatVersion {
|
||||
case "", cfgformat.Version_0, cfgformat.Version_1:
|
||||
default:
|
||||
return fmt.Errorf("%w: %q",
|
||||
ErrUnsupportedRepositoryFormatVersion,
|
||||
cfg.Core.RepositoryFormatVersion)
|
||||
}
|
||||
|
||||
if len(needed) > 0 {
|
||||
if cfg.Core.RepositoryFormatVersion == cfgformat.Version_0 ||
|
||||
cfg.Core.RepositoryFormatVersion == "" {
|
||||
var unsupported []string
|
||||
for _, ext := range needed {
|
||||
if _, ok := extensionsValidForV0[ext.name]; !ok {
|
||||
unsupported = append(unsupported, ext.name)
|
||||
}
|
||||
}
|
||||
if len(unsupported) > 0 {
|
||||
return fmt.Errorf("%w: %s",
|
||||
ErrUnsupportedExtensionRepositoryFormatVersion,
|
||||
strings.Join(unsupported, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
var missing []string
|
||||
for _, ext := range needed {
|
||||
if _, ok := builtinExtensions[ext.name]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
missing = append(missing, ext.name)
|
||||
}
|
||||
|
||||
if len(missing) > 0 {
|
||||
return fmt.Errorf("%w: %s", ErrUnknownExtension, strings.Join(missing, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+23
-2
@@ -3,6 +3,7 @@ package dotgit
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
@@ -137,14 +138,22 @@ func (w *PackWriter) save() error {
|
||||
}
|
||||
|
||||
if err := w.encodeIdx(idx); err != nil {
|
||||
_ = idx.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := idx.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
fixPermissions(w.fs, fmt.Sprintf("%s.idx", base))
|
||||
|
||||
return w.fs.Rename(w.fw.Name(), fmt.Sprintf("%s.pack", base))
|
||||
packPath := fmt.Sprintf("%s.pack", base)
|
||||
if err := w.fs.Rename(w.fw.Name(), packPath); err != nil {
|
||||
return err
|
||||
}
|
||||
fixPermissions(w.fs, packPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *PackWriter) encodeIdx(writer io.Writer) error {
|
||||
@@ -281,5 +290,17 @@ func (w *ObjectWriter) save() error {
|
||||
hex := w.Hash().String()
|
||||
file := w.fs.Join(objectsPath, hex[0:2], hex[2:hash.HexSize])
|
||||
|
||||
return w.fs.Rename(w.f.Name(), file)
|
||||
// Loose objects are content addressable, if they already exist
|
||||
// we can safely delete the temporary file and short-circuit the
|
||||
// operation.
|
||||
if _, err := w.fs.Stat(file); err == nil || os.IsExist(err) {
|
||||
return w.fs.Remove(w.f.Name())
|
||||
}
|
||||
|
||||
if err := w.fs.Rename(w.f.Name(), file); err != nil {
|
||||
return err
|
||||
}
|
||||
fixPermissions(w.fs, file)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
//go:build !windows
|
||||
|
||||
package dotgit
|
||||
|
||||
import (
|
||||
"github.com/go-git/go-billy/v5"
|
||||
"github.com/go-git/go-git/v5/utils/trace"
|
||||
)
|
||||
|
||||
func fixPermissions(fs billy.Filesystem, path string) {
|
||||
if chmodFS, ok := fs.(billy.Chmod); ok {
|
||||
if err := chmodFS.Chmod(path, 0o444); err != nil {
|
||||
trace.General.Printf("failed to chmod %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isReadOnly(fs billy.Filesystem, path string) (bool, error) {
|
||||
fi, err := fs.Stat(path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if fi.Mode().Perm() == 0o444 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
//go:build windows
|
||||
|
||||
package dotgit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/go-git/go-billy/v5"
|
||||
"github.com/go-git/go-git/v5/utils/trace"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func fixPermissions(fs billy.Filesystem, path string) {
|
||||
fullpath := filepath.Join(fs.Root(), path)
|
||||
p, err := windows.UTF16PtrFromString(fullpath)
|
||||
if err != nil {
|
||||
trace.General.Printf("failed to chmod %s: %v", fullpath, err)
|
||||
return
|
||||
}
|
||||
|
||||
attrs, err := windows.GetFileAttributes(p)
|
||||
if err != nil {
|
||||
trace.General.Printf("failed to chmod %s: %v", fullpath, err)
|
||||
return
|
||||
}
|
||||
|
||||
if attrs&windows.FILE_ATTRIBUTE_READONLY != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
err = windows.SetFileAttributes(p,
|
||||
attrs|windows.FILE_ATTRIBUTE_READONLY,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
trace.General.Printf("failed to chmod %s: %v", fullpath, err)
|
||||
}
|
||||
}
|
||||
|
||||
func isReadOnly(fs billy.Filesystem, path string) (bool, error) {
|
||||
fullpath := filepath.Join(fs.Root(), path)
|
||||
p, err := windows.UTF16PtrFromString(fullpath)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("%w: %q", err, fullpath)
|
||||
}
|
||||
|
||||
attrs, err := windows.GetFileAttributes(p)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("%w: %q", err, fullpath)
|
||||
}
|
||||
|
||||
if attrs&windows.FILE_ATTRIBUTE_READONLY != 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
+5
@@ -48,6 +48,11 @@ func (s *IndexStorage) Index() (i *index.Index, err error) {
|
||||
|
||||
defer ioutil.CheckClose(f, &err)
|
||||
|
||||
fi, statErr := s.dir.Fs().Stat(f.Name())
|
||||
if statErr == nil {
|
||||
idx.ModTime = fi.ModTime()
|
||||
}
|
||||
|
||||
d := index.NewDecoder(f)
|
||||
err = d.Decode(idx)
|
||||
return idx, err
|
||||
|
||||
+4
@@ -69,7 +69,11 @@ type IndexStorage struct {
|
||||
index *index.Index
|
||||
}
|
||||
|
||||
// SetIndex stores the given index.
|
||||
// Note: this method sets idx.ModTime to simulate filesystem storage behavior.
|
||||
func (c *IndexStorage) SetIndex(idx *index.Index) error {
|
||||
// Set ModTime to enable racy git detection in the metadata optimization.
|
||||
idx.ModTime = time.Now()
|
||||
c.index = idx
|
||||
return nil
|
||||
}
|
||||
|
||||
+103
-5
@@ -4,9 +4,11 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
||||
"github.com/go-git/go-git/v5/plumbing/format/index"
|
||||
"github.com/go-git/go-git/v5/utils/merkletrie/noder"
|
||||
|
||||
"github.com/go-git/go-billy/v5"
|
||||
@@ -16,6 +18,14 @@ var ignore = map[string]bool{
|
||||
".git": true,
|
||||
}
|
||||
|
||||
// Options contains configuration for the filesystem node.
|
||||
type Options struct {
|
||||
// Index is used to enable the metadata-first comparison optimization while
|
||||
// correctly handling the "racy git" condition. If no index is provided,
|
||||
// the function works without the optimization.
|
||||
Index *index.Index
|
||||
}
|
||||
|
||||
// The node represents a file or a directory in a billy.Filesystem. It
|
||||
// implements the interface noder.Noder of merkletrie package.
|
||||
//
|
||||
@@ -24,6 +34,8 @@ var ignore = map[string]bool{
|
||||
type node struct {
|
||||
fs billy.Filesystem
|
||||
submodules map[string]plumbing.Hash
|
||||
idx *index.Index
|
||||
idxMap map[string]*index.Entry
|
||||
|
||||
path string
|
||||
hash []byte
|
||||
@@ -31,6 +43,7 @@ type node struct {
|
||||
isDir bool
|
||||
mode os.FileMode
|
||||
size int64
|
||||
modTime time.Time
|
||||
}
|
||||
|
||||
// NewRootNode returns the root node based on a given billy.Filesystem.
|
||||
@@ -42,7 +55,41 @@ func NewRootNode(
|
||||
fs billy.Filesystem,
|
||||
submodules map[string]plumbing.Hash,
|
||||
) noder.Noder {
|
||||
return &node{fs: fs, submodules: submodules, isDir: true}
|
||||
return NewRootNodeWithOptions(fs, submodules, Options{})
|
||||
}
|
||||
|
||||
// NewRootNodeWithOptions returns the root node based on a given billy.Filesystem
|
||||
// with options to set an index. Providing an index enables the metadata-first
|
||||
// comparison optimization while correctly handling the "racy git" condition. If
|
||||
// no index is provided, the function works without the optimization.
|
||||
//
|
||||
// The index's ModTime field is used to detect the racy git condition. When a file's
|
||||
// mtime equals or is newer than the index ModTime, we must hash the file content
|
||||
// even if other metadata matches, because the file may have been modified in the
|
||||
// same second that the index was written.
|
||||
//
|
||||
// Reference: https://git-scm.com/docs/racy-git
|
||||
func NewRootNodeWithOptions(
|
||||
fs billy.Filesystem,
|
||||
submodules map[string]plumbing.Hash,
|
||||
options Options,
|
||||
) noder.Noder {
|
||||
var idxMap map[string]*index.Entry
|
||||
|
||||
if options.Index != nil {
|
||||
idxMap = make(map[string]*index.Entry, len(options.Index.Entries))
|
||||
for _, entry := range options.Index.Entries {
|
||||
idxMap[entry.Name] = entry
|
||||
}
|
||||
}
|
||||
|
||||
return &node{
|
||||
fs: fs,
|
||||
submodules: submodules,
|
||||
idx: options.Index,
|
||||
idxMap: idxMap,
|
||||
isDir: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Hash the hash of a filesystem is the result of concatenating the computed
|
||||
@@ -133,11 +180,14 @@ func (n *node) newChildNode(file os.FileInfo) (*node, error) {
|
||||
node := &node{
|
||||
fs: n.fs,
|
||||
submodules: n.submodules,
|
||||
idx: n.idx,
|
||||
idxMap: n.idxMap,
|
||||
|
||||
path: path,
|
||||
isDir: file.IsDir(),
|
||||
size: file.Size(),
|
||||
mode: file.Mode(),
|
||||
path: path,
|
||||
isDir: file.IsDir(),
|
||||
size: file.Size(),
|
||||
mode: file.Mode(),
|
||||
modTime: file.ModTime(),
|
||||
}
|
||||
|
||||
if _, isSubmodule := n.submodules[path]; isSubmodule {
|
||||
@@ -161,6 +211,16 @@ func (n *node) calculateHash() {
|
||||
n.hash = append(submoduleHash[:], filemode.Submodule.Bytes()...)
|
||||
return
|
||||
}
|
||||
|
||||
if n.idxMap != nil {
|
||||
if entry, ok := n.idxMap[n.path]; ok {
|
||||
if n.metadataMatches(entry) {
|
||||
n.hash = append(entry.Hash[:], mode.Bytes()...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hash plumbing.Hash
|
||||
if n.mode&os.ModeSymlink != 0 {
|
||||
hash = n.doCalculateHashForSymlink()
|
||||
@@ -170,6 +230,44 @@ func (n *node) calculateHash() {
|
||||
n.hash = append(hash[:], mode.Bytes()...)
|
||||
}
|
||||
|
||||
func (n *node) metadataMatches(entry *index.Entry) bool {
|
||||
if entry == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if uint32(n.size) != entry.Size {
|
||||
return false
|
||||
}
|
||||
|
||||
if !n.modTime.IsZero() && !n.modTime.Equal(entry.ModifiedAt) {
|
||||
return false
|
||||
}
|
||||
|
||||
mode, err := filemode.NewFromOSFileMode(n.mode)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if mode != entry.Mode {
|
||||
return false
|
||||
}
|
||||
|
||||
if n.idx != nil && !n.idx.ModTime.IsZero() && !n.modTime.IsZero() {
|
||||
if !n.modTime.Before(n.idx.ModTime) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// If we couldn't perform the racy git check (idx is nil or idx.ModTime is zero),
|
||||
// we cannot safely rely on metadata alone — force content hashing.
|
||||
// This can occur with in-memory storage where the index file timestamp is unavailable.
|
||||
if n.idx == nil || n.idx.ModTime.IsZero() {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (n *node) doCalculateHashForRegular() plumbing.Hash {
|
||||
f, err := n.fs.Open(n.path)
|
||||
if err != nil {
|
||||
|
||||
+22
-11
@@ -385,7 +385,8 @@ func (w *Worktree) resetIndex(t *object.Tree, dirs []string, files []string) ([]
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var removedFiles []string
|
||||
removedFiles := make([]string, 0, len(changes))
|
||||
filesMap := buildFilePathMap(files)
|
||||
for _, ch := range changes {
|
||||
a, err := ch.Action()
|
||||
if err != nil {
|
||||
@@ -407,7 +408,7 @@ func (w *Worktree) resetIndex(t *object.Tree, dirs []string, files []string) ([]
|
||||
}
|
||||
|
||||
if len(files) > 0 {
|
||||
contains := inFiles(files, name)
|
||||
contains := inFiles(filesMap, name)
|
||||
if !contains {
|
||||
continue
|
||||
}
|
||||
@@ -436,15 +437,11 @@ func (w *Worktree) resetIndex(t *object.Tree, dirs []string, files []string) ([]
|
||||
return removedFiles, w.r.Storer.SetIndex(idx)
|
||||
}
|
||||
|
||||
func inFiles(files []string, v string) bool {
|
||||
// inFiles checks if the given file is in the list of files. The incoming filepaths in files should be cleaned before calling this function.
|
||||
func inFiles(files map[string]struct{}, v string) bool {
|
||||
v = filepath.Clean(v)
|
||||
for _, s := range files {
|
||||
if filepath.Clean(s) == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
_, exists := files[v]
|
||||
return exists
|
||||
}
|
||||
|
||||
func (w *Worktree) resetWorktree(t *object.Tree, files []string) error {
|
||||
@@ -459,6 +456,7 @@ func (w *Worktree) resetWorktree(t *object.Tree, files []string) error {
|
||||
}
|
||||
b := newIndexBuilder(idx)
|
||||
|
||||
filesMap := buildFilePathMap(files)
|
||||
for _, ch := range changes {
|
||||
if err := w.validChange(ch); err != nil {
|
||||
return err
|
||||
@@ -476,7 +474,7 @@ func (w *Worktree) resetWorktree(t *object.Tree, files []string) error {
|
||||
continue
|
||||
}
|
||||
|
||||
contains := inFiles(files, file)
|
||||
contains := inFiles(filesMap, file)
|
||||
if !contains {
|
||||
continue
|
||||
}
|
||||
@@ -1206,3 +1204,16 @@ func (b *indexBuilder) Add(e *index.Entry) {
|
||||
func (b *indexBuilder) Remove(name string) {
|
||||
delete(b.entries, filepath.ToSlash(name))
|
||||
}
|
||||
|
||||
// buildFilePathMap creates a map of cleaned file paths for efficient lookup.
|
||||
// Returns nil if the input slice is empty.
|
||||
func buildFilePathMap(files []string) map[string]struct{} {
|
||||
if len(files) == 0 {
|
||||
return nil
|
||||
}
|
||||
filesMap := make(map[string]struct{}, len(files))
|
||||
for _, f := range files {
|
||||
filesMap[filepath.Clean(f)] = struct{}{}
|
||||
}
|
||||
return filesMap
|
||||
}
|
||||
|
||||
+1
-1
@@ -141,7 +141,7 @@ func (w *Worktree) diffStagingWithWorktree(reverse, excludeIgnoredChanges bool)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
to := filesystem.NewRootNode(w.Filesystem, submodules)
|
||||
to := filesystem.NewRootNodeWithOptions(w.Filesystem, submodules, filesystem.Options{Index: idx})
|
||||
|
||||
var c merkletrie.Changes
|
||||
if reverse {
|
||||
|
||||
+9
-1
@@ -414,6 +414,9 @@ func (ctx ecKeyGenerator) genKey() ([]byte, rawHeader, error) {
|
||||
|
||||
// Decrypt the given payload and return the content encryption key.
|
||||
func (ctx ecDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) {
|
||||
if recipient == nil {
|
||||
return nil, errors.New("go-jose/go-jose: missing recipient")
|
||||
}
|
||||
epk, err := headers.getEPK()
|
||||
if err != nil {
|
||||
return nil, errors.New("go-jose/go-jose: invalid epk header")
|
||||
@@ -461,13 +464,18 @@ func (ctx ecDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientI
|
||||
return nil, ErrUnsupportedAlgorithm
|
||||
}
|
||||
|
||||
encryptedKey := recipient.encryptedKey
|
||||
if len(encryptedKey) == 0 {
|
||||
return nil, errors.New("go-jose/go-jose: missing JWE Encrypted Key")
|
||||
}
|
||||
|
||||
key := deriveKey(string(algorithm), keySize)
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return josecipher.KeyUnwrap(block, recipient.encryptedKey)
|
||||
return josecipher.KeyUnwrap(block, encryptedKey)
|
||||
}
|
||||
|
||||
func (ctx edDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) {
|
||||
|
||||
+9
-1
@@ -66,12 +66,20 @@ func KeyWrap(block cipher.Block, cek []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
// KeyUnwrap implements NIST key unwrapping; it unwraps a content encryption key (cek) with the given block cipher.
|
||||
//
|
||||
// https://datatracker.ietf.org/doc/html/rfc7518#section-4.4
|
||||
// https://datatracker.ietf.org/doc/html/rfc7518#section-4.6
|
||||
// https://datatracker.ietf.org/doc/html/rfc7518#section-4.8
|
||||
func KeyUnwrap(block cipher.Block, ciphertext []byte) ([]byte, error) {
|
||||
n := (len(ciphertext) / 8) - 1
|
||||
if n <= 0 {
|
||||
return nil, errors.New("go-jose/go-jose: JWE Encrypted Key too short")
|
||||
}
|
||||
|
||||
if len(ciphertext)%8 != 0 {
|
||||
return nil, errors.New("go-jose/go-jose: key wrap input must be 8 byte blocks")
|
||||
}
|
||||
|
||||
n := (len(ciphertext) / 8) - 1
|
||||
r := make([][]byte, n)
|
||||
|
||||
for i := range r {
|
||||
|
||||
+18
-8
@@ -366,11 +366,21 @@ func (ctx *symmetricKeyCipher) encryptKey(cek []byte, alg KeyAlgorithm) (recipie
|
||||
|
||||
// Decrypt the content encryption key.
|
||||
func (ctx *symmetricKeyCipher) decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) {
|
||||
switch headers.getAlgorithm() {
|
||||
case DIRECT:
|
||||
cek := make([]byte, len(ctx.key))
|
||||
copy(cek, ctx.key)
|
||||
return cek, nil
|
||||
if recipient == nil {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: missing recipient")
|
||||
}
|
||||
|
||||
alg := headers.getAlgorithm()
|
||||
if alg == DIRECT {
|
||||
return bytes.Clone(ctx.key), nil
|
||||
}
|
||||
|
||||
encryptedKey := recipient.encryptedKey
|
||||
if len(encryptedKey) == 0 {
|
||||
return nil, fmt.Errorf("go-jose/go-jose: missing JWE Encrypted Key")
|
||||
}
|
||||
|
||||
switch alg {
|
||||
case A128GCMKW, A192GCMKW, A256GCMKW:
|
||||
aead := newAESGCM(len(ctx.key))
|
||||
|
||||
@@ -385,7 +395,7 @@ func (ctx *symmetricKeyCipher) decryptKey(headers rawHeader, recipient *recipien
|
||||
|
||||
parts := &aeadParts{
|
||||
iv: iv.bytes(),
|
||||
ciphertext: recipient.encryptedKey,
|
||||
ciphertext: encryptedKey,
|
||||
tag: tag.bytes(),
|
||||
}
|
||||
|
||||
@@ -401,7 +411,7 @@ func (ctx *symmetricKeyCipher) decryptKey(headers rawHeader, recipient *recipien
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cek, err := josecipher.KeyUnwrap(block, recipient.encryptedKey)
|
||||
cek, err := josecipher.KeyUnwrap(block, encryptedKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -445,7 +455,7 @@ func (ctx *symmetricKeyCipher) decryptKey(headers rawHeader, recipient *recipien
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cek, err := josecipher.KeyUnwrap(block, recipient.encryptedKey)
|
||||
cek, err := josecipher.KeyUnwrap(block, encryptedKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
-2
@@ -26,10 +26,8 @@ import (
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/json"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/ldap"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/machine"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/nextcloud"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/ocmshares"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/oidc"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/owncloudsql"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/publicshares"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/serviceaccounts"
|
||||
// Add your own here
|
||||
|
||||
-197
@@ -1,197 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
// Package nextcloud verifies a clientID and clientSecret against a Nextcloud backend.
|
||||
package nextcloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/auth"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("nextcloud", New)
|
||||
}
|
||||
|
||||
// Manager is the Nextcloud-based implementation of the auth.Manager interface
|
||||
// see https://github.com/cs3org/reva/blob/v1.13.0/pkg/auth/auth.go#L32-L35
|
||||
type Manager struct {
|
||||
client *http.Client
|
||||
sharedSecret string
|
||||
endPoint string
|
||||
}
|
||||
|
||||
// AuthManagerConfig contains config for a Nextcloud-based AuthManager
|
||||
type AuthManagerConfig struct {
|
||||
EndPoint string `mapstructure:"endpoint" docs:";The Nextcloud backend endpoint for user check"`
|
||||
SharedSecret string `mapstructure:"shared_secret"`
|
||||
MockHTTP bool `mapstructure:"mock_http"`
|
||||
}
|
||||
|
||||
// Action describes a REST request to forward to the Nextcloud backend
|
||||
type Action struct {
|
||||
verb string
|
||||
username string
|
||||
argS string
|
||||
}
|
||||
|
||||
func (c *AuthManagerConfig) init() {
|
||||
}
|
||||
|
||||
func parseConfig(m map[string]interface{}) (*AuthManagerConfig, error) {
|
||||
c := &AuthManagerConfig{}
|
||||
if err := mapstructure.Decode(m, c); err != nil {
|
||||
err = errors.Wrap(err, "error decoding conf")
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// New returns an auth manager implementation that verifies against a Nextcloud backend.
|
||||
func New(m map[string]interface{}) (auth.Manager, error) {
|
||||
c, err := parseConfig(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.init()
|
||||
|
||||
return NewAuthManager(c)
|
||||
}
|
||||
|
||||
// NewAuthManager returns a new Nextcloud-based AuthManager
|
||||
func NewAuthManager(c *AuthManagerConfig) (*Manager, error) {
|
||||
var client *http.Client
|
||||
if c.MockHTTP {
|
||||
// called := make([]string, 0)
|
||||
// nextcloudServerMock := GetNextcloudServerMock(&called)
|
||||
// client, _ = TestingHTTPClient(nextcloudServerMock)
|
||||
|
||||
// Wait for SetHTTPClient to be called later
|
||||
client = nil
|
||||
} else {
|
||||
if len(c.EndPoint) == 0 {
|
||||
return nil, errors.New("Please specify 'endpoint' in '[grpc.services.authprovider.auth_managers.nextcloud]'")
|
||||
}
|
||||
client = &http.Client{}
|
||||
}
|
||||
|
||||
return &Manager{
|
||||
endPoint: c.EndPoint, // e.g. "http://nc/apps/sciencemesh/"
|
||||
sharedSecret: c.SharedSecret,
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Configure method as defined in https://github.com/cs3org/reva/blob/v1.13.0/pkg/auth/auth.go#L32-L35
|
||||
func (am *Manager) Configure(ml map[string]interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetHTTPClient sets the HTTP client
|
||||
func (am *Manager) SetHTTPClient(c *http.Client) {
|
||||
am.client = c
|
||||
}
|
||||
|
||||
func (am *Manager) do(ctx context.Context, a Action) (int, []byte, error) {
|
||||
log := appctx.GetLogger(ctx)
|
||||
url := am.endPoint + "~" + a.username + "/api/auth/" + a.verb
|
||||
log.Info().Msgf("am.do %s %s %s", url, a.argS, am.sharedSecret)
|
||||
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(a.argS))
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
req.Header.Set("X-Reva-Secret", am.sharedSecret)
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := am.client.Do(req)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
log.Info().Msgf("am.do response %d %s", resp.StatusCode, body)
|
||||
return resp.StatusCode, body, nil
|
||||
}
|
||||
|
||||
// Authenticate method as defined in https://github.com/cs3org/reva/blob/28500a8/pkg/auth/auth.go#L31-L33
|
||||
func (am *Manager) Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, map[string]*authpb.Scope, error) {
|
||||
type paramsObj struct {
|
||||
ClientID string `json:"clientID"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
// Scope authpb.Scope
|
||||
}
|
||||
bodyObj := ¶msObj{
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
// Scope: authpb.Scope{
|
||||
// Resource: &types.OpaqueEntry{
|
||||
// Decoder: "json",
|
||||
// Value: []byte(`{"resource_id":{"storage_id":"storage-id","opaque_id":"opaque-id"},"path":"some/file/path.txt"}`),
|
||||
// },
|
||||
// Role: authpb.Role_ROLE_OWNER,
|
||||
// },
|
||||
}
|
||||
bodyStr, err := json.Marshal(bodyObj)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
log := appctx.GetLogger(ctx)
|
||||
log.Info().Msgf("Authenticate %s %s", clientID, bodyStr)
|
||||
|
||||
statusCode, body, err := am.do(ctx, Action{"Authenticate", clientID, string(bodyStr)})
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if statusCode != 200 {
|
||||
return nil, nil, errors.New("Username/password not recognized by Nextcloud backend")
|
||||
}
|
||||
|
||||
type resultsObj struct {
|
||||
User user.User `json:"user"`
|
||||
Scopes map[string]*authpb.Scope `json:"scopes"`
|
||||
}
|
||||
result := &resultsObj{}
|
||||
err = json.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var pointersMap = make(map[string]*authpb.Scope)
|
||||
for k := range result.Scopes {
|
||||
scope := result.Scopes[k]
|
||||
pointersMap[k] = scope
|
||||
}
|
||||
return &result.User, pointersMap, nil
|
||||
}
|
||||
Generated
Vendored
-99
@@ -1,99 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package nextcloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Response contains data for the Nextcloud mock server to respond
|
||||
// and to switch to a new server state
|
||||
type Response struct {
|
||||
code int
|
||||
body string
|
||||
newServerState string
|
||||
}
|
||||
|
||||
const serverStateError = "ERROR"
|
||||
const serverStateEmpty = "EMPTY"
|
||||
const serverStateHome = "HOME"
|
||||
|
||||
var serverState = serverStateEmpty
|
||||
|
||||
var responses = map[string]Response{
|
||||
`POST /apps/sciencemesh/~einstein/api/auth/Authenticate {"clientID":"einstein","clientSecret":"relativity"}`: {200, `{"user":{"id":{"idp":"some-idp","opaque_id":"some-opaque-user-id","type":1}},"scopes":{"user":{"resource":{"decoder":"json","value":"eyJyZXNvdXJjZV9pZCI6eyJzdG9yYWdlX2lkIjoic3RvcmFnZS1pZCIsIm9wYXF1ZV9pZCI6Im9wYXF1ZS1pZCJ9LCJwYXRoIjoic29tZS9maWxlL3BhdGgudHh0In0="},"role":1}}}`, serverStateHome},
|
||||
}
|
||||
|
||||
// GetNextcloudServerMock returns a handler that pretends to be a remote Nextcloud server
|
||||
func GetNextcloudServerMock(called *[]string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
buf := new(strings.Builder)
|
||||
_, err := io.Copy(buf, r.Body)
|
||||
if err != nil {
|
||||
panic("Error reading response into buffer")
|
||||
}
|
||||
var key = fmt.Sprintf("%s %s %s", r.Method, r.URL, buf.String())
|
||||
*called = append(*called, key)
|
||||
response := responses[key]
|
||||
if (response == Response{}) {
|
||||
key = fmt.Sprintf("%s %s %s %s", r.Method, r.URL, buf.String(), serverState)
|
||||
response = responses[key]
|
||||
}
|
||||
if (response == Response{}) {
|
||||
fmt.Printf("%s %s %s %s", r.Method, r.URL, buf.String(), serverState)
|
||||
response = Response{500, fmt.Sprintf("response not defined! %s", key), serverStateEmpty}
|
||||
}
|
||||
serverState = responses[key].newServerState
|
||||
if serverState == `` {
|
||||
serverState = serverStateError
|
||||
}
|
||||
w.WriteHeader(response.code)
|
||||
// w.Header().Set("Etag", "mocker-etag")
|
||||
_, err = w.Write([]byte(responses[key].body))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestingHTTPClient thanks to https://itnext.io/how-to-stub-requests-to-remote-hosts-with-go-6c2c1db32bf2
|
||||
// Ideally, this function would live in tests/helpers, but
|
||||
// if we put it there, it gets excluded by .dockerignore, and the
|
||||
// Docker build fails (see https://github.com/cs3org/reva/issues/1999)
|
||||
// So putting it here for now - open to suggestions if someone knows
|
||||
// a better way to inject this.
|
||||
func TestingHTTPClient(handler http.Handler) (*http.Client, func()) {
|
||||
s := httptest.NewServer(handler)
|
||||
|
||||
cli := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(_ context.Context, network, _ string) (net.Conn, error) {
|
||||
return net.Dial(network, s.Listener.Addr().String())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return cli, s.Close
|
||||
}
|
||||
Generated
Vendored
-165
@@ -1,165 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Accounts represents oc10-style Accounts
|
||||
type Accounts struct {
|
||||
driver string
|
||||
db *sql.DB
|
||||
joinUsername, joinUUID, enableMedialSearch bool
|
||||
selectSQL string
|
||||
}
|
||||
|
||||
// NewMysql returns a new accounts instance connecting to a MySQL database
|
||||
func NewMysql(dsn string, joinUsername, joinUUID, enableMedialSearch bool) (*Accounts, error) {
|
||||
sqldb, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error connecting to the database")
|
||||
}
|
||||
|
||||
// FIXME make configurable
|
||||
sqldb.SetConnMaxLifetime(time.Minute * 3)
|
||||
sqldb.SetConnMaxIdleTime(time.Second * 30)
|
||||
sqldb.SetMaxOpenConns(100)
|
||||
sqldb.SetMaxIdleConns(10)
|
||||
|
||||
err = sqldb.Ping()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error connecting to the database")
|
||||
}
|
||||
|
||||
return New("mysql", sqldb, joinUsername, joinUUID, enableMedialSearch)
|
||||
}
|
||||
|
||||
// New returns a new accounts instance connecting to the given sql.DB
|
||||
func New(driver string, sqldb *sql.DB, joinUsername, joinUUID, enableMedialSearch bool) (*Accounts, error) {
|
||||
|
||||
sel := "SELECT id, email, user_id, display_name, quota, last_login, backend, home, state, password"
|
||||
from := `
|
||||
FROM oc_accounts a
|
||||
LEFT JOIN oc_users u
|
||||
ON a.user_id=u.uid
|
||||
`
|
||||
if joinUsername {
|
||||
sel += ", p.configvalue AS username"
|
||||
from += `LEFT JOIN oc_preferences p
|
||||
ON a.user_id=p.userid
|
||||
AND p.appid='core'
|
||||
AND p.configkey='username'`
|
||||
} else {
|
||||
// fallback to user_id as username
|
||||
sel += ", user_id AS username"
|
||||
}
|
||||
if joinUUID {
|
||||
sel += ", p2.configvalue AS ownclouduuid"
|
||||
from += `LEFT JOIN oc_preferences p2
|
||||
ON a.user_id=p2.userid
|
||||
AND p2.appid='core'
|
||||
AND p2.configkey='ownclouduuid'`
|
||||
} else {
|
||||
// fallback to user_id as ownclouduuid
|
||||
sel += ", user_id AS ownclouduuid"
|
||||
}
|
||||
|
||||
return &Accounts{
|
||||
driver: driver,
|
||||
db: sqldb,
|
||||
joinUsername: joinUsername,
|
||||
joinUUID: joinUUID,
|
||||
enableMedialSearch: enableMedialSearch,
|
||||
selectSQL: sel + from,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Account stores information about accounts.
|
||||
type Account struct {
|
||||
ID uint64
|
||||
Email sql.NullString
|
||||
UserID string
|
||||
DisplayName sql.NullString
|
||||
Quota sql.NullString
|
||||
LastLogin int
|
||||
Backend string
|
||||
Home string
|
||||
State int8
|
||||
PasswordHash string // from oc_users
|
||||
Username sql.NullString // optional comes from the oc_preferences
|
||||
OwnCloudUUID sql.NullString // optional comes from the oc_preferences
|
||||
}
|
||||
|
||||
func (as *Accounts) rowToAccount(ctx context.Context, row Scannable) (*Account, error) {
|
||||
a := Account{}
|
||||
if err := row.Scan(&a.ID, &a.Email, &a.UserID, &a.DisplayName, &a.Quota, &a.LastLogin, &a.Backend, &a.Home, &a.State, &a.PasswordHash, &a.Username, &a.OwnCloudUUID); err != nil {
|
||||
appctx.GetLogger(ctx).Error().Err(err).Msg("could not scan row, skipping")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &a, nil
|
||||
}
|
||||
|
||||
// Scannable describes the interface providing a Scan method
|
||||
type Scannable interface {
|
||||
Scan(...interface{}) error
|
||||
}
|
||||
|
||||
// GetAccountByLogin fetches an account by mail or username
|
||||
func (as *Accounts) GetAccountByLogin(ctx context.Context, login string) (*Account, error) {
|
||||
var row *sql.Row
|
||||
username := strings.ToLower(login) // usernames are lowercased in owncloud classic
|
||||
if as.joinUsername {
|
||||
row = as.db.QueryRowContext(ctx, as.selectSQL+" WHERE a.email=? OR a.lower_user_id=? OR p.configvalue=?", login, username, login)
|
||||
} else {
|
||||
row = as.db.QueryRowContext(ctx, as.selectSQL+" WHERE a.email=? OR a.lower_user_id=?", login, username)
|
||||
}
|
||||
|
||||
return as.rowToAccount(ctx, row)
|
||||
}
|
||||
|
||||
// GetAccountGroups reads the groups for an account
|
||||
func (as *Accounts) GetAccountGroups(ctx context.Context, uid string) ([]string, error) {
|
||||
rows, err := as.db.QueryContext(ctx, "SELECT gid FROM oc_group_user WHERE uid=?", uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var group string
|
||||
groups := []string{}
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&group); err != nil {
|
||||
appctx.GetLogger(ctx).Error().Err(err).Msg("could not scan row, skipping")
|
||||
continue
|
||||
}
|
||||
groups = append(groups, group)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
-192
@@ -1,192 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package owncloudsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/auth"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/owncloudsql/accounts"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
// Provides mysql drivers
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("owncloudsql", NewMysql)
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
c *config
|
||||
db *accounts.Accounts
|
||||
}
|
||||
|
||||
type config struct {
|
||||
DbUsername string `mapstructure:"dbusername"`
|
||||
DbPassword string `mapstructure:"dbpassword"`
|
||||
DbHost string `mapstructure:"dbhost"`
|
||||
DbPort int `mapstructure:"dbport"`
|
||||
DbName string `mapstructure:"dbname"`
|
||||
Idp string `mapstructure:"idp"`
|
||||
Nobody int64 `mapstructure:"nobody"`
|
||||
LegacySalt string `mapstructure:"legacy_salt"`
|
||||
JoinUsername bool `mapstructure:"join_username"`
|
||||
JoinOwnCloudUUID bool `mapstructure:"join_ownclouduuid"`
|
||||
}
|
||||
|
||||
// NewMysql returns a new auth manager connection to an owncloud mysql database
|
||||
func NewMysql(m map[string]interface{}) (auth.Manager, error) {
|
||||
mgr := &manager{}
|
||||
err := mgr.Configure(m)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "error creating a new auth manager")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mgr.db, err = accounts.NewMysql(
|
||||
fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", mgr.c.DbUsername, mgr.c.DbPassword, mgr.c.DbHost, mgr.c.DbPort, mgr.c.DbName),
|
||||
mgr.c.JoinUsername,
|
||||
mgr.c.JoinOwnCloudUUID,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mgr, nil
|
||||
}
|
||||
|
||||
func (m *manager) Configure(ml map[string]interface{}) error {
|
||||
c, err := parseConfig(ml)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Nobody == 0 {
|
||||
c.Nobody = 99
|
||||
}
|
||||
|
||||
m.c = c
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
c := &config{}
|
||||
if err := mapstructure.Decode(m, &c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (m *manager) Authenticate(ctx context.Context, login, clientSecret string) (*user.User, map[string]*authpb.Scope, error) {
|
||||
log := appctx.GetLogger(ctx)
|
||||
|
||||
// 1. find user by login
|
||||
|
||||
account, err := m.db.GetAccountByLogin(ctx, login)
|
||||
if err != nil {
|
||||
return nil, nil, errtypes.NotFound(login)
|
||||
}
|
||||
// 2. verify the user password
|
||||
if !m.verify(clientSecret, account.PasswordHash) {
|
||||
return nil, nil, errtypes.InvalidCredentials(login)
|
||||
}
|
||||
|
||||
userID := &user.UserId{
|
||||
Idp: m.c.Idp,
|
||||
OpaqueId: account.OwnCloudUUID.String,
|
||||
Type: user.UserType_USER_TYPE_PRIMARY, // TODO: assign the appropriate user type for guest accounts
|
||||
}
|
||||
|
||||
u := &user.User{
|
||||
Id: userID,
|
||||
// TODO add more claims from the StandardClaims, eg EmailVerified and lastlogin
|
||||
Username: account.Username.String,
|
||||
Mail: account.Email.String,
|
||||
DisplayName: account.DisplayName.String,
|
||||
//UidNumber: uidNumber,
|
||||
//GidNumber: gidNumber,
|
||||
}
|
||||
|
||||
if u.Groups, err = m.db.GetAccountGroups(ctx, account.UserID); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var scopes map[string]*authpb.Scope
|
||||
if userID != nil && (userID.Type == user.UserType_USER_TYPE_LIGHTWEIGHT || userID.Type == user.UserType_USER_TYPE_FEDERATED) {
|
||||
scopes, err = scope.AddLightweightAccountScope(authpb.Role_ROLE_OWNER, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
} else {
|
||||
scopes, err = scope.AddOwnerScope(nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
// do not log password hash
|
||||
account.PasswordHash = "***redacted***"
|
||||
log.Debug().Interface("account", account).Interface("user", u).Msg("authenticated user")
|
||||
|
||||
return u, scopes, nil
|
||||
}
|
||||
|
||||
func (m *manager) verify(password, hash string) bool {
|
||||
splitHash := strings.SplitN(hash, "|", 2)
|
||||
switch len(splitHash) {
|
||||
case 2:
|
||||
if splitHash[0] == "1" {
|
||||
return m.verifyHashV1(password, splitHash[1])
|
||||
}
|
||||
case 1:
|
||||
return m.legacyHashVerify(password, hash)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *manager) legacyHashVerify(password, hash string) bool {
|
||||
// TODO rehash $newHash = $this->hash($message);
|
||||
switch len(hash) {
|
||||
case 60: // legacy PHPass hash
|
||||
return nil == bcrypt.CompareHashAndPassword([]byte(hash), []byte(password+m.c.LegacySalt))
|
||||
case 40: // legacy sha1 hash
|
||||
h := sha1.Sum([]byte(password))
|
||||
return hmac.Equal([]byte(hash), []byte(hex.EncodeToString(h[:])))
|
||||
}
|
||||
return false
|
||||
}
|
||||
func (m *manager) verifyHashV1(password, hash string) bool {
|
||||
// TODO implement password_needs_rehash
|
||||
return nil == bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
}
|
||||
Generated
Vendored
+1
@@ -435,6 +435,7 @@ func (session *DecomposedFsSession) Cleanup(revertNodeMetadata, cleanBin, cleanI
|
||||
revisionNode, err := node.ReadNode(ctx, session.store.lu, session.SpaceID(), versionID, "", false, n.SpaceRoot, false)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Str("versionID", versionID).Msg("reading revision node failed")
|
||||
return
|
||||
}
|
||||
|
||||
if !revisionNode.Exists {
|
||||
|
||||
-2
@@ -24,7 +24,5 @@ import (
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/user/manager/json"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/user/manager/ldap"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/user/manager/memory"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/user/manager/nextcloud"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/user/manager/owncloudsql"
|
||||
// Add your own here
|
||||
)
|
||||
|
||||
-249
@@ -1,249 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package nextcloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/user"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/user/manager/registry"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
// "github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("nextcloud", New)
|
||||
}
|
||||
|
||||
// Manager is the Nextcloud-based implementation of the share.Manager interface
|
||||
// see https://github.com/cs3org/reva/blob/v1.13.0/pkg/user/user.go#L29-L35
|
||||
type Manager struct {
|
||||
client *http.Client
|
||||
sharedSecret string
|
||||
endPoint string
|
||||
}
|
||||
|
||||
// UserManagerConfig contains config for a Nextcloud-based UserManager
|
||||
type UserManagerConfig struct {
|
||||
EndPoint string `mapstructure:"endpoint" docs:";The Nextcloud backend endpoint for user management"`
|
||||
SharedSecret string `mapstructure:"shared_secret"`
|
||||
MockHTTP bool `mapstructure:"mock_http"`
|
||||
}
|
||||
|
||||
func (c *UserManagerConfig) init() {
|
||||
if c.EndPoint == "" {
|
||||
c.EndPoint = "http://localhost/end/point?"
|
||||
}
|
||||
}
|
||||
|
||||
func parseConfig(m map[string]interface{}) (*UserManagerConfig, error) {
|
||||
c := &UserManagerConfig{}
|
||||
if err := mapstructure.Decode(m, c); err != nil {
|
||||
err = errors.Wrap(err, "error decoding conf")
|
||||
return nil, err
|
||||
}
|
||||
c.init()
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Action describes a REST request to forward to the Nextcloud backend
|
||||
type Action struct {
|
||||
verb string
|
||||
argS string
|
||||
}
|
||||
|
||||
// New returns a user manager implementation that reads a json file to provide user metadata.
|
||||
func New(m map[string]interface{}) (user.Manager, error) {
|
||||
c, err := parseConfig(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.init()
|
||||
|
||||
return NewUserManager(c)
|
||||
}
|
||||
|
||||
// NewUserManager returns a new Nextcloud-based UserManager
|
||||
func NewUserManager(c *UserManagerConfig) (*Manager, error) {
|
||||
var client *http.Client
|
||||
if c.MockHTTP {
|
||||
// Wait for SetHTTPClient to be called later
|
||||
client = nil
|
||||
} else {
|
||||
if len(c.EndPoint) == 0 {
|
||||
return nil, errors.New("Please specify 'endpoint' in '[grpc.services.userprovider.drivers.nextcloud]'")
|
||||
}
|
||||
client = &http.Client{}
|
||||
}
|
||||
|
||||
return &Manager{
|
||||
endPoint: c.EndPoint, // e.g. "http://nc/apps/sciencemesh/"
|
||||
sharedSecret: c.SharedSecret,
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetHTTPClient sets the HTTP client
|
||||
func (um *Manager) SetHTTPClient(c *http.Client) {
|
||||
um.client = c
|
||||
}
|
||||
|
||||
func getUser(ctx context.Context) (*userpb.User, error) {
|
||||
u, ok := ctxpkg.ContextGetUser(ctx)
|
||||
if !ok {
|
||||
err := errors.Wrap(errtypes.UserRequired(""), "nextcloud storage driver: error getting user from ctx")
|
||||
return nil, err
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (um *Manager) do(ctx context.Context, a Action, username string) (int, []byte, error) {
|
||||
url := um.endPoint + "~" + username + "/api/user/" + a.verb
|
||||
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(a.argS))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
req.Header.Set("X-Reva-Secret", um.sharedSecret)
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
fmt.Println(url)
|
||||
resp, err := um.client.Do(req)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
return resp.StatusCode, body, err
|
||||
}
|
||||
|
||||
// Configure method as defined in https://github.com/cs3org/reva/blob/v1.13.0/pkg/user/user.go#L29-L35
|
||||
func (um *Manager) Configure(ml map[string]interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUser method as defined in https://github.com/cs3org/reva/blob/v1.13.0/pkg/user/user.go#L29-L35
|
||||
func (um *Manager) GetUser(ctx context.Context, uid *userpb.UserId, skipFetchingGroups bool) (*userpb.User, error) {
|
||||
if uid.GetTenantId() != "" {
|
||||
return nil, errtypes.NotSupported("tenant filter not supported in nextcloud user manager")
|
||||
}
|
||||
bodyStr, _ := json.Marshal(uid)
|
||||
_, respBody, err := um.do(ctx, Action{"GetUser", string(bodyStr)}, "unauthenticated")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &userpb.User{}
|
||||
err = json.Unmarshal(respBody, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
// GetUserByClaim method as defined in https://github.com/cs3org/reva/blob/v1.13.0/pkg/user/user.go#L29-L35
|
||||
func (um *Manager) GetUserByClaim(ctx context.Context, claim, value, tenantID string, skipFetchingGroups bool) (*userpb.User, error) {
|
||||
if tenantID != "" {
|
||||
return nil, errtypes.NotSupported("tenant filter not supported in nextcloud user manager")
|
||||
}
|
||||
type paramsObj struct {
|
||||
Claim string `json:"claim"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
bodyObj := ¶msObj{
|
||||
Claim: claim,
|
||||
Value: value,
|
||||
}
|
||||
user, err := getUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bodyStr, _ := json.Marshal(bodyObj)
|
||||
_, respBody, err := um.do(ctx, Action{"GetUserByClaim", string(bodyStr)}, user.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &userpb.User{}
|
||||
err = json.Unmarshal(respBody, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
// GetUserGroups method as defined in https://github.com/cs3org/reva/blob/v1.13.0/pkg/user/user.go#L29-L35
|
||||
func (um *Manager) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]string, error) {
|
||||
bodyStr, err := json.Marshal(uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := getUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, respBody, err := um.do(ctx, Action{"GetUserGroups", string(bodyStr)}, user.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var gs []string
|
||||
err = json.Unmarshal(respBody, &gs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gs, err
|
||||
}
|
||||
|
||||
// FindUsers method as defined in https://github.com/cs3org/reva/blob/v1.13.0/pkg/user/user.go#L29-L35
|
||||
func (um *Manager) FindUsers(ctx context.Context, query, tenantID string, skipFetchingGroups bool) ([]*userpb.User, error) {
|
||||
if tenantID != "" {
|
||||
return nil, errtypes.NotSupported("tenant filter not supported in nextcloud user manager")
|
||||
}
|
||||
|
||||
user, err := getUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, respBody, err := um.do(ctx, Action{"FindUsers", query}, user.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var respArr []userpb.User
|
||||
err = json.Unmarshal(respBody, &respArr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pointers = make([]*userpb.User, len(respArr))
|
||||
for i := 0; i < len(respArr); i++ {
|
||||
pointers[i] = &respArr[i]
|
||||
}
|
||||
return pointers, err
|
||||
}
|
||||
Generated
Vendored
-103
@@ -1,103 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package nextcloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Response contains data for the Nextcloud mock server to respond
|
||||
// and to switch to a new server state
|
||||
type Response struct {
|
||||
code int
|
||||
body string
|
||||
newServerState string
|
||||
}
|
||||
|
||||
const serverStateError = "ERROR"
|
||||
const serverStateEmpty = "EMPTY"
|
||||
const serverStateHome = "HOME"
|
||||
|
||||
var serverState = serverStateEmpty
|
||||
|
||||
var responses = map[string]Response{
|
||||
`POST /apps/sciencemesh/~unauthenticated/api/user/GetUser {"idp":"some-idp","opaque_id":"some-opaque-user-id","type":1}`: {200, `{"id":{"idp":"some-idp","opaque_id":"some-opaque-user-id","type":1}}`, serverStateHome},
|
||||
`POST /apps/sciencemesh/~tester/api/user/GetUserByClaim {"claim":"claim-string","value":"value-string"}`: {200, `{"id":{"idp":"some-idp","opaque_id":"some-opaque-user-id","type":1}}`, serverStateHome},
|
||||
`POST /apps/sciencemesh/~tester/api/user/GetUserGroups {"idp":"some-idp","opaque_id":"some-opaque-user-id","type":1}`: {200, `["wine-lovers"]`, serverStateHome},
|
||||
`POST /apps/sciencemesh/~tester/api/user/FindUsers some-query`: {200, `[{"id":{"idp":"some-idp","opaque_id":"some-opaque-user-id","type":1}}]`, serverStateHome},
|
||||
}
|
||||
|
||||
// GetNextcloudServerMock returns a handler that pretends to be a remote Nextcloud server
|
||||
func GetNextcloudServerMock(called *[]string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
buf := new(strings.Builder)
|
||||
_, err := io.Copy(buf, r.Body)
|
||||
if err != nil {
|
||||
panic("Error reading response into buffer")
|
||||
}
|
||||
var key = fmt.Sprintf("%s %s %s", r.Method, r.URL, buf.String())
|
||||
*called = append(*called, key)
|
||||
response := responses[key]
|
||||
if (response == Response{}) {
|
||||
key = fmt.Sprintf("%s %s %s %s", r.Method, r.URL, buf.String(), serverState)
|
||||
// *called = append(*called, key)
|
||||
response = responses[key]
|
||||
}
|
||||
if (response == Response{}) {
|
||||
fmt.Printf("%s %s %s %s", r.Method, r.URL, buf.String(), serverState)
|
||||
response = Response{500, fmt.Sprintf("response not defined! %s", key), serverStateEmpty}
|
||||
}
|
||||
serverState = responses[key].newServerState
|
||||
if serverState == `` {
|
||||
serverState = serverStateError
|
||||
}
|
||||
w.WriteHeader(response.code)
|
||||
// w.Header().Set("Etag", "mocker-etag")
|
||||
_, err = w.Write([]byte(responses[key].body))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestingHTTPClient thanks to https://itnext.io/how-to-stub-requests-to-remote-hosts-with-go-6c2c1db32bf2
|
||||
// Ideally, this function would live in tests/helpers, but
|
||||
// if we put it there, it gets excluded by .dockerignore, and the
|
||||
// Docker build fails (see https://github.com/cs3org/reva/issues/1999)
|
||||
// So putting it here for now - open to suggestions if someone knows
|
||||
// a better way to inject this.
|
||||
func TestingHTTPClient(handler http.Handler) (*http.Client, func()) {
|
||||
s := httptest.NewServer(handler)
|
||||
|
||||
cli := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(_ context.Context, network, _ string) (net.Conn, error) {
|
||||
return net.Dial(network, s.Listener.Addr().String())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return cli, s.Close
|
||||
}
|
||||
Generated
Vendored
-228
@@ -1,228 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Accounts represents oc10-style Accounts
|
||||
type Accounts struct {
|
||||
driver string
|
||||
db *sql.DB
|
||||
joinUsername, joinUUID, enableMedialSearch bool
|
||||
selectSQL string
|
||||
}
|
||||
|
||||
// NewMysql returns a new Cache instance connecting to a MySQL database
|
||||
func NewMysql(dsn string, joinUsername, joinUUID, enableMedialSearch bool) (*Accounts, error) {
|
||||
sqldb, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error connecting to the database")
|
||||
}
|
||||
|
||||
// FIXME make configurable
|
||||
sqldb.SetConnMaxLifetime(time.Minute * 3)
|
||||
sqldb.SetConnMaxIdleTime(time.Second * 30)
|
||||
sqldb.SetMaxOpenConns(100)
|
||||
sqldb.SetMaxIdleConns(10)
|
||||
|
||||
err = sqldb.Ping()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error connecting to the database")
|
||||
}
|
||||
|
||||
return New("mysql", sqldb, joinUsername, joinUUID, enableMedialSearch)
|
||||
}
|
||||
|
||||
// New returns a new Cache instance connecting to the given sql.DB
|
||||
func New(driver string, sqldb *sql.DB, joinUsername, joinUUID, enableMedialSearch bool) (*Accounts, error) {
|
||||
|
||||
sel := "SELECT id, email, user_id, display_name, quota, last_login, backend, home, state"
|
||||
from := `
|
||||
FROM oc_accounts a
|
||||
`
|
||||
if joinUsername {
|
||||
sel += ", p.configvalue AS username"
|
||||
from += `LEFT JOIN oc_preferences p
|
||||
ON a.user_id=p.userid
|
||||
AND p.appid='core'
|
||||
AND p.configkey='username'`
|
||||
} else {
|
||||
// fallback to user_id as username
|
||||
sel += ", user_id AS username"
|
||||
}
|
||||
if joinUUID {
|
||||
sel += ", p2.configvalue AS ownclouduuid"
|
||||
from += `LEFT JOIN oc_preferences p2
|
||||
ON a.user_id=p2.userid
|
||||
AND p2.appid='core'
|
||||
AND p2.configkey='ownclouduuid'`
|
||||
} else {
|
||||
// fallback to user_id as ownclouduuid
|
||||
sel += ", user_id AS ownclouduuid"
|
||||
}
|
||||
|
||||
return &Accounts{
|
||||
driver: driver,
|
||||
db: sqldb,
|
||||
joinUsername: joinUsername,
|
||||
joinUUID: joinUUID,
|
||||
enableMedialSearch: enableMedialSearch,
|
||||
selectSQL: sel + from,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Account stores information about accounts.
|
||||
type Account struct {
|
||||
ID uint64
|
||||
Email sql.NullString
|
||||
UserID string
|
||||
DisplayName sql.NullString
|
||||
Quota sql.NullString
|
||||
LastLogin int
|
||||
Backend string
|
||||
Home string
|
||||
State int8
|
||||
Username sql.NullString // optional comes from the oc_preferences
|
||||
OwnCloudUUID sql.NullString // optional comes from the oc_preferences
|
||||
}
|
||||
|
||||
func (as *Accounts) rowToAccount(ctx context.Context, row Scannable) (*Account, error) {
|
||||
a := Account{}
|
||||
if err := row.Scan(&a.ID, &a.Email, &a.UserID, &a.DisplayName, &a.Quota, &a.LastLogin, &a.Backend, &a.Home, &a.State, &a.Username, &a.OwnCloudUUID); err != nil {
|
||||
appctx.GetLogger(ctx).Error().Err(err).Msg("could not scan row, skipping")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &a, nil
|
||||
}
|
||||
|
||||
// Scannable describes the interface providing a Scan method
|
||||
type Scannable interface {
|
||||
Scan(...interface{}) error
|
||||
}
|
||||
|
||||
// GetAccountByClaim fetches an account by mail, username or userid
|
||||
func (as *Accounts) GetAccountByClaim(ctx context.Context, claim, value string) (*Account, error) {
|
||||
// TODO align supported claims with rest driver and the others, maybe refactor into common mapping
|
||||
var row *sql.Row
|
||||
var where string
|
||||
switch claim {
|
||||
case "mail":
|
||||
where = "WHERE a.email=?"
|
||||
// case "uid":
|
||||
// claim = m.c.Schema.UIDNumber
|
||||
// case "gid":
|
||||
// claim = m.c.Schema.GIDNumber
|
||||
case "username":
|
||||
if as.joinUsername {
|
||||
where = "WHERE p.configvalue=?"
|
||||
} else {
|
||||
// use user_id as username
|
||||
where = "WHERE a.user_id=?"
|
||||
}
|
||||
case "userid":
|
||||
if as.joinUUID {
|
||||
where = "WHERE p2.configvalue=?"
|
||||
} else {
|
||||
// use user_id as uuid
|
||||
where = "WHERE a.user_id=?"
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("owncloudsql: invalid field " + claim)
|
||||
}
|
||||
|
||||
row = as.db.QueryRowContext(ctx, as.selectSQL+where, value)
|
||||
|
||||
return as.rowToAccount(ctx, row)
|
||||
}
|
||||
|
||||
func sanitizeWildcards(q string) string {
|
||||
return strings.ReplaceAll(strings.ReplaceAll(q, "%", `\%`), "_", `\_`)
|
||||
}
|
||||
|
||||
// FindAccounts searches userid, displayname and email using the given query. The Wildcard caracters % and _ are escaped.
|
||||
func (as *Accounts) FindAccounts(ctx context.Context, query string) ([]Account, error) {
|
||||
if as.enableMedialSearch {
|
||||
query = "%" + sanitizeWildcards(query) + "%"
|
||||
}
|
||||
// TODO join oc_account_terms
|
||||
where := "WHERE a.user_id LIKE ? OR a.display_name LIKE ? OR a.email LIKE ?"
|
||||
args := []interface{}{query, query, query}
|
||||
|
||||
if as.joinUsername {
|
||||
where += " OR p.configvalue LIKE ?"
|
||||
args = append(args, query)
|
||||
}
|
||||
if as.joinUUID {
|
||||
where += " OR p2.configvalue LIKE ?"
|
||||
args = append(args, query)
|
||||
}
|
||||
|
||||
rows, err := as.db.QueryContext(ctx, as.selectSQL+where, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
accounts := []Account{}
|
||||
for rows.Next() {
|
||||
a := Account{}
|
||||
if err := rows.Scan(&a.ID, &a.Email, &a.UserID, &a.DisplayName, &a.Quota, &a.LastLogin, &a.Backend, &a.Home, &a.State, &a.Username, &a.OwnCloudUUID); err != nil {
|
||||
appctx.GetLogger(ctx).Error().Err(err).Msg("could not scan row, skipping")
|
||||
continue
|
||||
}
|
||||
accounts = append(accounts, a)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
// GetAccountGroups lasts the groups for an account
|
||||
func (as *Accounts) GetAccountGroups(ctx context.Context, uid string) ([]string, error) {
|
||||
rows, err := as.db.QueryContext(ctx, "SELECT gid FROM oc_group_user WHERE uid=?", uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
groups := []string{}
|
||||
for rows.Next() {
|
||||
var group string
|
||||
if err := rows.Scan(&group); err != nil {
|
||||
appctx.GetLogger(ctx).Error().Err(err).Msg("could not scan row, skipping")
|
||||
continue
|
||||
}
|
||||
groups = append(groups, group)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
-199
@@ -1,199 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package owncloudsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/user"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/user/manager/owncloudsql/accounts"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/user/manager/registry"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
// Provides mysql drivers
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("owncloudsql", NewMysql)
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
c *config
|
||||
db *accounts.Accounts
|
||||
}
|
||||
|
||||
type config struct {
|
||||
DbUsername string `mapstructure:"dbusername"`
|
||||
DbPassword string `mapstructure:"dbpassword"`
|
||||
DbHost string `mapstructure:"dbhost"`
|
||||
DbPort int `mapstructure:"dbport"`
|
||||
DbName string `mapstructure:"dbname"`
|
||||
Idp string `mapstructure:"idp"`
|
||||
Nobody int64 `mapstructure:"nobody"`
|
||||
JoinUsername bool `mapstructure:"join_username"`
|
||||
JoinOwnCloudUUID bool `mapstructure:"join_ownclouduuid"`
|
||||
EnableMedialSearch bool `mapstructure:"enable_medial_search"`
|
||||
}
|
||||
|
||||
// NewMysql returns a new user manager connection to an owncloud mysql database
|
||||
func NewMysql(m map[string]interface{}) (user.Manager, error) {
|
||||
mgr := &manager{}
|
||||
err := mgr.Configure(m)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "error creating a new manager")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mgr.db, err = accounts.NewMysql(
|
||||
fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", mgr.c.DbUsername, mgr.c.DbPassword, mgr.c.DbHost, mgr.c.DbPort, mgr.c.DbName),
|
||||
mgr.c.JoinUsername,
|
||||
mgr.c.JoinOwnCloudUUID,
|
||||
mgr.c.EnableMedialSearch,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mgr, nil
|
||||
}
|
||||
|
||||
func (m *manager) Configure(ml map[string]interface{}) error {
|
||||
c, err := parseConfig(ml)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Nobody == 0 {
|
||||
c.Nobody = 99
|
||||
}
|
||||
|
||||
m.c = c
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
c := &config{}
|
||||
if err := mapstructure.Decode(m, &c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (m *manager) GetUser(ctx context.Context, uid *userpb.UserId, skipFetchingGroups bool) (*userpb.User, error) {
|
||||
if uid.GetTenantId() != "" {
|
||||
return nil, errtypes.NotSupported("tenant filter not supported in opencloudsql user manager")
|
||||
}
|
||||
// search via the user_id
|
||||
a, err := m.db.GetAccountByClaim(ctx, "userid", uid.OpaqueId)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound(uid.OpaqueId)
|
||||
}
|
||||
return m.convertToCS3User(ctx, a, skipFetchingGroups)
|
||||
}
|
||||
|
||||
func (m *manager) GetUserByClaim(ctx context.Context, claim, value, tenantID string, skipFetchingGroups bool) (*userpb.User, error) {
|
||||
if tenantID != "" {
|
||||
return nil, errtypes.NotSupported("tenant filter not supported in opencloudsql user manager")
|
||||
}
|
||||
|
||||
a, err := m.db.GetAccountByClaim(ctx, claim, value)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound(claim + "=" + value)
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.convertToCS3User(ctx, a, skipFetchingGroups)
|
||||
}
|
||||
|
||||
func (m *manager) FindUsers(ctx context.Context, query, tenantID string, skipFetchingGroups bool) ([]*userpb.User, error) {
|
||||
if tenantID != "" {
|
||||
return nil, errtypes.NotSupported("tenant filter not supported in opencloudsql user manager")
|
||||
}
|
||||
|
||||
accounts, err := m.db.FindAccounts(ctx, query)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound("no users found for " + query)
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users := make([]*userpb.User, 0, len(accounts))
|
||||
for i := range accounts {
|
||||
u, err := m.convertToCS3User(ctx, &accounts[i], skipFetchingGroups)
|
||||
if err != nil {
|
||||
appctx.GetLogger(ctx).Error().Err(err).Interface("account", accounts[i]).Msg("could not convert account, skipping")
|
||||
continue
|
||||
}
|
||||
users = append(users, u)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (m *manager) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]string, error) {
|
||||
groups, err := m.db.GetAccountGroups(ctx, uid.OpaqueId)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound("no groups found for uid " + uid.OpaqueId)
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (m *manager) convertToCS3User(ctx context.Context, a *accounts.Account, skipFetchingGroups bool) (*userpb.User, error) {
|
||||
u := &userpb.User{
|
||||
Id: &userpb.UserId{
|
||||
Idp: m.c.Idp,
|
||||
OpaqueId: a.OwnCloudUUID.String,
|
||||
Type: userpb.UserType_USER_TYPE_PRIMARY,
|
||||
},
|
||||
Username: a.Username.String,
|
||||
Mail: a.Email.String,
|
||||
DisplayName: a.DisplayName.String,
|
||||
//Groups: groups,
|
||||
GidNumber: m.c.Nobody,
|
||||
UidNumber: m.c.Nobody,
|
||||
}
|
||||
// https://github.com/cs3org/reva/pull/4135
|
||||
// fall back to userid
|
||||
if u.Id.OpaqueId == "" {
|
||||
u.Id.OpaqueId = a.UserID
|
||||
}
|
||||
if u.Username == "" {
|
||||
u.Username = u.Id.OpaqueId
|
||||
}
|
||||
if u.DisplayName == "" {
|
||||
u.DisplayName = u.Id.OpaqueId
|
||||
}
|
||||
|
||||
if !skipFetchingGroups {
|
||||
var err error
|
||||
if u.Groups, err = m.GetUserGroups(ctx, u.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
Generated
+2
@@ -23,3 +23,5 @@ _testmain.go
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
coverage.out
|
||||
|
||||
+175
-130
@@ -18,17 +18,18 @@ Find out [who uses zerolog](https://github.com/rs/zerolog/wiki/Who-uses-zerolog)
|
||||
|
||||
## Features
|
||||
|
||||
* [Blazing fast](#benchmarks)
|
||||
* [Low to zero allocation](#benchmarks)
|
||||
* [Leveled logging](#leveled-logging)
|
||||
* [Sampling](#log-sampling)
|
||||
* [Hooks](#hooks)
|
||||
* [Contextual fields](#contextual-logging)
|
||||
* [`context.Context` integration](#contextcontext-integration)
|
||||
* [Integration with `net/http`](#integration-with-nethttp)
|
||||
* [JSON and CBOR encoding formats](#binary-encoding)
|
||||
* [Pretty logging for development](#pretty-logging)
|
||||
* [Error Logging (with optional Stacktrace)](#error-logging)
|
||||
- [Blazing fast](#benchmarks)
|
||||
- [Low to zero allocation](#benchmarks)
|
||||
- [Leveled logging](#leveled-logging)
|
||||
- [Sampling](#log-sampling)
|
||||
- [Hooks](#hooks)
|
||||
- [Contextual fields](#contextual-logging)
|
||||
- [`context.Context` integration](#contextcontext-integration)
|
||||
- [Integration with `net/http`](#integration-with-nethttp)
|
||||
- [JSON and CBOR encoding formats](#binary-encoding)
|
||||
- [Pretty logging for development](#pretty-logging)
|
||||
- [Error Logging (with optional Stacktrace)](#error-logging)
|
||||
- [`log/slog` integration](#integration-with-logslog)
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -59,8 +60,9 @@ func main() {
|
||||
|
||||
// Output: {"time":1516134303,"level":"debug","message":"hello world"}
|
||||
```
|
||||
|
||||
> Note: By default log writes to `os.Stderr`
|
||||
> Note: The default log level for `log.Print` is *trace*
|
||||
> Note: The default log level for `log.Print` is _trace_
|
||||
|
||||
### Contextual Logging
|
||||
|
||||
@@ -81,7 +83,7 @@ func main() {
|
||||
Str("Scale", "833 cents").
|
||||
Float64("Interval", 833.09).
|
||||
Msg("Fibonacci is everywhere")
|
||||
|
||||
|
||||
log.Debug().
|
||||
Str("Name", "Tom").
|
||||
Send()
|
||||
@@ -118,15 +120,15 @@ func main() {
|
||||
|
||||
**zerolog** allows for logging at the following levels (from highest to lowest):
|
||||
|
||||
* panic (`zerolog.PanicLevel`, 5)
|
||||
* fatal (`zerolog.FatalLevel`, 4)
|
||||
* error (`zerolog.ErrorLevel`, 3)
|
||||
* warn (`zerolog.WarnLevel`, 2)
|
||||
* info (`zerolog.InfoLevel`, 1)
|
||||
* debug (`zerolog.DebugLevel`, 0)
|
||||
* trace (`zerolog.TraceLevel`, -1)
|
||||
- panic (`zerolog.PanicLevel`, 5)
|
||||
- fatal (`zerolog.FatalLevel`, 4)
|
||||
- error (`zerolog.ErrorLevel`, 3)
|
||||
- warn (`zerolog.WarnLevel`, 2)
|
||||
- info (`zerolog.InfoLevel`, 1)
|
||||
- debug (`zerolog.DebugLevel`, 0)
|
||||
- trace (`zerolog.TraceLevel`, -1)
|
||||
|
||||
You can set the Global logging level to any of these options using the `SetGlobalLevel` function in the zerolog package, passing in one of the given constants above, e.g. `zerolog.InfoLevel` would be the "info" level. Whichever level is chosen, all logs with a level greater than or equal to that level will be written. To turn off logging entirely, pass the `zerolog.Disabled` constant.
|
||||
You can set the Global logging level to any of these options using the `SetGlobalLevel` function in the zerolog package, passing in one of the given constants above, e.g. `zerolog.InfoLevel` would be the "info" level. Whichever level is chosen, all logs with a level greater than or equal to that level will be written. To turn off logging entirely, pass the `zerolog.Disabled` constant.
|
||||
|
||||
#### Setting Global Log Level
|
||||
|
||||
@@ -212,17 +214,17 @@ You can log errors using the `Err` method
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"errors"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
|
||||
err := errors.New("seems we have an error here")
|
||||
log.Error().Err(err).Msg("")
|
||||
err := errors.New("seems we have an error here")
|
||||
log.Error().Err(err).Msg("")
|
||||
}
|
||||
|
||||
// Output: {"level":"error","error":"seems we have an error here","time":1609085256}
|
||||
@@ -232,45 +234,45 @@ func main() {
|
||||
|
||||
#### Error Logging with Stacktrace
|
||||
|
||||
Using `github.com/pkg/errors`, you can add a formatted stacktrace to your errors.
|
||||
Using `github.com/pkg/errors`, you can add a formatted stacktrace to your errors.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/pkgerrors"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/pkgerrors"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
||||
|
||||
err := outer()
|
||||
log.Error().Stack().Err(err).Msg("")
|
||||
err := outer()
|
||||
log.Error().Stack().Err(err).Msg("")
|
||||
}
|
||||
|
||||
func inner() error {
|
||||
return errors.New("seems we have an error here")
|
||||
return errors.New("seems we have an error here")
|
||||
}
|
||||
|
||||
func middle() error {
|
||||
err := inner()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
err := inner()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func outer() error {
|
||||
err := middle()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
err := middle()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Output: {"level":"error","stack":[{"func":"inner","line":"20","source":"errors.go"},{"func":"middle","line":"24","source":"errors.go"},{"func":"outer","line":"32","source":"errors.go"},{"func":"main","line":"15","source":"errors.go"},{"func":"main","line":"204","source":"proc.go"},{"func":"goexit","line":"1374","source":"asm_amd64.s"}],"error":"seems we have an error here","time":1609086683}
|
||||
@@ -308,7 +310,6 @@ func main() {
|
||||
|
||||
> NOTE: Using `Msgf` generates one allocation even when the logger is disabled.
|
||||
|
||||
|
||||
### Create logger instance to manage different outputs
|
||||
|
||||
```go
|
||||
@@ -393,7 +394,7 @@ log.Info().Str("foo", "bar").
|
||||
Str("one", "test_one").
|
||||
Str("three", "test_three").
|
||||
Msg("Hello World")
|
||||
|
||||
|
||||
// Output: INFO TEST_ONE test_two (test_three) Hello World foo:bar
|
||||
```
|
||||
|
||||
@@ -457,8 +458,8 @@ If your writer might be slow or not thread-safe and you need your log producers
|
||||
|
||||
```go
|
||||
wr := diode.NewWriter(os.Stdout, 1000, 10*time.Millisecond, func(missed int) {
|
||||
fmt.Printf("Logger Dropped %d messages", missed)
|
||||
})
|
||||
fmt.Printf("Logger Dropped %d messages", missed)
|
||||
})
|
||||
log := zerolog.New(wr)
|
||||
log.Print("test")
|
||||
```
|
||||
@@ -537,7 +538,7 @@ stdlog.Print("hello world")
|
||||
### context.Context integration
|
||||
|
||||
Go contexts are commonly passed throughout Go code, and this can help you pass
|
||||
your Logger into places it might otherwise be hard to inject. The `Logger`
|
||||
your Logger into places it might otherwise be hard to inject. The `Logger`
|
||||
instance may be attached to Go context (`context.Context`) using
|
||||
`Logger.WithContext(ctx)` and extracted from it using `zerolog.Ctx(ctx)`.
|
||||
For example:
|
||||
@@ -562,7 +563,7 @@ func someFunc(ctx context.Context) {
|
||||
```
|
||||
|
||||
A second form of `context.Context` integration allows you to pass the current
|
||||
context.Context into the logged event, and retrieve it from hooks. This can be
|
||||
`context.Context` into the logged event, and retrieve it from hooks. This can be
|
||||
useful to log trace and span IDs or other information stored in the go context,
|
||||
and facilitates the unification of logging and tracing in some systems:
|
||||
|
||||
@@ -640,17 +641,17 @@ if err := http.ListenAndServe(":8080", nil); err != nil {
|
||||
```
|
||||
|
||||
## Multiple Log Output
|
||||
`zerolog.MultiLevelWriter` may be used to send the log message to multiple outputs.
|
||||
In this example, we send the log message to both `os.Stdout` and the in-built ConsoleWriter.
|
||||
|
||||
`zerolog.MultiLevelWriter` may be used to send the log message to multiple outputs.
|
||||
|
||||
In this example, we send the log message to both `os.Stdout` and the in-built `ConsoleWriter`.
|
||||
|
||||
```go
|
||||
func main() {
|
||||
consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
|
||||
|
||||
multi := zerolog.MultiLevelWriter(consoleWriter, os.Stdout)
|
||||
|
||||
logger := zerolog.New(multi).With().Timestamp().Logger()
|
||||
|
||||
logger.Info().Msg("Hello World!")
|
||||
consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
|
||||
multi := zerolog.MultiLevelWriter(consoleWriter, os.Stdout)
|
||||
logger := zerolog.New(multi).With().Timestamp().Logger()
|
||||
logger.Info().Msg("Hello World!")
|
||||
}
|
||||
|
||||
// Output (Line 1: Console; Line 2: Stdout)
|
||||
@@ -662,43 +663,45 @@ func main() {
|
||||
|
||||
Some settings can be changed and will be applied to all loggers:
|
||||
|
||||
* `log.Logger`: You can set this value to customize the global logger (the one used by package level methods).
|
||||
* `zerolog.SetGlobalLevel`: Can raise the minimum level of all loggers. Call this with `zerolog.Disabled` to disable logging altogether (quiet mode).
|
||||
* `zerolog.DisableSampling`: If argument is `true`, all sampled loggers will stop sampling and issue 100% of their log events.
|
||||
* `zerolog.TimestampFieldName`: Can be set to customize `Timestamp` field name.
|
||||
* `zerolog.LevelFieldName`: Can be set to customize level field name.
|
||||
* `zerolog.MessageFieldName`: Can be set to customize message field name.
|
||||
* `zerolog.ErrorFieldName`: Can be set to customize `Err` field name.
|
||||
* `zerolog.TimeFieldFormat`: Can be set to customize `Time` field value formatting. If set with `zerolog.TimeFormatUnix`, `zerolog.TimeFormatUnixMs` or `zerolog.TimeFormatUnixMicro`, times are formatted as UNIX timestamp.
|
||||
* `zerolog.DurationFieldUnit`: Can be set to customize the unit for time.Duration type fields added by `Dur` (default: `time.Millisecond`).
|
||||
* `zerolog.DurationFieldInteger`: If set to `true`, `Dur` fields are formatted as integers instead of floats (default: `false`).
|
||||
* `zerolog.ErrorHandler`: Called whenever zerolog fails to write an event on its output. If not set, an error is printed on the stderr. This handler must be thread safe and non-blocking.
|
||||
* `zerolog.FloatingPointPrecision`: If set to a value other than -1, controls the number
|
||||
of digits when formatting float numbers in JSON. See
|
||||
[strconv.FormatFloat](https://pkg.go.dev/strconv#FormatFloat)
|
||||
for more details.
|
||||
- `log.Logger`: You can set this value to customize the global logger (the one used by package level methods).
|
||||
- `zerolog.SetGlobalLevel`: Can raise the minimum level of all loggers. Call this with `zerolog.Disabled` to disable logging altogether (quiet mode).
|
||||
- `zerolog.DisableSampling`: If argument is `true`, all sampled loggers will stop sampling and issue 100% of their log events.
|
||||
- `zerolog.TimestampFieldName`: Can be set to customize `Timestamp` field name.
|
||||
- `zerolog.LevelFieldName`: Can be set to customize level field name.
|
||||
- `zerolog.MessageFieldName`: Can be set to customize message field name.
|
||||
- `zerolog.ErrorFieldName`: Can be set to customize `Err` field name.
|
||||
- `zerolog.TimeFieldFormat`: Can be set to customize `Time` field value formatting. If set with `zerolog.TimeFormatUnix`, `zerolog.TimeFormatUnixMs` or `zerolog.TimeFormatUnixMicro`, times are formatted as UNIX timestamp.
|
||||
- `zerolog.DurationFieldUnit`: Can be set to customize the unit for time.Duration type fields added by `Dur` (default: `time.Millisecond`).
|
||||
- `zerolog.DurationFieldFormat`: Can be set to `DurationFormatFloat`, `DurationFormatInt`, or `DurationFormatString` (default: `DurationFormatFloat`) to append the `Duration` as a `Float64`, `Int64`, or by calling `String()` (respectively).
|
||||
- `zerolog.DurationFieldInteger`: If set to `true`, `Dur` fields are formatted as integers instead of floats (default: `false`). Deprecated: Use `zerolog.DurationFieldFormat = DurationFormatInt` instead.
|
||||
- `zerolog.ErrorHandler`: Called whenever zerolog fails to write an event on its output. If not set, an error is printed on the stderr. This handler must be thread safe and non-blocking.
|
||||
- `zerolog.FloatingPointPrecision`: If set to a value other than -1, controls the number of digits when formatting float numbers in JSON. See [strconv.FormatFloat](https://pkg.go.dev/strconv#FormatFloat)
|
||||
for more details.
|
||||
|
||||
## Field Types
|
||||
|
||||
### Standard Types
|
||||
|
||||
* `Str`
|
||||
* `Bool`
|
||||
* `Int`, `Int8`, `Int16`, `Int32`, `Int64`
|
||||
* `Uint`, `Uint8`, `Uint16`, `Uint32`, `Uint64`
|
||||
* `Float32`, `Float64`
|
||||
- `Str`
|
||||
- `Bool`
|
||||
- `Int`, `Int8`, `Int16`, `Int32`, `Int64`
|
||||
- `Uint`, `Uint8`, `Uint16`, `Uint32`, `Uint64`
|
||||
- `Float32`, `Float64`
|
||||
|
||||
### Advanced Fields
|
||||
|
||||
* `Err`: Takes an `error` and renders it as a string using the `zerolog.ErrorFieldName` field name.
|
||||
* `Func`: Run a `func` only if the level is enabled.
|
||||
* `Timestamp`: Inserts a timestamp field with `zerolog.TimestampFieldName` field name, formatted using `zerolog.TimeFieldFormat`.
|
||||
* `Time`: Adds a field with time formatted with `zerolog.TimeFieldFormat`.
|
||||
* `Dur`: Adds a field with `time.Duration`.
|
||||
* `Dict`: Adds a sub-key/value as a field of the event.
|
||||
* `RawJSON`: Adds a field with an already encoded JSON (`[]byte`)
|
||||
* `Hex`: Adds a field with value formatted as a hexadecimal string (`[]byte`)
|
||||
* `Interface`: Uses reflection to marshal the type.
|
||||
- `Err`: Takes an `error` and renders it as a string using the `zerolog.ErrorFieldName` field name.
|
||||
- `Func`: Run a `func` only if the level is enabled.
|
||||
- `Timestamp`: Inserts a timestamp field with `zerolog.TimestampFieldName` field name, formatted using `zerolog.TimeFieldFormat`.
|
||||
- `Time`: Adds a field with time formatted with `zerolog.TimeFieldFormat`.
|
||||
- `Dur`: Adds a field with `time.Duration`.
|
||||
- `Dict`: Adds a sub-key/value as a field of the event.
|
||||
- `RawJSON`: Adds a field with an already encoded JSON (`[]byte`)
|
||||
- `Hex`: Adds a field with value formatted as a hexadecimal string (`[]byte`)
|
||||
- `Interface`: Uses reflection to marshal the type.
|
||||
- `IPAddr`: Adds a field with `net.IP`.
|
||||
- `IPPrefix`: Adds a field with `net.IPNet`.
|
||||
- `MACAddr`: Adds a field with `net.HardwareAddr`
|
||||
|
||||
Most fields are also available in the slice format (`Strs` for `[]string`, `Errs` for `[]error` etc.)
|
||||
|
||||
@@ -710,20 +713,48 @@ In addition to the default JSON encoding, `zerolog` can produce binary logs usin
|
||||
go build -tags binary_log .
|
||||
```
|
||||
|
||||
To Decode binary encoded log files you can use any CBOR decoder. One has been tested to work
|
||||
To decode binary encoded log files you can use any CBOR decoder. One has been tested to work
|
||||
with zerolog library is [CSD](https://github.com/toravir/csd/).
|
||||
|
||||
## Integration with `log/slog`
|
||||
|
||||
zerolog provides a `slog.Handler` implementation that routes `log/slog` records through a zerolog logger. This lets you use the standard library's `slog` API while keeping zerolog's performance and encoding:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
zl := log.Logger
|
||||
handler := zerolog.NewSlogHandler(zl)
|
||||
logger := slog.New(handler)
|
||||
|
||||
logger.Info("user logged in", "user", "alice", "role", "admin")
|
||||
}
|
||||
|
||||
// Output: {"level":"info","user":"alice","role":"admin","time":"...","message":"user logged in"}
|
||||
```
|
||||
|
||||
The handler supports all `slog` features including `WithAttrs`, `WithGroup`, nested groups, and `LogValuer` resolution. slog levels are mapped to zerolog levels (e.g. `slog.LevelDebug` to `zerolog.DebugLevel`).
|
||||
|
||||
## Related Projects
|
||||
|
||||
* [grpc-zerolog](https://github.com/cheapRoc/grpc-zerolog): Implementation of `grpclog.LoggerV2` interface using `zerolog`
|
||||
* [overlog](https://github.com/Trendyol/overlog): Implementation of `Mapped Diagnostic Context` interface using `zerolog`
|
||||
* [zerologr](https://github.com/go-logr/zerologr): Implementation of `logr.LogSink` interface using `zerolog`
|
||||
- [grpc-zerolog](https://github.com/cheapRoc/grpc-zerolog): Implementation of `grpclog.LoggerV2` interface using `zerolog`
|
||||
- [overlog](https://github.com/Trendyol/overlog): Implementation of `Mapped Diagnostic Context` interface using `zerolog`
|
||||
- [zerologr](https://github.com/go-logr/zerologr): Implementation of `logr.LogSink` interface using `zerolog`
|
||||
- [logze](https://github.com/maxbolgarin/logze): Implementation of `log/slog` interface using `zerolog`
|
||||
|
||||
## Benchmarks
|
||||
|
||||
See [logbench](http://bench.zerolog.io/) for more comprehensive and up-to-date benchmarks.
|
||||
|
||||
All operations are allocation free (those numbers *include* JSON encoding):
|
||||
All operations are allocation free (those numbers _include_ JSON encoding):
|
||||
|
||||
```text
|
||||
BenchmarkLogEmpty-8 100000000 19.1 ns/op 0 B/op 0 allocs/op
|
||||
@@ -735,50 +766,50 @@ BenchmarkLogFields-8 10000000 184 ns/op 0 B/op 0 allocs/op
|
||||
|
||||
There are a few Go logging benchmarks and comparisons that include zerolog.
|
||||
|
||||
* [imkira/go-loggers-bench](https://github.com/imkira/go-loggers-bench)
|
||||
* [uber-common/zap](https://github.com/uber-go/zap#performance)
|
||||
- [imkira/go-loggers-bench](https://github.com/imkira/go-loggers-bench)
|
||||
- [uber-common/zap](https://github.com/uber-go/zap#performance)
|
||||
|
||||
Using Uber's zap comparison benchmark:
|
||||
|
||||
Log a message and 10 fields:
|
||||
|
||||
| Library | Time | Bytes Allocated | Objects Allocated |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| zerolog | 767 ns/op | 552 B/op | 6 allocs/op |
|
||||
| :zap: zap | 848 ns/op | 704 B/op | 2 allocs/op |
|
||||
| :zap: zap (sugared) | 1363 ns/op | 1610 B/op | 20 allocs/op |
|
||||
| go-kit | 3614 ns/op | 2895 B/op | 66 allocs/op |
|
||||
| lion | 5392 ns/op | 5807 B/op | 63 allocs/op |
|
||||
| logrus | 5661 ns/op | 6092 B/op | 78 allocs/op |
|
||||
| apex/log | 15332 ns/op | 3832 B/op | 65 allocs/op |
|
||||
| log15 | 20657 ns/op | 5632 B/op | 93 allocs/op |
|
||||
| Library | Time | Bytes Allocated | Objects Allocated |
|
||||
| :------------------ | :---------: | :-------------: | :---------------: |
|
||||
| zerolog | 767 ns/op | 552 B/op | 6 allocs/op |
|
||||
| :zap: zap | 848 ns/op | 704 B/op | 2 allocs/op |
|
||||
| :zap: zap (sugared) | 1363 ns/op | 1610 B/op | 20 allocs/op |
|
||||
| go-kit | 3614 ns/op | 2895 B/op | 66 allocs/op |
|
||||
| lion | 5392 ns/op | 5807 B/op | 63 allocs/op |
|
||||
| logrus | 5661 ns/op | 6092 B/op | 78 allocs/op |
|
||||
| apex/log | 15332 ns/op | 3832 B/op | 65 allocs/op |
|
||||
| log15 | 20657 ns/op | 5632 B/op | 93 allocs/op |
|
||||
|
||||
Log a message with a logger that already has 10 fields of context:
|
||||
|
||||
| Library | Time | Bytes Allocated | Objects Allocated |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| zerolog | 52 ns/op | 0 B/op | 0 allocs/op |
|
||||
| :zap: zap | 283 ns/op | 0 B/op | 0 allocs/op |
|
||||
| :zap: zap (sugared) | 337 ns/op | 80 B/op | 2 allocs/op |
|
||||
| lion | 2702 ns/op | 4074 B/op | 38 allocs/op |
|
||||
| go-kit | 3378 ns/op | 3046 B/op | 52 allocs/op |
|
||||
| logrus | 4309 ns/op | 4564 B/op | 63 allocs/op |
|
||||
| apex/log | 13456 ns/op | 2898 B/op | 51 allocs/op |
|
||||
| log15 | 14179 ns/op | 2642 B/op | 44 allocs/op |
|
||||
| Library | Time | Bytes Allocated | Objects Allocated |
|
||||
| :------------------ | :---------: | :-------------: | :---------------: |
|
||||
| zerolog | 52 ns/op | 0 B/op | 0 allocs/op |
|
||||
| :zap: zap | 283 ns/op | 0 B/op | 0 allocs/op |
|
||||
| :zap: zap (sugared) | 337 ns/op | 80 B/op | 2 allocs/op |
|
||||
| lion | 2702 ns/op | 4074 B/op | 38 allocs/op |
|
||||
| go-kit | 3378 ns/op | 3046 B/op | 52 allocs/op |
|
||||
| logrus | 4309 ns/op | 4564 B/op | 63 allocs/op |
|
||||
| apex/log | 13456 ns/op | 2898 B/op | 51 allocs/op |
|
||||
| log15 | 14179 ns/op | 2642 B/op | 44 allocs/op |
|
||||
|
||||
Log a static string, without any context or `printf`-style templating:
|
||||
|
||||
| Library | Time | Bytes Allocated | Objects Allocated |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| zerolog | 50 ns/op | 0 B/op | 0 allocs/op |
|
||||
| :zap: zap | 236 ns/op | 0 B/op | 0 allocs/op |
|
||||
| standard library | 453 ns/op | 80 B/op | 2 allocs/op |
|
||||
| :zap: zap (sugared) | 337 ns/op | 80 B/op | 2 allocs/op |
|
||||
| go-kit | 508 ns/op | 656 B/op | 13 allocs/op |
|
||||
| lion | 771 ns/op | 1224 B/op | 10 allocs/op |
|
||||
| logrus | 1244 ns/op | 1505 B/op | 27 allocs/op |
|
||||
| apex/log | 2751 ns/op | 584 B/op | 11 allocs/op |
|
||||
| log15 | 5181 ns/op | 1592 B/op | 26 allocs/op |
|
||||
| Library | Time | Bytes Allocated | Objects Allocated |
|
||||
| :------------------ | :--------: | :-------------: | :---------------: |
|
||||
| zerolog | 50 ns/op | 0 B/op | 0 allocs/op |
|
||||
| :zap: zap | 236 ns/op | 0 B/op | 0 allocs/op |
|
||||
| standard library | 453 ns/op | 80 B/op | 2 allocs/op |
|
||||
| :zap: zap (sugared) | 337 ns/op | 80 B/op | 2 allocs/op |
|
||||
| go-kit | 508 ns/op | 656 B/op | 13 allocs/op |
|
||||
| lion | 771 ns/op | 1224 B/op | 10 allocs/op |
|
||||
| logrus | 1244 ns/op | 1505 B/op | 27 allocs/op |
|
||||
| apex/log | 2751 ns/op | 584 B/op | 11 allocs/op |
|
||||
| log15 | 5181 ns/op | 1592 B/op | 26 allocs/op |
|
||||
|
||||
## Caveats
|
||||
|
||||
@@ -798,7 +829,7 @@ In this case, many consumers will take the last value, but this is not guarantee
|
||||
|
||||
### Concurrency safety
|
||||
|
||||
Be careful when calling UpdateContext. It is not concurrency safe. Use the With method to create a child logger:
|
||||
Be careful when calling `UpdateContext`. It is not concurrency safe. Use the `With()` method to create a child logger:
|
||||
|
||||
```go
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -811,3 +842,17 @@ func handler(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The `Event` object returned from the `Logger` level-specific message functions (e.g. `Log()`, `Trace()`, `Debug()`, etc.)
|
||||
is allocated in `sync.Pool` memory that will be returned to the pool as soon as the `Msg()`, `Msgf()`, `Send()`,
|
||||
or `MsgFunc()` writes the message and **must not** be accessed afterwards.
|
||||
|
||||
**Do not** hold a reference to the `*Event` while in callback functions or your own code. This is especially important in
|
||||
`Hook.Run()` and `HookFunc` functions or `MarshalZerologObject(e *Event)` callback (e.g. `LogObjectMarshaler` implementations).
|
||||
|
||||
Any `Array` objects returned from `Context.CreateArray()` or `Event.CreateArray()` are from a `sync.Pool` so **do not** hold
|
||||
references to them from within any `MarshalZerologArray(a *Array)` callback (e.g. `LogArrayMarshaler` implementations) or your
|
||||
own code as they will be cleared and returned to the pool after being buffered by a call to `Context.Array()` or `Event.Array()`.
|
||||
|
||||
Any _dictionary_ `Event` returned from `Context.CreateDict()` or `Event.CreateDict()` **must not** be referenced after being
|
||||
buffered by a call to `Array.Dict()`, `Context.Dict()`, or `Event.Dict()` as they will be cleared and returned to the pool.
|
||||
|
||||
+57
-21
@@ -1,6 +1,7 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -17,10 +18,19 @@ var arrayPool = &sync.Pool{
|
||||
// Array is used to prepopulate an array of items
|
||||
// which can be re-used to add to log messages.
|
||||
type Array struct {
|
||||
buf []byte
|
||||
buf []byte
|
||||
stack bool // enable error stack trace
|
||||
ctx context.Context // Optional Go context
|
||||
ch []Hook // hooks
|
||||
}
|
||||
|
||||
func putArray(a *Array) {
|
||||
// prevent any subsequent use of the Array contextual state and truncate the buffer
|
||||
a.stack = false
|
||||
a.ctx = nil
|
||||
a.ch = nil
|
||||
a.buf = a.buf[:0]
|
||||
|
||||
// Proper usage of a sync.Pool requires each entry to have approximately
|
||||
// the same memory cost. To obtain this property when the stored type
|
||||
// contains a variably-sized buffer, we add a hard limit on the maximum buffer
|
||||
@@ -28,22 +38,28 @@ func putArray(a *Array) {
|
||||
//
|
||||
// See https://golang.org/issue/23199
|
||||
const maxSize = 1 << 16 // 64KiB
|
||||
if cap(a.buf) > maxSize {
|
||||
return
|
||||
if cap(a.buf) <= maxSize {
|
||||
arrayPool.Put(a)
|
||||
}
|
||||
arrayPool.Put(a)
|
||||
}
|
||||
|
||||
// Arr creates an array to be added to an Event or Context.
|
||||
// WARNING: This function is deprecated because it does not preserve
|
||||
// the stack, hooks, and context from the parent event.
|
||||
// Deprecated: Use Event.CreateArray or Context.CreateArray instead.
|
||||
func Arr() *Array {
|
||||
a := arrayPool.Get().(*Array)
|
||||
a.buf = a.buf[:0]
|
||||
a.stack = false
|
||||
a.ctx = nil
|
||||
a.ch = nil
|
||||
return a
|
||||
}
|
||||
|
||||
// MarshalZerologArray method here is no-op - since data is
|
||||
// already in the needed format.
|
||||
func (*Array) MarshalZerologArray(*Array) {
|
||||
// untestable: there's no code to be covered
|
||||
}
|
||||
|
||||
func (a *Array) write(dst []byte) []byte {
|
||||
@@ -59,11 +75,7 @@ func (a *Array) write(dst []byte) []byte {
|
||||
// Object marshals an object that implement the LogObjectMarshaler
|
||||
// interface and appends it to the array.
|
||||
func (a *Array) Object(obj LogObjectMarshaler) *Array {
|
||||
e := Dict()
|
||||
obj.MarshalZerologObject(e)
|
||||
e.buf = enc.AppendEndMarker(e.buf)
|
||||
a.buf = append(enc.AppendArrayDelim(a.buf), e.buf...)
|
||||
putEvent(e)
|
||||
a.buf = appendObject(enc.AppendArrayDelim(a.buf), obj, a.stack, a.ctx, a.ch)
|
||||
return a
|
||||
}
|
||||
|
||||
@@ -94,16 +106,12 @@ func (a *Array) RawJSON(val []byte) *Array {
|
||||
// Err serializes and appends the err to the array.
|
||||
func (a *Array) Err(err error) *Array {
|
||||
switch m := ErrorMarshalFunc(err).(type) {
|
||||
case nil:
|
||||
a.buf = enc.AppendNil(enc.AppendArrayDelim(a.buf))
|
||||
case LogObjectMarshaler:
|
||||
e := newEvent(nil, 0)
|
||||
e.buf = e.buf[:0]
|
||||
e.appendObject(m)
|
||||
a.buf = append(enc.AppendArrayDelim(a.buf), e.buf...)
|
||||
putEvent(e)
|
||||
a = a.Object(m)
|
||||
case error:
|
||||
if m == nil || isNilValue(m) {
|
||||
a.buf = enc.AppendNil(enc.AppendArrayDelim(a.buf))
|
||||
} else {
|
||||
if !isNilValue(m) {
|
||||
a.buf = enc.AppendString(enc.AppendArrayDelim(a.buf), m.Error())
|
||||
}
|
||||
case string:
|
||||
@@ -115,6 +123,27 @@ func (a *Array) Err(err error) *Array {
|
||||
return a
|
||||
}
|
||||
|
||||
// Errs serializes and appends errors to the array.
|
||||
func (a *Array) Errs(errs []error) *Array {
|
||||
for _, err := range errs {
|
||||
switch m := ErrorMarshalFunc(err).(type) {
|
||||
case nil:
|
||||
a = a.Interface(nil)
|
||||
case LogObjectMarshaler:
|
||||
a = a.Object(m)
|
||||
case error:
|
||||
if !isNilValue(m) {
|
||||
a = a.Str(m.Error())
|
||||
}
|
||||
case string:
|
||||
a = a.Str(m)
|
||||
default:
|
||||
a = a.Interface(m)
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// Bool appends the val as a bool to the array.
|
||||
func (a *Array) Bool(b bool) *Array {
|
||||
a.buf = enc.AppendBool(enc.AppendArrayDelim(a.buf), b)
|
||||
@@ -201,7 +230,7 @@ func (a *Array) Time(t time.Time) *Array {
|
||||
|
||||
// Dur appends d to the array.
|
||||
func (a *Array) Dur(d time.Duration) *Array {
|
||||
a.buf = enc.AppendDuration(enc.AppendArrayDelim(a.buf), d, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision)
|
||||
a.buf = enc.AppendDuration(enc.AppendArrayDelim(a.buf), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
return a
|
||||
}
|
||||
|
||||
@@ -214,19 +243,19 @@ func (a *Array) Interface(i interface{}) *Array {
|
||||
return a
|
||||
}
|
||||
|
||||
// IPAddr adds IPv4 or IPv6 address to the array
|
||||
// IPAddr adds a net.IP IPv4 or IPv6 address to the array
|
||||
func (a *Array) IPAddr(ip net.IP) *Array {
|
||||
a.buf = enc.AppendIPAddr(enc.AppendArrayDelim(a.buf), ip)
|
||||
return a
|
||||
}
|
||||
|
||||
// IPPrefix adds IPv4 or IPv6 Prefix (IP + mask) to the array
|
||||
// IPPrefix adds a net.IPNet IPv4 or IPv6 Prefix (IP + mask) to the array
|
||||
func (a *Array) IPPrefix(pfx net.IPNet) *Array {
|
||||
a.buf = enc.AppendIPPrefix(enc.AppendArrayDelim(a.buf), pfx)
|
||||
return a
|
||||
}
|
||||
|
||||
// MACAddr adds a MAC (Ethernet) address to the array
|
||||
// MACAddr adds a net.HardwareAddr MAC (Ethernet) address to the array
|
||||
func (a *Array) MACAddr(ha net.HardwareAddr) *Array {
|
||||
a.buf = enc.AppendMACAddr(enc.AppendArrayDelim(a.buf), ha)
|
||||
return a
|
||||
@@ -236,5 +265,12 @@ func (a *Array) MACAddr(ha net.HardwareAddr) *Array {
|
||||
func (a *Array) Dict(dict *Event) *Array {
|
||||
dict.buf = enc.AppendEndMarker(dict.buf)
|
||||
a.buf = append(enc.AppendArrayDelim(a.buf), dict.buf...)
|
||||
putEvent(dict)
|
||||
return a
|
||||
}
|
||||
|
||||
// Type adds the val's type using reflection to the array.
|
||||
func (a *Array) Type(val interface{}) *Array {
|
||||
a.buf = enc.AppendType(enc.AppendArrayDelim(a.buf), val)
|
||||
return a
|
||||
}
|
||||
|
||||
+3
-3
@@ -101,9 +101,9 @@ type ConsoleWriter struct {
|
||||
// NewConsoleWriter creates and initializes a new ConsoleWriter.
|
||||
func NewConsoleWriter(options ...func(w *ConsoleWriter)) ConsoleWriter {
|
||||
w := ConsoleWriter{
|
||||
Out: os.Stdout,
|
||||
TimeFormat: consoleDefaultTimeFormat,
|
||||
PartsOrder: consoleDefaultPartsOrder(),
|
||||
Out: os.Stdout,
|
||||
TimeFormat: consoleDefaultTimeFormat,
|
||||
PartsOrder: consoleDefaultPartsOrder(),
|
||||
}
|
||||
|
||||
for _, opt := range options {
|
||||
|
||||
+98
-43
@@ -3,7 +3,6 @@ package zerolog
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"time"
|
||||
@@ -23,7 +22,7 @@ func (c Context) Logger() Logger {
|
||||
// Only map[string]interface{} and []interface{} are accepted. []interface{} must
|
||||
// alternate string keys and arbitrary values, and extraneous ones are ignored.
|
||||
func (c Context) Fields(fields interface{}) Context {
|
||||
c.l.context = appendFields(c.l.context, fields, c.l.stack)
|
||||
c.l.context = appendFields(c.l.context, fields, c.l.stack, c.l.ctx, c.l.hooks)
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -35,8 +34,28 @@ func (c Context) Dict(key string, dict *Event) Context {
|
||||
return c
|
||||
}
|
||||
|
||||
// CreateDict creates an Event to be used with the Context.Dict method.
|
||||
// It preserves the stack, hooks, and context from the logger.
|
||||
// Call usual field methods like Str, Int etc to add fields to this
|
||||
// event and give it as argument the Context.Dict method.
|
||||
func (c Context) CreateDict() *Event {
|
||||
return newEvent(nil, DebugLevel, c.l.stack, c.l.ctx, c.l.hooks)
|
||||
}
|
||||
|
||||
// CreateArray creates an Array to be used with the Context.Array method.
|
||||
// It preserves the stack, hooks, and context from the logger.
|
||||
// Call usual field methods like Str, Int etc to add elements to this
|
||||
// array and give it as argument the Context.Array method.
|
||||
func (c Context) CreateArray() *Array {
|
||||
a := Arr()
|
||||
a.stack = c.l.stack
|
||||
a.ctx = c.l.ctx
|
||||
a.ch = c.l.hooks
|
||||
return a
|
||||
}
|
||||
|
||||
// Array adds the field key with an array to the event context.
|
||||
// Use zerolog.Arr() to create the array or pass a type that
|
||||
// Use c.CreateArray() to create the array or pass a type that
|
||||
// implement the LogArrayMarshaler interface.
|
||||
func (c Context) Array(key string, arr LogArrayMarshaler) Context {
|
||||
c.l.context = enc.AppendKey(c.l.context, key)
|
||||
@@ -44,29 +63,44 @@ func (c Context) Array(key string, arr LogArrayMarshaler) Context {
|
||||
c.l.context = arr.write(c.l.context)
|
||||
return c
|
||||
}
|
||||
var a *Array
|
||||
if aa, ok := arr.(*Array); ok {
|
||||
a = aa
|
||||
} else {
|
||||
a = Arr()
|
||||
arr.MarshalZerologArray(a)
|
||||
}
|
||||
a := c.CreateArray()
|
||||
arr.MarshalZerologArray(a)
|
||||
c.l.context = a.write(c.l.context)
|
||||
return c
|
||||
}
|
||||
|
||||
// Object marshals an object that implement the LogObjectMarshaler interface.
|
||||
func (c Context) Object(key string, obj LogObjectMarshaler) Context {
|
||||
e := newEvent(LevelWriterAdapter{io.Discard}, 0)
|
||||
e := c.l.scratchEvent()
|
||||
e.Object(key, obj)
|
||||
c.l.context = enc.AppendObjectData(c.l.context, e.buf)
|
||||
putEvent(e)
|
||||
return c
|
||||
}
|
||||
|
||||
// Objects adds the field key with objs to the logger context as an array of
|
||||
// objects that implement the LogObjectMarshaler interface.
|
||||
//
|
||||
// This is the array version that accepts a slice of LogObjectMarshaler objects.
|
||||
func (c Context) Objects(key string, objs []LogObjectMarshaler) Context {
|
||||
e := c.l.scratchEvent()
|
||||
e.Objects(key, objs)
|
||||
c.l.context = enc.AppendObjectData(c.l.context, e.buf)
|
||||
putEvent(e)
|
||||
return c
|
||||
}
|
||||
|
||||
// ObjectsV adds the field key with objs to the logger context as an array of
|
||||
// objects that implement the LogObjectMarshaler interface.
|
||||
//
|
||||
// This is a variadic version that accepts a list of individual LogObjectMarshaler objects.
|
||||
func (c Context) ObjectsV(key string, objs ...LogObjectMarshaler) Context {
|
||||
return c.Objects(key, objs)
|
||||
}
|
||||
|
||||
// EmbedObject marshals and Embeds an object that implement the LogObjectMarshaler interface.
|
||||
func (c Context) EmbedObject(obj LogObjectMarshaler) Context {
|
||||
e := newEvent(LevelWriterAdapter{io.Discard}, 0)
|
||||
e := c.l.scratchEvent()
|
||||
e.EmbedObject(obj)
|
||||
c.l.context = enc.AppendObjectData(c.l.context, e.buf)
|
||||
putEvent(e)
|
||||
@@ -80,11 +114,20 @@ func (c Context) Str(key, val string) Context {
|
||||
}
|
||||
|
||||
// Strs adds the field key with val as a string to the logger context.
|
||||
//
|
||||
// This is the array version that accepts a slice of string values.
|
||||
func (c Context) Strs(key string, vals []string) Context {
|
||||
c.l.context = enc.AppendStrings(enc.AppendKey(c.l.context, key), vals)
|
||||
return c
|
||||
}
|
||||
|
||||
// StrsV adds the field key with vals as a []string to the logger context.
|
||||
//
|
||||
// This is a variadic version that accepts a list of individual strings.
|
||||
func (c Context) StrsV(key string, vals ...string) Context {
|
||||
return c.Strs(key, vals)
|
||||
}
|
||||
|
||||
// Stringer adds the field key with val.String() (or null if val is nil) to the logger context.
|
||||
func (c Context) Stringer(key string, val fmt.Stringer) Context {
|
||||
if val != nil {
|
||||
@@ -96,6 +139,24 @@ func (c Context) Stringer(key string, val fmt.Stringer) Context {
|
||||
return c
|
||||
}
|
||||
|
||||
// Stringers adds the field key with vals to the logger context where each
|
||||
// individual val is added by calling val.String().
|
||||
//
|
||||
// This is the array version that accepts a slice of fmt.Stringer values.
|
||||
func (c Context) Stringers(key string, vals []fmt.Stringer) Context {
|
||||
c.l.context = enc.AppendStringers(enc.AppendKey(c.l.context, key), vals)
|
||||
return c
|
||||
}
|
||||
|
||||
// StringersV adds the field key with vals to the logger context where each
|
||||
// individual val is added by calling val.String().
|
||||
//
|
||||
// This is a variadic version that accepts a list of individual
|
||||
// fmt.Stringer values.
|
||||
func (c Context) StringersV(key string, vals ...fmt.Stringer) Context {
|
||||
return c.Stringers(key, vals)
|
||||
}
|
||||
|
||||
// Bytes adds the field key with val as a []byte to the logger context.
|
||||
func (c Context) Bytes(key string, val []byte) Context {
|
||||
c.l.context = enc.AppendBytes(enc.AppendKey(c.l.context, key), val)
|
||||
@@ -118,6 +179,7 @@ func (c Context) RawJSON(key string, b []byte) Context {
|
||||
}
|
||||
|
||||
// AnErr adds the field key with serialized err to the logger context.
|
||||
// If err is nil, no field is added.
|
||||
func (c Context) AnErr(key string, err error) Context {
|
||||
switch m := ErrorMarshalFunc(err).(type) {
|
||||
case nil:
|
||||
@@ -125,11 +187,10 @@ func (c Context) AnErr(key string, err error) Context {
|
||||
case LogObjectMarshaler:
|
||||
return c.Object(key, m)
|
||||
case error:
|
||||
if m == nil || isNilValue(m) {
|
||||
if isNilValue(m) {
|
||||
return c
|
||||
} else {
|
||||
return c.Str(key, m.Error())
|
||||
}
|
||||
return c.Str(key, m.Error())
|
||||
case string:
|
||||
return c.Str(key, m)
|
||||
default:
|
||||
@@ -140,24 +201,7 @@ func (c Context) AnErr(key string, err error) Context {
|
||||
// Errs adds the field key with errs as an array of serialized errors to the
|
||||
// logger context.
|
||||
func (c Context) Errs(key string, errs []error) Context {
|
||||
arr := Arr()
|
||||
for _, err := range errs {
|
||||
switch m := ErrorMarshalFunc(err).(type) {
|
||||
case LogObjectMarshaler:
|
||||
arr = arr.Object(m)
|
||||
case error:
|
||||
if m == nil || isNilValue(m) {
|
||||
arr = arr.Interface(nil)
|
||||
} else {
|
||||
arr = arr.Str(m.Error())
|
||||
}
|
||||
case string:
|
||||
arr = arr.Str(m)
|
||||
default:
|
||||
arr = arr.Interface(m)
|
||||
}
|
||||
}
|
||||
|
||||
arr := c.CreateArray().Errs(errs)
|
||||
return c.Array(key, arr)
|
||||
}
|
||||
|
||||
@@ -166,12 +210,11 @@ func (c Context) Err(err error) Context {
|
||||
if c.l.stack && ErrorStackMarshaler != nil {
|
||||
switch m := ErrorStackMarshaler(err).(type) {
|
||||
case nil:
|
||||
return c // do nothing with nil errors
|
||||
case LogObjectMarshaler:
|
||||
c = c.Object(ErrorStackFieldName, m)
|
||||
case error:
|
||||
if m != nil && !isNilValue(m) {
|
||||
c = c.Str(ErrorStackFieldName, m.Error())
|
||||
}
|
||||
c = c.Str(ErrorStackFieldName, m.Error())
|
||||
case string:
|
||||
c = c.Str(ErrorStackFieldName, m)
|
||||
default:
|
||||
@@ -377,15 +420,15 @@ func (c Context) Times(key string, t []time.Time) Context {
|
||||
return c
|
||||
}
|
||||
|
||||
// Dur adds the fields key with d divided by unit and stored as a float.
|
||||
// Dur adds the field key with d divided by unit and stored as a float.
|
||||
func (c Context) Dur(key string, d time.Duration) Context {
|
||||
c.l.context = enc.AppendDuration(enc.AppendKey(c.l.context, key), d, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision)
|
||||
c.l.context = enc.AppendDuration(enc.AppendKey(c.l.context, key), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
return c
|
||||
}
|
||||
|
||||
// Durs adds the fields key with d divided by unit and stored as a float.
|
||||
// Durs adds the field key with d divided by unit and stored as a float.
|
||||
func (c Context) Durs(key string, d []time.Duration) Context {
|
||||
c.l.context = enc.AppendDurations(enc.AppendKey(c.l.context, key), d, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision)
|
||||
c.l.context = enc.AppendDurations(enc.AppendKey(c.l.context, key), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -461,19 +504,31 @@ func (c Context) Stack() Context {
|
||||
return c
|
||||
}
|
||||
|
||||
// IPAddr adds IPv4 or IPv6 Address to the context
|
||||
// IPAddr adds adds the field key with ip as a net.IP IPv4 or IPv6 Address to the context
|
||||
func (c Context) IPAddr(key string, ip net.IP) Context {
|
||||
c.l.context = enc.AppendIPAddr(enc.AppendKey(c.l.context, key), ip)
|
||||
return c
|
||||
}
|
||||
|
||||
// IPPrefix adds IPv4 or IPv6 Prefix (address and mask) to the context
|
||||
// IPAddrs adds the field key with ip as a []net.IP array of IPv4 or IPv6 Address to the context
|
||||
func (c Context) IPAddrs(key string, ip []net.IP) Context {
|
||||
c.l.context = enc.AppendIPAddrs(enc.AppendKey(c.l.context, key), ip)
|
||||
return c
|
||||
}
|
||||
|
||||
// IPPrefix adds adds the field key with pfx as a []net.IPNet IPv4 or IPv6 Prefix (address and mask) to the context
|
||||
func (c Context) IPPrefix(key string, pfx net.IPNet) Context {
|
||||
c.l.context = enc.AppendIPPrefix(enc.AppendKey(c.l.context, key), pfx)
|
||||
return c
|
||||
}
|
||||
|
||||
// MACAddr adds MAC address to the context
|
||||
// IPPrefix adds adds the field key with pfx as a []net.IPNet array of IPv4 or IPv6 Prefix (address and mask) to the context
|
||||
func (c Context) IPPrefixes(key string, pfx []net.IPNet) Context {
|
||||
c.l.context = enc.AppendIPPrefixes(enc.AppendKey(c.l.context, key), pfx)
|
||||
return c
|
||||
}
|
||||
|
||||
// MACAddr adds adds the field key with ha as a net.HardwareAddr MAC address to the context
|
||||
func (c Context) MACAddr(key string, ha net.HardwareAddr) Context {
|
||||
c.l.context = enc.AppendMACAddr(enc.AppendKey(c.l.context, key), ha)
|
||||
return c
|
||||
|
||||
+5
-6
@@ -25,12 +25,11 @@ type ctxKey struct{}
|
||||
// replacing it in a new Context), use UpdateContext with the following
|
||||
// notation:
|
||||
//
|
||||
// ctx := r.Context()
|
||||
// l := zerolog.Ctx(ctx)
|
||||
// l.UpdateContext(func(c Context) Context {
|
||||
// return c.Str("bar", "baz")
|
||||
// })
|
||||
//
|
||||
// ctx := r.Context()
|
||||
// l := zerolog.Ctx(ctx)
|
||||
// l.UpdateContext(func(c Context) Context {
|
||||
// return c.Str("bar", "baz")
|
||||
// })
|
||||
func (l Logger) WithContext(ctx context.Context) context.Context {
|
||||
if _, ok := ctx.Value(ctxKey{}).(*Logger); !ok && l.level == Disabled {
|
||||
// Do not store disabled logger.
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@ type encoder interface {
|
||||
AppendBool(dst []byte, val bool) []byte
|
||||
AppendBools(dst []byte, vals []bool) []byte
|
||||
AppendBytes(dst, s []byte) []byte
|
||||
AppendDuration(dst []byte, d time.Duration, unit time.Duration, useInt bool, precision int) []byte
|
||||
AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, useInt bool, precision int) []byte
|
||||
AppendDuration(dst []byte, d time.Duration, unit time.Duration, format string, useInt bool, precision int) []byte
|
||||
AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, format string, useInt bool, precision int) []byte
|
||||
AppendEndMarker(dst []byte) []byte
|
||||
AppendFloat32(dst []byte, val float32, precision int) []byte
|
||||
AppendFloat64(dst []byte, val float64, precision int) []byte
|
||||
|
||||
+143
-55
@@ -32,6 +32,15 @@ type Event struct {
|
||||
}
|
||||
|
||||
func putEvent(e *Event) {
|
||||
// prevent any subsequent use of the Event contextual state and truncate the buffer
|
||||
e.w = nil
|
||||
e.done = nil
|
||||
e.stack = false
|
||||
e.ch = nil
|
||||
e.skipFrame = 0
|
||||
e.ctx = nil
|
||||
e.buf = e.buf[:0]
|
||||
|
||||
// Proper usage of a sync.Pool requires each entry to have approximately
|
||||
// the same memory cost. To obtain this property when the stored type
|
||||
// contains a variably-sized buffer, we add a hard limit on the maximum buffer
|
||||
@@ -39,10 +48,9 @@ func putEvent(e *Event) {
|
||||
//
|
||||
// See https://golang.org/issue/23199
|
||||
const maxSize = 1 << 16 // 64KiB
|
||||
if cap(e.buf) > maxSize {
|
||||
return
|
||||
if cap(e.buf) <= maxSize {
|
||||
eventPool.Put(e)
|
||||
}
|
||||
eventPool.Put(e)
|
||||
}
|
||||
|
||||
// LogObjectMarshaler provides a strongly-typed and encoding-agnostic interface
|
||||
@@ -57,14 +65,15 @@ type LogArrayMarshaler interface {
|
||||
MarshalZerologArray(a *Array)
|
||||
}
|
||||
|
||||
func newEvent(w LevelWriter, level Level) *Event {
|
||||
func newEvent(w LevelWriter, level Level, stack bool, ctx context.Context, hooks []Hook) *Event {
|
||||
e := eventPool.Get().(*Event)
|
||||
e.buf = e.buf[:0]
|
||||
e.ch = nil
|
||||
e.stack = stack
|
||||
e.ctx = ctx
|
||||
e.ch = hooks
|
||||
e.buf = enc.AppendBeginMarker(e.buf)
|
||||
e.w = w
|
||||
e.level = level
|
||||
e.stack = false
|
||||
e.skipFrame = 0
|
||||
return e
|
||||
}
|
||||
@@ -164,31 +173,58 @@ func (e *Event) Fields(fields interface{}) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = appendFields(e.buf, fields, e.stack)
|
||||
e.buf = appendFields(e.buf, fields, e.stack, e.ctx, e.ch)
|
||||
return e
|
||||
}
|
||||
|
||||
// Dict adds the field key with a dict to the event context.
|
||||
// Use zerolog.Dict() to create the dictionary.
|
||||
// Use e.CreateDict() to create the dictionary.
|
||||
func (e *Event) Dict(key string, dict *Event) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
if e != nil {
|
||||
dict.buf = enc.AppendEndMarker(dict.buf)
|
||||
e.buf = append(enc.AppendKey(e.buf, key), dict.buf...)
|
||||
}
|
||||
dict.buf = enc.AppendEndMarker(dict.buf)
|
||||
e.buf = append(enc.AppendKey(e.buf, key), dict.buf...)
|
||||
putEvent(dict)
|
||||
return e
|
||||
}
|
||||
|
||||
// CreateDict creates an Event to be used with the *Event.Dict method.
|
||||
// It preserves the stack, hooks, and context from the parent event.
|
||||
// Call usual field methods like Str, Int etc to add fields to this
|
||||
// event and give it as argument the *Event.Dict method.
|
||||
func (e *Event) CreateDict() *Event {
|
||||
if e == nil {
|
||||
return newEvent(nil, DebugLevel, false, nil, nil)
|
||||
}
|
||||
return newEvent(nil, DebugLevel, e.stack, e.ctx, e.ch)
|
||||
}
|
||||
|
||||
// Dict creates an Event to be used with the *Event.Dict method.
|
||||
// Call usual field methods like Str, Int etc to add fields to this
|
||||
// event and give it as argument the *Event.Dict method.
|
||||
// NOTE: This function is deprecated because it does not preserve
|
||||
// the stack, hooks, and context from the parent event.
|
||||
// Deprecated: Use Event.CreateDict instead.
|
||||
func Dict() *Event {
|
||||
return newEvent(nil, 0)
|
||||
return newEvent(nil, DebugLevel, false, nil, nil)
|
||||
}
|
||||
|
||||
// CreateArray creates an Array to be used with the *Event.Array method.
|
||||
// It preserves the stack, hooks, and context from the parent event.
|
||||
// Call usual field methods like Str, Int etc to add elements to this
|
||||
// array and give it as argument the *Event.Array method.
|
||||
func (e *Event) CreateArray() *Array {
|
||||
a := Arr()
|
||||
if e != nil {
|
||||
a.stack = e.stack
|
||||
a.ctx = e.ctx
|
||||
a.ch = e.ch
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// Array adds the field key with an array to the event context.
|
||||
// Use zerolog.Arr() to create the array or pass a type that
|
||||
// Use e.CreateArray() to create the array or pass a type that
|
||||
// implement the LogArrayMarshaler interface.
|
||||
func (e *Event) Array(key string, arr LogArrayMarshaler) *Event {
|
||||
if e == nil {
|
||||
@@ -199,7 +235,7 @@ func (e *Event) Array(key string, arr LogArrayMarshaler) *Event {
|
||||
if aa, ok := arr.(*Array); ok {
|
||||
a = aa
|
||||
} else {
|
||||
a = Arr()
|
||||
a = e.CreateArray()
|
||||
arr.MarshalZerologArray(a)
|
||||
}
|
||||
e.buf = a.write(e.buf)
|
||||
@@ -228,6 +264,33 @@ func (e *Event) Object(key string, obj LogObjectMarshaler) *Event {
|
||||
return e
|
||||
}
|
||||
|
||||
// Objects adds the field key with objs as an array of objects that
|
||||
// implement the LogObjectMarshaler interface to the event.
|
||||
//
|
||||
// This is the array version that accepts a slice of LogObjectMarshaler objects.
|
||||
func (e *Event) Objects(key string, objs []LogObjectMarshaler) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendArrayStart(enc.AppendKey(e.buf, key))
|
||||
for i, obj := range objs {
|
||||
e.buf = appendObject(e.buf, obj, e.stack, e.ctx, e.ch)
|
||||
if i < (len(objs) - 1) {
|
||||
e.buf = enc.AppendArrayDelim(e.buf)
|
||||
}
|
||||
}
|
||||
e.buf = enc.AppendArrayEnd(e.buf)
|
||||
return e
|
||||
}
|
||||
|
||||
// ObjectsV adds the field key with objs as an array of objects that
|
||||
// implement the LogObjectMarshaler interface to the event.
|
||||
//
|
||||
// This is a variadic version that accepts a list of individual LogObjectMarshaler objects.
|
||||
func (e *Event) ObjectsV(key string, objs ...LogObjectMarshaler) *Event {
|
||||
return e.Objects(key, objs)
|
||||
}
|
||||
|
||||
// Func allows an anonymous func to run only if the event is enabled.
|
||||
func (e *Event) Func(f func(e *Event)) *Event {
|
||||
if e != nil && e.Enabled() {
|
||||
@@ -258,6 +321,8 @@ func (e *Event) Str(key, val string) *Event {
|
||||
}
|
||||
|
||||
// Strs adds the field key with vals as a []string to the *Event context.
|
||||
//
|
||||
// This is the array version that accepts a slice of string values.
|
||||
func (e *Event) Strs(key string, vals []string) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
@@ -266,8 +331,16 @@ func (e *Event) Strs(key string, vals []string) *Event {
|
||||
return e
|
||||
}
|
||||
|
||||
// Stringer adds the field key with val.String() (or null if val is nil)
|
||||
// to the *Event context.
|
||||
// StrsV adds the field key with vals as a []string to the *Event context.
|
||||
//
|
||||
// This is a variadic version that accepts a list of individual strings.
|
||||
func (e *Event) StrsV(key string, vals ...string) *Event {
|
||||
return e.Strs(key, vals)
|
||||
}
|
||||
|
||||
// Stringer adds the field key and a val to the *Event context.
|
||||
// If val is not nil, it is added by calling val.String().
|
||||
// If val is nil, it is encoded as null without calling String().
|
||||
func (e *Event) Stringer(key string, val fmt.Stringer) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
@@ -276,9 +349,11 @@ func (e *Event) Stringer(key string, val fmt.Stringer) *Event {
|
||||
return e
|
||||
}
|
||||
|
||||
// Stringers adds the field key with vals where each individual val
|
||||
// is used as val.String() (or null if val is empty) to the *Event
|
||||
// context.
|
||||
// Stringers adds the field key with vals to the *Event context.
|
||||
// If a val is not nil, it is added by calling val.String().
|
||||
// If a val is nil, it is encoded as null without calling String().
|
||||
//
|
||||
// This is the array version that accepts a slice of fmt.Stringer values.
|
||||
func (e *Event) Stringers(key string, vals []fmt.Stringer) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
@@ -287,6 +362,16 @@ func (e *Event) Stringers(key string, vals []fmt.Stringer) *Event {
|
||||
return e
|
||||
}
|
||||
|
||||
// StringersV adds the field key with vals to the *Event context.
|
||||
// If a val is not nil, it is added by calling val.String().
|
||||
// If a val is nil, it is encoded as null without calling String().
|
||||
//
|
||||
// This is a variadic version that accepts a list of individual
|
||||
// fmt.Stringer values.
|
||||
func (e *Event) StringersV(key string, vals ...fmt.Stringer) *Event {
|
||||
return e.Stringers(key, vals)
|
||||
}
|
||||
|
||||
// Bytes adds the field key with val as a string to the *Event context.
|
||||
//
|
||||
// Runes outside of normal ASCII ranges will be hex-encoded in the resulting
|
||||
@@ -344,11 +429,10 @@ func (e *Event) AnErr(key string, err error) *Event {
|
||||
case LogObjectMarshaler:
|
||||
return e.Object(key, m)
|
||||
case error:
|
||||
if m == nil || isNilValue(m) {
|
||||
if isNilValue(m) {
|
||||
return e
|
||||
} else {
|
||||
return e.Str(key, m.Error())
|
||||
}
|
||||
return e.Str(key, m.Error())
|
||||
case string:
|
||||
return e.Str(key, m)
|
||||
default:
|
||||
@@ -362,20 +446,7 @@ func (e *Event) Errs(key string, errs []error) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
arr := Arr()
|
||||
for _, err := range errs {
|
||||
switch m := ErrorMarshalFunc(err).(type) {
|
||||
case LogObjectMarshaler:
|
||||
arr = arr.Object(m)
|
||||
case error:
|
||||
arr = arr.Err(m)
|
||||
case string:
|
||||
arr = arr.Str(m)
|
||||
default:
|
||||
arr = arr.Interface(m)
|
||||
}
|
||||
}
|
||||
|
||||
arr := e.CreateArray().Errs(errs)
|
||||
return e.Array(key, arr)
|
||||
}
|
||||
|
||||
@@ -391,21 +462,22 @@ func (e *Event) Err(err error) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
|
||||
if e.stack && ErrorStackMarshaler != nil {
|
||||
switch m := ErrorStackMarshaler(err).(type) {
|
||||
case nil:
|
||||
return e
|
||||
case LogObjectMarshaler:
|
||||
e.Object(ErrorStackFieldName, m)
|
||||
e = e.Object(ErrorStackFieldName, m)
|
||||
case error:
|
||||
if m != nil && !isNilValue(m) {
|
||||
e.Str(ErrorStackFieldName, m.Error())
|
||||
}
|
||||
e = e.Str(ErrorStackFieldName, m.Error())
|
||||
case string:
|
||||
e.Str(ErrorStackFieldName, m)
|
||||
e = e.Str(ErrorStackFieldName, m)
|
||||
default:
|
||||
e.Interface(ErrorStackFieldName, m)
|
||||
e = e.Interface(ErrorStackFieldName, m)
|
||||
}
|
||||
}
|
||||
|
||||
return e.AnErr(ErrorFieldName, err)
|
||||
}
|
||||
|
||||
@@ -431,8 +503,8 @@ func (e *Event) Ctx(ctx context.Context) *Event {
|
||||
}
|
||||
|
||||
// GetCtx retrieves the Go context.Context which is optionally stored in the
|
||||
// Event. This allows Hooks and functions passed to Func() to retrieve values
|
||||
// which are stored in the context.Context. This can be useful in tracing,
|
||||
// Event. This allows Hooks and functions passed to Func() to retrieve values
|
||||
// which are stored in the context.Context. This can be useful in tracing,
|
||||
// where span information is commonly propagated in the context.Context.
|
||||
func (e *Event) GetCtx() context.Context {
|
||||
if e == nil || e.ctx == nil {
|
||||
@@ -713,7 +785,7 @@ func (e *Event) Dur(key string, d time.Duration) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendDuration(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision)
|
||||
e.buf = enc.AppendDuration(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -724,7 +796,7 @@ func (e *Event) Durs(key string, d []time.Duration) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendDurations(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision)
|
||||
e.buf = enc.AppendDurations(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -739,7 +811,7 @@ func (e *Event) TimeDiff(key string, t time.Time, start time.Time) *Event {
|
||||
if t.After(start) {
|
||||
d = t.Sub(start)
|
||||
}
|
||||
e.buf = enc.AppendDuration(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision)
|
||||
e.buf = enc.AppendDuration(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -794,15 +866,13 @@ func (e *Event) caller(skip int) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
pc, file, line, ok := runtime.Caller(skip + e.skipFrame)
|
||||
if !ok {
|
||||
return e
|
||||
if pc, file, line, ok := runtime.Caller(skip + e.skipFrame); ok {
|
||||
e.buf = enc.AppendString(enc.AppendKey(e.buf, CallerFieldName), CallerMarshalFunc(pc, file, line))
|
||||
}
|
||||
e.buf = enc.AppendString(enc.AppendKey(e.buf, CallerFieldName), CallerMarshalFunc(pc, file, line))
|
||||
return e
|
||||
}
|
||||
|
||||
// IPAddr adds IPv4 or IPv6 Address to the event
|
||||
// IPAddr adds the field key with ip as a net.IP IPv4 or IPv6 Address to the event
|
||||
func (e *Event) IPAddr(key string, ip net.IP) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
@@ -811,7 +881,16 @@ func (e *Event) IPAddr(key string, ip net.IP) *Event {
|
||||
return e
|
||||
}
|
||||
|
||||
// IPPrefix adds IPv4 or IPv6 Prefix (address and mask) to the event
|
||||
// IPAddrs adds the field key with ip as a net.IP array of IPv4 or IPv6 Address to the event
|
||||
func (e *Event) IPAddrs(key string, ip []net.IP) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendIPAddrs(enc.AppendKey(e.buf, key), ip)
|
||||
return e
|
||||
}
|
||||
|
||||
// IPPrefix adds the field key with pfx as a net.IPNet IPv4 or IPv6 Prefix (address and mask) to the event
|
||||
func (e *Event) IPPrefix(key string, pfx net.IPNet) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
@@ -820,7 +899,16 @@ func (e *Event) IPPrefix(key string, pfx net.IPNet) *Event {
|
||||
return e
|
||||
}
|
||||
|
||||
// MACAddr adds MAC address to the event
|
||||
// IPPrefixes the field key with pfx as a net.IPNet array of IPv4 or IPv6 Prefixes (address and mask) to the event
|
||||
func (e *Event) IPPrefixes(key string, pfx []net.IPNet) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendIPPrefixes(enc.AppendKey(e.buf, key), pfx)
|
||||
return e
|
||||
}
|
||||
|
||||
// MACAddr the field key with ha as a net.HardwareAddr MAC address to the event
|
||||
func (e *Event) MACAddr(key string, ha net.HardwareAddr) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
|
||||
+62
-42
@@ -1,24 +1,31 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"reflect"
|
||||
"sort"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func isNilValue(i interface{}) bool {
|
||||
return (*[2]uintptr)(unsafe.Pointer(&i))[1] == 0
|
||||
func isNilValue(e error) bool {
|
||||
switch reflect.TypeOf(e).Kind() {
|
||||
case reflect.Ptr:
|
||||
return reflect.ValueOf(e).IsNil()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func appendFields(dst []byte, fields interface{}, stack bool) []byte {
|
||||
func appendFields(dst []byte, fields interface{}, stack bool, ctx context.Context, hooks []Hook) []byte {
|
||||
switch fields := fields.(type) {
|
||||
case []interface{}:
|
||||
if n := len(fields); n&0x1 == 1 { // odd number
|
||||
fields = fields[:n-1]
|
||||
}
|
||||
dst = appendFieldList(dst, fields, stack)
|
||||
dst = appendFieldList(dst, fields, stack, ctx, hooks)
|
||||
case map[string]interface{}:
|
||||
keys := make([]string, 0, len(fields))
|
||||
for key := range fields {
|
||||
@@ -28,13 +35,22 @@ func appendFields(dst []byte, fields interface{}, stack bool) []byte {
|
||||
kv := make([]interface{}, 2)
|
||||
for _, key := range keys {
|
||||
kv[0], kv[1] = key, fields[key]
|
||||
dst = appendFieldList(dst, kv, stack)
|
||||
dst = appendFieldList(dst, kv, stack, ctx, hooks)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte {
|
||||
func appendObject(dst []byte, obj LogObjectMarshaler, stack bool, ctx context.Context, hooks []Hook) []byte {
|
||||
e := newEvent(LevelWriterAdapter{io.Discard}, DebugLevel, stack, ctx, hooks)
|
||||
e.buf = e.buf[:0] // discard the beginning marker added by newEvent
|
||||
e.appendObject(obj)
|
||||
dst = append(dst, e.buf...)
|
||||
putEvent(e)
|
||||
return dst
|
||||
}
|
||||
|
||||
func appendFieldList(dst []byte, kvList []interface{}, stack bool, ctx context.Context, hooks []Hook) []byte {
|
||||
for i, n := 0, len(kvList); i < n; i += 2 {
|
||||
key, val := kvList[i], kvList[i+1]
|
||||
if key, ok := key.(string); ok {
|
||||
@@ -42,14 +58,6 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte {
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
if val, ok := val.(LogObjectMarshaler); ok {
|
||||
e := newEvent(nil, 0)
|
||||
e.buf = e.buf[:0]
|
||||
e.appendObject(val)
|
||||
dst = append(dst, e.buf...)
|
||||
putEvent(e)
|
||||
continue
|
||||
}
|
||||
switch val := val.(type) {
|
||||
case string:
|
||||
dst = enc.AppendString(dst, val)
|
||||
@@ -57,16 +65,12 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte {
|
||||
dst = enc.AppendBytes(dst, val)
|
||||
case error:
|
||||
switch m := ErrorMarshalFunc(val).(type) {
|
||||
case nil:
|
||||
dst = enc.AppendNil(dst)
|
||||
case LogObjectMarshaler:
|
||||
e := newEvent(nil, 0)
|
||||
e.buf = e.buf[:0]
|
||||
e.appendObject(m)
|
||||
dst = append(dst, e.buf...)
|
||||
putEvent(e)
|
||||
dst = appendObject(dst, m, stack, ctx, hooks)
|
||||
case error:
|
||||
if m == nil || isNilValue(m) {
|
||||
dst = enc.AppendNil(dst)
|
||||
} else {
|
||||
if !isNilValue(m) {
|
||||
dst = enc.AppendString(dst, m.Error())
|
||||
}
|
||||
case string:
|
||||
@@ -76,16 +80,20 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte {
|
||||
}
|
||||
|
||||
if stack && ErrorStackMarshaler != nil {
|
||||
dst = enc.AppendKey(dst, ErrorStackFieldName)
|
||||
switch m := ErrorStackMarshaler(val).(type) {
|
||||
case nil:
|
||||
return dst // do nothing with nil errors
|
||||
case LogObjectMarshaler:
|
||||
dst = enc.AppendKey(dst, ErrorStackFieldName)
|
||||
dst = appendObject(dst, m, stack, ctx, hooks)
|
||||
case error:
|
||||
if m != nil && !isNilValue(m) {
|
||||
dst = enc.AppendString(dst, m.Error())
|
||||
}
|
||||
dst = enc.AppendKey(dst, ErrorStackFieldName)
|
||||
dst = enc.AppendString(dst, m.Error())
|
||||
case string:
|
||||
dst = enc.AppendKey(dst, ErrorStackFieldName)
|
||||
dst = enc.AppendString(dst, m)
|
||||
default:
|
||||
dst = enc.AppendKey(dst, ErrorStackFieldName)
|
||||
dst = enc.AppendInterface(dst, m)
|
||||
}
|
||||
}
|
||||
@@ -93,16 +101,12 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte {
|
||||
dst = enc.AppendArrayStart(dst)
|
||||
for i, err := range val {
|
||||
switch m := ErrorMarshalFunc(err).(type) {
|
||||
case nil:
|
||||
dst = enc.AppendNil(dst)
|
||||
case LogObjectMarshaler:
|
||||
e := newEvent(nil, 0)
|
||||
e.buf = e.buf[:0]
|
||||
e.appendObject(m)
|
||||
dst = append(dst, e.buf...)
|
||||
putEvent(e)
|
||||
dst = appendObject(dst, m, stack, ctx, hooks)
|
||||
case error:
|
||||
if m == nil || isNilValue(m) {
|
||||
dst = enc.AppendNil(dst)
|
||||
} else {
|
||||
if !isNilValue(m) {
|
||||
dst = enc.AppendString(dst, m.Error())
|
||||
}
|
||||
case string:
|
||||
@@ -112,7 +116,16 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte {
|
||||
}
|
||||
|
||||
if i < (len(val) - 1) {
|
||||
enc.AppendArrayDelim(dst)
|
||||
dst = enc.AppendArrayDelim(dst)
|
||||
}
|
||||
}
|
||||
dst = enc.AppendArrayEnd(dst)
|
||||
case []LogObjectMarshaler:
|
||||
dst = enc.AppendArrayStart(dst)
|
||||
for i, obj := range val {
|
||||
dst = appendObject(dst, obj, stack, ctx, hooks)
|
||||
if i < (len(val) - 1) {
|
||||
dst = enc.AppendArrayDelim(dst)
|
||||
}
|
||||
}
|
||||
dst = enc.AppendArrayEnd(dst)
|
||||
@@ -145,7 +158,7 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte {
|
||||
case time.Time:
|
||||
dst = enc.AppendTime(dst, val, TimeFieldFormat)
|
||||
case time.Duration:
|
||||
dst = enc.AppendDuration(dst, val, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision)
|
||||
dst = enc.AppendDuration(dst, val, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
case *string:
|
||||
if val != nil {
|
||||
dst = enc.AppendString(dst, *val)
|
||||
@@ -238,7 +251,7 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte {
|
||||
}
|
||||
case *time.Duration:
|
||||
if val != nil {
|
||||
dst = enc.AppendDuration(dst, *val, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision)
|
||||
dst = enc.AppendDuration(dst, *val, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
@@ -258,8 +271,7 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte {
|
||||
dst = enc.AppendInts64(dst, val)
|
||||
case []uint:
|
||||
dst = enc.AppendUints(dst, val)
|
||||
// case []uint8:
|
||||
// dst = enc.AppendUints8(dst, val)
|
||||
// case []uint8: is handled as []byte above
|
||||
case []uint16:
|
||||
dst = enc.AppendUints16(dst, val)
|
||||
case []uint32:
|
||||
@@ -273,19 +285,27 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte {
|
||||
case []time.Time:
|
||||
dst = enc.AppendTimes(dst, val, TimeFieldFormat)
|
||||
case []time.Duration:
|
||||
dst = enc.AppendDurations(dst, val, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision)
|
||||
dst = enc.AppendDurations(dst, val, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
case nil:
|
||||
dst = enc.AppendNil(dst)
|
||||
case net.IP:
|
||||
dst = enc.AppendIPAddr(dst, val)
|
||||
case []net.IP:
|
||||
dst = enc.AppendIPAddrs(dst, val)
|
||||
case net.IPNet:
|
||||
dst = enc.AppendIPPrefix(dst, val)
|
||||
case []net.IPNet:
|
||||
dst = enc.AppendIPPrefixes(dst, val)
|
||||
case net.HardwareAddr:
|
||||
dst = enc.AppendMACAddr(dst, val)
|
||||
case json.RawMessage:
|
||||
dst = appendJSON(dst, val)
|
||||
default:
|
||||
dst = enc.AppendInterface(dst, val)
|
||||
if lom, ok := val.(LogObjectMarshaler); ok {
|
||||
dst = appendObject(dst, lom, stack, ctx, hooks)
|
||||
} else {
|
||||
dst = enc.AppendInterface(dst, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
return dst
|
||||
|
||||
+18
@@ -24,6 +24,16 @@ const (
|
||||
// TimeFormatUnixNano defines a time format that makes time fields to be
|
||||
// serialized as Unix timestamp integers in nanoseconds.
|
||||
TimeFormatUnixNano = "UNIXNANO"
|
||||
|
||||
// DurationFormatFloat defines a format for Duration fields that makes duration fields to be
|
||||
// serialized as floating point numbers.
|
||||
DurationFormatFloat = "float"
|
||||
// DurationFormatInt defines a format for Duration fields that makes duration fields to be
|
||||
// serialized as integers.
|
||||
DurationFormatInt = "int"
|
||||
// DurationFormatString defines a format for Duration fields that makes duration fields to be
|
||||
// serialized as string.
|
||||
DurationFormatString = "string"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -107,12 +117,16 @@ var (
|
||||
// TimestampFunc defines the function called to generate a timestamp.
|
||||
TimestampFunc = time.Now
|
||||
|
||||
// DurationFieldFormat defines the format of the Duration field type.
|
||||
DurationFieldFormat = DurationFormatFloat
|
||||
|
||||
// DurationFieldUnit defines the unit for time.Duration type fields added
|
||||
// using the Dur method.
|
||||
DurationFieldUnit = time.Millisecond
|
||||
|
||||
// DurationFieldInteger renders Dur fields as integer instead of float if
|
||||
// set to true.
|
||||
// Deprecated: use DurationFieldFormat with DurationFormatInt instead.
|
||||
DurationFieldInteger = false
|
||||
|
||||
// ErrorHandler is called whenever zerolog fails to write an event on its
|
||||
@@ -120,6 +134,10 @@ var (
|
||||
// be thread safe and non-blocking.
|
||||
ErrorHandler func(err error)
|
||||
|
||||
// FatalExitFunc is called by log.Fatal() instead of os.Exit(1). If not set,
|
||||
// os.Exit(1) is called.
|
||||
FatalExitFunc func()
|
||||
|
||||
// DefaultContextLogger is returned from Ctx() if there is no logger associated
|
||||
// with the context.
|
||||
DefaultContextLogger *Logger
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func AsLogObjectMarshalers[T LogObjectMarshaler](objs []T) []LogObjectMarshaler {
|
||||
if objs == nil {
|
||||
return nil
|
||||
}
|
||||
s := make([]LogObjectMarshaler, len(objs))
|
||||
for i, v := range objs {
|
||||
s[i] = v
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func AsStringers[T fmt.Stringer](objs []T) []fmt.Stringer {
|
||||
if objs == nil {
|
||||
return nil
|
||||
}
|
||||
s := make([]fmt.Stringer, len(objs))
|
||||
for i, v := range objs {
|
||||
s[i] = v
|
||||
}
|
||||
return s
|
||||
}
|
||||
+7
-7
@@ -55,12 +55,12 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
float32Nan = "\xfa\x7f\xc0\x00\x00"
|
||||
float32PosInfinity = "\xfa\x7f\x80\x00\x00"
|
||||
float32NegInfinity = "\xfa\xff\x80\x00\x00"
|
||||
float64Nan = "\xfb\x7f\xf8\x00\x00\x00\x00\x00\x00"
|
||||
float64PosInfinity = "\xfb\x7f\xf0\x00\x00\x00\x00\x00\x00"
|
||||
float64NegInfinity = "\xfb\xff\xf0\x00\x00\x00\x00\x00\x00"
|
||||
float32Nan = "\x7f\xc0\x00\x00"
|
||||
float32PosInfinity = "\x7f\x80\x00\x00"
|
||||
float32NegInfinity = "\xff\x80\x00\x00"
|
||||
float64Nan = "\x7f\xf8\x00\x00\x00\x00\x00\x00"
|
||||
float64PosInfinity = "\x7f\xf0\x00\x00\x00\x00\x00\x00"
|
||||
float64NegInfinity = "\xff\xf0\x00\x00\x00\x00\x00\x00"
|
||||
)
|
||||
|
||||
// IntegerTimeFieldFormat indicates the format of timestamp decoded
|
||||
@@ -72,7 +72,7 @@ var IntegerTimeFieldFormat = time.RFC3339
|
||||
var NanoTimeFieldFormat = time.RFC3339Nano
|
||||
|
||||
func appendCborTypePrefix(dst []byte, major byte, number uint64) []byte {
|
||||
byteCount := 8
|
||||
var byteCount int
|
||||
var minor byte
|
||||
switch {
|
||||
case number < 256:
|
||||
|
||||
+1
-1
@@ -490,7 +490,7 @@ func decodeTimeStamp(src *bufio.Reader) []byte {
|
||||
tsb = append(tsb, '"')
|
||||
return tsb
|
||||
}
|
||||
panic(fmt.Errorf("TS format is neigther int nor float: %d", tsMajor))
|
||||
panic(fmt.Errorf("TS format is neither int nor float: %d", tsMajor))
|
||||
}
|
||||
|
||||
func decodeSimpleFloat(src *bufio.Reader) []byte {
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ func (Encoder) AppendString(dst []byte, s string) []byte {
|
||||
// AppendStringers encodes and adds an array of Stringer values
|
||||
// to the dst byte array.
|
||||
func (e Encoder) AppendStringers(dst []byte, vals []fmt.Stringer) []byte {
|
||||
if len(vals) == 0 {
|
||||
if vals == nil || len(vals) == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
dst = e.AppendArrayStart(dst)
|
||||
|
||||
+23
-5
@@ -4,6 +4,17 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// Import from zerolog/global.go
|
||||
timeFormatUnix = ""
|
||||
timeFormatUnixMs = "UNIXMS"
|
||||
timeFormatUnixMicro = "UNIXMICRO"
|
||||
timeFormatUnixNano = "UNIXNANO"
|
||||
durationFormatFloat = "float"
|
||||
durationFormatInt = "int"
|
||||
durationFormatString = "string"
|
||||
)
|
||||
|
||||
func appendIntegerTimestamp(dst []byte, t time.Time) []byte {
|
||||
major := majorTypeTags
|
||||
minor := additionalTypeTimestamp
|
||||
@@ -27,8 +38,7 @@ func (e Encoder) appendFloatTimestamp(dst []byte, t time.Time) []byte {
|
||||
dst = append(dst, major|minor)
|
||||
secs := t.Unix()
|
||||
nanos := t.Nanosecond()
|
||||
var val float64
|
||||
val = float64(secs)*1.0 + float64(nanos)*1e-9
|
||||
val := float64(secs)*1.0 + float64(nanos)*1e-9
|
||||
return e.AppendFloat64(dst, val, -1)
|
||||
}
|
||||
|
||||
@@ -64,17 +74,25 @@ func (e Encoder) AppendTimes(dst []byte, vals []time.Time, unused string) []byte
|
||||
// AppendDuration encodes and adds a duration to the dst byte array.
|
||||
// useInt field indicates whether to store the duration as seconds (integer) or
|
||||
// as seconds+nanoseconds (float).
|
||||
func (e Encoder) AppendDuration(dst []byte, d time.Duration, unit time.Duration, useInt bool, unused int) []byte {
|
||||
func (e Encoder) AppendDuration(dst []byte, d time.Duration, unit time.Duration, format string, useInt bool, unused int) []byte {
|
||||
if useInt {
|
||||
return e.AppendInt64(dst, int64(d/unit))
|
||||
}
|
||||
switch format {
|
||||
case durationFormatFloat:
|
||||
return e.AppendFloat64(dst, float64(d)/float64(unit), unused)
|
||||
case durationFormatInt:
|
||||
return e.AppendInt64(dst, int64(d/unit))
|
||||
case durationFormatString:
|
||||
return e.AppendString(dst, d.String())
|
||||
}
|
||||
return e.AppendFloat64(dst, float64(d)/float64(unit), unused)
|
||||
}
|
||||
|
||||
// AppendDurations encodes and adds an array of durations to the dst byte array.
|
||||
// useInt field indicates whether to store the duration as seconds (integer) or
|
||||
// as seconds+nanoseconds (float).
|
||||
func (e Encoder) AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, useInt bool, unused int) []byte {
|
||||
func (e Encoder) AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, format string, useInt bool, unused int) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(vals)
|
||||
if l == 0 {
|
||||
@@ -87,7 +105,7 @@ func (e Encoder) AppendDurations(dst []byte, vals []time.Duration, unit time.Dur
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, d := range vals {
|
||||
dst = e.AppendDuration(dst, d, unit, useInt, unused)
|
||||
dst = e.AppendDuration(dst, d, unit, format, useInt, unused)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
+40
-2
@@ -447,7 +447,7 @@ func (e Encoder) AppendType(dst []byte, i interface{}) []byte {
|
||||
return e.AppendString(dst, reflect.TypeOf(i).String())
|
||||
}
|
||||
|
||||
// AppendIPAddr encodes and inserts an IP Address (IPv4 or IPv6).
|
||||
// AppendIPAddr adds a net.IP IPv4 or IPv6 address into the dst byte array.
|
||||
func (e Encoder) AppendIPAddr(dst []byte, ip net.IP) []byte {
|
||||
dst = append(dst, majorTypeTags|additionalTypeIntUint16)
|
||||
dst = append(dst, byte(additionalTypeTagNetworkAddr>>8))
|
||||
@@ -455,7 +455,26 @@ func (e Encoder) AppendIPAddr(dst []byte, ip net.IP) []byte {
|
||||
return e.AppendBytes(dst, ip)
|
||||
}
|
||||
|
||||
// AppendIPPrefix encodes and inserts an IP Address Prefix (Address + Mask Length).
|
||||
// AppendIPAddrs adds a []net.IP array of IPv4 or IPv6 address into the dst byte array.
|
||||
func (e Encoder) AppendIPAddrs(dst []byte, ips []net.IP) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(ips)
|
||||
if l == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, v := range ips {
|
||||
dst = e.AppendIPAddr(dst, v)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendIPPrefix adds a net.IPNet IPv4 or IPv6 Prefix (address & mask) into the dst byte array.
|
||||
func (e Encoder) AppendIPPrefix(dst []byte, pfx net.IPNet) []byte {
|
||||
dst = append(dst, majorTypeTags|additionalTypeIntUint16)
|
||||
dst = append(dst, byte(additionalTypeTagNetworkPrefix>>8))
|
||||
@@ -469,6 +488,25 @@ func (e Encoder) AppendIPPrefix(dst []byte, pfx net.IPNet) []byte {
|
||||
return e.AppendUint8(dst, uint8(maskLen))
|
||||
}
|
||||
|
||||
// AppendIPPrefixes adds a []net.IPNet array of IPv4 or IPv6 Prefix (address & mask) into the dst byte array.
|
||||
func (e Encoder) AppendIPPrefixes(dst []byte, pfxs []net.IPNet) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(pfxs)
|
||||
if l == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, v := range pfxs {
|
||||
dst = e.AppendIPPrefix(dst, v)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendMACAddr encodes and inserts a Hardware (MAC) address.
|
||||
func (e Encoder) AppendMACAddr(dst []byte, ha net.HardwareAddr) []byte {
|
||||
dst = append(dst, majorTypeTags|additionalTypeIntUint16)
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@ func (Encoder) AppendBytes(dst, s []byte) []byte {
|
||||
func (Encoder) AppendHex(dst, s []byte) []byte {
|
||||
dst = append(dst, '"')
|
||||
for _, v := range s {
|
||||
dst = append(dst, hex[v>>4], hex[v&0x0f])
|
||||
dst = append(dst, hexCharacters[v>>4], hexCharacters[v&0x0f])
|
||||
}
|
||||
return append(dst, '"')
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func appendBytesComplex(dst, s []byte, i int) []byte {
|
||||
case '\t':
|
||||
dst = append(dst, '\\', 't')
|
||||
default:
|
||||
dst = append(dst, '\\', 'u', '0', '0', hex[b>>4], hex[b&0xF])
|
||||
dst = append(dst, '\\', 'u', '0', '0', hexCharacters[b>>4], hexCharacters[b&0xF])
|
||||
}
|
||||
i++
|
||||
start = i
|
||||
|
||||
+4
-4
@@ -5,7 +5,7 @@ import (
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const hex = "0123456789abcdef"
|
||||
const hexCharacters = "0123456789abcdef"
|
||||
|
||||
var noEscapeTable = [256]bool{}
|
||||
|
||||
@@ -66,7 +66,7 @@ func (Encoder) AppendString(dst []byte, s string) []byte {
|
||||
// AppendStringers encodes the provided Stringer list to json and
|
||||
// appends the encoded Stringer list to the input byte slice.
|
||||
func (e Encoder) AppendStringers(dst []byte, vals []fmt.Stringer) []byte {
|
||||
if len(vals) == 0 {
|
||||
if vals == nil || len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
@@ -88,7 +88,7 @@ func (e Encoder) AppendStringer(dst []byte, val fmt.Stringer) []byte {
|
||||
return e.AppendString(dst, val.String())
|
||||
}
|
||||
|
||||
//// appendStringComplex is used by appendString to take over an in
|
||||
// appendStringComplex is used by appendString to take over an in
|
||||
// progress JSON string encoding that encountered a character that needs
|
||||
// to be encoded.
|
||||
func appendStringComplex(dst []byte, s string, i int) []byte {
|
||||
@@ -137,7 +137,7 @@ func appendStringComplex(dst []byte, s string, i int) []byte {
|
||||
case '\t':
|
||||
dst = append(dst, '\\', 't')
|
||||
default:
|
||||
dst = append(dst, '\\', 'u', '0', '0', hex[b>>4], hex[b&0xF])
|
||||
dst = append(dst, '\\', 'u', '0', '0', hexCharacters[b>>4], hexCharacters[b&0xF])
|
||||
}
|
||||
i++
|
||||
start = i
|
||||
|
||||
+19
-8
@@ -7,10 +7,13 @@ import (
|
||||
|
||||
const (
|
||||
// Import from zerolog/global.go
|
||||
timeFormatUnix = ""
|
||||
timeFormatUnixMs = "UNIXMS"
|
||||
timeFormatUnixMicro = "UNIXMICRO"
|
||||
timeFormatUnixNano = "UNIXNANO"
|
||||
timeFormatUnix = ""
|
||||
timeFormatUnixMs = "UNIXMS"
|
||||
timeFormatUnixMicro = "UNIXMICRO"
|
||||
timeFormatUnixNano = "UNIXNANO"
|
||||
durationFormatFloat = "float"
|
||||
durationFormatInt = "int"
|
||||
durationFormatString = "string"
|
||||
)
|
||||
|
||||
// AppendTime formats the input time with the given format
|
||||
@@ -88,24 +91,32 @@ func appendUnixNanoTimes(dst []byte, vals []time.Time, div int64) []byte {
|
||||
|
||||
// AppendDuration formats the input duration with the given unit & format
|
||||
// and appends the encoded string to the input byte slice.
|
||||
func (e Encoder) AppendDuration(dst []byte, d time.Duration, unit time.Duration, useInt bool, precision int) []byte {
|
||||
func (e Encoder) AppendDuration(dst []byte, d time.Duration, unit time.Duration, format string, useInt bool, precision int) []byte {
|
||||
if useInt {
|
||||
return strconv.AppendInt(dst, int64(d/unit), 10)
|
||||
}
|
||||
switch format {
|
||||
case durationFormatFloat:
|
||||
return e.AppendFloat64(dst, float64(d)/float64(unit), precision)
|
||||
case durationFormatInt:
|
||||
return e.AppendInt64(dst, int64(d/unit))
|
||||
case durationFormatString:
|
||||
return e.AppendString(dst, d.String())
|
||||
}
|
||||
return e.AppendFloat64(dst, float64(d)/float64(unit), precision)
|
||||
}
|
||||
|
||||
// AppendDurations formats the input durations with the given unit & format
|
||||
// and appends the encoded string list to the input byte slice.
|
||||
func (e Encoder) AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, useInt bool, precision int) []byte {
|
||||
func (e Encoder) AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, format string, useInt bool, precision int) []byte {
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = e.AppendDuration(dst, vals[0], unit, useInt, precision)
|
||||
dst = e.AppendDuration(dst, vals[0], unit, format, useInt, precision)
|
||||
if len(vals) > 1 {
|
||||
for _, d := range vals[1:] {
|
||||
dst = e.AppendDuration(append(dst, ','), d, unit, useInt, precision)
|
||||
dst = e.AppendDuration(append(dst, ','), d, unit, format, useInt, precision)
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
|
||||
+37
-6
@@ -418,18 +418,49 @@ func (Encoder) AppendObjectData(dst []byte, o []byte) []byte {
|
||||
return append(dst, o...)
|
||||
}
|
||||
|
||||
// AppendIPAddr adds IPv4 or IPv6 address to dst.
|
||||
// AppendIPAddr adds a net.IP IPv4 or IPv6 address to dst.
|
||||
func (e Encoder) AppendIPAddr(dst []byte, ip net.IP) []byte {
|
||||
return e.AppendString(dst, ip.String())
|
||||
}
|
||||
|
||||
// AppendIPPrefix adds IPv4 or IPv6 Prefix (address & mask) to dst.
|
||||
func (e Encoder) AppendIPPrefix(dst []byte, pfx net.IPNet) []byte {
|
||||
return e.AppendString(dst, pfx.String())
|
||||
|
||||
// AppendIPAddrs adds a []net.IP array of IPv4 or IPv6 address to dst.
|
||||
func (e Encoder) AppendIPAddrs(dst []byte, ips []net.IP) []byte {
|
||||
if len(ips) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = e.AppendString(dst, ips[0].String())
|
||||
if len(ips) > 1 {
|
||||
for _, ip := range ips[1:] {
|
||||
dst = e.AppendString(append(dst, ','), ip.String())
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendMACAddr adds MAC address to dst.
|
||||
// AppendIPPrefix adds a net.IPNet IPv4 or IPv6 Prefix (address & mask) to dst.
|
||||
func (e Encoder) AppendIPPrefix(dst []byte, pfx net.IPNet) []byte {
|
||||
return e.AppendString(dst, pfx.String())
|
||||
}
|
||||
|
||||
// AppendIPPrefixes adds a []net.IPNet array of IPv4 or IPv6 Prefix (address & mask) to dst.
|
||||
func (e Encoder) AppendIPPrefixes(dst []byte, pfxs []net.IPNet) []byte {
|
||||
if len(pfxs) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = e.AppendString(dst, pfxs[0].String())
|
||||
if len(pfxs) > 1 {
|
||||
for _, pfx := range pfxs[1:] {
|
||||
dst = e.AppendString(append(dst, ','), pfx.String())
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendMACAddr adds a net.HardwareAddr MAC address to dst.
|
||||
func (e Encoder) AppendMACAddr(dst []byte, ha net.HardwareAddr) []byte {
|
||||
return e.AppendString(dst, ha.String())
|
||||
}
|
||||
|
||||
+79
-69
@@ -2,85 +2,85 @@
|
||||
//
|
||||
// A global Logger can be use for simple logging:
|
||||
//
|
||||
// import "github.com/rs/zerolog/log"
|
||||
// import "github.com/rs/zerolog/log"
|
||||
//
|
||||
// log.Info().Msg("hello world")
|
||||
// // Output: {"time":1494567715,"level":"info","message":"hello world"}
|
||||
// log.Info().Msg("hello world")
|
||||
// // Output: {"time":1494567715,"level":"info","message":"hello world"}
|
||||
//
|
||||
// NOTE: To import the global logger, import the "log" subpackage "github.com/rs/zerolog/log".
|
||||
//
|
||||
// Fields can be added to log messages:
|
||||
//
|
||||
// log.Info().Str("foo", "bar").Msg("hello world")
|
||||
// // Output: {"time":1494567715,"level":"info","message":"hello world","foo":"bar"}
|
||||
// log.Info().Str("foo", "bar").Msg("hello world")
|
||||
// // Output: {"time":1494567715,"level":"info","message":"hello world","foo":"bar"}
|
||||
//
|
||||
// Create logger instance to manage different outputs:
|
||||
//
|
||||
// logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
|
||||
// logger.Info().
|
||||
// Str("foo", "bar").
|
||||
// Msg("hello world")
|
||||
// // Output: {"time":1494567715,"level":"info","message":"hello world","foo":"bar"}
|
||||
// logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
|
||||
// logger.Info().
|
||||
// Str("foo", "bar").
|
||||
// Msg("hello world")
|
||||
// // Output: {"time":1494567715,"level":"info","message":"hello world","foo":"bar"}
|
||||
//
|
||||
// Sub-loggers let you chain loggers with additional context:
|
||||
//
|
||||
// sublogger := log.With().Str("component", "foo").Logger()
|
||||
// sublogger.Info().Msg("hello world")
|
||||
// // Output: {"time":1494567715,"level":"info","message":"hello world","component":"foo"}
|
||||
// sublogger := log.With().Str("component", "foo").Logger()
|
||||
// sublogger.Info().Msg("hello world")
|
||||
// // Output: {"time":1494567715,"level":"info","message":"hello world","component":"foo"}
|
||||
//
|
||||
// Level logging
|
||||
//
|
||||
// zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
// zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
//
|
||||
// log.Debug().Msg("filtered out message")
|
||||
// log.Info().Msg("routed message")
|
||||
// log.Debug().Msg("filtered out message")
|
||||
// log.Info().Msg("routed message")
|
||||
//
|
||||
// if e := log.Debug(); e.Enabled() {
|
||||
// // Compute log output only if enabled.
|
||||
// value := compute()
|
||||
// e.Str("foo": value).Msg("some debug message")
|
||||
// }
|
||||
// // Output: {"level":"info","time":1494567715,"routed message"}
|
||||
// if e := log.Debug(); e.Enabled() {
|
||||
// // Compute log output only if enabled.
|
||||
// value := compute()
|
||||
// e.Str("foo": value).Msg("some debug message")
|
||||
// }
|
||||
// // Output: {"level":"info","time":1494567715,"routed message"}
|
||||
//
|
||||
// Customize automatic field names:
|
||||
//
|
||||
// log.TimestampFieldName = "t"
|
||||
// log.LevelFieldName = "p"
|
||||
// log.MessageFieldName = "m"
|
||||
// log.TimestampFieldName = "t"
|
||||
// log.LevelFieldName = "p"
|
||||
// log.MessageFieldName = "m"
|
||||
//
|
||||
// log.Info().Msg("hello world")
|
||||
// // Output: {"t":1494567715,"p":"info","m":"hello world"}
|
||||
// log.Info().Msg("hello world")
|
||||
// // Output: {"t":1494567715,"p":"info","m":"hello world"}
|
||||
//
|
||||
// Log with no level and message:
|
||||
//
|
||||
// log.Log().Str("foo","bar").Msg("")
|
||||
// // Output: {"time":1494567715,"foo":"bar"}
|
||||
// log.Log().Str("foo","bar").Msg("")
|
||||
// // Output: {"time":1494567715,"foo":"bar"}
|
||||
//
|
||||
// Add contextual fields to global Logger:
|
||||
//
|
||||
// log.Logger = log.With().Str("foo", "bar").Logger()
|
||||
// log.Logger = log.With().Str("foo", "bar").Logger()
|
||||
//
|
||||
// Sample logs:
|
||||
//
|
||||
// sampled := log.Sample(&zerolog.BasicSampler{N: 10})
|
||||
// sampled.Info().Msg("will be logged every 10 messages")
|
||||
// sampled := log.Sample(&zerolog.BasicSampler{N: 10})
|
||||
// sampled.Info().Msg("will be logged every 10 messages")
|
||||
//
|
||||
// Log with contextual hooks:
|
||||
//
|
||||
// // Create the hook:
|
||||
// type SeverityHook struct{}
|
||||
// // Create the hook:
|
||||
// type SeverityHook struct{}
|
||||
//
|
||||
// func (h SeverityHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
|
||||
// if level != zerolog.NoLevel {
|
||||
// e.Str("severity", level.String())
|
||||
// }
|
||||
// }
|
||||
// func (h SeverityHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
|
||||
// if level != zerolog.NoLevel {
|
||||
// e.Str("severity", level.String())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // And use it:
|
||||
// var h SeverityHook
|
||||
// log := zerolog.New(os.Stdout).Hook(h)
|
||||
// log.Warn().Msg("")
|
||||
// // Output: {"level":"warn","severity":"warn"}
|
||||
// // And use it:
|
||||
// var h SeverityHook
|
||||
// log := zerolog.New(os.Stdout).Hook(h)
|
||||
// log.Warn().Msg("")
|
||||
// // Output: {"level":"warn","severity":"warn"}
|
||||
//
|
||||
// # Caveats
|
||||
//
|
||||
@@ -89,11 +89,11 @@
|
||||
// There is no fields deduplication out-of-the-box.
|
||||
// Using the same key multiple times creates new key in final JSON each time.
|
||||
//
|
||||
// logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
|
||||
// logger.Info().
|
||||
// Timestamp().
|
||||
// Msg("dup")
|
||||
// // Output: {"level":"info","time":1494567715,"time":1494567715,"message":"dup"}
|
||||
// logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
|
||||
// logger.Info().
|
||||
// Timestamp().
|
||||
// Msg("dup")
|
||||
// // Output: {"level":"info","time":1494567715,"time":1494567715,"message":"dup"}
|
||||
//
|
||||
// In this case, many consumers will take the last value,
|
||||
// but this is not guaranteed; check yours if in doubt.
|
||||
@@ -102,15 +102,15 @@
|
||||
//
|
||||
// Be careful when calling UpdateContext. It is not concurrency safe. Use the With method to create a child logger:
|
||||
//
|
||||
// func handler(w http.ResponseWriter, r *http.Request) {
|
||||
// // Create a child logger for concurrency safety
|
||||
// logger := log.Logger.With().Logger()
|
||||
// func handler(w http.ResponseWriter, r *http.Request) {
|
||||
// // Create a child logger for concurrency safety
|
||||
// logger := log.Logger.With().Logger()
|
||||
//
|
||||
// // Add context fields, for example User-Agent from HTTP headers
|
||||
// logger.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
||||
// ...
|
||||
// })
|
||||
// }
|
||||
// // Add context fields, for example User-Agent from HTTP headers
|
||||
// logger.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
||||
// ...
|
||||
// })
|
||||
// }
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
@@ -294,7 +294,7 @@ func (l Logger) With() Context {
|
||||
// Caution: This method is not concurrency safe.
|
||||
// Use the With method to create a child logger before modifying the context from concurrent goroutines.
|
||||
func (l *Logger) UpdateContext(update func(c Context) Context) {
|
||||
if l == disabledLogger {
|
||||
if l.disabled() {
|
||||
return
|
||||
}
|
||||
if cap(l.context) == 0 {
|
||||
@@ -382,18 +382,24 @@ func (l *Logger) Err(err error) *Event {
|
||||
return l.Info()
|
||||
}
|
||||
|
||||
// Fatal starts a new message with fatal level. The os.Exit(1) function
|
||||
// is called by the Msg method, which terminates the program immediately.
|
||||
// Fatal starts a new message with fatal level. The FatalExitFunc interceptor function
|
||||
// is called by the Msg method, which by default terminates the program immediately
|
||||
// using os.Exit(1), any desired behavior can be implemented by setting FatalExitFunc.
|
||||
//
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) Fatal() *Event {
|
||||
return l.newEvent(FatalLevel, func(msg string) {
|
||||
if closer, ok := l.w.(io.Closer); ok {
|
||||
// Close the writer to flush any buffered message. Otherwise the message
|
||||
// will be lost as os.Exit() terminates the program immediately.
|
||||
// could be lost if FatalExitFunc() terminates the program immediately or
|
||||
// os.Exit(1) is called if not FatalExitFunc isn't set (default).
|
||||
closer.Close()
|
||||
}
|
||||
os.Exit(1)
|
||||
if FatalExitFunc != nil {
|
||||
FatalExitFunc()
|
||||
} else {
|
||||
os.Exit(1) // untestable: terminates the program, cannot be covered
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -487,25 +493,29 @@ func (l *Logger) newEvent(level Level, done func(string)) *Event {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
e := newEvent(l.w, level)
|
||||
e := newEvent(l.w, level, l.stack, l.ctx, l.hooks)
|
||||
e.done = done
|
||||
e.ch = l.hooks
|
||||
e.ctx = l.ctx
|
||||
if level != NoLevel && LevelFieldName != "" {
|
||||
e.Str(LevelFieldName, LevelFieldMarshalFunc(level))
|
||||
}
|
||||
if len(l.context) > 1 {
|
||||
e.buf = enc.AppendObjectData(e.buf, l.context)
|
||||
}
|
||||
if l.stack {
|
||||
e.Stack()
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (l *Logger) scratchEvent() *Event {
|
||||
return newEvent(LevelWriterAdapter{io.Discard}, DebugLevel, l.stack, l.ctx, l.hooks)
|
||||
}
|
||||
|
||||
// disabled returns true if the logger is a disabled or nop logger.
|
||||
func (l *Logger) disabled() bool {
|
||||
return l.w == nil || l.level == Disabled
|
||||
}
|
||||
|
||||
// should returns true if the log event should be logged.
|
||||
func (l *Logger) should(lvl Level) bool {
|
||||
if l.w == nil {
|
||||
if l.disabled() {
|
||||
return false
|
||||
}
|
||||
if lvl < l.level || lvl < GlobalLevel() {
|
||||
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SlogHandler implements the slog.Handler interface using a zerolog.Logger
|
||||
// as the underlying log backend. This allows code that uses the standard
|
||||
// library's slog package to route log output through zerolog.
|
||||
type SlogHandler struct {
|
||||
logger Logger
|
||||
prefix string // group prefix for nested groups
|
||||
attrs []slog.Attr
|
||||
}
|
||||
|
||||
// NewSlogHandler creates a new slog.Handler that writes log records to the
|
||||
// given zerolog.Logger. The handler maps slog levels to zerolog levels and
|
||||
// converts slog attributes to zerolog fields.
|
||||
func NewSlogHandler(logger Logger) *SlogHandler {
|
||||
return &SlogHandler{logger: logger}
|
||||
}
|
||||
|
||||
// Enabled reports whether the handler handles records at the given level.
|
||||
// It mirrors Logger.should's level and writer checks (without sampling).
|
||||
func (h *SlogHandler) Enabled(_ context.Context, level slog.Level) bool {
|
||||
if h.logger.w == nil {
|
||||
return false
|
||||
}
|
||||
zl := slogToZerologLevel(level)
|
||||
if zl < GlobalLevel() {
|
||||
return false
|
||||
}
|
||||
return zl >= h.logger.level
|
||||
}
|
||||
|
||||
// Handle handles the Record. It converts the slog.Record into a zerolog event
|
||||
// and writes it using the underlying zerolog.Logger.
|
||||
func (h *SlogHandler) Handle(ctx context.Context, record slog.Record) error {
|
||||
zlevel := slogToZerologLevel(record.Level)
|
||||
event := h.logger.WithLevel(zlevel)
|
||||
if event == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Propagate slog context to the zerolog event so that hooks
|
||||
// relying on Event.GetCtx() (e.g. tracing) can access it.
|
||||
if ctx != nil {
|
||||
event = event.Ctx(ctx)
|
||||
}
|
||||
|
||||
// Add pre-attached attrs from WithAttrs
|
||||
for _, a := range h.attrs {
|
||||
event = appendSlogAttr(event, a, h.prefix)
|
||||
}
|
||||
|
||||
// Add attrs from the record itself
|
||||
record.Attrs(func(a slog.Attr) bool {
|
||||
event = appendSlogAttr(event, a, h.prefix)
|
||||
return true
|
||||
})
|
||||
|
||||
// Add timestamp from the slog record, but only if the logger doesn't
|
||||
// already have a timestampHook (added via .With().Timestamp()) to
|
||||
// avoid duplicate timestamp keys in the output.
|
||||
if !record.Time.IsZero() && !h.hasTimestampHook() {
|
||||
event.Time(TimestampFieldName, record.Time)
|
||||
}
|
||||
|
||||
event.Msg(record.Message)
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasTimestampHook reports whether the logger has a timestampHook installed,
|
||||
// which would cause duplicate timestamp fields if we also emit record.Time.
|
||||
func (h *SlogHandler) hasTimestampHook() bool {
|
||||
for _, hook := range h.logger.hooks {
|
||||
if _, ok := hook.(timestampHook); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// WithAttrs returns a new Handler with the given attributes pre-attached.
|
||||
// These attributes will be included in every subsequent log record.
|
||||
func (h *SlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
if len(attrs) == 0 {
|
||||
return h
|
||||
}
|
||||
h2 := h.clone()
|
||||
h2.attrs = append(h2.attrs, attrs...)
|
||||
return h2
|
||||
}
|
||||
|
||||
// WithGroup returns a new Handler with the given group name. All subsequent
|
||||
// attributes will be nested under this group name in the output.
|
||||
func (h *SlogHandler) WithGroup(name string) slog.Handler {
|
||||
if name == "" {
|
||||
return h
|
||||
}
|
||||
h2 := h.clone()
|
||||
if h2.prefix != "" {
|
||||
h2.prefix = h2.prefix + "." + name
|
||||
} else {
|
||||
h2.prefix = name
|
||||
}
|
||||
return h2
|
||||
}
|
||||
|
||||
func (h *SlogHandler) clone() *SlogHandler {
|
||||
h2 := &SlogHandler{
|
||||
logger: h.logger,
|
||||
prefix: h.prefix,
|
||||
}
|
||||
if len(h.attrs) > 0 {
|
||||
h2.attrs = make([]slog.Attr, len(h.attrs))
|
||||
copy(h2.attrs, h.attrs)
|
||||
}
|
||||
return h2
|
||||
}
|
||||
|
||||
// slogToZerologLevel maps slog levels to zerolog levels.
|
||||
//
|
||||
// slog levels: Debug=-4, Info=0, Warn=4, Error=8
|
||||
// zerolog levels: Trace=-1, Debug=0, Info=1, Warn=2, Error=3, Fatal=4, Panic=5
|
||||
func slogToZerologLevel(level slog.Level) Level {
|
||||
switch {
|
||||
case level < slog.LevelDebug:
|
||||
return TraceLevel
|
||||
case level < slog.LevelInfo:
|
||||
return DebugLevel
|
||||
case level < slog.LevelWarn:
|
||||
return InfoLevel
|
||||
case level < slog.LevelError:
|
||||
return WarnLevel
|
||||
default:
|
||||
return ErrorLevel
|
||||
}
|
||||
}
|
||||
|
||||
// zerologToSlogLevel maps zerolog levels to slog levels.
|
||||
func zerologToSlogLevel(level Level) slog.Level {
|
||||
switch level {
|
||||
case TraceLevel:
|
||||
return slog.LevelDebug - 4
|
||||
case DebugLevel:
|
||||
return slog.LevelDebug
|
||||
case InfoLevel:
|
||||
return slog.LevelInfo
|
||||
case WarnLevel:
|
||||
return slog.LevelWarn
|
||||
case ErrorLevel:
|
||||
return slog.LevelError
|
||||
case FatalLevel:
|
||||
return slog.LevelError + 4
|
||||
case PanicLevel:
|
||||
return slog.LevelError + 8
|
||||
default:
|
||||
return slog.LevelInfo
|
||||
}
|
||||
}
|
||||
|
||||
// joinPrefix concatenates a prefix and key with a dot separator.
|
||||
// It avoids allocations when either prefix or key is empty.
|
||||
func joinPrefix(prefix, key string) string {
|
||||
if prefix == "" {
|
||||
return key
|
||||
}
|
||||
if key == "" {
|
||||
return prefix
|
||||
}
|
||||
return prefix + "." + key
|
||||
}
|
||||
|
||||
// appendSlogAttr appends a single slog.Attr to the zerolog event, handling
|
||||
// type-specific encoding to avoid reflection where possible.
|
||||
func appendSlogAttr(event *Event, attr slog.Attr, prefix string) *Event {
|
||||
if event == nil {
|
||||
return event
|
||||
}
|
||||
|
||||
// Resolve the attribute to handle LogValuer types.
|
||||
// This handles slog.KindLogValuer implicitly by unwrapping
|
||||
// any values that implement slog.LogValuer to their resolved form.
|
||||
attr.Value = attr.Value.Resolve()
|
||||
|
||||
// For group kinds, handle grouping before key concatenation
|
||||
if attr.Value.Kind() == slog.KindGroup {
|
||||
attrs := attr.Value.Group()
|
||||
if len(attrs) == 0 {
|
||||
return event
|
||||
}
|
||||
groupPrefix := joinPrefix(prefix, attr.Key)
|
||||
for _, ga := range attrs {
|
||||
event = appendSlogAttr(event, ga, groupPrefix)
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
// Skip empty keys for non-group attributes
|
||||
if attr.Key == "" {
|
||||
return event
|
||||
}
|
||||
|
||||
key := joinPrefix(prefix, attr.Key)
|
||||
val := attr.Value
|
||||
|
||||
switch val.Kind() {
|
||||
case slog.KindString:
|
||||
event = event.Str(key, val.String())
|
||||
case slog.KindInt64:
|
||||
event = event.Int64(key, val.Int64())
|
||||
case slog.KindUint64:
|
||||
event = event.Uint64(key, val.Uint64())
|
||||
case slog.KindFloat64:
|
||||
event = event.Float64(key, val.Float64())
|
||||
case slog.KindBool:
|
||||
event = event.Bool(key, val.Bool())
|
||||
case slog.KindDuration:
|
||||
event = event.Dur(key, val.Duration())
|
||||
case slog.KindTime:
|
||||
event = event.Time(key, val.Time())
|
||||
case slog.KindAny:
|
||||
v := val.Any()
|
||||
switch cv := v.(type) {
|
||||
case error:
|
||||
event = event.AnErr(key, cv)
|
||||
case time.Duration:
|
||||
event = event.Dur(key, cv)
|
||||
case time.Time:
|
||||
event = event.Time(key, cv)
|
||||
case []byte:
|
||||
event = event.Bytes(key, cv)
|
||||
default:
|
||||
event = event.Interface(key, v)
|
||||
}
|
||||
default:
|
||||
event = event.Interface(key, val.Any())
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
// Verify at compile time that SlogHandler satisfies the slog.Handler interface.
|
||||
var _ slog.Handler = (*SlogHandler)(nil)
|
||||
+1
-1
@@ -40,7 +40,7 @@ linters:
|
||||
|
||||
# disable noisy/controversial ones which you might enable later
|
||||
disable:
|
||||
- lll # line length — handled by gfmt/gofumpt
|
||||
- lll # line length — handled by gofmt/gofumpt
|
||||
|
||||
settings:
|
||||
dupl:
|
||||
|
||||
+7
@@ -1,9 +1,14 @@
|
||||
# Only build/test/lint exp/simd when Go version is >= 1.26 (requires goexperiment.simd)
|
||||
GO_VERSION := $(shell go version 2>/dev/null | sed -n 's/.*go\([0-9]*\)\.\([0-9]*\).*/\1.\2/p')
|
||||
GO_SIMD_SUPPORT := $(shell ver="$(GO_VERSION)"; [ -n "$$ver" ] && [ "$$(printf '%s\n1.26\n' "$$ver" | sort -V | tail -1)" = "$$ver" ] && echo yes)
|
||||
|
||||
build:
|
||||
go build -v ./...
|
||||
@if [ -n "$(GO_SIMD_SUPPORT)" ]; then cd ./exp/simd && GOEXPERIMENT=simd go build -v ./; fi
|
||||
|
||||
test:
|
||||
go test -race ./...
|
||||
@if [ -n "$(GO_SIMD_SUPPORT)" ]; then cd ./exp/simd && GOEXPERIMENT=simd go test -race ./; fi
|
||||
watch-test:
|
||||
reflex -t 50ms -s -- sh -c 'gotest -race ./...'
|
||||
|
||||
@@ -32,9 +37,11 @@ tools:
|
||||
|
||||
lint:
|
||||
golangci-lint run --timeout 60s --max-same-issues 50 ./...
|
||||
@if [ -n "$(GO_SIMD_SUPPORT)" ]; then cd ./exp/simd && golangci-lint run --timeout 60s --max-same-issues 50 ./; fi
|
||||
# mdsf verify --debug --log-level warn docs/
|
||||
lint-fix:
|
||||
golangci-lint run --timeout 60s --max-same-issues 50 --fix ./...
|
||||
@if [ -n "$(GO_SIMD_SUPPORT)" ]; then cd ./exp/simd && golangci-lint run --timeout 60s --max-same-issues 50 --fix ./; fi
|
||||
# mdsf format --debug --log-level warn docs/
|
||||
|
||||
audit:
|
||||
|
||||
+618
-17
@@ -18,18 +18,28 @@ A utility library based on Go 1.18+ generics that makes it easier to work with s
|
||||
|
||||
**See also:**
|
||||
|
||||
- [samber/ro](https://github.com/samber/ro): Reactive Programming for Go: declarative and composable API for event-driven applications
|
||||
- [samber/do](https://github.com/samber/do): A dependency injection toolkit based on Go 1.18+ Generics
|
||||
- [samber/mo](https://github.com/samber/mo): Monads based on Go 1.18+ Generics (Option, Result, Either...)
|
||||
|
||||
What makes it different from **samber/ro**?
|
||||
- lo: synchronous helpers across finite sequences (maps, slices...)
|
||||
- ro: processing of infinite data streams for event-driven scenarios
|
||||
|
||||
----
|
||||
|
||||
<h3 align="center">💖 Support This Project</h3>
|
||||
|
||||
<p align="center">
|
||||
I’m going all-in on open-source for the coming months.
|
||||
<br>
|
||||
Help sustain development: Become an <a href="http://github.com/sponsors/samber">individual sponsor</a> or join as a <a href="mailto:hey@samuel-berthe.fr">corporate sponsor</a>.
|
||||
</p>
|
||||
<div align="center">
|
||||
<sup><b>💖 Sponsored by:</b></sup>
|
||||
<br>
|
||||
<a href="https://www.dbos.dev/?utm_campaign=gh-smbr">
|
||||
<div>
|
||||
<img width="200" alt="dbos" src="https://github.com/user-attachments/assets/d583cb62-7735-4d3c-beb7-e6ef1a5faf49" />
|
||||
</div>
|
||||
<div>
|
||||
DBOS - Durable workflow orchestration library for Go
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
----
|
||||
|
||||
@@ -37,7 +47,7 @@ A utility library based on Go 1.18+ generics that makes it easier to work with s
|
||||
|
||||
I wanted a **short name**, similar to "Lodash", and no Go package uses this name.
|
||||
|
||||

|
||||

|
||||
|
||||
## 🚀 Install
|
||||
|
||||
@@ -47,7 +57,7 @@ go get github.com/samber/lo@v1
|
||||
|
||||
This library is v1 and follows SemVer strictly.
|
||||
|
||||
No breaking changes will be made to exported APIs before v2.0.0.
|
||||
No breaking changes will be made to exported APIs before v2.0.0, except for experimental packages under `exp/`.
|
||||
|
||||
This library has no dependencies outside the Go standard library.
|
||||
|
||||
@@ -106,8 +116,11 @@ Supported helpers for slices:
|
||||
- [GroupBy](#groupby)
|
||||
- [GroupByMap](#groupbymap)
|
||||
- [Chunk](#chunk)
|
||||
- [Window](#window)
|
||||
- [Sliding](#sliding)
|
||||
- [PartitionBy](#partitionby)
|
||||
- [Flatten](#flatten)
|
||||
- [Concat](#concat)
|
||||
- [Interleave](#interleave)
|
||||
- [Shuffle](#shuffle)
|
||||
- [Reverse](#reverse)
|
||||
@@ -118,6 +131,9 @@ Supported helpers for slices:
|
||||
- [SliceToMap / Associate](#slicetomap-alias-associate)
|
||||
- [FilterSliceToMap](#filterslicetomap)
|
||||
- [Keyify](#keyify)
|
||||
- [Take](#take)
|
||||
- [TakeWhile](#takewhile)
|
||||
- [TakeFilter](#takefilter)
|
||||
- [Drop](#drop)
|
||||
- [DropRight](#dropright)
|
||||
- [DropWhile](#dropwhile)
|
||||
@@ -134,9 +150,10 @@ Supported helpers for slices:
|
||||
- [Slice](#slice)
|
||||
- [Replace](#replace)
|
||||
- [ReplaceAll](#replaceall)
|
||||
- [Clone](#clone)
|
||||
- [Compact](#compact)
|
||||
- [IsSorted](#issorted)
|
||||
- [IsSortedByKey](#issortedbykey)
|
||||
- [IsSortedBy](#issortedby)
|
||||
- [Splice](#Splice)
|
||||
- [Cut](#Cut)
|
||||
- [CutPrefix](#CutPrefix)
|
||||
@@ -239,6 +256,7 @@ Supported intersection helpers:
|
||||
- [None](#none)
|
||||
- [NoneBy](#noneby)
|
||||
- [Intersect](#intersect)
|
||||
- [IntersectBy](#intersectby)
|
||||
- [Difference](#difference)
|
||||
- [Union](#union)
|
||||
- [Without](#without)
|
||||
@@ -375,6 +393,17 @@ even := lo.Filter([]int{1, 2, 3, 4}, func(x int, index int) bool {
|
||||
// []int{2, 4}
|
||||
```
|
||||
|
||||
```go
|
||||
// Use FilterErr when the predicate can return an error
|
||||
even, err := lo.FilterErr([]int{1, 2, 3, 4}, func(x int, _ int) (bool, error) {
|
||||
if x == 3 {
|
||||
return false, fmt.Errorf("number 3 is not allowed")
|
||||
}
|
||||
return x%2 == 0, nil
|
||||
})
|
||||
// []int(nil), error("number 3 is not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/Apjg3WeSi7K)]
|
||||
|
||||
Mutable: like `lo.Filter()`, but the slice is updated in place.
|
||||
@@ -407,9 +436,20 @@ lo.Map([]int64{1, 2, 3, 4}, func(x int64, index int) string {
|
||||
// []string{"1", "2", "3", "4"}
|
||||
```
|
||||
|
||||
```go
|
||||
// Use MapErr when the transform function can return an error
|
||||
result, err := lo.MapErr([]int{1, 2, 3, 4}, func(x int, _ int) (string, error) {
|
||||
if x == 3 {
|
||||
return "", fmt.Errorf("number 3 is not allowed")
|
||||
}
|
||||
return strconv.Itoa(x), nil
|
||||
})
|
||||
// []string(nil), error("number 3 is not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/OkPcYAhBo0D)]
|
||||
|
||||
Parallel processing: like `lo.Map()`, but the mapper function is called in a goroutine. Results are returned in the same order.
|
||||
Parallel processing: like `lo.Map()`, but the transform function is called in a goroutine. Results are returned in the same order.
|
||||
|
||||
```go
|
||||
import lop "github.com/samber/lo/parallel"
|
||||
@@ -487,6 +527,17 @@ lo.FlatMap([]int64{0, 1, 2}, func(x int64, _ int) []string {
|
||||
// []string{"0", "0", "1", "1", "2", "2"}
|
||||
```
|
||||
|
||||
```go
|
||||
// Use FlatMapErr when the transform function can return an error
|
||||
result, err := lo.FlatMapErr([]int64{0, 1, 2, 3}, func(x int64, _ int) ([]string, error) {
|
||||
if x == 2 {
|
||||
return nil, fmt.Errorf("number 2 is not allowed")
|
||||
}
|
||||
return []string{strconv.FormatInt(x, 10), strconv.FormatInt(x, 10)}, nil
|
||||
})
|
||||
// []string(nil), error("number 2 is not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/YSoYmQTA8-U)]
|
||||
|
||||
### Reduce
|
||||
@@ -500,6 +551,17 @@ sum := lo.Reduce([]int{1, 2, 3, 4}, func(agg int, item int, _ int) int {
|
||||
// 10
|
||||
```
|
||||
|
||||
```go
|
||||
// Use ReduceErr when the accumulator function can return an error
|
||||
result, err := lo.ReduceErr([]int{1, 2, 3, 4}, func(agg int, item int, _ int) (int, error) {
|
||||
if item == 3 {
|
||||
return 0, fmt.Errorf("number 3 is not allowed")
|
||||
}
|
||||
return agg + item, nil
|
||||
}, 0)
|
||||
// 0, error("number 3 is not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/R4UHXZNaaUG)]
|
||||
|
||||
### ReduceRight
|
||||
@@ -513,6 +575,17 @@ result := lo.ReduceRight([][]int{{0, 1}, {2, 3}, {4, 5}}, func(agg []int, item [
|
||||
// []int{4, 5, 2, 3, 0, 1}
|
||||
```
|
||||
|
||||
```go
|
||||
// Use ReduceRightErr when the accumulator function can return an error
|
||||
result, err := lo.ReduceRightErr([]int{1, 2, 3, 4}, func(agg int, item int, _ int) (int, error) {
|
||||
if item == 2 {
|
||||
return 0, fmt.Errorf("number 2 is not allowed")
|
||||
}
|
||||
return agg + item, nil
|
||||
}, 0)
|
||||
// 0, error("number 2 is not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/Fq3W70l7wXF)]
|
||||
|
||||
### ForEach
|
||||
@@ -609,6 +682,17 @@ uniqValues := lo.UniqBy([]int{0, 1, 2, 3, 4, 5}, func(i int) int {
|
||||
// []int{0, 1, 2}
|
||||
```
|
||||
|
||||
```go
|
||||
// Use UniqByErr when the iteratee function can return an error
|
||||
result, err := lo.UniqByErr([]int{0, 1, 2, 3, 4, 5}, func(i int) (int, error) {
|
||||
if i == 3 {
|
||||
return 0, fmt.Errorf("number 3 is not allowed")
|
||||
}
|
||||
return i % 3, nil
|
||||
})
|
||||
// []int(nil), error("number 3 is not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/g42Z3QSb53u)]
|
||||
|
||||
### GroupBy
|
||||
@@ -624,6 +708,17 @@ groups := lo.GroupBy([]int{0, 1, 2, 3, 4, 5}, func(i int) int {
|
||||
// map[int][]int{0: []int{0, 3}, 1: []int{1, 4}, 2: []int{2, 5}}
|
||||
```
|
||||
|
||||
```go
|
||||
// Use GroupByErr when the iteratee function can return an error
|
||||
result, err := lo.GroupByErr([]int{0, 1, 2, 3, 4, 5}, func(i int) (int, error) {
|
||||
if i == 3 {
|
||||
return 0, fmt.Errorf("number 3 is not allowed")
|
||||
}
|
||||
return i % 3, nil
|
||||
})
|
||||
// map[int][]int(nil), error("number 3 is not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/XnQBd_v6brd)]
|
||||
|
||||
Parallel processing: like `lo.GroupBy()`, but callback is called in goroutine.
|
||||
@@ -650,6 +745,17 @@ groups := lo.GroupByMap([]int{0, 1, 2, 3, 4, 5}, func(i int) (int, int) {
|
||||
// map[int][]int{0: []int{0, 6}, 1: []int{2, 8}, 2: []int{4, 10}}
|
||||
```
|
||||
|
||||
```go
|
||||
// Use GroupByMapErr when the transform function can return an error
|
||||
result, err := lo.GroupByMapErr([]int{0, 1, 2, 3, 4, 5}, func(i int) (int, int, error) {
|
||||
if i == 3 {
|
||||
return 0, 0, fmt.Errorf("number 3 is not allowed")
|
||||
}
|
||||
return i % 3, i * 2, nil
|
||||
})
|
||||
// map[int][]int(nil), error("number 3 is not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/iMeruQ3_W80)]
|
||||
|
||||
### Chunk
|
||||
@@ -672,6 +778,36 @@ lo.Chunk([]int{0}, 2)
|
||||
|
||||
[[play](https://go.dev/play/p/kEMkFbdu85g)]
|
||||
|
||||
### Window
|
||||
|
||||
Creates a slice of sliding windows of a given size. Each window shares size-1 elements with the previous one. This is equivalent to `Sliding(collection, size, 1)`.
|
||||
|
||||
```go
|
||||
lo.Window([]int{1, 2, 3, 4, 5}, 3)
|
||||
// [][]int{{1, 2, 3}, {2, 3, 4}, {3, 4, 5}}
|
||||
|
||||
lo.Window([]float64{20, 22, 21, 23, 24}, 3)
|
||||
// [][]float64{{20, 22, 21}, {22, 21, 23}, {21, 23, 24}}
|
||||
```
|
||||
|
||||
### Sliding
|
||||
|
||||
Creates a slice of sliding windows of a given size with a given step. If step is equal to size, windows have no common elements (similar to Chunk). If step is less than size, windows share common elements.
|
||||
|
||||
```go
|
||||
// Windows with shared elements (step < size)
|
||||
lo.Sliding([]int{1, 2, 3, 4, 5, 6}, 3, 1)
|
||||
// [][]int{{1, 2, 3}, {2, 3, 4}, {3, 4, 5}, {4, 5, 6}}
|
||||
|
||||
// Windows with no shared elements (step == size, like Chunk)
|
||||
lo.Sliding([]int{1, 2, 3, 4, 5, 6}, 3, 3)
|
||||
// [][]int{{1, 2, 3}, {4, 5, 6}}
|
||||
|
||||
// Step > size (skipping elements)
|
||||
lo.Sliding([]int{1, 2, 3, 4, 5, 6, 7, 8}, 2, 3)
|
||||
// [][]int{{1, 2}, {4, 5}, {7, 8}}
|
||||
```
|
||||
|
||||
### PartitionBy
|
||||
|
||||
Returns a slice of elements split into groups. The order of grouped values is determined by the order they occur in collection. The grouping is generated from the results of running each element of collection through iteratee.
|
||||
@@ -690,6 +826,22 @@ partitions := lo.PartitionBy([]int{-2, -1, 0, 1, 2, 3, 4, 5}, func(x int) string
|
||||
// [][]int{{-2, -1}, {0, 2, 4}, {1, 3, 5}}
|
||||
```
|
||||
|
||||
```go
|
||||
// Use PartitionByErr when the iteratee function can return an error
|
||||
result, err := lo.PartitionByErr([]int{-2, -1, 0, 1, 2}, func(x int) (string, error) {
|
||||
if x == 0 {
|
||||
return "", fmt.Errorf("zero is not allowed")
|
||||
}
|
||||
if x < 0 {
|
||||
return "negative", nil
|
||||
} else if x%2 == 0 {
|
||||
return "even", nil
|
||||
}
|
||||
return "odd", nil
|
||||
})
|
||||
// [][]int(nil), error("zero is not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/NfQ_nGjkgXW)]
|
||||
|
||||
Parallel processing: like `lo.PartitionBy()`, but callback is called in goroutine. Results are returned in the same order.
|
||||
@@ -719,6 +871,20 @@ flat := lo.Flatten([][]int{{0, 1}, {2, 3, 4, 5}})
|
||||
|
||||
[[play](https://go.dev/play/p/rbp9ORaMpjw)]
|
||||
|
||||
### Concat
|
||||
|
||||
Returns a new slice containing all the elements in collections. Concat conserves the order of the elements.
|
||||
|
||||
```go
|
||||
slice := lo.Concat([]int{1, 2}, []int{3, 4})
|
||||
// []int{1, 2, 3, 4}
|
||||
|
||||
slice := lo.Concat(nil, []int{1, 2}, nil, []int{3, 4}, nil)
|
||||
// []int{1, 2, 3, 4}
|
||||
|
||||
slice := lo.Concat[int]()
|
||||
// []int{}
|
||||
```
|
||||
### Interleave
|
||||
|
||||
Round-robin alternating input slices and sequentially appending value at index into result.
|
||||
@@ -825,6 +991,18 @@ slice := lo.RepeatBy(5, func(i int) string {
|
||||
|
||||
[[play](https://go.dev/play/p/ozZLCtX_hNU)]
|
||||
|
||||
With error handling:
|
||||
|
||||
```go
|
||||
slice, err := lo.RepeatByErr(5, func(i int) (string, error) {
|
||||
if i == 3 {
|
||||
return "", fmt.Errorf("index 3 is not allowed")
|
||||
}
|
||||
return fmt.Sprintf("item-%d", i), nil
|
||||
})
|
||||
// []string(nil), error("index 3 is not allowed")
|
||||
```
|
||||
|
||||
### KeyBy
|
||||
|
||||
Transforms a slice or a slice of structs to a map based on a pivot callback.
|
||||
@@ -849,6 +1027,16 @@ result := lo.KeyBy(characters, func(char Character) string {
|
||||
//map[a:{dir:left code:97} d:{dir:right code:100}]
|
||||
```
|
||||
|
||||
```go
|
||||
result, err := lo.KeyByErr([]string{"a", "aa", "aaa", ""}, func(str string) (int, error) {
|
||||
if str == "" {
|
||||
return 0, fmt.Errorf("empty string not allowed")
|
||||
}
|
||||
return len(str), nil
|
||||
})
|
||||
// map[int]string(nil), error("empty string not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/mdaClUAT-zZ)]
|
||||
|
||||
### SliceToMap (alias: Associate)
|
||||
@@ -901,6 +1089,50 @@ set := lo.Keyify([]int{1, 1, 2, 3, 4})
|
||||
|
||||
[[play](https://go.dev/play/p/RYhhM_csqIG)]
|
||||
|
||||
### Take
|
||||
|
||||
Takes the first n elements from a slice.
|
||||
|
||||
```go
|
||||
l := lo.Take([]int{0, 1, 2, 3, 4, 5}, 3)
|
||||
// []int{0, 1, 2}
|
||||
|
||||
l := lo.Take([]int{0, 1, 2}, 5)
|
||||
// []int{0, 1, 2}
|
||||
```
|
||||
|
||||
### TakeWhile
|
||||
|
||||
Takes elements from the beginning while the predicate returns true.
|
||||
|
||||
```go
|
||||
l := lo.TakeWhile([]int{0, 1, 2, 3, 4, 5}, func(val int) bool {
|
||||
return val < 3
|
||||
})
|
||||
// []int{0, 1, 2}
|
||||
|
||||
l := lo.TakeWhile([]string{"a", "aa", "aaa", "aa"}, func(val string) bool {
|
||||
return len(val) <= 2
|
||||
})
|
||||
// []string{"a", "aa"}
|
||||
```
|
||||
|
||||
### TakeFilter
|
||||
|
||||
Filters elements and takes the first n elements that match the predicate. Equivalent to calling Take(Filter(...)), but more efficient as it stops after finding n matches.
|
||||
|
||||
```go
|
||||
l := lo.TakeFilter([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 3, func(val int, index int) bool {
|
||||
return val%2 == 0
|
||||
})
|
||||
// []int{2, 4, 6}
|
||||
|
||||
l := lo.TakeFilter([]string{"a", "aa", "aaa", "aaaa"}, 2, func(val string, index int) bool {
|
||||
return len(val) > 1
|
||||
})
|
||||
// []string{"aa", "aaa"}
|
||||
```
|
||||
|
||||
### Drop
|
||||
|
||||
Drops n elements from the beginning of a slice.
|
||||
@@ -971,6 +1203,17 @@ odd := lo.Reject([]int{1, 2, 3, 4}, func(x int, _ int) bool {
|
||||
// []int{1, 3}
|
||||
```
|
||||
|
||||
```go
|
||||
// Use RejectErr when the predicate can return an error
|
||||
odd, err := lo.RejectErr([]int{1, 2, 3, 4}, func(x int, _ int) (bool, error) {
|
||||
if x == 3 {
|
||||
return false, fmt.Errorf("number 3 is not allowed")
|
||||
}
|
||||
return x%2 == 0, nil
|
||||
})
|
||||
// []int(nil), error("number 3 is not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/YkLMODy1WEL)]
|
||||
|
||||
### RejectMap
|
||||
@@ -1023,6 +1266,17 @@ count := lo.CountBy([]int{1, 5, 1}, func(i int) bool {
|
||||
// 2
|
||||
```
|
||||
|
||||
```go
|
||||
// Use CountByErr when the predicate can return an error
|
||||
count, err := lo.CountByErr([]int{1, 5, 1}, func(i int) (bool, error) {
|
||||
if i == 5 {
|
||||
return false, fmt.Errorf("5 not allowed")
|
||||
}
|
||||
return i < 4, nil
|
||||
})
|
||||
// 0, error("5 not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/ByQbNYQQi4X)]
|
||||
|
||||
### CountValues
|
||||
@@ -1158,6 +1412,20 @@ slice := lo.ReplaceAll(in, -1, 42)
|
||||
|
||||
[[play](https://go.dev/play/p/a9xZFUHfYcV)]
|
||||
|
||||
### Clone
|
||||
|
||||
Returns a shallow copy of the collection.
|
||||
|
||||
```go
|
||||
in := []int{1, 2, 3, 4, 5}
|
||||
cloned := lo.Clone(in)
|
||||
// Verify it's a different slice by checking that modifying one doesn't affect the other
|
||||
in[0] = 99
|
||||
// cloned is []int{1, 2, 3, 4, 5}
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/hgHmoOIxmuH)]
|
||||
|
||||
### Compact
|
||||
|
||||
Returns a slice of all non-zero elements.
|
||||
@@ -1182,12 +1450,12 @@ slice := lo.IsSorted([]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
|
||||
|
||||
[[play](https://go.dev/play/p/mc3qR-t4mcx)]
|
||||
|
||||
### IsSortedByKey
|
||||
### IsSortedBy
|
||||
|
||||
Checks if a slice is sorted by iteratee.
|
||||
|
||||
```go
|
||||
slice := lo.IsSortedByKey([]string{"a", "bb", "ccc"}, func(s string) int {
|
||||
slice := lo.IsSortedBy([]string{"a", "bb", "ccc"}, func(s string) int {
|
||||
return len(s)
|
||||
})
|
||||
// true
|
||||
@@ -1455,6 +1723,17 @@ m := lo.PickBy(map[string]int{"foo": 1, "bar": 2, "baz": 3}, func(key string, va
|
||||
// map[string]int{"foo": 1, "baz": 3}
|
||||
```
|
||||
|
||||
```go
|
||||
// Use PickByErr when the predicate can return an error
|
||||
m, err := lo.PickByErr(map[string]int{"foo": 1, "bar": 2, "baz": 3}, func(key string, value int) (bool, error) {
|
||||
if key == "bar" {
|
||||
return false, fmt.Errorf("bar not allowed")
|
||||
}
|
||||
return value%2 == 1, nil
|
||||
})
|
||||
// map[string]int(nil), error("bar not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/kdg8GR_QMmf)]
|
||||
|
||||
### PickByKeys
|
||||
@@ -1490,6 +1769,17 @@ m := lo.OmitBy(map[string]int{"foo": 1, "bar": 2, "baz": 3}, func(key string, va
|
||||
// map[string]int{"bar": 2}
|
||||
```
|
||||
|
||||
```go
|
||||
// Use OmitByErr when the predicate can return an error
|
||||
m, err := lo.OmitByErr(map[string]int{"foo": 1, "bar": 2, "baz": 3}, func(key string, value int) (bool, error) {
|
||||
if key == "bar" {
|
||||
return false, fmt.Errorf("bar not allowed")
|
||||
}
|
||||
return value%2 == 1, nil
|
||||
})
|
||||
// map[string]int(nil), error("bar not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/EtBsR43bdsd)]
|
||||
|
||||
### OmitByKeys
|
||||
@@ -1615,6 +1905,17 @@ m2 := lo.MapKeys(map[int]int{1: 1, 2: 2, 3: 3, 4: 4}, func(_ int, v int) string
|
||||
// map[string]int{"1": 1, "2": 2, "3": 3, "4": 4}
|
||||
```
|
||||
|
||||
```go
|
||||
// Use MapKeysErr when the iteratee can return an error
|
||||
m2, err := lo.MapKeysErr(map[int]int{1: 1, 2: 2, 3: 3}, func(_ int, v int) (string, error) {
|
||||
if v == 2 {
|
||||
return "", fmt.Errorf("even number not allowed")
|
||||
}
|
||||
return strconv.FormatInt(int64(v), 10), nil
|
||||
})
|
||||
// map[string]int(nil), error("even number not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/9_4WPIqOetJ)]
|
||||
|
||||
### MapValues
|
||||
@@ -1630,6 +1931,18 @@ m2 := lo.MapValues(m1, func(x int64, _ int) string {
|
||||
// map[int]string{1: "1", 2: "2", 3: "3"}
|
||||
```
|
||||
|
||||
```go
|
||||
// Use MapValuesErr when the iteratee can return an error
|
||||
m1 := map[int]int64{1: 1, 2: 2, 3: 3}
|
||||
m2, err := lo.MapValuesErr(m1, func(x int64, _ int) (string, error) {
|
||||
if x == 2 {
|
||||
return "", fmt.Errorf("even number not allowed")
|
||||
}
|
||||
return strconv.FormatInt(x, 10), nil
|
||||
})
|
||||
// map[int]string(nil), error("even number not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/T_8xAfvcf0W)]
|
||||
|
||||
### MapEntries
|
||||
@@ -1645,6 +1958,18 @@ out := lo.MapEntries(in, func(k string, v int) (int, string) {
|
||||
// map[int]string{1: "foo", 2: "bar"}
|
||||
```
|
||||
|
||||
```go
|
||||
// Use MapEntriesErr when the iteratee can return an error
|
||||
in := map[string]int{"foo": 1, "bar": 2, "baz": 3}
|
||||
out, err := lo.MapEntriesErr(in, func(k string, v int) (int, string, error) {
|
||||
if k == "bar" {
|
||||
return 0, "", fmt.Errorf("bar not allowed")
|
||||
}
|
||||
return v, k, nil
|
||||
})
|
||||
// map[int]string(nil), error("bar not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/VuvNQzxKimT)]
|
||||
|
||||
### MapToSlice
|
||||
@@ -1660,6 +1985,18 @@ s := lo.MapToSlice(m, func(k int, v int64) string {
|
||||
// []string{"1_4", "2_5", "3_6"}
|
||||
```
|
||||
|
||||
```go
|
||||
// Use MapToSliceErr when the iteratee can return an error
|
||||
m := map[int]int64{1: 4, 2: 5, 3: 6}
|
||||
s, err := lo.MapToSliceErr(m, func(k int, v int64) (string, error) {
|
||||
if k == 2 {
|
||||
return "", fmt.Errorf("key 2 not allowed")
|
||||
}
|
||||
return fmt.Sprintf("%d_%d", k, v), nil
|
||||
})
|
||||
// []string(nil), error("key 2 not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/ZuiCZpDt6LD)]
|
||||
|
||||
### FilterMapToSlice
|
||||
@@ -1677,6 +2014,18 @@ result := lo.FilterMapToSlice(kv, func(k int, v int64) (string, bool) {
|
||||
// []{"2_2", "4_4"}
|
||||
```
|
||||
|
||||
```go
|
||||
kv := map[int]int64{1: 1, 2: 2, 3: 3, 4: 4}
|
||||
|
||||
result, err := lo.FilterMapToSliceErr(kv, func(k int, v int64) (string, bool, error) {
|
||||
if k == 3 {
|
||||
return "", false, fmt.Errorf("key 3 not allowed")
|
||||
}
|
||||
return fmt.Sprintf("%d_%d", k, v), k%2 == 0, nil
|
||||
})
|
||||
// []string(nil), error("key 3 not allowed")
|
||||
```
|
||||
|
||||
### FilterKeys
|
||||
|
||||
Transforms a map into a slice based on predicate returns true for specific elements. It is a mix of `lo.Filter()` and `lo.Keys()`.
|
||||
@@ -1690,6 +2039,17 @@ result := FilterKeys(kv, func(k int, v string) bool {
|
||||
// [1]
|
||||
```
|
||||
|
||||
```go
|
||||
// Use FilterKeysErr when the predicate can return an error
|
||||
result, err := lo.FilterKeysErr(map[int]string{1: "foo", 2: "bar", 3: "baz"}, func(k int, v string) (bool, error) {
|
||||
if k == 3 {
|
||||
return false, fmt.Errorf("key 3 not allowed")
|
||||
}
|
||||
return v == "foo", nil
|
||||
})
|
||||
// []int(nil), error("key 3 not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/OFlKXlPrBAe)]
|
||||
|
||||
### FilterValues
|
||||
@@ -1705,6 +2065,17 @@ result := FilterValues(kv, func(k int, v string) bool {
|
||||
// ["foo"]
|
||||
```
|
||||
|
||||
```go
|
||||
// Use FilterValuesErr when the predicate can return an error
|
||||
result, err := lo.FilterValuesErr(map[int]string{1: "foo", 2: "bar", 3: "baz"}, func(k int, v string) (bool, error) {
|
||||
if k == 3 {
|
||||
return false, fmt.Errorf("key 3 not allowed")
|
||||
}
|
||||
return v == "foo", nil
|
||||
})
|
||||
// []string(nil), error("key 3 not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/YVD5r_h-LX-)]
|
||||
|
||||
### Range / RangeFrom / RangeWithSteps
|
||||
@@ -1784,6 +2155,19 @@ sum := lo.SumBy(strings, func(item string) int {
|
||||
// 6
|
||||
```
|
||||
|
||||
With error handling:
|
||||
|
||||
```go
|
||||
strings := []string{"foo", "bar", "baz"}
|
||||
sum, err := lo.SumByErr(strings, func(item string) (int, error) {
|
||||
if item == "bar" {
|
||||
return 0, fmt.Errorf("invalid item: %s", item)
|
||||
}
|
||||
return len(item), nil
|
||||
})
|
||||
// sum: 3, err: invalid item: bar
|
||||
```
|
||||
|
||||
### Product
|
||||
|
||||
Calculates the product of the values in a collection.
|
||||
@@ -1812,6 +2196,18 @@ product := lo.ProductBy(strings, func(item string) int {
|
||||
// 9
|
||||
```
|
||||
|
||||
```go
|
||||
// Use ProductByErr when the transform function can return an error
|
||||
strings := []string{"foo", "bar", "baz"}
|
||||
product, err := lo.ProductByErr(strings, func(item string) (int, error) {
|
||||
if item == "bar" {
|
||||
return 0, fmt.Errorf("bar is not allowed")
|
||||
}
|
||||
return len(item), nil
|
||||
})
|
||||
// 3, error("bar is not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/wadzrWr9Aer)]
|
||||
|
||||
### Mean
|
||||
@@ -1850,6 +2246,18 @@ mean := lo.MeanBy([]float64{}, mapper)
|
||||
// 0
|
||||
```
|
||||
|
||||
```go
|
||||
// Use MeanByErr when the transform function can return an error
|
||||
list := []string{"aa", "bbb", "cccc", "ddddd"}
|
||||
mean, err := lo.MeanByErr(list, func(item string) (float64, error) {
|
||||
if item == "cccc" {
|
||||
return 0, fmt.Errorf("cccc is not allowed")
|
||||
}
|
||||
return float64(len(item)), nil
|
||||
})
|
||||
// 0, error("cccc is not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/j7TsVwBOZ7P)]
|
||||
|
||||
### Mode
|
||||
@@ -1920,6 +2328,8 @@ lo.ChunkString("1", 2)
|
||||
// []string{"1"}
|
||||
```
|
||||
|
||||
Note: `lo.ChunkString` and `lo.Chunk` functions behave inconsistently for empty input: `lo.ChunkString("", n)` returns `[""]` instead of `[]`. See [#788](https://github.com/samber/lo/issues/788).
|
||||
|
||||
[[play](https://go.dev/play/p/__FLTuJVz54)]
|
||||
|
||||
### RuneLength
|
||||
@@ -2004,7 +2414,7 @@ str := lo.Capitalize("heLLO")
|
||||
|
||||
### Ellipsis
|
||||
|
||||
Trims and truncates a string to a specified length in `bytes` and appends an ellipsis if truncated. If the string contains non-ASCII characters (which may occupy multiple bytes in UTF-8), truncating by byte length may split a character in the middle, potentially resulting in garbled output.
|
||||
Trims and truncates a string to a specified length in runes (Unicode code points) and appends an ellipsis if truncated. Multi-byte characters such as emoji or CJK ideographs are never split in the middle.
|
||||
|
||||
```go
|
||||
str := lo.Ellipsis(" Lorem Ipsum ", 5)
|
||||
@@ -2015,6 +2425,12 @@ str := lo.Ellipsis("Lorem Ipsum", 100)
|
||||
|
||||
str := lo.Ellipsis("Lorem Ipsum", 3)
|
||||
// ...
|
||||
|
||||
str := lo.Ellipsis("hello 世界! 你好", 8)
|
||||
// hello...
|
||||
|
||||
str := lo.Ellipsis("🏠🐶🐱🌟", 4)
|
||||
// 🏠🐶🐱🌟
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/qE93rgqe1TW)]
|
||||
@@ -2079,6 +2495,18 @@ items := lo.ZipBy2([]string{"a", "b"}, []int{1, 2}, func(a string, b int) string
|
||||
// []string{"a-1", "b-2"}
|
||||
```
|
||||
|
||||
With error handling:
|
||||
|
||||
```go
|
||||
items, err := lo.ZipByErr2([]string{"a", "b"}, []int{1, 2}, func(a string, b int) (string, error) {
|
||||
if b == 2 {
|
||||
return "", fmt.Errorf("number 2 is not allowed")
|
||||
}
|
||||
return fmt.Sprintf("%s-%d", a, b), nil
|
||||
})
|
||||
// []string(nil), error("number 2 is not allowed")
|
||||
```
|
||||
|
||||
### Unzip2 -> Unzip9
|
||||
|
||||
Unzip accepts a slice of grouped elements and creates a slice regrouping the elements to their pre-zip configuration.
|
||||
@@ -2103,6 +2531,18 @@ a, b := lo.UnzipBy2([]string{"hello", "john", "doe"}, func(str string) (string,
|
||||
// []int{5, 4, 3}
|
||||
```
|
||||
|
||||
```go
|
||||
a, b, err := lo.UnzipByErr2([]string{"hello", "error", "world"}, func(str string) (string, int, error) {
|
||||
if str == "error" {
|
||||
return "", 0, fmt.Errorf("error string not allowed")
|
||||
}
|
||||
return str, len(str), nil
|
||||
})
|
||||
// []string{}
|
||||
// []int{}
|
||||
// error string not allowed
|
||||
```
|
||||
|
||||
### CrossJoin2 -> CrossJoin9
|
||||
|
||||
Combines every item from one list with every item from others. It is the cartesian product of lists received as arguments. Returns an empty list if a list is empty.
|
||||
@@ -2119,7 +2559,7 @@ result := lo.CrossJoin2([]string{"hello", "john", "doe"}, []int{1, 2})
|
||||
|
||||
### CrossJoinBy2 -> CrossJoinBy9
|
||||
|
||||
Combines every item from one list with every item from others. It is the cartesian product of lists received as arguments. The project function is used to create the output values. Returns an empty list if a list is empty.
|
||||
Combines every item from one list with every item from others. It is the cartesian product of lists received as arguments. The transform function is used to create the output values. Returns an empty list if a list is empty.
|
||||
|
||||
```go
|
||||
result := lo.CrossJoinBy2([]string{"hello", "john", "doe"}, []int{1, 2}, func(a A, b B) string {
|
||||
@@ -2133,6 +2573,18 @@ result := lo.CrossJoinBy2([]string{"hello", "john", "doe"}, []int{1, 2}, func(a
|
||||
// "doe - 2"
|
||||
```
|
||||
|
||||
With error handling:
|
||||
|
||||
```go
|
||||
result, err := lo.CrossJoinByErr2([]string{"hello", "john"}, []int{1, 2}, func(a string, b int) (string, error) {
|
||||
if a == "john" {
|
||||
return "", fmt.Errorf("john not allowed")
|
||||
}
|
||||
return fmt.Sprintf("%s - %d", a, b), nil
|
||||
})
|
||||
// []string(nil), error("john not allowed")
|
||||
```
|
||||
|
||||
### Duration
|
||||
|
||||
Returns the time taken to execute a function.
|
||||
@@ -2550,7 +3002,7 @@ b := NoneBy([]int{1, 2, 3, 4}, func(x int) bool {
|
||||
|
||||
### Intersect
|
||||
|
||||
Returns the intersection between two collections.
|
||||
Returns the intersection between collections.
|
||||
|
||||
```go
|
||||
result1 := lo.Intersect([]int{0, 1, 2, 3, 4, 5}, []int{0, 2})
|
||||
@@ -2561,6 +3013,31 @@ result2 := lo.Intersect([]int{0, 1, 2, 3, 4, 5}, []int{0, 6})
|
||||
|
||||
result3 := lo.Intersect([]int{0, 1, 2, 3, 4, 5}, []int{-1, 6})
|
||||
// []int{}
|
||||
|
||||
result4 := lo.Intersect([]int{0, 3, 5, 7}, []int{3, 5}, []int{0, 1, 2, 0, 3, 0})
|
||||
// []int{3}
|
||||
```
|
||||
|
||||
### IntersectBy
|
||||
|
||||
Returns the intersection between two collections using a custom key selector function.
|
||||
|
||||
```go
|
||||
transform := func(v int) string {
|
||||
return strconv.Itoa(v)
|
||||
}
|
||||
|
||||
result1 := lo.IntersectBy(transform, []int{0, 1, 2, 3, 4, 5}, []int{0, 2})
|
||||
// []int{0, 2}
|
||||
|
||||
result2 := lo.IntersectBy(transform, []int{0, 1, 2, 3, 4, 5}, []int{0, 6})
|
||||
// []int{0}
|
||||
|
||||
result3 := lo.IntersectBy(transform, []int{0, 1, 2, 3, 4, 5}, []int{-1, 6})
|
||||
// []int{}
|
||||
|
||||
result4 := lo.IntersectBy(transform, []int{0, 3, 5, 7}, []int{3, 5}, []int{0, 1, 2, 0, 3, 0})
|
||||
// []int{3}
|
||||
```
|
||||
|
||||
### Difference
|
||||
@@ -2608,7 +3085,7 @@ Filters a slice by excluding elements whose extracted keys match any in the excl
|
||||
Returns a new slice containing only the elements whose keys are not in the exclude list.
|
||||
|
||||
```go
|
||||
type struct User {
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
@@ -2633,6 +3110,30 @@ filteredUsers := lo.WithoutBy(users, getID, excludedIDs...)
|
||||
// []User[{ID: 1, Name: "Alice"}]
|
||||
```
|
||||
|
||||
```go
|
||||
// Use WithoutByErr when the iteratee can return an error
|
||||
type struct User {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
users := []User{
|
||||
{ID: 1, Name: "Alice"},
|
||||
{ID: 2, Name: "Bob"},
|
||||
{ID: 3, Name: "Charlie"},
|
||||
}
|
||||
|
||||
getID := func(user User) (int, error) {
|
||||
if user.ID == 2 {
|
||||
return 0, fmt.Errorf("Bob not allowed")
|
||||
}
|
||||
return user.ID, nil
|
||||
}
|
||||
|
||||
filteredUsers, err := lo.WithoutByErr(users, getID, 2, 3)
|
||||
// []User(nil), error("Bob not allowed")
|
||||
```
|
||||
|
||||
### WithoutEmpty
|
||||
|
||||
Returns a slice excluding zero values.
|
||||
@@ -2751,6 +3252,25 @@ str, ok := lo.Find([]string{"foobar"}, func(i string) bool {
|
||||
// "", false
|
||||
```
|
||||
|
||||
```go
|
||||
// Use FindErr when the predicate can return an error
|
||||
str, err := lo.FindErr([]string{"a", "b", "c", "d"}, func(i string) (bool, error) {
|
||||
if i == "c" {
|
||||
return false, fmt.Errorf("c is not allowed")
|
||||
}
|
||||
return i == "b", nil
|
||||
})
|
||||
// "b", nil
|
||||
|
||||
str, err = lo.FindErr([]string{"a", "b", "c"}, func(i string) (bool, error) {
|
||||
if i == "b" {
|
||||
return false, fmt.Errorf("b is not allowed")
|
||||
}
|
||||
return i == "b", nil
|
||||
})
|
||||
// "", error("b is not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/Eo7W0lvKTky)]
|
||||
|
||||
### FindIndexOf
|
||||
@@ -2879,6 +3399,18 @@ duplicatedValues := lo.FindDuplicatesBy([]int{3, 4, 5, 6, 7}, func(i int) int {
|
||||
// []int{3, 4}
|
||||
```
|
||||
|
||||
With error handling:
|
||||
|
||||
```go
|
||||
duplicatedValues, err := lo.FindDuplicatesByErr([]int{3, 4, 5, 6, 7}, func(i int) (int, error) {
|
||||
if i == 5 {
|
||||
return 0, fmt.Errorf("number 5 is not allowed")
|
||||
}
|
||||
return i % 3, nil
|
||||
})
|
||||
// []int(nil), error("number 5 is not allowed")
|
||||
```
|
||||
|
||||
### Min
|
||||
|
||||
Search the minimum value of a collection.
|
||||
@@ -2935,6 +3467,17 @@ min := lo.MinBy([]string{}, func(item string, min string) bool {
|
||||
// ""
|
||||
```
|
||||
|
||||
```go
|
||||
// Use MinByErr when the comparison function can return an error
|
||||
min, err := lo.MinByErr([]string{"s1", "string2", "s3"}, func(item string, min string) (bool, error) {
|
||||
if item == "string2" {
|
||||
return false, fmt.Errorf("string2 is not allowed")
|
||||
}
|
||||
return len(item) < len(min), nil
|
||||
})
|
||||
// "s1", error("string2 is not allowed")
|
||||
```
|
||||
|
||||
### MinIndexBy
|
||||
|
||||
Search the minimum value of a collection using the given comparison function and the index of the minimum value.
|
||||
@@ -2955,6 +3498,16 @@ min, index := lo.MinIndexBy([]string{}, func(item string, min string) bool {
|
||||
// "", -1
|
||||
```
|
||||
|
||||
```go
|
||||
min, index, err := lo.MinIndexByErr([]string{"s1", "string2", "s3"}, func(item string, min string) (bool, error) {
|
||||
if item == "s2" {
|
||||
return false, fmt.Errorf("s2 is not allowed")
|
||||
}
|
||||
return len(item) < len(min), nil
|
||||
})
|
||||
// "s1", 0, error("s2 is not allowed")
|
||||
```
|
||||
|
||||
### Earliest
|
||||
|
||||
Search the minimum time.Time of a collection.
|
||||
@@ -2983,6 +3536,17 @@ earliest := lo.EarliestBy([]foo{{time.Now()}, {}}, func(i foo) time.Time {
|
||||
// {bar:{2023-04-01 01:02:03 +0000 UTC}}
|
||||
```
|
||||
|
||||
```go
|
||||
// Use EarliestByErr when the iteratee function can return an error
|
||||
earliest, err := lo.EarliestByErr([]foo{{time.Now()}, {}}, func(i foo) (time.Time, error) {
|
||||
if i.bar.IsZero() {
|
||||
return time.Time{}, fmt.Errorf("zero time not allowed")
|
||||
}
|
||||
return i.bar, nil
|
||||
})
|
||||
// {bar:{...}}, error("zero time not allowed")
|
||||
```
|
||||
|
||||
### Max
|
||||
|
||||
Search the maximum value of a collection.
|
||||
@@ -3037,6 +3601,19 @@ max := lo.MaxBy([]string{}, func(item string, max string) bool {
|
||||
// ""
|
||||
```
|
||||
|
||||
```go
|
||||
// Use MaxByErr when the comparison function can return an error
|
||||
max, err := lo.MaxByErr([]string{"string1", "s2", "string3"}, func(item string, max string) (bool, error) {
|
||||
if item == "s2" {
|
||||
return false, fmt.Errorf("s2 is not allowed")
|
||||
}
|
||||
return len(item) > len(max), nil
|
||||
})
|
||||
// "string1", error("s2 is not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/JW1qu-ECwF7)]
|
||||
|
||||
### MaxIndexBy
|
||||
|
||||
Search the maximum value of a collection using the given comparison function and the index of the maximum value.
|
||||
@@ -3057,6 +3634,19 @@ max, index := lo.MaxIndexBy([]string{}, func(item string, max string) bool {
|
||||
// "", -1
|
||||
```
|
||||
|
||||
```go
|
||||
// Use MaxIndexByErr when the comparison function can return an error
|
||||
max, index, err := lo.MaxIndexByErr([]string{"string1", "s2", "string3"}, func(item string, max string) (bool, error) {
|
||||
if item == "s2" {
|
||||
return false, fmt.Errorf("s2 is not allowed")
|
||||
}
|
||||
return len(item) > len(max), nil
|
||||
})
|
||||
// "string1", 0, error("s2 is not allowed")
|
||||
```
|
||||
|
||||
[[play](https://go.dev/play/p/uaUszc-c9QK)]
|
||||
|
||||
### Latest
|
||||
|
||||
Search the maximum time.Time of a collection.
|
||||
@@ -3085,6 +3675,17 @@ latest := lo.LatestBy([]foo{{time.Now()}, {}}, func(i foo) time.Time {
|
||||
// {bar:{2023-04-01 01:02:03 +0000 UTC}}
|
||||
```
|
||||
|
||||
```go
|
||||
// Use LatestByErr when the iteratee function can return an error
|
||||
result, err := lo.LatestByErr([]foo{{time.Now()}, {}}, func(i foo) (time.Time, error) {
|
||||
if i.bar.IsZero() {
|
||||
return time.Time{}, fmt.Errorf("zero time not allowed")
|
||||
}
|
||||
return i.bar, nil
|
||||
})
|
||||
// foo{}, error("zero time not allowed")
|
||||
```
|
||||
|
||||
### First
|
||||
|
||||
Returns the first element of a collection and check for availability of the first element.
|
||||
|
||||
+13
-41
@@ -26,12 +26,7 @@ func ChannelDispatcher[T any](stream <-chan T, count, channelBufferCap int, stra
|
||||
|
||||
var i uint64
|
||||
|
||||
for {
|
||||
msg, ok := <-stream
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for msg := range stream {
|
||||
destination := strategy(msg, i, roChildren) % count
|
||||
children[destination] <- msg
|
||||
|
||||
@@ -107,8 +102,8 @@ func DispatchingStrategyRandom[T any](msg T, index uint64, channels []<-chan T)
|
||||
func DispatchingStrategyWeightedRandom[T any](weights []int) DispatchingStrategy[T] {
|
||||
seq := []int{}
|
||||
|
||||
for i := 0; i < len(weights); i++ {
|
||||
for j := 0; j < weights[i]; j++ {
|
||||
for i, weight := range weights {
|
||||
for j := 0; j < weight; j++ {
|
||||
seq = append(seq, i)
|
||||
}
|
||||
}
|
||||
@@ -143,22 +138,22 @@ func DispatchingStrategyFirst[T any](msg T, index uint64, channels []<-chan T) i
|
||||
// DispatchingStrategyLeast distributes messages in the emptiest channel.
|
||||
// Play: https://go.dev/play/p/ypy0jrRcEe7
|
||||
func DispatchingStrategyLeast[T any](msg T, index uint64, channels []<-chan T) int {
|
||||
seq := Range(len(channels))
|
||||
|
||||
return MinBy(seq, func(item, mIn int) bool {
|
||||
return len(channels[item]) < len(channels[mIn])
|
||||
_, i := MinIndexBy(channels, func(a, b <-chan T) bool {
|
||||
return len(a) < len(b)
|
||||
})
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// DispatchingStrategyMost distributes messages in the fullest channel.
|
||||
// If the channel capacity is exceeded, the next channel will be selected and so on.
|
||||
// Play: https://go.dev/play/p/erHHone7rF9
|
||||
func DispatchingStrategyMost[T any](msg T, index uint64, channels []<-chan T) int {
|
||||
seq := Range(len(channels))
|
||||
|
||||
return MaxBy(seq, func(item, mAx int) bool {
|
||||
return len(channels[item]) > len(channels[mAx]) && channelIsNotFull(channels[item])
|
||||
_, i := MaxIndexBy(channels, func(a, b <-chan T) bool {
|
||||
return len(a) > len(b) && channelIsNotFull(a)
|
||||
})
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// SliceToChannel returns a read-only channel of collection elements.
|
||||
@@ -213,10 +208,9 @@ func Generator[T any](bufferSize int, generator func(yield func(T))) <-chan T {
|
||||
// Play: https://go.dev/play/p/gPQ-6xmcKQI
|
||||
func Buffer[T any](ch <-chan T, size int) (collection []T, length int, readTime time.Duration, ok bool) {
|
||||
buffer := make([]T, 0, size)
|
||||
index := 0
|
||||
now := time.Now()
|
||||
|
||||
for ; index < size; index++ {
|
||||
for index := 0; index < size; index++ {
|
||||
item, ok := <-ch
|
||||
if !ok {
|
||||
return buffer, index, time.Since(now), false
|
||||
@@ -225,14 +219,7 @@ func Buffer[T any](ch <-chan T, size int) (collection []T, length int, readTime
|
||||
buffer = append(buffer, item)
|
||||
}
|
||||
|
||||
return buffer, index, time.Since(now), true
|
||||
}
|
||||
|
||||
// Batch creates a slice of n elements from a channel. Returns the slice and the slice length.
|
||||
//
|
||||
// Deprecated: Use [Buffer] instead.
|
||||
func Batch[T any](ch <-chan T, size int) (collection []T, length int, readTime time.Duration, ok bool) {
|
||||
return Buffer(ch, size)
|
||||
return buffer, size, time.Since(now), true
|
||||
}
|
||||
|
||||
// BufferWithContext creates a slice of n elements from a channel, with context. Returns the slice and the slice length.
|
||||
@@ -267,13 +254,6 @@ func BufferWithTimeout[T any](ch <-chan T, size int, timeout time.Duration) (col
|
||||
return BufferWithContext(ctx, ch, size)
|
||||
}
|
||||
|
||||
// BatchWithTimeout creates a slice of n elements from a channel, with timeout. Returns the slice and the slice length.
|
||||
//
|
||||
// Deprecated: Use [BufferWithTimeout] instead.
|
||||
func BatchWithTimeout[T any](ch <-chan T, size int, timeout time.Duration) (collection []T, length int, readTime time.Duration, ok bool) {
|
||||
return BufferWithTimeout(ch, size, timeout)
|
||||
}
|
||||
|
||||
// FanIn collects messages from multiple input channels into a single buffered channel.
|
||||
// Output messages have no priority. When all upstream channels reach EOF, downstream channel closes.
|
||||
// Play: https://go.dev/play/p/FH8Wq-T04Jb
|
||||
@@ -300,14 +280,6 @@ func FanIn[T any](channelBufferCap int, upstreams ...<-chan T) <-chan T {
|
||||
return out
|
||||
}
|
||||
|
||||
// ChannelMerge collects messages from multiple input channels into a single buffered channel.
|
||||
// Output messages have no priority. When all upstream channels reach EOF, downstream channel closes.
|
||||
//
|
||||
// Deprecated: Use [FanIn] instead.
|
||||
func ChannelMerge[T any](channelBufferCap int, upstreams ...<-chan T) <-chan T {
|
||||
return FanIn(channelBufferCap, upstreams...)
|
||||
}
|
||||
|
||||
// FanOut broadcasts all the upstream messages to multiple downstream channels.
|
||||
// When upstream channel reaches EOF, downstream channels close. If any downstream
|
||||
// channels is full, broadcasting is paused.
|
||||
|
||||
+8
-8
@@ -10,9 +10,9 @@ type synchronize struct {
|
||||
locker sync.Locker
|
||||
}
|
||||
|
||||
func (s *synchronize) Do(cb func()) {
|
||||
func (s *synchronize) Do(callback func()) {
|
||||
s.locker.Lock()
|
||||
Try0(cb)
|
||||
Try0(callback)
|
||||
s.locker.Unlock()
|
||||
}
|
||||
|
||||
@@ -52,13 +52,13 @@ func Async0(f func()) <-chan struct{} {
|
||||
}
|
||||
|
||||
// Async1 is an alias to Async.
|
||||
// Play: https://go.dev/play/p/uo35gosuTLw
|
||||
// Play: https://go.dev/play/p/RBQWtIn4PsF
|
||||
func Async1[A any](f func() A) <-chan A {
|
||||
return Async(f)
|
||||
}
|
||||
|
||||
// Async2 has the same behavior as Async, but returns the 2 results as a tuple inside the channel.
|
||||
// Play: https://go.dev/play/p/7W7mKQi0AhA
|
||||
// Play: https://go.dev/play/p/5SzzDjssXOH
|
||||
func Async2[A, B any](f func() (A, B)) <-chan Tuple2[A, B] {
|
||||
ch := make(chan Tuple2[A, B], 1)
|
||||
go func() {
|
||||
@@ -68,7 +68,7 @@ func Async2[A, B any](f func() (A, B)) <-chan Tuple2[A, B] {
|
||||
}
|
||||
|
||||
// Async3 has the same behavior as Async, but returns the 3 results as a tuple inside the channel.
|
||||
// Play: https://go.dev/play/p/L1d6o6l6q0d
|
||||
// Play: https://go.dev/play/p/cZpZsDXNmlx
|
||||
func Async3[A, B, C any](f func() (A, B, C)) <-chan Tuple3[A, B, C] {
|
||||
ch := make(chan Tuple3[A, B, C], 1)
|
||||
go func() {
|
||||
@@ -78,7 +78,7 @@ func Async3[A, B, C any](f func() (A, B, C)) <-chan Tuple3[A, B, C] {
|
||||
}
|
||||
|
||||
// Async4 has the same behavior as Async, but returns the 4 results as a tuple inside the channel.
|
||||
// Play: https://go.dev/play/p/1X7q6oL0TqF
|
||||
// Play: https://go.dev/play/p/9X5O2VrLzkR
|
||||
func Async4[A, B, C, D any](f func() (A, B, C, D)) <-chan Tuple4[A, B, C, D] {
|
||||
ch := make(chan Tuple4[A, B, C, D], 1)
|
||||
go func() {
|
||||
@@ -88,7 +88,7 @@ func Async4[A, B, C, D any](f func() (A, B, C, D)) <-chan Tuple4[A, B, C, D] {
|
||||
}
|
||||
|
||||
// Async5 has the same behavior as Async, but returns the 5 results as a tuple inside the channel.
|
||||
// Play: https://go.dev/play/p/2W7q4oL1TqG
|
||||
// Play: https://go.dev/play/p/MqnUJpkmopA
|
||||
func Async5[A, B, C, D, E any](f func() (A, B, C, D, E)) <-chan Tuple5[A, B, C, D, E] {
|
||||
ch := make(chan Tuple5[A, B, C, D, E], 1)
|
||||
go func() {
|
||||
@@ -98,7 +98,7 @@ func Async5[A, B, C, D, E any](f func() (A, B, C, D, E)) <-chan Tuple5[A, B, C,
|
||||
}
|
||||
|
||||
// Async6 has the same behavior as Async, but returns the 6 results as a tuple inside the channel.
|
||||
// Play: https://go.dev/play/p/3X8q5pM2UrH
|
||||
// Play: https://go.dev/play/p/kM1X67JPdSP
|
||||
func Async6[A, B, C, D, E, F any](f func() (A, B, C, D, E, F)) <-chan Tuple6[A, B, C, D, E, F] {
|
||||
ch := make(chan Tuple6[A, B, C, D, E, F], 1)
|
||||
go func() {
|
||||
|
||||
+4
-4
@@ -121,9 +121,9 @@ func (s *switchCase[T, R]) Case(val T, result R) *switchCase[T, R] {
|
||||
|
||||
// CaseF.
|
||||
// Play: https://go.dev/play/p/TGbKUMAeRUd
|
||||
func (s *switchCase[T, R]) CaseF(val T, cb func() R) *switchCase[T, R] {
|
||||
func (s *switchCase[T, R]) CaseF(val T, callback func() R) *switchCase[T, R] {
|
||||
if !s.done && s.predicate == val {
|
||||
s.result = cb()
|
||||
s.result = callback()
|
||||
s.done = true
|
||||
}
|
||||
|
||||
@@ -142,9 +142,9 @@ func (s *switchCase[T, R]) Default(result R) R {
|
||||
|
||||
// DefaultF.
|
||||
// Play: https://go.dev/play/p/TGbKUMAeRUd
|
||||
func (s *switchCase[T, R]) DefaultF(cb func() R) R {
|
||||
func (s *switchCase[T, R]) DefaultF(callback func() R) R {
|
||||
if !s.done {
|
||||
s.result = cb()
|
||||
s.result = callback()
|
||||
}
|
||||
|
||||
return s.result
|
||||
|
||||
+11
-11
@@ -30,8 +30,8 @@ func messageFromMsgAndArgs(msgAndArgs ...any) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// must panics if err is error or false.
|
||||
func must(err any, messageArgs ...any) {
|
||||
// MustChecker panics if err is error or false.
|
||||
var MustChecker = func(err any, messageArgs ...any) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
@@ -63,14 +63,14 @@ func must(err any, messageArgs ...any) {
|
||||
// and panics if err is error or false.
|
||||
// Play: https://go.dev/play/p/fOqtX5HudtN
|
||||
func Must[T any](val T, err any, messageArgs ...any) T {
|
||||
must(err, messageArgs...)
|
||||
MustChecker(err, messageArgs...)
|
||||
return val
|
||||
}
|
||||
|
||||
// Must0 has the same behavior as Must, but callback returns no variable.
|
||||
// Play: https://go.dev/play/p/TMoWrRp3DyC
|
||||
func Must0(err any, messageArgs ...any) {
|
||||
must(err, messageArgs...)
|
||||
MustChecker(err, messageArgs...)
|
||||
}
|
||||
|
||||
// Must1 is an alias to Must.
|
||||
@@ -82,35 +82,35 @@ func Must1[T any](val T, err any, messageArgs ...any) T {
|
||||
// Must2 has the same behavior as Must, but callback returns 2 variables.
|
||||
// Play: https://go.dev/play/p/TMoWrRp3DyC
|
||||
func Must2[T1, T2 any](val1 T1, val2 T2, err any, messageArgs ...any) (T1, T2) {
|
||||
must(err, messageArgs...)
|
||||
MustChecker(err, messageArgs...)
|
||||
return val1, val2
|
||||
}
|
||||
|
||||
// Must3 has the same behavior as Must, but callback returns 3 variables.
|
||||
// Play: https://go.dev/play/p/TMoWrRp3DyC
|
||||
func Must3[T1, T2, T3 any](val1 T1, val2 T2, val3 T3, err any, messageArgs ...any) (T1, T2, T3) {
|
||||
must(err, messageArgs...)
|
||||
MustChecker(err, messageArgs...)
|
||||
return val1, val2, val3
|
||||
}
|
||||
|
||||
// Must4 has the same behavior as Must, but callback returns 4 variables.
|
||||
// Play: https://go.dev/play/p/TMoWrRp3DyC
|
||||
func Must4[T1, T2, T3, T4 any](val1 T1, val2 T2, val3 T3, val4 T4, err any, messageArgs ...any) (T1, T2, T3, T4) {
|
||||
must(err, messageArgs...)
|
||||
MustChecker(err, messageArgs...)
|
||||
return val1, val2, val3, val4
|
||||
}
|
||||
|
||||
// Must5 has the same behavior as Must, but callback returns 5 variables.
|
||||
// Play: https://go.dev/play/p/TMoWrRp3DyC
|
||||
func Must5[T1, T2, T3, T4, T5 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, err any, messageArgs ...any) (T1, T2, T3, T4, T5) {
|
||||
must(err, messageArgs...)
|
||||
MustChecker(err, messageArgs...)
|
||||
return val1, val2, val3, val4, val5
|
||||
}
|
||||
|
||||
// Must6 has the same behavior as Must, but callback returns 6 variables.
|
||||
// Play: https://go.dev/play/p/TMoWrRp3DyC
|
||||
func Must6[T1, T2, T3, T4, T5, T6 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, val6 T6, err any, messageArgs ...any) (T1, T2, T3, T4, T5, T6) {
|
||||
must(err, messageArgs...)
|
||||
MustChecker(err, messageArgs...)
|
||||
return val1, val2, val3, val4, val5, val6
|
||||
}
|
||||
|
||||
@@ -356,7 +356,7 @@ func ErrorsAs[T error](err error) (T, bool) {
|
||||
|
||||
// Assert does nothing when the condition is true, otherwise it panics with an optional message.
|
||||
// Play: https://go.dev/play/p/Xv8LLKBMNwI
|
||||
func Assert(condition bool, message ...string) {
|
||||
var Assert = func(condition bool, message ...string) {
|
||||
if condition {
|
||||
return
|
||||
}
|
||||
@@ -370,7 +370,7 @@ func Assert(condition bool, message ...string) {
|
||||
|
||||
// Assertf does nothing when the condition is true, otherwise it panics with a formatted message.
|
||||
// Play: https://go.dev/play/p/TVPEmVcyrdY
|
||||
func Assertf(condition bool, format string, args ...any) {
|
||||
var Assertf = func(condition bool, format string, args ...any) {
|
||||
if condition {
|
||||
return
|
||||
}
|
||||
|
||||
+346
-57
@@ -1,7 +1,6 @@
|
||||
package lo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo/internal/constraints"
|
||||
@@ -81,6 +80,26 @@ func Find[T any](collection []T, predicate func(item T) bool) (T, bool) {
|
||||
return result, false
|
||||
}
|
||||
|
||||
// FindErr searches for an element in a slice based on a predicate that can return an error.
|
||||
// Returns the element and nil error if the element is found.
|
||||
// Returns zero value and nil error if the element is not found.
|
||||
// If the predicate returns an error, iteration stops immediately and returns zero value and the error.
|
||||
func FindErr[T any](collection []T, predicate func(item T) (bool, error)) (T, error) {
|
||||
for i := range collection {
|
||||
matches, err := predicate(collection[i])
|
||||
if err != nil {
|
||||
var result T
|
||||
return result, err
|
||||
}
|
||||
if matches {
|
||||
return collection[i], nil
|
||||
}
|
||||
}
|
||||
|
||||
var result T
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// FindIndexOf searches for an element in a slice based on a predicate and returns the index and true.
|
||||
// Returns -1 and false if the element is not found.
|
||||
// Play: https://go.dev/play/p/XWSEM4Ic_t0
|
||||
@@ -152,16 +171,20 @@ func FindKeyBy[K comparable, V any](object map[K]V, predicate func(key K, value
|
||||
func FindUniques[T comparable, Slice ~[]T](collection Slice) Slice {
|
||||
isDupl := make(map[T]bool, len(collection))
|
||||
|
||||
duplicates := 0
|
||||
|
||||
for i := range collection {
|
||||
duplicated, ok := isDupl[collection[i]]
|
||||
if !ok {
|
||||
isDupl[collection[i]] = false
|
||||
} else if !duplicated {
|
||||
isDupl[collection[i]] = true
|
||||
duplicated, seen := isDupl[collection[i]]
|
||||
if !duplicated {
|
||||
isDupl[collection[i]] = seen
|
||||
|
||||
if seen {
|
||||
duplicates++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make(Slice, 0, len(collection)-len(isDupl))
|
||||
result := make(Slice, 0, len(isDupl)-duplicates)
|
||||
|
||||
for i := range collection {
|
||||
if duplicated := isDupl[collection[i]]; !duplicated {
|
||||
@@ -178,18 +201,22 @@ func FindUniques[T comparable, Slice ~[]T](collection Slice) Slice {
|
||||
func FindUniquesBy[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(item T) U) Slice {
|
||||
isDupl := make(map[U]bool, len(collection))
|
||||
|
||||
duplicates := 0
|
||||
|
||||
for i := range collection {
|
||||
key := iteratee(collection[i])
|
||||
|
||||
duplicated, ok := isDupl[key]
|
||||
if !ok {
|
||||
isDupl[key] = false
|
||||
} else if !duplicated {
|
||||
isDupl[key] = true
|
||||
duplicated, seen := isDupl[key]
|
||||
if !duplicated {
|
||||
isDupl[key] = seen
|
||||
|
||||
if seen {
|
||||
duplicates++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make(Slice, 0, len(collection)-len(isDupl))
|
||||
result := make(Slice, 0, len(isDupl)-duplicates)
|
||||
|
||||
for i := range collection {
|
||||
key := iteratee(collection[i])
|
||||
@@ -207,16 +234,20 @@ func FindUniquesBy[T any, U comparable, Slice ~[]T](collection Slice, iteratee f
|
||||
func FindDuplicates[T comparable, Slice ~[]T](collection Slice) Slice {
|
||||
isDupl := make(map[T]bool, len(collection))
|
||||
|
||||
duplicates := 0
|
||||
|
||||
for i := range collection {
|
||||
duplicated, ok := isDupl[collection[i]]
|
||||
if !ok {
|
||||
isDupl[collection[i]] = false
|
||||
} else if !duplicated {
|
||||
isDupl[collection[i]] = true
|
||||
duplicated, seen := isDupl[collection[i]]
|
||||
if !duplicated {
|
||||
isDupl[collection[i]] = seen
|
||||
|
||||
if seen {
|
||||
duplicates++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make(Slice, 0, len(collection)-len(isDupl))
|
||||
result := make(Slice, 0, duplicates)
|
||||
|
||||
for i := range collection {
|
||||
if duplicated := isDupl[collection[i]]; duplicated {
|
||||
@@ -234,18 +265,22 @@ func FindDuplicates[T comparable, Slice ~[]T](collection Slice) Slice {
|
||||
func FindDuplicatesBy[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(item T) U) Slice {
|
||||
isDupl := make(map[U]bool, len(collection))
|
||||
|
||||
duplicates := 0
|
||||
|
||||
for i := range collection {
|
||||
key := iteratee(collection[i])
|
||||
|
||||
duplicated, ok := isDupl[key]
|
||||
if !ok {
|
||||
isDupl[key] = false
|
||||
} else if !duplicated {
|
||||
isDupl[key] = true
|
||||
duplicated, seen := isDupl[key]
|
||||
if !duplicated {
|
||||
isDupl[key] = seen
|
||||
|
||||
if seen {
|
||||
duplicates++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make(Slice, 0, len(collection)-len(isDupl))
|
||||
result := make(Slice, 0, duplicates)
|
||||
|
||||
for i := range collection {
|
||||
key := iteratee(collection[i])
|
||||
@@ -259,6 +294,52 @@ func FindDuplicatesBy[T any, U comparable, Slice ~[]T](collection Slice, iterate
|
||||
return result
|
||||
}
|
||||
|
||||
// FindDuplicatesByErr returns a slice with the first occurrence of each duplicated element in the collection.
|
||||
// The order of result values is determined by the order they occur in the slice. It accepts `iteratee` which is
|
||||
// invoked for each element in the slice to generate the criterion by which uniqueness is computed.
|
||||
// If the iteratee returns an error, iteration stops immediately and the error is returned with a nil slice.
|
||||
func FindDuplicatesByErr[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(item T) (U, error)) (Slice, error) {
|
||||
isDupl := make(map[U]bool, len(collection))
|
||||
|
||||
duplicates := 0
|
||||
|
||||
// First pass: identify duplicates
|
||||
for i := range collection {
|
||||
key, err := iteratee(collection[i])
|
||||
if err != nil {
|
||||
var result Slice
|
||||
return result, err
|
||||
}
|
||||
|
||||
duplicated, seen := isDupl[key]
|
||||
if !duplicated {
|
||||
isDupl[key] = seen
|
||||
|
||||
if seen {
|
||||
duplicates++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make(Slice, 0, duplicates)
|
||||
|
||||
// Second pass: collect first occurrences of duplicates
|
||||
for i := range collection {
|
||||
key, err := iteratee(collection[i])
|
||||
if err != nil {
|
||||
var result Slice
|
||||
return result, err
|
||||
}
|
||||
|
||||
if duplicated := isDupl[key]; duplicated {
|
||||
result = append(result, collection[i])
|
||||
isDupl[key] = false
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Min search the minimum value of a collection.
|
||||
// Returns zero value when the collection is empty.
|
||||
// Play: https://go.dev/play/p/r6e-Z8JozS8
|
||||
@@ -311,7 +392,7 @@ func MinIndex[T constraints.Ordered](collection []T) (T, int) {
|
||||
// MinBy search the minimum value of a collection using the given comparison function.
|
||||
// If several values of the collection are equal to the smallest value, returns the first such value.
|
||||
// Returns zero value when the collection is empty.
|
||||
func MinBy[T any](collection []T, comparison func(a, b T) bool) T {
|
||||
func MinBy[T any](collection []T, less func(a, b T) bool) T {
|
||||
var mIn T
|
||||
|
||||
if len(collection) == 0 {
|
||||
@@ -323,7 +404,7 @@ func MinBy[T any](collection []T, comparison func(a, b T) bool) T {
|
||||
for i := 1; i < len(collection); i++ {
|
||||
item := collection[i]
|
||||
|
||||
if comparison(item, mIn) {
|
||||
if less(item, mIn) {
|
||||
mIn = item
|
||||
}
|
||||
}
|
||||
@@ -331,10 +412,39 @@ func MinBy[T any](collection []T, comparison func(a, b T) bool) T {
|
||||
return mIn
|
||||
}
|
||||
|
||||
// MinByErr search the minimum value of a collection using the given comparison function.
|
||||
// If several values of the collection are equal to the smallest value, returns the first such value.
|
||||
// Returns zero value and nil error when the collection is empty.
|
||||
// If the comparison function returns an error, iteration stops and the error is returned.
|
||||
func MinByErr[T any](collection []T, less func(a, b T) (bool, error)) (T, error) {
|
||||
var mIn T
|
||||
|
||||
if len(collection) == 0 {
|
||||
return mIn, nil
|
||||
}
|
||||
|
||||
mIn = collection[0]
|
||||
|
||||
for i := 1; i < len(collection); i++ {
|
||||
item := collection[i]
|
||||
|
||||
isLess, err := less(item, mIn)
|
||||
if err != nil {
|
||||
var zero T
|
||||
return zero, err
|
||||
}
|
||||
if isLess {
|
||||
mIn = item
|
||||
}
|
||||
}
|
||||
|
||||
return mIn, nil
|
||||
}
|
||||
|
||||
// MinIndexBy search the minimum value of a collection using the given comparison function and the index of the minimum value.
|
||||
// If several values of the collection are equal to the smallest value, returns the first such value.
|
||||
// Returns (zero value, -1) when the collection is empty.
|
||||
func MinIndexBy[T any](collection []T, comparison func(a, b T) bool) (T, int) {
|
||||
func MinIndexBy[T any](collection []T, less func(a, b T) bool) (T, int) {
|
||||
var (
|
||||
mIn T
|
||||
index int
|
||||
@@ -349,7 +459,7 @@ func MinIndexBy[T any](collection []T, comparison func(a, b T) bool) (T, int) {
|
||||
for i := 1; i < len(collection); i++ {
|
||||
item := collection[i]
|
||||
|
||||
if comparison(item, mIn) {
|
||||
if less(item, mIn) {
|
||||
mIn = item
|
||||
index = i
|
||||
}
|
||||
@@ -358,6 +468,40 @@ func MinIndexBy[T any](collection []T, comparison func(a, b T) bool) (T, int) {
|
||||
return mIn, index
|
||||
}
|
||||
|
||||
// MinIndexByErr search the minimum value of a collection using the given comparison function and the index of the minimum value.
|
||||
// If several values of the collection are equal to the smallest value, returns the first such value.
|
||||
// Returns (zero value, -1) when the collection is empty.
|
||||
// Comparison function can return an error to stop iteration immediately.
|
||||
func MinIndexByErr[T any](collection []T, less func(a, b T) (bool, error)) (T, int, error) {
|
||||
var (
|
||||
mIn T
|
||||
index int
|
||||
)
|
||||
|
||||
if len(collection) == 0 {
|
||||
return mIn, -1, nil
|
||||
}
|
||||
|
||||
mIn = collection[0]
|
||||
|
||||
for i := 1; i < len(collection); i++ {
|
||||
item := collection[i]
|
||||
|
||||
isLess, err := less(item, mIn)
|
||||
if err != nil {
|
||||
var zero T
|
||||
return zero, -1, err
|
||||
}
|
||||
|
||||
if isLess {
|
||||
mIn = item
|
||||
index = i
|
||||
}
|
||||
}
|
||||
|
||||
return mIn, index, nil
|
||||
}
|
||||
|
||||
// Earliest search the minimum time.Time of a collection.
|
||||
// Returns zero value when the collection is empty.
|
||||
func Earliest(times ...time.Time) time.Time {
|
||||
@@ -404,6 +548,37 @@ func EarliestBy[T any](collection []T, iteratee func(item T) time.Time) T {
|
||||
return earliest
|
||||
}
|
||||
|
||||
// EarliestByErr search the minimum time.Time of a collection using the given iteratee function.
|
||||
// Returns zero value and nil error when the collection is empty.
|
||||
// If the iteratee returns an error, iteration stops and the error is returned.
|
||||
func EarliestByErr[T any](collection []T, iteratee func(item T) (time.Time, error)) (T, error) {
|
||||
var earliest T
|
||||
|
||||
if len(collection) == 0 {
|
||||
return earliest, nil
|
||||
}
|
||||
|
||||
earliestTime, err := iteratee(collection[0])
|
||||
if err != nil {
|
||||
return earliest, err
|
||||
}
|
||||
earliest = collection[0]
|
||||
|
||||
for i := 1; i < len(collection); i++ {
|
||||
itemTime, err := iteratee(collection[i])
|
||||
if err != nil {
|
||||
return earliest, err
|
||||
}
|
||||
|
||||
if itemTime.Before(earliestTime) {
|
||||
earliest = collection[i]
|
||||
earliestTime = itemTime
|
||||
}
|
||||
}
|
||||
|
||||
return earliest, nil
|
||||
}
|
||||
|
||||
// Max searches the maximum value of a collection.
|
||||
// Returns zero value when the collection is empty.
|
||||
// Play: https://go.dev/play/p/r6e-Z8JozS8
|
||||
@@ -456,7 +631,12 @@ func MaxIndex[T constraints.Ordered](collection []T) (T, int) {
|
||||
// MaxBy search the maximum value of a collection using the given comparison function.
|
||||
// If several values of the collection are equal to the greatest value, returns the first such value.
|
||||
// Returns zero value when the collection is empty.
|
||||
func MaxBy[T any](collection []T, comparison func(a, b T) bool) T {
|
||||
//
|
||||
// Note: the comparison function is inconsistent with most languages, since we use the opposite of the usual convention.
|
||||
// See https://github.com/samber/lo/issues/129
|
||||
//
|
||||
// Play: https://go.dev/play/p/JW1qu-ECwF7
|
||||
func MaxBy[T any](collection []T, greater func(a, b T) bool) T {
|
||||
var mAx T
|
||||
|
||||
if len(collection) == 0 {
|
||||
@@ -468,7 +648,7 @@ func MaxBy[T any](collection []T, comparison func(a, b T) bool) T {
|
||||
for i := 1; i < len(collection); i++ {
|
||||
item := collection[i]
|
||||
|
||||
if comparison(item, mAx) {
|
||||
if greater(item, mAx) {
|
||||
mAx = item
|
||||
}
|
||||
}
|
||||
@@ -476,10 +656,46 @@ func MaxBy[T any](collection []T, comparison func(a, b T) bool) T {
|
||||
return mAx
|
||||
}
|
||||
|
||||
// MaxByErr search the maximum value of a collection using the given comparison function.
|
||||
// If several values of the collection are equal to the greatest value, returns the first such value.
|
||||
// Returns zero value and nil error when the collection is empty.
|
||||
// If the comparison function returns an error, iteration stops and the error is returned.
|
||||
//
|
||||
// Note: the comparison function is inconsistent with most languages, since we use the opposite of the usual convention.
|
||||
// See https://github.com/samber/lo/issues/129
|
||||
func MaxByErr[T any](collection []T, greater func(a, b T) (bool, error)) (T, error) {
|
||||
var mAx T
|
||||
|
||||
if len(collection) == 0 {
|
||||
return mAx, nil
|
||||
}
|
||||
|
||||
mAx = collection[0]
|
||||
|
||||
for i := 1; i < len(collection); i++ {
|
||||
item := collection[i]
|
||||
|
||||
isGreater, err := greater(item, mAx)
|
||||
if err != nil {
|
||||
return mAx, err
|
||||
}
|
||||
if isGreater {
|
||||
mAx = item
|
||||
}
|
||||
}
|
||||
|
||||
return mAx, nil
|
||||
}
|
||||
|
||||
// MaxIndexBy search the maximum value of a collection using the given comparison function and the index of the maximum value.
|
||||
// If several values of the collection are equal to the greatest value, returns the first such value.
|
||||
// Returns (zero value, -1) when the collection is empty.
|
||||
func MaxIndexBy[T any](collection []T, comparison func(a, b T) bool) (T, int) {
|
||||
//
|
||||
// Note: the comparison function is inconsistent with most languages, since we use the opposite of the usual convention.
|
||||
// See https://github.com/samber/lo/issues/129
|
||||
//
|
||||
// Play: https://go.dev/play/p/uaUszc-c9QK
|
||||
func MaxIndexBy[T any](collection []T, greater func(a, b T) bool) (T, int) {
|
||||
var (
|
||||
mAx T
|
||||
index int
|
||||
@@ -494,7 +710,7 @@ func MaxIndexBy[T any](collection []T, comparison func(a, b T) bool) (T, int) {
|
||||
for i := 1; i < len(collection); i++ {
|
||||
item := collection[i]
|
||||
|
||||
if comparison(item, mAx) {
|
||||
if greater(item, mAx) {
|
||||
mAx = item
|
||||
index = i
|
||||
}
|
||||
@@ -503,6 +719,42 @@ func MaxIndexBy[T any](collection []T, comparison func(a, b T) bool) (T, int) {
|
||||
return mAx, index
|
||||
}
|
||||
|
||||
// MaxIndexByErr search the maximum value of a collection using the given comparison function and the index of the maximum value.
|
||||
// If several values of the collection are equal to the greatest value, returns the first such value.
|
||||
// Returns (zero value, -1, nil) when the collection is empty.
|
||||
// If the comparison function returns an error, iteration stops and the error is returned.
|
||||
//
|
||||
// Note: the comparison function is inconsistent with most languages, since we use the opposite of the usual convention.
|
||||
// See https://github.com/samber/lo/issues/129
|
||||
func MaxIndexByErr[T any](collection []T, greater func(a, b T) (bool, error)) (T, int, error) {
|
||||
var (
|
||||
mAx T
|
||||
index int
|
||||
)
|
||||
|
||||
if len(collection) == 0 {
|
||||
return mAx, -1, nil
|
||||
}
|
||||
|
||||
mAx = collection[0]
|
||||
|
||||
for i := 1; i < len(collection); i++ {
|
||||
item := collection[i]
|
||||
|
||||
isGreater, err := greater(item, mAx)
|
||||
if err != nil {
|
||||
var zero T
|
||||
return zero, -1, err
|
||||
}
|
||||
if isGreater {
|
||||
mAx = item
|
||||
index = i
|
||||
}
|
||||
}
|
||||
|
||||
return mAx, index, nil
|
||||
}
|
||||
|
||||
// Latest search the maximum time.Time of a collection.
|
||||
// Returns zero value when the collection is empty.
|
||||
func Latest(times ...time.Time) time.Time {
|
||||
@@ -549,6 +801,37 @@ func LatestBy[T any](collection []T, iteratee func(item T) time.Time) T {
|
||||
return latest
|
||||
}
|
||||
|
||||
// LatestByErr search the maximum time.Time of a collection using the given iteratee function.
|
||||
// Returns zero value and nil error when the collection is empty.
|
||||
// If the iteratee returns an error, iteration stops and the error is returned.
|
||||
func LatestByErr[T any](collection []T, iteratee func(item T) (time.Time, error)) (T, error) {
|
||||
var latest T
|
||||
|
||||
if len(collection) == 0 {
|
||||
return latest, nil
|
||||
}
|
||||
|
||||
latestTime, err := iteratee(collection[0])
|
||||
if err != nil {
|
||||
return latest, err
|
||||
}
|
||||
latest = collection[0]
|
||||
|
||||
for i := 1; i < len(collection); i++ {
|
||||
itemTime, err := iteratee(collection[i])
|
||||
if err != nil {
|
||||
return latest, err
|
||||
}
|
||||
|
||||
if itemTime.After(latestTime) {
|
||||
latest = collection[i]
|
||||
latestTime = itemTime
|
||||
}
|
||||
}
|
||||
|
||||
return latest, nil
|
||||
}
|
||||
|
||||
// First returns the first element of a collection and check for availability of the first element.
|
||||
// Play: https://go.dev/play/p/ul45Z0y2EFO
|
||||
func First[T any](collection []T) (T, bool) {
|
||||
@@ -615,17 +898,22 @@ func LastOr[T any](collection []T, fallback T) T {
|
||||
// from the end is returned. An error is returned when nth is out of slice bounds.
|
||||
// Play: https://go.dev/play/p/sHoh88KWt6B
|
||||
func Nth[T any, N constraints.Integer](collection []T, nth N) (T, error) {
|
||||
value, ok := sliceNth(collection, nth)
|
||||
|
||||
return value, Validate(ok, "nth: %d out of slice bounds", nth)
|
||||
}
|
||||
|
||||
func sliceNth[T any, N constraints.Integer](collection []T, nth N) (T, bool) {
|
||||
n := int(nth)
|
||||
l := len(collection)
|
||||
if n >= l || -n > l {
|
||||
var t T
|
||||
return t, fmt.Errorf("nth: %d out of slice bounds", n)
|
||||
return Empty[T](), false
|
||||
}
|
||||
|
||||
if n >= 0 {
|
||||
return collection[n], nil
|
||||
return collection[n], true
|
||||
}
|
||||
return collection[l+n], nil
|
||||
return collection[l+n], true
|
||||
}
|
||||
|
||||
// NthOr returns the element at index `nth` of collection.
|
||||
@@ -633,8 +921,8 @@ func Nth[T any, N constraints.Integer](collection []T, nth N) (T, error) {
|
||||
// If `nth` is out of slice bounds, it returns the fallback value instead of an error.
|
||||
// Play: https://go.dev/play/p/sHoh88KWt6B
|
||||
func NthOr[T any, N constraints.Integer](collection []T, nth N, fallback T) T {
|
||||
value, err := Nth(collection, nth)
|
||||
if err != nil {
|
||||
value, ok := sliceNth(collection, nth)
|
||||
if !ok {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
@@ -645,11 +933,7 @@ func NthOr[T any, N constraints.Integer](collection []T, nth N, fallback T) T {
|
||||
// If `nth` is out of slice bounds, it returns the zero value (empty value) for that type.
|
||||
// Play: https://go.dev/play/p/sHoh88KWt6B
|
||||
func NthOrEmpty[T any, N constraints.Integer](collection []T, nth N) T {
|
||||
value, err := Nth(collection, nth)
|
||||
if err != nil {
|
||||
var zeroValue T
|
||||
return zeroValue
|
||||
}
|
||||
value, _ := sliceNth(collection, nth)
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -660,8 +944,7 @@ type randomIntGenerator func(n int) int
|
||||
// Sample returns a random item from collection.
|
||||
// Play: https://go.dev/play/p/vCcSJbh5s6l
|
||||
func Sample[T any](collection []T) T {
|
||||
result := SampleBy(collection, xrand.IntN)
|
||||
return result
|
||||
return SampleBy(collection, xrand.IntN)
|
||||
}
|
||||
|
||||
// SampleBy returns a random item from collection, using randomIntGenerator as the random index generator.
|
||||
@@ -677,29 +960,35 @@ func SampleBy[T any](collection []T, randomIntGenerator randomIntGenerator) T {
|
||||
// Samples returns N random unique items from collection.
|
||||
// Play: https://go.dev/play/p/vCcSJbh5s6l
|
||||
func Samples[T any, Slice ~[]T](collection Slice, count int) Slice {
|
||||
results := SamplesBy(collection, count, xrand.IntN)
|
||||
return results
|
||||
return SamplesBy(collection, count, xrand.IntN)
|
||||
}
|
||||
|
||||
// SamplesBy returns N random unique items from collection, using randomIntGenerator as the random index generator.
|
||||
// Play: https://go.dev/play/p/HDmKmMgq0XN
|
||||
func SamplesBy[T any, Slice ~[]T](collection Slice, count int, randomIntGenerator randomIntGenerator) Slice {
|
||||
if count <= 0 {
|
||||
return Slice{}
|
||||
}
|
||||
|
||||
size := len(collection)
|
||||
|
||||
cOpy := append(Slice{}, collection...)
|
||||
if size < count {
|
||||
count = size
|
||||
}
|
||||
|
||||
results := Slice{}
|
||||
indexes := Range(size)
|
||||
results := make(Slice, count)
|
||||
|
||||
for i := 0; i < size && i < count; i++ {
|
||||
copyLength := size - i
|
||||
for i := range results {
|
||||
n := len(indexes)
|
||||
|
||||
index := randomIntGenerator(size - i)
|
||||
results = append(results, cOpy[index])
|
||||
index := randomIntGenerator(n)
|
||||
results[i] = collection[indexes[index]]
|
||||
|
||||
// Removes element.
|
||||
// Removes index.
|
||||
// It is faster to swap with last element and remove it.
|
||||
cOpy[index] = cOpy[copyLength-1]
|
||||
cOpy = cOpy[:copyLength-1]
|
||||
indexes[index] = indexes[n-1]
|
||||
indexes = indexes[:n-1]
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
+125
-43
@@ -27,8 +27,14 @@ func ContainsBy[T any](collection []T, predicate func(item T) bool) bool {
|
||||
// Every returns true if all elements of a subset are contained in a collection or if the subset is empty.
|
||||
// Play: https://go.dev/play/p/W1EvyqY6t9j
|
||||
func Every[T comparable](collection, subset []T) bool {
|
||||
for i := range subset {
|
||||
if !Contains(collection, subset[i]) {
|
||||
if len(subset) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
seen := Keyify(collection)
|
||||
|
||||
for _, item := range subset {
|
||||
if _, ok := seen[item]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -52,8 +58,13 @@ func EveryBy[T any](collection []T, predicate func(item T) bool) bool {
|
||||
// If the subset is empty Some returns false.
|
||||
// Play: https://go.dev/play/p/Lj4ceFkeT9V
|
||||
func Some[T comparable](collection, subset []T) bool {
|
||||
for i := range subset {
|
||||
if Contains(collection, subset[i]) {
|
||||
if len(subset) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
seen := Keyify(subset)
|
||||
for i := range collection {
|
||||
if _, ok := seen[collection[i]]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -77,8 +88,13 @@ func SomeBy[T any](collection []T, predicate func(item T) bool) bool {
|
||||
// None returns true if no element of a subset is contained in a collection or if the subset is empty.
|
||||
// Play: https://go.dev/play/p/fye7JsmxzPV
|
||||
func None[T comparable](collection, subset []T) bool {
|
||||
for i := range subset {
|
||||
if Contains(collection, subset[i]) {
|
||||
if len(subset) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
seen := Keyify(subset)
|
||||
for i := range collection {
|
||||
if _, ok := seen[collection[i]]; ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -98,19 +114,88 @@ func NoneBy[T any](collection []T, predicate func(item T) bool) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Intersect returns the intersection between two collections.
|
||||
// Intersect returns the intersection between collections.
|
||||
// Play: https://go.dev/play/p/uuElL9X9e58
|
||||
func Intersect[T comparable, Slice ~[]T](list1, list2 Slice) Slice {
|
||||
result := Slice{}
|
||||
seen := map[T]struct{}{}
|
||||
|
||||
for i := range list1 {
|
||||
seen[list1[i]] = struct{}{}
|
||||
func Intersect[T comparable, Slice ~[]T](lists ...Slice) Slice {
|
||||
if len(lists) == 0 {
|
||||
return Slice{}
|
||||
}
|
||||
|
||||
for i := range list2 {
|
||||
if _, ok := seen[list2[i]]; ok {
|
||||
result = append(result, list2[i])
|
||||
last := lists[len(lists)-1]
|
||||
|
||||
seen := make(map[T]bool, len(last))
|
||||
|
||||
for _, item := range last {
|
||||
seen[item] = false
|
||||
}
|
||||
|
||||
for i := len(lists) - 2; i > 0 && len(seen) != 0; i-- {
|
||||
for _, item := range lists[i] {
|
||||
if _, ok := seen[item]; ok {
|
||||
seen[item] = true
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range seen {
|
||||
if v {
|
||||
seen[k] = false
|
||||
} else {
|
||||
delete(seen, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make(Slice, 0, len(seen))
|
||||
|
||||
for _, item := range lists[0] {
|
||||
if _, ok := seen[item]; ok {
|
||||
result = append(result, item)
|
||||
delete(seen, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// IntersectBy returns the intersection between two collections using a custom key selector function.
|
||||
func IntersectBy[T any, K comparable, Slice ~[]T](transform func(T) K, lists ...Slice) Slice {
|
||||
if len(lists) == 0 {
|
||||
return Slice{}
|
||||
}
|
||||
|
||||
last := lists[len(lists)-1]
|
||||
|
||||
seen := make(map[K]bool, len(last))
|
||||
|
||||
for _, item := range last {
|
||||
k := transform(item)
|
||||
seen[k] = false
|
||||
}
|
||||
|
||||
for i := len(lists) - 2; i > 0 && len(seen) != 0; i-- {
|
||||
for _, item := range lists[i] {
|
||||
k := transform(item)
|
||||
if _, ok := seen[k]; ok {
|
||||
seen[k] = true
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range seen {
|
||||
if v {
|
||||
seen[k] = false
|
||||
} else {
|
||||
delete(seen, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make(Slice, 0, len(seen))
|
||||
|
||||
for _, item := range lists[0] {
|
||||
k := transform(item)
|
||||
if _, ok := seen[k]; ok {
|
||||
result = append(result, item)
|
||||
delete(seen, k)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,16 +210,8 @@ func Difference[T comparable, Slice ~[]T](list1, list2 Slice) (Slice, Slice) {
|
||||
left := Slice{}
|
||||
right := Slice{}
|
||||
|
||||
seenLeft := map[T]struct{}{}
|
||||
seenRight := map[T]struct{}{}
|
||||
|
||||
for i := range list1 {
|
||||
seenLeft[list1[i]] = struct{}{}
|
||||
}
|
||||
|
||||
for i := range list2 {
|
||||
seenRight[list2[i]] = struct{}{}
|
||||
}
|
||||
seenLeft := Keyify(list1)
|
||||
seenRight := Keyify(list2)
|
||||
|
||||
for i := range list1 {
|
||||
if _, ok := seenRight[list1[i]]; !ok {
|
||||
@@ -179,10 +256,7 @@ func Union[T comparable, Slice ~[]T](lists ...Slice) Slice {
|
||||
// Without returns a slice excluding all given values.
|
||||
// Play: https://go.dev/play/p/5j30Ux8TaD0
|
||||
func Without[T comparable, Slice ~[]T](collection Slice, exclude ...T) Slice {
|
||||
excludeMap := make(map[T]struct{}, len(exclude))
|
||||
for i := range exclude {
|
||||
excludeMap[exclude[i]] = struct{}{}
|
||||
}
|
||||
excludeMap := Keyify(exclude)
|
||||
|
||||
result := make(Slice, 0, len(collection))
|
||||
for i := range collection {
|
||||
@@ -197,10 +271,7 @@ func Without[T comparable, Slice ~[]T](collection Slice, exclude ...T) Slice {
|
||||
// Returns a new slice containing only the elements whose keys are not in the exclude list.
|
||||
// Play: https://go.dev/play/p/VgWJOF01NbJ
|
||||
func WithoutBy[T any, K comparable, Slice ~[]T](collection Slice, iteratee func(item T) K, exclude ...K) Slice {
|
||||
excludeMap := make(map[K]struct{}, len(exclude))
|
||||
for _, e := range exclude {
|
||||
excludeMap[e] = struct{}{}
|
||||
}
|
||||
excludeMap := Keyify(exclude)
|
||||
|
||||
result := make(Slice, 0, len(collection))
|
||||
for _, item := range collection {
|
||||
@@ -211,6 +282,24 @@ func WithoutBy[T any, K comparable, Slice ~[]T](collection Slice, iteratee func(
|
||||
return result
|
||||
}
|
||||
|
||||
// WithoutByErr filters a slice by excluding elements whose extracted keys match any in the exclude list.
|
||||
// It returns the first error returned by the iteratee.
|
||||
func WithoutByErr[T any, K comparable, Slice ~[]T](collection Slice, iteratee func(item T) (K, error), exclude ...K) (Slice, error) {
|
||||
excludeMap := Keyify(exclude)
|
||||
|
||||
result := make(Slice, 0, len(collection))
|
||||
for _, item := range collection {
|
||||
key, err := iteratee(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := excludeMap[key]; !ok {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// WithoutEmpty returns a slice excluding zero values.
|
||||
//
|
||||
// Deprecated: Use lo.Compact instead.
|
||||
@@ -220,15 +309,8 @@ func WithoutEmpty[T comparable, Slice ~[]T](collection Slice) Slice {
|
||||
|
||||
// WithoutNth returns a slice excluding the nth value.
|
||||
// Play: https://go.dev/play/p/5g3F9R2H1xL
|
||||
func WithoutNth[T comparable, Slice ~[]T](collection Slice, nths ...int) Slice {
|
||||
length := len(collection)
|
||||
|
||||
toRemove := make(map[int]struct{}, len(nths))
|
||||
for i := range nths {
|
||||
if nths[i] >= 0 && nths[i] <= length-1 {
|
||||
toRemove[nths[i]] = struct{}{}
|
||||
}
|
||||
}
|
||||
func WithoutNth[T any, Slice ~[]T](collection Slice, nths ...int) Slice {
|
||||
toRemove := Keyify(nths)
|
||||
|
||||
result := make(Slice, 0, len(collection))
|
||||
for i := range collection {
|
||||
|
||||
+167
-8
@@ -112,6 +112,22 @@ func PickBy[K comparable, V any, Map ~map[K]V](in Map, predicate func(key K, val
|
||||
return r
|
||||
}
|
||||
|
||||
// PickByErr returns same map type filtered by given predicate.
|
||||
// It returns the first error returned by the predicate.
|
||||
func PickByErr[K comparable, V any, Map ~map[K]V](in Map, predicate func(key K, value V) (bool, error)) (Map, error) {
|
||||
r := Map{}
|
||||
for k, v := range in {
|
||||
ok, err := predicate(k, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
r[k] = v
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// PickByKeys returns same map type filtered by given keys.
|
||||
// Play: https://go.dev/play/p/R1imbuci9qU
|
||||
func PickByKeys[K comparable, V any, Map ~map[K]V](in Map, keys []K) Map {
|
||||
@@ -128,8 +144,10 @@ func PickByKeys[K comparable, V any, Map ~map[K]V](in Map, keys []K) Map {
|
||||
// Play: https://go.dev/play/p/1zdzSvbfsJc
|
||||
func PickByValues[K, V comparable, Map ~map[K]V](in Map, values []V) Map {
|
||||
r := Map{}
|
||||
|
||||
seen := Keyify(values)
|
||||
for k, v := range in {
|
||||
if Contains(values, v) {
|
||||
if _, ok := seen[v]; ok {
|
||||
r[k] = v
|
||||
}
|
||||
}
|
||||
@@ -148,6 +166,22 @@ func OmitBy[K comparable, V any, Map ~map[K]V](in Map, predicate func(key K, val
|
||||
return r
|
||||
}
|
||||
|
||||
// OmitByErr returns same map type filtered by given predicate.
|
||||
// It returns the first error returned by the predicate.
|
||||
func OmitByErr[K comparable, V any, Map ~map[K]V](in Map, predicate func(key K, value V) (bool, error)) (Map, error) {
|
||||
r := Map{}
|
||||
for k, v := range in {
|
||||
ok, err := predicate(k, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
r[k] = v
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// OmitByKeys returns same map type filtered by given keys.
|
||||
// Play: https://go.dev/play/p/t1QjCrs-ysk
|
||||
func OmitByKeys[K comparable, V any, Map ~map[K]V](in Map, keys []K) Map {
|
||||
@@ -165,11 +199,14 @@ func OmitByKeys[K comparable, V any, Map ~map[K]V](in Map, keys []K) Map {
|
||||
// Play: https://go.dev/play/p/9UYZi-hrs8j
|
||||
func OmitByValues[K, V comparable, Map ~map[K]V](in Map, values []V) Map {
|
||||
r := Map{}
|
||||
|
||||
seen := Keyify(values)
|
||||
for k, v := range in {
|
||||
if !Contains(values, v) {
|
||||
if _, ok := seen[v]; !ok {
|
||||
r[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -259,12 +296,7 @@ func ChunkEntries[K comparable, V any](m map[K]V, size int) []map[K]V {
|
||||
return []map[K]V{}
|
||||
}
|
||||
|
||||
chunksNum := count / size
|
||||
if count%size != 0 {
|
||||
chunksNum++
|
||||
}
|
||||
|
||||
result := make([]map[K]V, 0, chunksNum)
|
||||
result := make([]map[K]V, 0, ((count-1)/size)+1)
|
||||
|
||||
for k, v := range m {
|
||||
if len(result) == 0 || len(result[len(result)-1]) == size {
|
||||
@@ -289,6 +321,22 @@ func MapKeys[K comparable, V any, R comparable](in map[K]V, iteratee func(value
|
||||
return result
|
||||
}
|
||||
|
||||
// MapKeysErr manipulates map keys and transforms it to a map of another type.
|
||||
// It returns the first error returned by the iteratee.
|
||||
func MapKeysErr[K comparable, V any, R comparable](in map[K]V, iteratee func(value V, key K) (R, error)) (map[R]V, error) {
|
||||
result := make(map[R]V, len(in))
|
||||
|
||||
for k, v := range in {
|
||||
r, err := iteratee(v, k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[r] = v
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// MapValues manipulates map values and transforms it to a map of another type.
|
||||
// Play: https://go.dev/play/p/T_8xAfvcf0W
|
||||
func MapValues[K comparable, V, R any](in map[K]V, iteratee func(value V, key K) R) map[K]R {
|
||||
@@ -301,6 +349,22 @@ func MapValues[K comparable, V, R any](in map[K]V, iteratee func(value V, key K)
|
||||
return result
|
||||
}
|
||||
|
||||
// MapValuesErr manipulates map values and transforms it to a map of another type.
|
||||
// It returns the first error returned by the iteratee.
|
||||
func MapValuesErr[K comparable, V, R any](in map[K]V, iteratee func(value V, key K) (R, error)) (map[K]R, error) {
|
||||
result := make(map[K]R, len(in))
|
||||
|
||||
for k, v := range in {
|
||||
r, err := iteratee(v, k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[k] = r
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// MapEntries manipulates map entries and transforms it to a map of another type.
|
||||
// Play: https://go.dev/play/p/VuvNQzxKimT
|
||||
func MapEntries[K1 comparable, V1 any, K2 comparable, V2 any](in map[K1]V1, iteratee func(key K1, value V1) (K2, V2)) map[K2]V2 {
|
||||
@@ -314,6 +378,22 @@ func MapEntries[K1 comparable, V1 any, K2 comparable, V2 any](in map[K1]V1, iter
|
||||
return result
|
||||
}
|
||||
|
||||
// MapEntriesErr manipulates map entries and transforms it to a map of another type.
|
||||
// It returns the first error returned by the iteratee.
|
||||
func MapEntriesErr[K1 comparable, V1 any, K2 comparable, V2 any](in map[K1]V1, iteratee func(key K1, value V1) (K2, V2, error)) (map[K2]V2, error) {
|
||||
result := make(map[K2]V2, len(in))
|
||||
|
||||
for k1 := range in {
|
||||
k2, v2, err := iteratee(k1, in[k1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[k2] = v2
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// MapToSlice transforms a map into a slice based on specified iteratee.
|
||||
// Play: https://go.dev/play/p/ZuiCZpDt6LD
|
||||
func MapToSlice[K comparable, V, R any](in map[K]V, iteratee func(key K, value V) R) []R {
|
||||
@@ -326,6 +406,22 @@ func MapToSlice[K comparable, V, R any](in map[K]V, iteratee func(key K, value V
|
||||
return result
|
||||
}
|
||||
|
||||
// MapToSliceErr transforms a map into a slice based on specified iteratee.
|
||||
// It returns the first error returned by the iteratee.
|
||||
func MapToSliceErr[K comparable, V, R any](in map[K]V, iteratee func(key K, value V) (R, error)) ([]R, error) {
|
||||
result := make([]R, 0, len(in))
|
||||
|
||||
for k, v := range in {
|
||||
r, err := iteratee(k, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, r)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// FilterMapToSlice transforms a map into a slice based on specified iteratee.
|
||||
// The iteratee returns a value and a boolean. If the boolean is true, the value is added to the result slice.
|
||||
// If the boolean is false, the value is not added to the result slice.
|
||||
@@ -343,6 +439,27 @@ func FilterMapToSlice[K comparable, V, R any](in map[K]V, iteratee func(key K, v
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterMapToSliceErr transforms a map into a slice based on specified iteratee.
|
||||
// The iteratee returns a value, a boolean, and an error. If the boolean is true, the value is added to the result slice.
|
||||
// If the boolean is false, the value is not added to the result slice.
|
||||
// If an error is returned, iteration stops immediately and returns the error.
|
||||
// The order of the keys in the input map is not specified and the order of the keys in the output slice is not guaranteed.
|
||||
func FilterMapToSliceErr[K comparable, V, R any](in map[K]V, iteratee func(key K, value V) (R, bool, error)) ([]R, error) {
|
||||
result := make([]R, 0, len(in))
|
||||
|
||||
for k, v := range in {
|
||||
r, ok, err := iteratee(k, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
result = append(result, r)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// FilterKeys transforms a map into a slice based on predicate returns true for specific elements.
|
||||
// It is a mix of lo.Filter() and lo.Keys().
|
||||
// Play: https://go.dev/play/p/OFlKXlPrBAe
|
||||
@@ -372,3 +489,45 @@ func FilterValues[K comparable, V any](in map[K]V, predicate func(key K, value V
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterKeysErr transforms a map into a slice of keys based on predicate that can return an error.
|
||||
// It is a mix of lo.Filter() and lo.Keys() with error handling.
|
||||
// If the predicate returns true, the key is added to the result slice.
|
||||
// If the predicate returns an error, iteration stops immediately and returns the error.
|
||||
// The order of the keys in the input map is not specified.
|
||||
func FilterKeysErr[K comparable, V any](in map[K]V, predicate func(key K, value V) (bool, error)) ([]K, error) {
|
||||
result := make([]K, 0)
|
||||
|
||||
for k, v := range in {
|
||||
ok, err := predicate(k, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
result = append(result, k)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// FilterValuesErr transforms a map into a slice of values based on predicate that can return an error.
|
||||
// It is a mix of lo.Filter() and lo.Values() with error handling.
|
||||
// If the predicate returns true, the value is added to the result slice.
|
||||
// If the predicate returns an error, iteration stops immediately and returns the error.
|
||||
// The order of the keys in the input map is not specified.
|
||||
func FilterValuesErr[K comparable, V any](in map[K]V, predicate func(key K, value V) (bool, error)) ([]V, error) {
|
||||
result := make([]V, 0)
|
||||
|
||||
for k, v := range in {
|
||||
ok, err := predicate(k, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
+65
-24
@@ -1,15 +1,17 @@
|
||||
package lo
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/samber/lo/internal/constraints"
|
||||
)
|
||||
|
||||
// Range creates a slice of numbers (positive and/or negative) with given length.
|
||||
// Play: https://go.dev/play/p/0r6VimXAi9H
|
||||
func Range(elementNum int) []int {
|
||||
length := If(elementNum < 0, -elementNum).Else(elementNum)
|
||||
step := Ternary(elementNum < 0, -1, 1)
|
||||
length := elementNum * step
|
||||
result := make([]int, length)
|
||||
step := If(elementNum < 0, -1).Else(1)
|
||||
for i, j := 0, 0; i < length; i, j = i+1, j+step {
|
||||
result[i] = j
|
||||
}
|
||||
@@ -19,9 +21,9 @@ func Range(elementNum int) []int {
|
||||
// RangeFrom creates a slice of numbers from start with specified length.
|
||||
// Play: https://go.dev/play/p/0r6VimXAi9H
|
||||
func RangeFrom[T constraints.Integer | constraints.Float](start T, elementNum int) []T {
|
||||
length := If(elementNum < 0, -elementNum).Else(elementNum)
|
||||
step := Ternary(elementNum < 0, -1, 1)
|
||||
length := elementNum * step
|
||||
result := make([]T, length)
|
||||
step := If(elementNum < 0, -1).Else(1)
|
||||
for i, j := 0, start; i < length; i, j = i+1, j+T(step) {
|
||||
result[i] = j
|
||||
}
|
||||
@@ -32,22 +34,32 @@ func RangeFrom[T constraints.Integer | constraints.Float](start T, elementNum in
|
||||
// step set to zero will return an empty slice.
|
||||
// Play: https://go.dev/play/p/0r6VimXAi9H
|
||||
func RangeWithSteps[T constraints.Integer | constraints.Float](start, end, step T) []T {
|
||||
result := []T{}
|
||||
if start == end || step == 0 {
|
||||
return result
|
||||
return []T{}
|
||||
}
|
||||
|
||||
capacity := func(count, delta T) int {
|
||||
// Use math.Ceil instead of (count-1)/delta+1 because integer division
|
||||
// fails for floats (e.g., 5.5/2.5=2.2 → ceil=3, not 2).
|
||||
return int(math.Ceil(float64(count) / float64(delta)))
|
||||
}
|
||||
|
||||
if start < end {
|
||||
if step < 0 {
|
||||
return result
|
||||
return []T{}
|
||||
}
|
||||
|
||||
result := make([]T, 0, capacity(end-start, step))
|
||||
for i := start; i < end; i += step {
|
||||
result = append(result, i)
|
||||
}
|
||||
return result
|
||||
}
|
||||
if step > 0 {
|
||||
return result
|
||||
return []T{}
|
||||
}
|
||||
|
||||
result := make([]T, 0, capacity(start-end, -step))
|
||||
for i := start; i > end; i += step {
|
||||
result = append(result, i)
|
||||
}
|
||||
@@ -85,17 +97,24 @@ func SumBy[T any, R constraints.Float | constraints.Integer | constraints.Comple
|
||||
return sum
|
||||
}
|
||||
|
||||
// SumByErr summarizes the values in a collection using the given return value from the iteration function.
|
||||
// If the iteratee returns an error, iteration stops and the error is returned.
|
||||
// If collection is empty 0 and nil error are returned.
|
||||
func SumByErr[T any, R constraints.Float | constraints.Integer | constraints.Complex](collection []T, iteratee func(item T) (R, error)) (R, error) {
|
||||
var sum R
|
||||
for i := range collection {
|
||||
v, err := iteratee(collection[i])
|
||||
if err != nil {
|
||||
return sum, err
|
||||
}
|
||||
sum += v
|
||||
}
|
||||
return sum, nil
|
||||
}
|
||||
|
||||
// Product gets the product of the values in a collection. If collection is empty 1 is returned.
|
||||
// Play: https://go.dev/play/p/2_kjM_smtAH
|
||||
func Product[T constraints.Float | constraints.Integer | constraints.Complex](collection []T) T {
|
||||
if collection == nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
if len(collection) == 0 {
|
||||
return 1
|
||||
}
|
||||
|
||||
var product T = 1
|
||||
for i := range collection {
|
||||
product *= collection[i]
|
||||
@@ -106,14 +125,6 @@ func Product[T constraints.Float | constraints.Integer | constraints.Complex](co
|
||||
// ProductBy summarizes the values in a collection using the given return value from the iteration function. If collection is empty 1 is returned.
|
||||
// Play: https://go.dev/play/p/wadzrWr9Aer
|
||||
func ProductBy[T any, R constraints.Float | constraints.Integer | constraints.Complex](collection []T, iteratee func(item T) R) R {
|
||||
if collection == nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
if len(collection) == 0 {
|
||||
return 1
|
||||
}
|
||||
|
||||
var product R = 1
|
||||
for i := range collection {
|
||||
product *= iteratee(collection[i])
|
||||
@@ -121,6 +132,21 @@ func ProductBy[T any, R constraints.Float | constraints.Integer | constraints.Co
|
||||
return product
|
||||
}
|
||||
|
||||
// ProductByErr summarizes the values in a collection using the given return value from the iteration function.
|
||||
// If the iteratee returns an error, iteration stops and the error is returned.
|
||||
// If collection is empty 1 and nil error are returned.
|
||||
func ProductByErr[T any, R constraints.Float | constraints.Integer | constraints.Complex](collection []T, iteratee func(item T) (R, error)) (R, error) {
|
||||
var product R = 1
|
||||
for i := range collection {
|
||||
v, err := iteratee(collection[i])
|
||||
if err != nil {
|
||||
return product, err
|
||||
}
|
||||
product *= v
|
||||
}
|
||||
return product, nil
|
||||
}
|
||||
|
||||
// Mean calculates the mean of a collection of numbers.
|
||||
// Play: https://go.dev/play/p/tPURSuteUsP
|
||||
func Mean[T constraints.Float | constraints.Integer](collection []T) T {
|
||||
@@ -143,6 +169,21 @@ func MeanBy[T any, R constraints.Float | constraints.Integer](collection []T, it
|
||||
return sum / length
|
||||
}
|
||||
|
||||
// MeanByErr calculates the mean of a collection of numbers using the given return value from the iteration function.
|
||||
// If the iteratee returns an error, iteration stops and the error is returned.
|
||||
// If collection is empty 0 and nil error are returned.
|
||||
func MeanByErr[T any, R constraints.Float | constraints.Integer](collection []T, iteratee func(item T) (R, error)) (R, error) {
|
||||
length := R(len(collection))
|
||||
if length == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
sum, err := SumByErr(collection, iteratee)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return sum / length, nil
|
||||
}
|
||||
|
||||
// Mode returns the mode (most frequent value) of a collection.
|
||||
// If multiple values have the same highest frequency, then multiple values are returned.
|
||||
// If the collection is empty, then the zero value of T is returned.
|
||||
|
||||
+10
-10
@@ -10,9 +10,9 @@ import "github.com/samber/lo/internal/xrand"
|
||||
// Play: https://go.dev/play/p/0jY3Z0B7O_5
|
||||
func Filter[T any, Slice ~[]T](collection Slice, predicate func(item T) bool) Slice {
|
||||
j := 0
|
||||
for _, item := range collection {
|
||||
if predicate(item) {
|
||||
collection[j] = item
|
||||
for i := range collection {
|
||||
if predicate(collection[i]) {
|
||||
collection[j] = collection[i]
|
||||
j++
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,9 @@ func Filter[T any, Slice ~[]T](collection Slice, predicate func(item T) bool) Sl
|
||||
// Note that the order of elements in the original slice is preserved in the output.
|
||||
func FilterI[T any, Slice ~[]T](collection Slice, predicate func(item T, index int) bool) Slice {
|
||||
j := 0
|
||||
for i, item := range collection {
|
||||
if predicate(item, i) {
|
||||
collection[j] = item
|
||||
for i := range collection {
|
||||
if predicate(collection[i], i) {
|
||||
collection[j] = collection[i]
|
||||
j++
|
||||
}
|
||||
}
|
||||
@@ -38,17 +38,17 @@ func FilterI[T any, Slice ~[]T](collection Slice, predicate func(item T, index i
|
||||
// Map is a generic function that modifies the input slice in-place to contain the result of applying the provided
|
||||
// function to each element of the slice. The function returns the modified slice, which has the same length as the original.
|
||||
// Play: https://go.dev/play/p/0jY3Z0B7O_5
|
||||
func Map[T any, Slice ~[]T](collection Slice, fn func(item T) T) {
|
||||
func Map[T any, Slice ~[]T](collection Slice, transform func(item T) T) {
|
||||
for i := range collection {
|
||||
collection[i] = fn(collection[i])
|
||||
collection[i] = transform(collection[i])
|
||||
}
|
||||
}
|
||||
|
||||
// MapI is a generic function that modifies the input slice in-place to contain the result of applying the provided
|
||||
// function to each element of the slice. The function returns the modified slice, which has the same length as the original.
|
||||
func MapI[T any, Slice ~[]T](collection Slice, fn func(item T, index int) T) {
|
||||
func MapI[T any, Slice ~[]T](collection Slice, transform func(item T, index int) T) {
|
||||
for i := range collection {
|
||||
collection[i] = fn(collection[i], i)
|
||||
collection[i] = transform(collection[i], i)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-6
@@ -315,10 +315,6 @@ func (th *throttleBy[T]) throttledFunc(key T) {
|
||||
th.mu.Lock()
|
||||
defer th.mu.Unlock()
|
||||
|
||||
if _, ok := th.count[key]; !ok {
|
||||
th.count[key] = 0
|
||||
}
|
||||
|
||||
if th.count[key] < th.countLimit {
|
||||
th.count[key]++
|
||||
|
||||
@@ -361,7 +357,7 @@ func NewThrottleWithCount(interval time.Duration, count int, f ...func()) (throt
|
||||
}
|
||||
})
|
||||
|
||||
throttleFn, reset := NewThrottleByWithCount[struct{}](interval, count, callbacks...)
|
||||
throttleFn, reset := NewThrottleByWithCount(interval, count, callbacks...)
|
||||
return func() {
|
||||
throttleFn(struct{}{})
|
||||
}, reset
|
||||
@@ -371,7 +367,7 @@ func NewThrottleWithCount(interval time.Duration, count int, f ...func()) (throt
|
||||
// This returns 2 functions, First one is throttled function and Second one is a function to reset interval.
|
||||
// Play: https://go.dev/play/p/0Wv6oX7dHdC
|
||||
func NewThrottleBy[T comparable](interval time.Duration, f ...func(key T)) (throttle func(key T), reset func()) {
|
||||
return NewThrottleByWithCount[T](interval, 1, f...)
|
||||
return NewThrottleByWithCount(interval, 1, f...)
|
||||
}
|
||||
|
||||
// NewThrottleByWithCount is NewThrottleBy with count limit, throttled function will be invoked count times in every interval.
|
||||
|
||||
+444
-92
@@ -21,32 +21,66 @@ func Filter[T any, Slice ~[]T](collection Slice, predicate func(item T, index in
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterErr iterates over elements of collection, returning a slice of all elements predicate returns true for.
|
||||
// If the predicate returns an error, iteration stops immediately and returns the error.
|
||||
// Play: https://go.dev/play/p/Apjg3WeSi7K
|
||||
func FilterErr[T any, Slice ~[]T](collection Slice, predicate func(item T, index int) (bool, error)) (Slice, error) {
|
||||
result := make(Slice, 0, len(collection))
|
||||
|
||||
for i := range collection {
|
||||
ok, err := predicate(collection[i], i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
result = append(result, collection[i])
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Map manipulates a slice and transforms it to a slice of another type.
|
||||
// Play: https://go.dev/play/p/OkPcYAhBo0D
|
||||
func Map[T, R any](collection []T, iteratee func(item T, index int) R) []R {
|
||||
func Map[T, R any](collection []T, transform func(item T, index int) R) []R {
|
||||
result := make([]R, len(collection))
|
||||
|
||||
for i := range collection {
|
||||
result[i] = iteratee(collection[i], i)
|
||||
result[i] = transform(collection[i], i)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// MapErr manipulates a slice and transforms it to a slice of another type.
|
||||
// It returns the first error returned by the transform function.
|
||||
func MapErr[T, R any](collection []T, transform func(item T, index int) (R, error)) ([]R, error) {
|
||||
result := make([]R, len(collection))
|
||||
|
||||
for i := range collection {
|
||||
r, err := transform(collection[i], i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[i] = r
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// UniqMap manipulates a slice and transforms it to a slice of another type with unique values.
|
||||
// Play: https://go.dev/play/p/fygzLBhvUdB
|
||||
func UniqMap[T any, R comparable](collection []T, iteratee func(item T, index int) R) []R {
|
||||
result := make([]R, 0, len(collection))
|
||||
func UniqMap[T any, R comparable](collection []T, transform func(item T, index int) R) []R {
|
||||
seen := make(map[R]struct{}, len(collection))
|
||||
|
||||
for i := range collection {
|
||||
r := iteratee(collection[i], i)
|
||||
r := transform(collection[i], i)
|
||||
if _, ok := seen[r]; !ok {
|
||||
result = append(result, r)
|
||||
seen[r] = struct{}{}
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
return Keys(seen)
|
||||
}
|
||||
|
||||
// FilterMap returns a slice obtained after both filtering and mapping using the given callback function.
|
||||
@@ -71,16 +105,34 @@ func FilterMap[T, R any](collection []T, callback func(item T, index int) (R, bo
|
||||
// The transform function can either return a slice or a `nil`, and in the `nil` case
|
||||
// no value is added to the final slice.
|
||||
// Play: https://go.dev/play/p/pFCF5WVB225
|
||||
func FlatMap[T, R any](collection []T, iteratee func(item T, index int) []R) []R {
|
||||
func FlatMap[T, R any](collection []T, transform func(item T, index int) []R) []R {
|
||||
result := make([]R, 0, len(collection))
|
||||
|
||||
for i := range collection {
|
||||
result = append(result, iteratee(collection[i], i)...)
|
||||
result = append(result, transform(collection[i], i)...)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FlatMapErr manipulates a slice and transforms and flattens it to a slice of another type.
|
||||
// The transform function can either return a slice or a `nil`, and in the `nil` case
|
||||
// no value is added to the final slice.
|
||||
// It returns the first error returned by the transform function.
|
||||
func FlatMapErr[T, R any](collection []T, transform func(item T, index int) ([]R, error)) ([]R, error) {
|
||||
result := make([]R, 0, len(collection))
|
||||
|
||||
for i := range collection {
|
||||
r, err := transform(collection[i], i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, r...)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Reduce reduces collection to a value which is the accumulated result of running each element in collection
|
||||
// through accumulator, where each successive invocation is supplied the return value of the previous.
|
||||
// Play: https://go.dev/play/p/CgHYNUpOd1I
|
||||
@@ -92,6 +144,22 @@ func Reduce[T, R any](collection []T, accumulator func(agg R, item T, index int)
|
||||
return initial
|
||||
}
|
||||
|
||||
// ReduceErr reduces collection to a value which is the accumulated result of running each element in collection
|
||||
// through accumulator, where each successive invocation is supplied the return value of the previous.
|
||||
// It returns the first error returned by the accumulator function.
|
||||
func ReduceErr[T, R any](collection []T, accumulator func(agg R, item T, index int) (R, error), initial R) (R, error) {
|
||||
for i := range collection {
|
||||
result, err := accumulator(initial, collection[i], i)
|
||||
if err != nil {
|
||||
var zero R
|
||||
return zero, err
|
||||
}
|
||||
initial = result
|
||||
}
|
||||
|
||||
return initial, nil
|
||||
}
|
||||
|
||||
// ReduceRight is like Reduce except that it iterates over elements of collection from right to left.
|
||||
// Play: https://go.dev/play/p/Fq3W70l7wXF
|
||||
func ReduceRight[T, R any](collection []T, accumulator func(agg R, item T, index int) R, initial R) R {
|
||||
@@ -102,20 +170,35 @@ func ReduceRight[T, R any](collection []T, accumulator func(agg R, item T, index
|
||||
return initial
|
||||
}
|
||||
|
||||
// ForEach iterates over elements of collection and invokes iteratee for each element.
|
||||
// ReduceRightErr is like ReduceRight except that the accumulator function can return an error.
|
||||
// It returns the first error returned by the accumulator function.
|
||||
func ReduceRightErr[T, R any](collection []T, accumulator func(agg R, item T, index int) (R, error), initial R) (R, error) {
|
||||
for i := len(collection) - 1; i >= 0; i-- {
|
||||
result, err := accumulator(initial, collection[i], i)
|
||||
if err != nil {
|
||||
var zero R
|
||||
return zero, err
|
||||
}
|
||||
initial = result
|
||||
}
|
||||
|
||||
return initial, nil
|
||||
}
|
||||
|
||||
// ForEach iterates over elements of collection and invokes callback for each element.
|
||||
// Play: https://go.dev/play/p/oofyiUPRf8t
|
||||
func ForEach[T any](collection []T, iteratee func(item T, index int)) {
|
||||
func ForEach[T any](collection []T, callback func(item T, index int)) {
|
||||
for i := range collection {
|
||||
iteratee(collection[i], i)
|
||||
callback(collection[i], i)
|
||||
}
|
||||
}
|
||||
|
||||
// ForEachWhile iterates over elements of collection and invokes iteratee for each element
|
||||
// ForEachWhile iterates over elements of collection and invokes predicate for each element
|
||||
// collection return value decide to continue or break, like do while().
|
||||
// Play: https://go.dev/play/p/QnLGt35tnow
|
||||
func ForEachWhile[T any](collection []T, iteratee func(item T, index int) bool) {
|
||||
func ForEachWhile[T any](collection []T, predicate func(item T, index int) bool) {
|
||||
for i := range collection {
|
||||
if !iteratee(collection[i], i) {
|
||||
if !predicate(collection[i], i) {
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -175,6 +258,31 @@ func UniqBy[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(ite
|
||||
return result
|
||||
}
|
||||
|
||||
// UniqByErr returns a duplicate-free version of a slice, in which only the first occurrence of each element is kept.
|
||||
// The order of result values is determined by the order they occur in the slice. It accepts `iteratee` which is
|
||||
// invoked for each element in the slice to generate the criterion by which uniqueness is computed.
|
||||
// It returns the first error returned by the iteratee function.
|
||||
func UniqByErr[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(item T) (U, error)) (Slice, error) {
|
||||
result := make(Slice, 0, len(collection))
|
||||
seen := make(map[U]struct{}, len(collection))
|
||||
|
||||
for i := range collection {
|
||||
key, err := iteratee(collection[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
seen[key] = struct{}{}
|
||||
result = append(result, collection[i])
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GroupBy returns an object composed of keys generated from the results of running each element of collection through iteratee.
|
||||
// Play: https://go.dev/play/p/XnQBd_v6brd
|
||||
func GroupBy[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(item T) U) map[U]Slice {
|
||||
@@ -189,13 +297,30 @@ func GroupBy[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(it
|
||||
return result
|
||||
}
|
||||
|
||||
// GroupByMap returns an object composed of keys generated from the results of running each element of collection through iteratee.
|
||||
// GroupByErr returns an object composed of keys generated from the results of running each element of collection through iteratee.
|
||||
// It returns the first error returned by the iteratee function.
|
||||
func GroupByErr[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(item T) (U, error)) (map[U]Slice, error) {
|
||||
result := map[U]Slice{}
|
||||
|
||||
for i := range collection {
|
||||
key, err := iteratee(collection[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result[key] = append(result[key], collection[i])
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GroupByMap returns an object composed of keys generated from the results of running each element of collection through transform.
|
||||
// Play: https://go.dev/play/p/iMeruQ3_W80
|
||||
func GroupByMap[T any, K comparable, V any](collection []T, iteratee func(item T) (K, V)) map[K][]V {
|
||||
func GroupByMap[T any, K comparable, V any](collection []T, transform func(item T) (K, V)) map[K][]V {
|
||||
result := map[K][]V{}
|
||||
|
||||
for i := range collection {
|
||||
k, v := iteratee(collection[i])
|
||||
k, v := transform(collection[i])
|
||||
|
||||
result[k] = append(result[k], v)
|
||||
}
|
||||
@@ -203,6 +328,23 @@ func GroupByMap[T any, K comparable, V any](collection []T, iteratee func(item T
|
||||
return result
|
||||
}
|
||||
|
||||
// GroupByMapErr returns an object composed of keys generated from the results of running each element of collection through transform.
|
||||
// It returns the first error returned by the transform function.
|
||||
func GroupByMapErr[T any, K comparable, V any](collection []T, transform func(item T) (K, V, error)) (map[K][]V, error) {
|
||||
result := map[K][]V{}
|
||||
|
||||
for i := range collection {
|
||||
k, v, err := transform(collection[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result[k] = append(result[k], v)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Chunk returns a slice of elements split into groups of length size. If the slice can't be split evenly,
|
||||
// the final chunk will be the remaining elements.
|
||||
// Play: https://go.dev/play/p/kEMkFbdu85g
|
||||
@@ -245,13 +387,12 @@ func PartitionBy[T any, K comparable, Slice ~[]T](collection Slice, iteratee fun
|
||||
key := iteratee(collection[i])
|
||||
|
||||
resultIndex, ok := seen[key]
|
||||
if !ok {
|
||||
resultIndex = len(result)
|
||||
seen[key] = resultIndex
|
||||
result = append(result, Slice{})
|
||||
if ok {
|
||||
result[resultIndex] = append(result[resultIndex], collection[i])
|
||||
} else {
|
||||
seen[key] = len(result)
|
||||
result = append(result, Slice{collection[i]})
|
||||
}
|
||||
|
||||
result[resultIndex] = append(result[resultIndex], collection[i])
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -261,7 +402,34 @@ func PartitionBy[T any, K comparable, Slice ~[]T](collection Slice, iteratee fun
|
||||
// return Values[K, []T](groups)
|
||||
}
|
||||
|
||||
// PartitionByErr partitions a slice into groups determined by a key computed from each element.
|
||||
// The order of the partitions is determined by the order they occur in collection. The grouping
|
||||
// is generated from the results of running each element of collection through iteratee.
|
||||
// It returns the first error returned by the iteratee function.
|
||||
func PartitionByErr[T any, K comparable, Slice ~[]T](collection Slice, iteratee func(item T) (K, error)) ([]Slice, error) {
|
||||
result := []Slice{}
|
||||
seen := map[K]int{}
|
||||
|
||||
for i := range collection {
|
||||
key, err := iteratee(collection[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resultIndex, ok := seen[key]
|
||||
if ok {
|
||||
result[resultIndex] = append(result[resultIndex], collection[i])
|
||||
} else {
|
||||
seen[key] = len(result)
|
||||
result = append(result, Slice{collection[i]})
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Flatten returns a slice a single level deep.
|
||||
// See also: Concat
|
||||
// Play: https://go.dev/play/p/rbp9ORaMpjw
|
||||
func Flatten[T any, Slice ~[]T](collection []Slice) Slice {
|
||||
totalLen := 0
|
||||
@@ -277,6 +445,50 @@ func Flatten[T any, Slice ~[]T](collection []Slice) Slice {
|
||||
return result
|
||||
}
|
||||
|
||||
// Concat returns a new slice containing all the elements in collections. Concat conserves the order of the elements.
|
||||
// See also: Flatten, Union.
|
||||
func Concat[T any, Slice ~[]T](collections ...Slice) Slice {
|
||||
return Flatten(collections)
|
||||
}
|
||||
|
||||
// Window creates a slice of sliding windows of a given size.
|
||||
// Each window overlaps with the previous one by size-1 elements.
|
||||
// This is equivalent to Sliding(collection, size, 1).
|
||||
func Window[T any, Slice ~[]T](collection Slice, size int) []Slice {
|
||||
if size <= 0 {
|
||||
panic("lo.Window: size must be greater than 0")
|
||||
}
|
||||
return Sliding(collection, size, 1)
|
||||
}
|
||||
|
||||
// Sliding creates a slice of sliding windows of a given size with a given step.
|
||||
// If step is equal to size, windows don't overlap (similar to Chunk).
|
||||
// If step is less than size, windows overlap.
|
||||
func Sliding[T any, Slice ~[]T](collection Slice, size, step int) []Slice {
|
||||
if size <= 0 {
|
||||
panic("lo.Sliding: size must be greater than 0")
|
||||
}
|
||||
|
||||
if step <= 0 {
|
||||
panic("lo.Sliding: step must be greater than 0")
|
||||
}
|
||||
|
||||
n := len(collection) - size
|
||||
if n < 0 {
|
||||
return []Slice{}
|
||||
}
|
||||
|
||||
result := make([]Slice, 0, n/step+1)
|
||||
|
||||
for i := 0; i <= n; i += step {
|
||||
window := make(Slice, size)
|
||||
copy(window, collection[i:i+size])
|
||||
result = append(result, window)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Interleave round-robin alternating input slices and sequentially appending value at index into result.
|
||||
// Play: https://go.dev/play/p/-RJkTLQEDVt
|
||||
func Interleave[T any, Slice ~[]T](collections ...Slice) Slice {
|
||||
@@ -359,16 +571,32 @@ func Repeat[T Clonable[T]](count int, initial T) []T {
|
||||
|
||||
// RepeatBy builds a slice with values returned by N calls of callback.
|
||||
// Play: https://go.dev/play/p/ozZLCtX_hNU
|
||||
func RepeatBy[T any](count int, predicate func(index int) T) []T {
|
||||
func RepeatBy[T any](count int, callback func(index int) T) []T {
|
||||
result := make([]T, 0, count)
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
result = append(result, predicate(i))
|
||||
result = append(result, callback(i))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// RepeatByErr builds a slice with values returned by N calls of callback.
|
||||
// It returns the first error returned by the callback function.
|
||||
func RepeatByErr[T any](count int, callback func(index int) (T, error)) ([]T, error) {
|
||||
result := make([]T, 0, count)
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
r, err := callback(i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, r)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// KeyBy transforms a slice or a slice of structs to a map based on a pivot callback.
|
||||
// Play: https://go.dev/play/p/ccUiUL_Lnel
|
||||
func KeyBy[K comparable, V any](collection []V, iteratee func(item V) K) map[K]V {
|
||||
@@ -382,6 +610,23 @@ func KeyBy[K comparable, V any](collection []V, iteratee func(item V) K) map[K]V
|
||||
return result
|
||||
}
|
||||
|
||||
// KeyByErr transforms a slice or a slice of structs to a map based on a pivot callback to compute keys.
|
||||
// Iteratee can return an error to stop iteration immediately.
|
||||
// Play: https://go.dev/play/p/ccUiUL_Lnel
|
||||
func KeyByErr[K comparable, V any](collection []V, iteratee func(item V) (K, error)) (map[K]V, error) {
|
||||
result := make(map[K]V, len(collection))
|
||||
|
||||
for i := range collection {
|
||||
k, err := iteratee(collection[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[k] = collection[i]
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Associate returns a map containing key-value pairs provided by transform function applied to elements of the given slice.
|
||||
// If any of two pairs have the same key the last one gets added to the map.
|
||||
// The order of keys in returned map is not specified and is not guaranteed to be the same from the original slice.
|
||||
@@ -525,35 +770,110 @@ func DropRightWhile[T any, Slice ~[]T](collection Slice, predicate func(item T)
|
||||
return append(result, collection[:i+1]...)
|
||||
}
|
||||
|
||||
// Take takes the first n elements from a slice.
|
||||
func Take[T any, Slice ~[]T](collection Slice, n int) Slice {
|
||||
if n < 0 {
|
||||
panic("lo.Take: n must not be negative")
|
||||
}
|
||||
|
||||
if n == 0 {
|
||||
return make(Slice, 0)
|
||||
}
|
||||
|
||||
size := len(collection)
|
||||
if size == 0 {
|
||||
return make(Slice, 0)
|
||||
}
|
||||
|
||||
if n >= size {
|
||||
result := make(Slice, size)
|
||||
copy(result, collection)
|
||||
return result
|
||||
}
|
||||
|
||||
result := make(Slice, n)
|
||||
copy(result, collection)
|
||||
return result
|
||||
}
|
||||
|
||||
// TakeWhile takes elements from the beginning of a slice while the predicate returns true.
|
||||
func TakeWhile[T any, Slice ~[]T](collection Slice, predicate func(item T) bool) Slice {
|
||||
i := 0
|
||||
for ; i < len(collection); i++ {
|
||||
if !predicate(collection[i]) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
result := make(Slice, i)
|
||||
copy(result, collection[:i])
|
||||
return result
|
||||
}
|
||||
|
||||
// DropByIndex drops elements from a slice by the index.
|
||||
// A negative index will drop elements from the end of the slice.
|
||||
// Play: https://go.dev/play/p/bPIH4npZRxS
|
||||
func DropByIndex[T any, Slice ~[]T](collection Slice, indexes ...int) Slice {
|
||||
initialSize := len(collection)
|
||||
if initialSize == 0 {
|
||||
return Slice{}
|
||||
}
|
||||
|
||||
// do not change the input
|
||||
indexes = append(make([]int, 0, len(indexes)), indexes...)
|
||||
|
||||
for i, index := range indexes {
|
||||
if index < 0 {
|
||||
indexes[i] += initialSize
|
||||
}
|
||||
}
|
||||
|
||||
sort.Ints(indexes)
|
||||
|
||||
prev := -1
|
||||
indexes = mutable.Filter(indexes, func(index int) bool {
|
||||
ok := index != prev && // uniq
|
||||
uint(index) < uint(initialSize) // in range
|
||||
|
||||
prev = index
|
||||
return ok
|
||||
})
|
||||
|
||||
result := make(Slice, 0, initialSize-len(indexes))
|
||||
|
||||
i := 0
|
||||
for _, index := range indexes {
|
||||
result = append(result, collection[i:index]...)
|
||||
i = index + 1
|
||||
}
|
||||
|
||||
return append(result, collection[i:]...)
|
||||
}
|
||||
|
||||
// TakeFilter filters elements and takes the first n elements that match the predicate.
|
||||
// Equivalent to calling Take(Filter(...)), but more efficient as it stops after finding n matches.
|
||||
func TakeFilter[T any, Slice ~[]T](collection Slice, n int, predicate func(item T, index int) bool) Slice {
|
||||
if n < 0 {
|
||||
panic("lo.TakeFilter: n must not be negative")
|
||||
}
|
||||
|
||||
if n == 0 {
|
||||
return make(Slice, 0)
|
||||
}
|
||||
|
||||
for i := range indexes {
|
||||
if indexes[i] < 0 {
|
||||
indexes[i] = initialSize + indexes[i]
|
||||
result := make(Slice, 0, n)
|
||||
count := 0
|
||||
|
||||
for i := range collection {
|
||||
if predicate(collection[i], i) {
|
||||
result = append(result, collection[i])
|
||||
count++
|
||||
if count >= n {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
indexes = Uniq(indexes)
|
||||
sort.Ints(indexes)
|
||||
|
||||
result := make(Slice, 0, initialSize)
|
||||
result = append(result, collection...)
|
||||
|
||||
for i := range indexes {
|
||||
if indexes[i]-i < 0 || indexes[i]-i >= initialSize-i {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result[:indexes[i]-i], result[indexes[i]-i+1:]...)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -571,6 +891,25 @@ func Reject[T any, Slice ~[]T](collection Slice, predicate func(item T, index in
|
||||
return result
|
||||
}
|
||||
|
||||
// RejectErr is the opposite of FilterErr, this method returns the elements of collection that predicate does not return true for.
|
||||
// If the predicate returns an error, iteration stops immediately and returns the error.
|
||||
// Play: https://go.dev/play/p/pFCF5WVB225
|
||||
func RejectErr[T any, Slice ~[]T](collection Slice, predicate func(item T, index int) (bool, error)) (Slice, error) {
|
||||
result := Slice{}
|
||||
|
||||
for i := range collection {
|
||||
match, err := predicate(collection[i], i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !match {
|
||||
result = append(result, collection[i])
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// RejectMap is the opposite of FilterMap, this method returns a slice obtained after both filtering and mapping using the given callback function.
|
||||
// The callback function should return two values:
|
||||
// - the result of the mapping operation and
|
||||
@@ -635,6 +974,24 @@ func CountBy[T any](collection []T, predicate func(item T) bool) int {
|
||||
return count
|
||||
}
|
||||
|
||||
// CountByErr counts the number of elements in the collection for which predicate is true.
|
||||
// It returns the first error returned by the predicate.
|
||||
func CountByErr[T any](collection []T, predicate func(item T) (bool, error)) (int, error) {
|
||||
var count int
|
||||
|
||||
for i := range collection {
|
||||
ok, err := predicate(collection[i])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if ok {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CountValues counts the number of each element in the collection.
|
||||
// Play: https://go.dev/play/p/-p-PyLT4dfy
|
||||
func CountValues[T comparable](collection []T) map[T]int {
|
||||
@@ -647,14 +1004,14 @@ func CountValues[T comparable](collection []T) map[T]int {
|
||||
return result
|
||||
}
|
||||
|
||||
// CountValuesBy counts the number of each element returned from mapper function.
|
||||
// CountValuesBy counts the number of each element returned from transform function.
|
||||
// Is equivalent to chaining lo.Map and lo.CountValues.
|
||||
// Play: https://go.dev/play/p/2U0dG1SnOmS
|
||||
func CountValuesBy[T any, U comparable](collection []T, mapper func(item T) U) map[U]int {
|
||||
func CountValuesBy[T any, U comparable](collection []T, transform func(item T) U) map[U]int {
|
||||
result := make(map[U]int)
|
||||
|
||||
for i := range collection {
|
||||
result[mapper(collection[i])]++
|
||||
result[transform(collection[i])]++
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -683,27 +1040,24 @@ func Subset[T any, Slice ~[]T](collection Slice, offset int, length uint) Slice
|
||||
return collection[offset : offset+int(length)]
|
||||
}
|
||||
|
||||
// Slice returns a copy of a slice from `start` up to, but not including `end`. Like `slice[start:end]`, but does not panic on overflow.
|
||||
// Slice returns a slice from `start` up to, but not including `end`. Like `slice[start:end]`, but does not panic on overflow.
|
||||
// Play: https://go.dev/play/p/8XWYhfMMA1h
|
||||
func Slice[T any, Slice ~[]T](collection Slice, start, end int) Slice {
|
||||
size := len(collection)
|
||||
|
||||
if start >= end {
|
||||
return Slice{}
|
||||
}
|
||||
|
||||
if start > size {
|
||||
start = size
|
||||
}
|
||||
size := len(collection)
|
||||
if start < 0 {
|
||||
start = 0
|
||||
} else if start > size {
|
||||
start = size
|
||||
}
|
||||
|
||||
if end > size {
|
||||
end = size
|
||||
}
|
||||
if end < 0 {
|
||||
end = 0
|
||||
} else if end > size {
|
||||
end = size
|
||||
}
|
||||
|
||||
return collection[start:end]
|
||||
@@ -731,6 +1085,20 @@ func ReplaceAll[T comparable, Slice ~[]T](collection Slice, old, nEw T) Slice {
|
||||
return Replace(collection, old, nEw, -1)
|
||||
}
|
||||
|
||||
// Clone returns a shallow copy of the collection.
|
||||
func Clone[T any, Slice ~[]T](collection Slice) Slice {
|
||||
// backporting from slices.Clone in Go 1.21
|
||||
// when we drop support for Go 1.20, this can be replaced with: return slices.Clone(collection)
|
||||
|
||||
// Preserve nilness in case it matters.
|
||||
if collection == nil {
|
||||
return nil
|
||||
}
|
||||
// Avoid s[:0:0] as it leads to unwanted liveness when cloning a
|
||||
// zero-length slice of a large array; see https://go.dev/issue/68488.
|
||||
return append(Slice{}, collection...)
|
||||
}
|
||||
|
||||
// Compact returns a slice of all non-zero elements.
|
||||
// Play: https://go.dev/play/p/tXiy-iK6PAc
|
||||
func Compact[T comparable, Slice ~[]T](collection Slice) Slice {
|
||||
@@ -759,8 +1127,8 @@ func IsSorted[T constraints.Ordered](collection []T) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsSortedByKey checks if a slice is sorted by iteratee.
|
||||
func IsSortedByKey[T any, K constraints.Ordered](collection []T, iteratee func(item T) K) bool {
|
||||
// IsSortedBy checks if a slice is sorted by iteratee.
|
||||
func IsSortedBy[T any, K constraints.Ordered](collection []T, iteratee func(item T) K) bool {
|
||||
size := len(collection)
|
||||
|
||||
for i := 0; i < size-1; i++ {
|
||||
@@ -772,6 +1140,13 @@ func IsSortedByKey[T any, K constraints.Ordered](collection []T, iteratee func(i
|
||||
return true
|
||||
}
|
||||
|
||||
// IsSortedByKey checks if a slice is sorted by iteratee.
|
||||
//
|
||||
// Deprecated: Use lo.IsSortedBy instead.
|
||||
func IsSortedByKey[T any, K constraints.Ordered](collection []T, iteratee func(item T) K) bool {
|
||||
return IsSortedBy(collection, iteratee)
|
||||
}
|
||||
|
||||
// Splice inserts multiple elements at index i. A negative index counts back
|
||||
// from the end of the slice. The helper is protected against overflow errors.
|
||||
// Play: https://go.dev/play/p/G5_GhkeSUBA
|
||||
@@ -828,20 +1203,10 @@ func Cut[T comparable, Slice ~[]T](collection, separator Slice) (before, after S
|
||||
// If prefix is the empty []T, CutPrefix returns collection, true.
|
||||
// Play: https://go.dev/play/p/7Plak4a1ICl
|
||||
func CutPrefix[T comparable, Slice ~[]T](collection, separator Slice) (after Slice, found bool) {
|
||||
if len(separator) == 0 {
|
||||
return collection, true
|
||||
if HasPrefix(collection, separator) {
|
||||
return collection[len(separator):], true
|
||||
}
|
||||
if len(separator) > len(collection) {
|
||||
return collection, false
|
||||
}
|
||||
|
||||
for i := range separator {
|
||||
if collection[i] != separator[i] {
|
||||
return collection, false
|
||||
}
|
||||
}
|
||||
|
||||
return collection[len(separator):], true
|
||||
return collection, false
|
||||
}
|
||||
|
||||
// CutSuffix returns collection without the provided ending suffix []T and reports
|
||||
@@ -849,21 +1214,10 @@ func CutPrefix[T comparable, Slice ~[]T](collection, separator Slice) (after Sli
|
||||
// If suffix is the empty []T, CutSuffix returns collection, true.
|
||||
// Play: https://go.dev/play/p/7FKfBFvPTaT
|
||||
func CutSuffix[T comparable, Slice ~[]T](collection, separator Slice) (before Slice, found bool) {
|
||||
if len(separator) == 0 {
|
||||
return collection, true
|
||||
if HasSuffix(collection, separator) {
|
||||
return collection[:len(collection)-len(separator)], true
|
||||
}
|
||||
if len(separator) > len(collection) {
|
||||
return collection, false
|
||||
}
|
||||
|
||||
start := len(collection) - len(separator)
|
||||
for i := range separator {
|
||||
if collection[start+i] != separator[i] {
|
||||
return collection, false
|
||||
}
|
||||
}
|
||||
|
||||
return collection[:start], true
|
||||
return collection, false
|
||||
}
|
||||
|
||||
// Trim removes all the leading and trailing cutset from the collection.
|
||||
@@ -911,12 +1265,11 @@ func TrimPrefix[T comparable, Slice ~[]T](collection, prefix Slice) Slice {
|
||||
return collection
|
||||
}
|
||||
|
||||
for {
|
||||
if !HasPrefix(collection, prefix) {
|
||||
return collection
|
||||
}
|
||||
for HasPrefix(collection, prefix) {
|
||||
collection = collection[len(prefix):]
|
||||
}
|
||||
|
||||
return collection
|
||||
}
|
||||
|
||||
// TrimRight removes all the trailing cutset from the collection.
|
||||
@@ -937,10 +1290,9 @@ func TrimSuffix[T comparable, Slice ~[]T](collection, suffix Slice) Slice {
|
||||
return collection
|
||||
}
|
||||
|
||||
for {
|
||||
if !HasSuffix(collection, suffix) {
|
||||
return collection
|
||||
}
|
||||
for HasSuffix(collection, suffix) {
|
||||
collection = collection[:len(collection)-len(suffix)]
|
||||
}
|
||||
|
||||
return collection
|
||||
}
|
||||
|
||||
+101
-36
@@ -7,10 +7,10 @@ import (
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/samber/lo/internal/xrand"
|
||||
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/samber/lo/internal/xrand"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -41,7 +41,7 @@ func RandomString(size int, charset []rune) string {
|
||||
}
|
||||
|
||||
// see https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go
|
||||
sb := strings.Builder{}
|
||||
var sb strings.Builder
|
||||
sb.Grow(size)
|
||||
|
||||
if len(charset) == 1 {
|
||||
@@ -100,42 +100,107 @@ func nearestPowerOfTwo(capacity int) int {
|
||||
return n + 1
|
||||
}
|
||||
|
||||
// Substring return part of a string.
|
||||
// Substring extracts a substring from a string with Unicode character (rune) awareness.
|
||||
// offset - starting position of the substring (can be positive, negative, or zero)
|
||||
// length - number of characters to extract
|
||||
// With positive offset, counting starts from the beginning of the string
|
||||
// With negative offset, counting starts from the end of the string
|
||||
// Play: https://go.dev/play/p/TQlxQi82Lu1
|
||||
func Substring[T ~string](str T, offset int, length uint) T {
|
||||
rs := []rune(str)
|
||||
size := len(rs)
|
||||
str = substring(str, offset, length)
|
||||
|
||||
if offset < 0 {
|
||||
offset = size + offset
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
// Validate UTF-8 and fix invalid sequences
|
||||
if !utf8.ValidString(string(str)) {
|
||||
// Convert to []rune to replicate behavior with duplicated �
|
||||
str = T([]rune(str))
|
||||
}
|
||||
|
||||
// Remove null bytes from result
|
||||
return T(strings.ReplaceAll(string(str), "\x00", ""))
|
||||
}
|
||||
|
||||
func substring[T ~string](str T, offset int, length uint) T {
|
||||
switch {
|
||||
// Empty length or offset beyond string bounds - return empty string
|
||||
case length == 0, offset >= len(str):
|
||||
return ""
|
||||
|
||||
// Positive offset - count from the beginning
|
||||
case offset > 0:
|
||||
// Skip offset runes from the start
|
||||
for i, r := range str {
|
||||
if offset--; offset == 0 {
|
||||
str = str[i+utf8.RuneLen(r):]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if offset >= size {
|
||||
return Empty[T]()
|
||||
}
|
||||
// If couldn't skip enough runes - string is shorter than offset
|
||||
if offset != 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if length > uint(size)-uint(offset) {
|
||||
length = uint(size - offset)
|
||||
}
|
||||
// If remaining string is shorter than or equal to length - return it entirely
|
||||
if uint(len(str)) <= length {
|
||||
return str
|
||||
}
|
||||
|
||||
return T(strings.ReplaceAll(string(rs[offset:offset+int(length)]), "\x00", ""))
|
||||
// Otherwise proceed to trimming by length
|
||||
fallthrough
|
||||
|
||||
// Zero offset or offset less than minus string length - start from beginning
|
||||
case offset < -len(str), offset == 0:
|
||||
// Count length runes from the start
|
||||
for i := range str {
|
||||
if length == 0 {
|
||||
return str[:i]
|
||||
}
|
||||
length--
|
||||
}
|
||||
|
||||
return str
|
||||
|
||||
// Negative offset - count from the end of string
|
||||
default: // -len(str) < offset < 0
|
||||
// Helper function to move backward through runes
|
||||
backwardPos := func(end int, count uint) (start int) {
|
||||
for {
|
||||
_, i := utf8.DecodeLastRuneInString(string(str[:end]))
|
||||
end -= i
|
||||
|
||||
if count--; count == 0 || end == 0 {
|
||||
return end
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
offset := uint(-offset)
|
||||
|
||||
// If offset is less than or equal to length - take from position to end
|
||||
if offset <= length {
|
||||
start := backwardPos(len(str), offset)
|
||||
return str[start:]
|
||||
}
|
||||
|
||||
// Otherwise calculate start and end positions
|
||||
end := backwardPos(len(str), offset-length)
|
||||
start := backwardPos(end, length)
|
||||
|
||||
return str[start:end]
|
||||
}
|
||||
}
|
||||
|
||||
// ChunkString returns a slice of strings split into groups of length size. If the string can't be split evenly,
|
||||
// the final chunk will be the remaining characters.
|
||||
// Play: https://go.dev/play/p/__FLTuJVz54
|
||||
//
|
||||
// Note: lo.ChunkString and lo.Chunk functions behave inconsistently for empty input: lo.ChunkString("", n) returns [""] instead of [].
|
||||
// See https://github.com/samber/lo/issues/788
|
||||
func ChunkString[T ~string](str T, size int) []T {
|
||||
if size <= 0 {
|
||||
panic("lo.ChunkString: size must be greater than 0")
|
||||
}
|
||||
|
||||
if len(str) == 0 {
|
||||
return []T{""}
|
||||
}
|
||||
|
||||
if size >= len(str) {
|
||||
return []T{str}
|
||||
}
|
||||
@@ -212,6 +277,7 @@ func Words(str string) []string {
|
||||
// example: Int8Value => Int 8Value => Int 8 Value
|
||||
str = splitNumberLetterReg.ReplaceAllString(str, "$1 $2")
|
||||
var result strings.Builder
|
||||
result.Grow(len(str))
|
||||
for _, r := range str {
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||||
result.WriteRune(r)
|
||||
@@ -228,26 +294,25 @@ func Capitalize(str string) string {
|
||||
return cases.Title(language.English).String(str)
|
||||
}
|
||||
|
||||
// Ellipsis trims and truncates a string to a specified length **in bytes** and appends an ellipsis
|
||||
// if truncated. If the string contains non-ASCII characters (which may occupy multiple bytes in UTF-8),
|
||||
// truncating by byte length may split a character in the middle, potentially resulting in garbled output.
|
||||
// Ellipsis trims and truncates a string to a specified length in runes and appends an ellipsis
|
||||
// if truncated. The length parameter counts Unicode code points (runes), not bytes, so multi-byte
|
||||
// characters such as emoji or CJK ideographs are never split in the middle.
|
||||
// Play: https://go.dev/play/p/qE93rgqe1TW
|
||||
func Ellipsis(str string, length int) string {
|
||||
str = strings.TrimSpace(str)
|
||||
|
||||
if len(str) > length {
|
||||
if len(str) < 3 || length < 3 {
|
||||
return "..."
|
||||
const ellipsis = "..."
|
||||
|
||||
cutPosition := 0
|
||||
for i := range str {
|
||||
if length == len(ellipsis) {
|
||||
cutPosition = i
|
||||
}
|
||||
|
||||
if length--; length < 0 {
|
||||
return strings.TrimSpace(str[:cutPosition]) + ellipsis
|
||||
}
|
||||
return strings.TrimSpace(str[0:length-3]) + "..."
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
// Elipse trims and truncates a string to a specified length and appends an ellipsis if truncated.
|
||||
//
|
||||
// Deprecated: Use Ellipsis instead.
|
||||
func Elipse(str string, length int) string {
|
||||
return Ellipsis(str, length)
|
||||
}
|
||||
|
||||
+24
-24
@@ -6,94 +6,94 @@ import (
|
||||
|
||||
// Duration returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/HQfbBbAXaFP
|
||||
func Duration(cb func()) time.Duration {
|
||||
return Duration0(cb)
|
||||
func Duration(callback func()) time.Duration {
|
||||
return Duration0(callback)
|
||||
}
|
||||
|
||||
// Duration0 returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/HQfbBbAXaFP
|
||||
func Duration0(cb func()) time.Duration {
|
||||
func Duration0(callback func()) time.Duration {
|
||||
start := time.Now()
|
||||
cb()
|
||||
callback()
|
||||
return time.Since(start)
|
||||
}
|
||||
|
||||
// Duration1 returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/HQfbBbAXaFP
|
||||
func Duration1[A any](cb func() A) (A, time.Duration) {
|
||||
func Duration1[A any](callback func() A) (A, time.Duration) {
|
||||
start := time.Now()
|
||||
a := cb()
|
||||
a := callback()
|
||||
return a, time.Since(start)
|
||||
}
|
||||
|
||||
// Duration2 returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/HQfbBbAXaFP
|
||||
func Duration2[A, B any](cb func() (A, B)) (A, B, time.Duration) {
|
||||
func Duration2[A, B any](callback func() (A, B)) (A, B, time.Duration) {
|
||||
start := time.Now()
|
||||
a, b := cb()
|
||||
a, b := callback()
|
||||
return a, b, time.Since(start)
|
||||
}
|
||||
|
||||
// Duration3 returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/xr863iwkAxQ
|
||||
func Duration3[A, B, C any](cb func() (A, B, C)) (A, B, C, time.Duration) {
|
||||
func Duration3[A, B, C any](callback func() (A, B, C)) (A, B, C, time.Duration) {
|
||||
start := time.Now()
|
||||
a, b, c := cb()
|
||||
a, b, c := callback()
|
||||
return a, b, c, time.Since(start)
|
||||
}
|
||||
|
||||
// Duration4 returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/xr863iwkAxQ
|
||||
func Duration4[A, B, C, D any](cb func() (A, B, C, D)) (A, B, C, D, time.Duration) {
|
||||
func Duration4[A, B, C, D any](callback func() (A, B, C, D)) (A, B, C, D, time.Duration) {
|
||||
start := time.Now()
|
||||
a, b, c, d := cb()
|
||||
a, b, c, d := callback()
|
||||
return a, b, c, d, time.Since(start)
|
||||
}
|
||||
|
||||
// Duration5 returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/xr863iwkAxQ
|
||||
func Duration5[A, B, C, D, E any](cb func() (A, B, C, D, E)) (A, B, C, D, E, time.Duration) {
|
||||
func Duration5[A, B, C, D, E any](callback func() (A, B, C, D, E)) (A, B, C, D, E, time.Duration) {
|
||||
start := time.Now()
|
||||
a, b, c, d, e := cb()
|
||||
a, b, c, d, e := callback()
|
||||
return a, b, c, d, e, time.Since(start)
|
||||
}
|
||||
|
||||
// Duration6 returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/mR4bTQKO-Tf
|
||||
func Duration6[A, B, C, D, E, F any](cb func() (A, B, C, D, E, F)) (A, B, C, D, E, F, time.Duration) {
|
||||
func Duration6[A, B, C, D, E, F any](callback func() (A, B, C, D, E, F)) (A, B, C, D, E, F, time.Duration) {
|
||||
start := time.Now()
|
||||
a, b, c, d, e, f := cb()
|
||||
a, b, c, d, e, f := callback()
|
||||
return a, b, c, d, e, f, time.Since(start)
|
||||
}
|
||||
|
||||
// Duration7 returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/jgIAcBWWInS
|
||||
func Duration7[A, B, C, D, E, F, G any](cb func() (A, B, C, D, E, F, G)) (A, B, C, D, E, F, G, time.Duration) {
|
||||
func Duration7[A, B, C, D, E, F, G any](callback func() (A, B, C, D, E, F, G)) (A, B, C, D, E, F, G, time.Duration) {
|
||||
start := time.Now()
|
||||
a, b, c, d, e, f, g := cb()
|
||||
a, b, c, d, e, f, g := callback()
|
||||
return a, b, c, d, e, f, g, time.Since(start)
|
||||
}
|
||||
|
||||
// Duration8 returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/T8kxpG1c5Na
|
||||
func Duration8[A, B, C, D, E, F, G, H any](cb func() (A, B, C, D, E, F, G, H)) (A, B, C, D, E, F, G, H, time.Duration) {
|
||||
func Duration8[A, B, C, D, E, F, G, H any](callback func() (A, B, C, D, E, F, G, H)) (A, B, C, D, E, F, G, H, time.Duration) {
|
||||
start := time.Now()
|
||||
a, b, c, d, e, f, g, h := cb()
|
||||
a, b, c, d, e, f, g, h := callback()
|
||||
return a, b, c, d, e, f, g, h, time.Since(start)
|
||||
}
|
||||
|
||||
// Duration9 returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/bg9ix2VrZ0j
|
||||
func Duration9[A, B, C, D, E, F, G, H, I any](cb func() (A, B, C, D, E, F, G, H, I)) (A, B, C, D, E, F, G, H, I, time.Duration) {
|
||||
func Duration9[A, B, C, D, E, F, G, H, I any](callback func() (A, B, C, D, E, F, G, H, I)) (A, B, C, D, E, F, G, H, I, time.Duration) {
|
||||
start := time.Now()
|
||||
a, b, c, d, e, f, g, h, i := cb()
|
||||
a, b, c, d, e, f, g, h, i := callback()
|
||||
return a, b, c, d, e, f, g, h, i, time.Since(start)
|
||||
}
|
||||
|
||||
// Duration10 returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/Y3n7oJXqJbk
|
||||
func Duration10[A, B, C, D, E, F, G, H, I, J any](cb func() (A, B, C, D, E, F, G, H, I, J)) (A, B, C, D, E, F, G, H, I, J, time.Duration) {
|
||||
func Duration10[A, B, C, D, E, F, G, H, I, J any](callback func() (A, B, C, D, E, F, G, H, I, J)) (A, B, C, D, E, F, G, H, I, J, time.Duration) {
|
||||
start := time.Now()
|
||||
a, b, c, d, e, f, g, h, i, j := cb()
|
||||
a, b, c, d, e, f, g, h, i, j := callback()
|
||||
return a, b, c, d, e, f, g, h, i, j, time.Since(start)
|
||||
}
|
||||
|
||||
+860
-244
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -173,7 +173,7 @@ func CoalesceOrEmpty[T comparable](v ...T) T {
|
||||
// Play: https://go.dev/play/p/Gyo9otyvFHH
|
||||
func CoalesceSlice[T any](v ...[]T) ([]T, bool) {
|
||||
for i := range v {
|
||||
if v[i] != nil && len(v[i]) > 0 {
|
||||
if len(v[i]) > 0 {
|
||||
return v[i], true
|
||||
}
|
||||
}
|
||||
@@ -184,7 +184,7 @@ func CoalesceSlice[T any](v ...[]T) ([]T, bool) {
|
||||
// Play: https://go.dev/play/p/Gyo9otyvFHH
|
||||
func CoalesceSliceOrEmpty[T any](v ...[]T) []T {
|
||||
for i := range v {
|
||||
if v[i] != nil && len(v[i]) > 0 {
|
||||
if len(v[i]) > 0 {
|
||||
return v[i]
|
||||
}
|
||||
}
|
||||
@@ -195,7 +195,7 @@ func CoalesceSliceOrEmpty[T any](v ...[]T) []T {
|
||||
// Play: https://go.dev/play/p/Gyo9otyvFHH
|
||||
func CoalesceMap[K comparable, V any](v ...map[K]V) (map[K]V, bool) {
|
||||
for i := range v {
|
||||
if v[i] != nil && len(v[i]) > 0 {
|
||||
if len(v[i]) > 0 {
|
||||
return v[i], true
|
||||
}
|
||||
}
|
||||
@@ -206,7 +206,7 @@ func CoalesceMap[K comparable, V any](v ...map[K]V) (map[K]V, bool) {
|
||||
// Play: https://go.dev/play/p/Gyo9otyvFHH
|
||||
func CoalesceMapOrEmpty[K comparable, V any](v ...map[K]V) map[K]V {
|
||||
for i := range v {
|
||||
if v[i] != nil && len(v[i]) > 0 {
|
||||
if len(v[i]) > 0 {
|
||||
return v[i]
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -12,6 +12,15 @@ bench:
|
||||
watch-bench:
|
||||
reflex -t 50ms -s -- sh -c 'go test -benchmem -count 3 -bench ./...'
|
||||
|
||||
fuzz:
|
||||
go test -fuzz=FuzzAttrsToMap -fuzztime=10s ./...
|
||||
go test -fuzz=FuzzValueToString -fuzztime=10s ./...
|
||||
go test -fuzz=FuzzAnyValueToString -fuzztime=10s ./...
|
||||
go test -fuzz=FuzzFindAttribute -fuzztime=10s ./...
|
||||
go test -fuzz=FuzzRemoveEmptyAttrs -fuzztime=10s ./...
|
||||
go test -fuzz=FuzzUniqAttrs -fuzztime=10s ./...
|
||||
go test -fuzz=FuzzAttrsToString -fuzztime=10s ./...
|
||||
|
||||
coverage:
|
||||
go test -v -coverprofile=cover.out -covermode=atomic ./...
|
||||
go tool cover -html=cover.out -o cover.html
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ func AttrsToMap(attrs ...slog.Attr) map[string]any {
|
||||
}
|
||||
|
||||
func RecordToAttrsMap(r slog.Record) map[string]any {
|
||||
attrs := make([]slog.Attr, r.NumAttrs())
|
||||
attrs := make([]slog.Attr, 0, r.NumAttrs())
|
||||
r.Attrs(func(attr slog.Attr) bool {
|
||||
attrs = append(attrs, attr)
|
||||
return true
|
||||
|
||||
+5
-3
@@ -18,10 +18,12 @@ func FindAttrByGroupAndKey(attrs []slog.Attr, groups []string, key string) (slog
|
||||
}
|
||||
|
||||
for i := range attrs {
|
||||
if attrs[i].Key == key && attrs[i].Value.Kind() == slog.KindGroup {
|
||||
return FindAttrByGroupAndKey(attrs[i].Value.Group(), groups[1:], key)
|
||||
if attrs[i].Key == groups[0] && attrs[i].Value.Kind() == slog.KindGroup {
|
||||
attr, found := FindAttrByGroupAndKey(attrs[i].Value.Group(), groups[1:], key)
|
||||
if found {
|
||||
return attr, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return slog.Attr{}, false
|
||||
}
|
||||
|
||||
Generated
Vendored
+32
@@ -5,6 +5,8 @@ package otelgrpc // import "go.opentelemetry.io/contrib/instrumentation/google.g
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
@@ -29,6 +31,14 @@ type InterceptorFilter func(*InterceptorInfo) bool
|
||||
// A Filter must return true if the request should be instrumented.
|
||||
type Filter func(*stats.RPCTagInfo) bool
|
||||
|
||||
type semconvMode int
|
||||
|
||||
const (
|
||||
semconvModeNew semconvMode = iota // Default
|
||||
semconvModeOld
|
||||
semconvModeDup
|
||||
)
|
||||
|
||||
// config is a group of options for this instrumentation.
|
||||
type config struct {
|
||||
Filter Filter
|
||||
@@ -47,6 +57,8 @@ type config struct {
|
||||
|
||||
ReceivedEvent bool
|
||||
SentEvent bool
|
||||
|
||||
semconvMode semconvMode
|
||||
}
|
||||
|
||||
// Option applies an option value for a config.
|
||||
@@ -66,13 +78,33 @@ func newConfig(opts []Option) *config {
|
||||
Propagators: otel.GetTextMapPropagator(),
|
||||
TracerProvider: otel.GetTracerProvider(),
|
||||
MeterProvider: otel.GetMeterProvider(),
|
||||
semconvMode: parseSemconvMode(),
|
||||
}
|
||||
for _, o := range opts {
|
||||
o.apply(c)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func parseSemconvMode() semconvMode {
|
||||
val := os.Getenv("OTEL_SEMCONV_STABILITY_OPT_IN")
|
||||
if val == "" {
|
||||
return semconvModeNew
|
||||
}
|
||||
parts := strings.SplitSeq(val, ",")
|
||||
for p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "rpc/dup" {
|
||||
return semconvModeDup
|
||||
}
|
||||
if p == "rpc/old" {
|
||||
return semconvModeOld
|
||||
}
|
||||
}
|
||||
return semconvModeNew
|
||||
}
|
||||
|
||||
// WithPublicEndpoint configures the Handler to link the span with an incoming
|
||||
// span context. If this option is not provided, then the association is a child
|
||||
// association instead of a link.
|
||||
|
||||
vendor/go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc/internal/parse.go
Generated
Vendored
+26
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
oldsemconv "go.opentelemetry.io/otel/semconv/v1.37.0" //nolint:depguard // Use of v1.37.0 is required for backward compatibility stability opt-in.
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||
)
|
||||
|
||||
@@ -25,3 +26,28 @@ func ParseFullMethod(fullMethod string) (string, []attribute.KeyValue) {
|
||||
name := fullMethod[1:]
|
||||
return name, []attribute.KeyValue{semconv.RPCMethod(name)}
|
||||
}
|
||||
|
||||
// ParseFullMethodOld returns a span name following the old OpenTelemetry semantic
|
||||
// conventions as well as all applicable span attribute.KeyValue attributes based
|
||||
// on a gRPC's FullMethod.
|
||||
// Based on the implementation in:
|
||||
// https://github.com/open-telemetry/opentelemetry-go-contrib/blob/072dcf8ad7e5e48b506e05720b29d8b078759606/instrumentation/google.golang.org/grpc/otelgrpc/internal/parse.go#L20
|
||||
func ParseFullMethodOld(fullMethod string) (string, []attribute.KeyValue) {
|
||||
if !strings.HasPrefix(fullMethod, "/") {
|
||||
return fullMethod, nil
|
||||
}
|
||||
name := fullMethod[1:]
|
||||
parts := strings.Split(name, "/")
|
||||
if len(parts) < 2 {
|
||||
return name, []attribute.KeyValue{
|
||||
attribute.String("rpc.system", "grpc"),
|
||||
}
|
||||
}
|
||||
service := parts[0]
|
||||
method := parts[1]
|
||||
return name, []attribute.KeyValue{
|
||||
oldsemconv.RPCSystemKey.String("grpc"),
|
||||
oldsemconv.RPCServiceKey.String(service),
|
||||
oldsemconv.RPCMethodKey.String(method),
|
||||
}
|
||||
}
|
||||
|
||||
Generated
Vendored
+122
-31
@@ -12,11 +12,12 @@ import (
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
oldrpcconv "go.opentelemetry.io/otel/semconv/v1.37.0/rpcconv" //nolint:depguard // Use of v1.37.0 is required for backward compatibility stability opt-in.
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.40.0/rpcconv"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
grpc_codes "google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/peer"
|
||||
"google.golang.org/grpc/stats"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
@@ -35,7 +36,8 @@ type serverHandler struct {
|
||||
|
||||
tracer trace.Tracer
|
||||
|
||||
duration rpcconv.ServerCallDuration
|
||||
duration rpcconv.ServerCallDuration
|
||||
oldDuration oldrpcconv.ServerDuration
|
||||
}
|
||||
|
||||
// NewServerHandler creates a stats.Handler for a gRPC server.
|
||||
@@ -59,15 +61,26 @@ func NewServerHandler(opts ...Option) stats.Handler {
|
||||
)
|
||||
|
||||
var err error
|
||||
h.duration, err = rpcconv.NewServerCallDuration(
|
||||
meter,
|
||||
metric.WithExplicitBucketBoundaries(
|
||||
0.005, 0.01, 0.025, 0.05, 0.075, 0.1,
|
||||
0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10,
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
otel.Handle(err)
|
||||
if c.semconvMode == semconvModeOld || c.semconvMode == semconvModeDup {
|
||||
oldDur, err := oldrpcconv.NewServerDuration(meter)
|
||||
if err != nil {
|
||||
otel.Handle(err)
|
||||
} else {
|
||||
h.oldDuration = oldDur
|
||||
}
|
||||
}
|
||||
|
||||
if c.semconvMode == semconvModeNew || c.semconvMode == semconvModeDup {
|
||||
h.duration, err = rpcconv.NewServerCallDuration(
|
||||
meter,
|
||||
metric.WithExplicitBucketBoundaries(
|
||||
0.005, 0.01, 0.025, 0.05, 0.075, 0.1,
|
||||
0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10,
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
otel.Handle(err)
|
||||
}
|
||||
}
|
||||
|
||||
return h
|
||||
@@ -86,8 +99,23 @@ func (*serverHandler) HandleConn(context.Context, stats.ConnStats) {
|
||||
func (h *serverHandler) TagRPC(ctx context.Context, info *stats.RPCTagInfo) context.Context {
|
||||
ctx = extract(ctx, h.Propagators)
|
||||
|
||||
name, attrs := internal.ParseFullMethod(info.FullMethodName)
|
||||
attrs = append(attrs, semconv.RPCSystemNameGRPC)
|
||||
var name string
|
||||
var attrs []attribute.KeyValue
|
||||
|
||||
switch h.semconvMode {
|
||||
case semconvModeOld:
|
||||
name, attrs = internal.ParseFullMethodOld(info.FullMethodName)
|
||||
case semconvModeDup:
|
||||
var attrsNew, attrsOld []attribute.KeyValue
|
||||
name, attrsNew = internal.ParseFullMethod(info.FullMethodName)
|
||||
_, attrsOld = internal.ParseFullMethodOld(info.FullMethodName)
|
||||
// Combine both. We append New last so its rpc.method (fully qualified) wins when deduplicated.
|
||||
attrs = append(append([]attribute.KeyValue{}, attrsOld...), attrsNew...)
|
||||
attrs = append(attrs, semconv.RPCSystemNameGRPC) // New convention
|
||||
default: // semconvModeNew
|
||||
name, attrs = internal.ParseFullMethod(info.FullMethodName)
|
||||
attrs = append(attrs, semconv.RPCSystemNameGRPC)
|
||||
}
|
||||
|
||||
record := true
|
||||
if h.Filter != nil {
|
||||
@@ -131,10 +159,19 @@ func (h *serverHandler) TagRPC(ctx context.Context, info *stats.RPCTagInfo) cont
|
||||
|
||||
// HandleRPC processes the RPC stats.
|
||||
func (h *serverHandler) HandleRPC(ctx context.Context, rs stats.RPCStats) {
|
||||
var dur metric.Float64Histogram
|
||||
if h.semconvMode == semconvModeNew || h.semconvMode == semconvModeDup {
|
||||
dur = h.duration.Inst()
|
||||
}
|
||||
var oldDur metric.Float64Histogram
|
||||
if h.semconvMode == semconvModeOld || h.semconvMode == semconvModeDup {
|
||||
oldDur = h.oldDuration.Inst()
|
||||
}
|
||||
h.handleRPC(
|
||||
ctx,
|
||||
rs,
|
||||
h.duration.Inst(),
|
||||
dur,
|
||||
oldDur,
|
||||
serverStatus,
|
||||
)
|
||||
}
|
||||
@@ -144,7 +181,8 @@ type clientHandler struct {
|
||||
|
||||
tracer trace.Tracer
|
||||
|
||||
duration rpcconv.ClientCallDuration
|
||||
duration rpcconv.ClientCallDuration
|
||||
oldDuration oldrpcconv.ClientDuration
|
||||
}
|
||||
|
||||
// NewClientHandler creates a stats.Handler for a gRPC client.
|
||||
@@ -168,15 +206,26 @@ func NewClientHandler(opts ...Option) stats.Handler {
|
||||
)
|
||||
|
||||
var err error
|
||||
h.duration, err = rpcconv.NewClientCallDuration(
|
||||
meter,
|
||||
metric.WithExplicitBucketBoundaries(
|
||||
0.005, 0.01, 0.025, 0.05, 0.075, 0.1,
|
||||
0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10,
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
otel.Handle(err)
|
||||
if c.semconvMode == semconvModeOld || c.semconvMode == semconvModeDup {
|
||||
oldDur, err := oldrpcconv.NewClientDuration(meter)
|
||||
if err != nil {
|
||||
otel.Handle(err)
|
||||
} else {
|
||||
h.oldDuration = oldDur
|
||||
}
|
||||
}
|
||||
|
||||
if c.semconvMode == semconvModeNew || c.semconvMode == semconvModeDup {
|
||||
h.duration, err = rpcconv.NewClientCallDuration(
|
||||
meter,
|
||||
metric.WithExplicitBucketBoundaries(
|
||||
0.005, 0.01, 0.025, 0.05, 0.075, 0.1,
|
||||
0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10,
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
otel.Handle(err)
|
||||
}
|
||||
}
|
||||
|
||||
return h
|
||||
@@ -184,8 +233,23 @@ func NewClientHandler(opts ...Option) stats.Handler {
|
||||
|
||||
// TagRPC can attach some information to the given context.
|
||||
func (h *clientHandler) TagRPC(ctx context.Context, info *stats.RPCTagInfo) context.Context {
|
||||
name, attrs := internal.ParseFullMethod(info.FullMethodName)
|
||||
attrs = append(attrs, semconv.RPCSystemNameGRPC)
|
||||
var name string
|
||||
var attrs []attribute.KeyValue
|
||||
|
||||
switch h.semconvMode {
|
||||
case semconvModeOld:
|
||||
name, attrs = internal.ParseFullMethodOld(info.FullMethodName)
|
||||
case semconvModeDup:
|
||||
var attrsNew, attrsOld []attribute.KeyValue
|
||||
name, attrsNew = internal.ParseFullMethod(info.FullMethodName)
|
||||
_, attrsOld = internal.ParseFullMethodOld(info.FullMethodName)
|
||||
// Combine both. We append New last so its rpc.method (fully qualified) wins when deduplicated.
|
||||
attrs = append(append([]attribute.KeyValue{}, attrsOld...), attrsNew...)
|
||||
attrs = append(attrs, semconv.RPCSystemNameGRPC) // New convention
|
||||
default: // semconvModeNew
|
||||
name, attrs = internal.ParseFullMethod(info.FullMethodName)
|
||||
attrs = append(attrs, semconv.RPCSystemNameGRPC)
|
||||
}
|
||||
|
||||
record := true
|
||||
if h.Filter != nil {
|
||||
@@ -219,10 +283,19 @@ func (h *clientHandler) TagRPC(ctx context.Context, info *stats.RPCTagInfo) cont
|
||||
|
||||
// HandleRPC processes the RPC stats.
|
||||
func (h *clientHandler) HandleRPC(ctx context.Context, rs stats.RPCStats) {
|
||||
var dur metric.Float64Histogram
|
||||
if h.semconvMode == semconvModeNew || h.semconvMode == semconvModeDup {
|
||||
dur = h.duration.Inst()
|
||||
}
|
||||
var oldDur metric.Float64Histogram
|
||||
if h.semconvMode == semconvModeOld || h.semconvMode == semconvModeDup {
|
||||
oldDur = h.oldDuration.Inst()
|
||||
}
|
||||
h.handleRPC(
|
||||
ctx,
|
||||
rs,
|
||||
h.duration.Inst(),
|
||||
dur,
|
||||
oldDur,
|
||||
func(s *status.Status) (codes.Code, string) {
|
||||
return codes.Error, s.Message()
|
||||
},
|
||||
@@ -243,6 +316,7 @@ func (*config) handleRPC(
|
||||
ctx context.Context,
|
||||
rs stats.RPCStats,
|
||||
duration metric.Float64Histogram,
|
||||
oldDuration metric.Float64Histogram,
|
||||
recordStatus func(*status.Status) (codes.Code, string),
|
||||
) {
|
||||
gctx, _ := ctx.Value(gRPCContextKey{}).(*gRPCContext)
|
||||
@@ -255,12 +329,23 @@ func (*config) handleRPC(
|
||||
switch rs := rs.(type) {
|
||||
case *stats.Begin:
|
||||
case *stats.InPayload:
|
||||
case *stats.InHeader:
|
||||
if !rs.Client && rs.LocalAddr != nil {
|
||||
if span.IsRecording() {
|
||||
span.SetAttributes(serverAddrAttrs(rs.LocalAddr.String())...)
|
||||
}
|
||||
// TODO: add server.address and server.port to metrics once the API supports opt-in attributes.
|
||||
}
|
||||
case *stats.OutPayload:
|
||||
case *stats.OutTrailer:
|
||||
case *stats.OutHeader:
|
||||
if span.IsRecording() {
|
||||
if p, ok := peer.FromContext(ctx); ok {
|
||||
span.SetAttributes(serverAddrAttrs(p.Addr.String())...)
|
||||
if rs.Client && rs.RemoteAddr != nil && (span.IsRecording() || gctx != nil) {
|
||||
attrs := serverAddrAttrs(rs.RemoteAddr.String())
|
||||
if span.IsRecording() {
|
||||
span.SetAttributes(attrs...)
|
||||
}
|
||||
if gctx != nil {
|
||||
gctx.metricAttrs = append(gctx.metricAttrs, attrs...)
|
||||
}
|
||||
}
|
||||
case *stats.End:
|
||||
@@ -298,7 +383,13 @@ func (*config) handleRPC(
|
||||
// Measure right before calling Record() to capture as much elapsed time as possible.
|
||||
elapsedTime := float64(rs.EndTime.Sub(rs.BeginTime)) / float64(time.Second)
|
||||
|
||||
duration.Record(ctx, elapsedTime, recordOpts...)
|
||||
if duration != nil {
|
||||
duration.Record(ctx, elapsedTime, recordOpts...)
|
||||
}
|
||||
if oldDuration != nil {
|
||||
oldDuration.Record(ctx, elapsedTime*1000.0, recordOpts...)
|
||||
}
|
||||
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
Generated
Vendored
+1
-1
@@ -4,4 +4,4 @@
|
||||
package otelgrpc // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
||||
|
||||
// Version is the current release version of the gRPC instrumentation.
|
||||
const Version = "0.67.0"
|
||||
const Version = "0.68.0"
|
||||
|
||||
+11
@@ -17,6 +17,7 @@ linters:
|
||||
- ineffassign
|
||||
- misspell
|
||||
- modernize
|
||||
- noctx
|
||||
- perfsprint
|
||||
- revive
|
||||
- staticcheck
|
||||
@@ -88,6 +89,16 @@ linters:
|
||||
deny:
|
||||
- pkg: go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal
|
||||
desc: Do not use cross-module internal packages.
|
||||
semconv:
|
||||
list-mode: lax
|
||||
files:
|
||||
- "!**/semconv/**"
|
||||
- "!**/exporters/zipkin/**"
|
||||
deny:
|
||||
- pkg: go.opentelemetry.io/otel/semconv
|
||||
desc: "Use go.opentelemetry.io/otel/semconv/v1.40.0 instead. If a newer semconv version has been released, update the depguard rule."
|
||||
allow:
|
||||
- go.opentelemetry.io/otel/semconv/v1.40.0
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- appendAssign
|
||||
|
||||
+45
-1
@@ -11,6 +11,49 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
||||
<!-- Released section -->
|
||||
<!-- Don't change this section unless doing release -->
|
||||
|
||||
## [1.43.0/0.65.0/0.19.0] 2026-04-02
|
||||
|
||||
### Added
|
||||
|
||||
- Add `IsRandom` and `WithRandom` on `TraceFlags`, and `IsRandom` on `SpanContext` in `go.opentelemetry.io/otel/trace` for [W3C Trace Context Level 2 Random Trace ID Flag](https://www.w3.org/TR/trace-context-2/#random-trace-id-flag) support. (#8012)
|
||||
- Add service detection with `WithService` in `go.opentelemetry.io/otel/sdk/resource`. (#7642)
|
||||
- Add `DefaultWithContext` and `EnvironmentWithContext` in `go.opentelemetry.io/otel/sdk/resource` to support plumbing `context.Context` through default and environment detectors. (#8051)
|
||||
- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc`. (#8038)
|
||||
- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc`. (#8038)
|
||||
- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`. (#8038)
|
||||
- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`. (#8038)
|
||||
- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`. (#8038)
|
||||
- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#8038)
|
||||
- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest`. (#8038)
|
||||
- Add support for per-series start time tracking for cumulative metrics in `go.opentelemetry.io/otel/sdk/metric`.
|
||||
Set `OTEL_GO_X_PER_SERIES_START_TIMESTAMPS=true` to enable. (#8060)
|
||||
- Add `WithCardinalityLimitSelector` for metric reader for configuring cardinality limits specific to the instrument kind. (#7855)
|
||||
|
||||
### Changed
|
||||
|
||||
- Introduce the `EMPTY` Type in `go.opentelemetry.io/otel/attribute` to reflect that an empty value is now a valid value, with `INVALID` remaining as a deprecated alias of `EMPTY`. (#8038)
|
||||
- Improve slice handling in `go.opentelemetry.io/otel/attribute` to optimize short slice values with fixed-size fast paths. (#8039)
|
||||
- Improve performance of span metric recording in `go.opentelemetry.io/otel/sdk/trace` by returning early if self-observability is not enabled. (#8067)
|
||||
- Improve formatting of metric data diffs in `go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest`. (#8073)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Deprecate `INVALID` in `go.opentelemetry.io/otel/attribute`. Use `EMPTY` instead. (#8038)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Return spec-compliant `TraceIdRatioBased` description. This is a breaking behavioral change, but it is necessary to
|
||||
make the implementation [spec-compliant](https://opentelemetry.io/docs/specs/otel/trace/sdk/#traceidratiobased). (#8027)
|
||||
- Fix a race condition in `go.opentelemetry.io/otel/sdk/metric` where the lastvalue aggregation could collect the value 0 even when no zero-value measurements were recorded. (#8056)
|
||||
- Limit HTTP response body to 4 MiB in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp` to mitigate excessive memory usage caused by a misconfigured or malicious server.
|
||||
Responses exceeding the limit are treated as non-retryable errors. (#8108)
|
||||
- Limit HTTP response body to 4 MiB in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp` to mitigate excessive memory usage caused by a misconfigured or malicious server.
|
||||
Responses exceeding the limit are treated as non-retryable errors. (#8108)
|
||||
- Limit HTTP response body to 4 MiB in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp` to mitigate excessive memory usage caused by a misconfigured or malicious server.
|
||||
Responses exceeding the limit are treated as non-retryable errors. (#8108)
|
||||
- `WithHostID` detector in `go.opentelemetry.io/otel/sdk/resource` to use full path for `kenv` command on BSD. (#8113)
|
||||
- Fix missing `request.GetBody` in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp` to correctly handle HTTP2 GOAWAY frame. (#8096)
|
||||
|
||||
## [1.42.0/0.64.0/0.18.0/0.0.16] 2026-03-06
|
||||
|
||||
### Added
|
||||
@@ -3576,7 +3619,8 @@ It contains api and sdk for trace and meter.
|
||||
- CircleCI build CI manifest files.
|
||||
- CODEOWNERS file to track owners of this project.
|
||||
|
||||
[Unreleased]: https://github.com/open-telemetry/opentelemetry-go/compare/v1.42.0...HEAD
|
||||
[Unreleased]: https://github.com/open-telemetry/opentelemetry-go/compare/v1.43.0...HEAD
|
||||
[1.43.0/0.65.0/0.19.0]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.43.0
|
||||
[1.42.0/0.64.0/0.18.0/0.0.16]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.42.0
|
||||
[1.41.0/0.63.0/0.17.0/0.0.15]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.41.0
|
||||
[1.40.0/0.62.0/0.16.0]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.40.0
|
||||
|
||||
+4
@@ -38,10 +38,14 @@ CROSSLINK = $(TOOLS)/crosslink
|
||||
$(TOOLS)/crosslink: PACKAGE=go.opentelemetry.io/build-tools/crosslink
|
||||
|
||||
SEMCONVKIT = $(TOOLS)/semconvkit
|
||||
SEMCONVKIT_FILES := $(sort $(shell find $(TOOLS_MOD_DIR)/semconvkit -type f))
|
||||
$(TOOLS)/semconvkit: PACKAGE=go.opentelemetry.io/otel/$(TOOLS_MOD_DIR)/semconvkit
|
||||
$(TOOLS)/semconvkit: $(SEMCONVKIT_FILES)
|
||||
|
||||
VERIFYREADMES = $(TOOLS)/verifyreadmes
|
||||
VERIFYREADMES_FILES := $(sort $(shell find $(TOOLS_MOD_DIR)/verifyreadmes -type f))
|
||||
$(TOOLS)/verifyreadmes: PACKAGE=go.opentelemetry.io/otel/$(TOOLS_MOD_DIR)/verifyreadmes
|
||||
$(TOOLS)/verifyreadmes: $(VERIFYREADMES_FILES)
|
||||
|
||||
GOLANGCI_LINT = $(TOOLS)/golangci-lint
|
||||
$(TOOLS)/golangci-lint: PACKAGE=github.com/golangci/golangci-lint/v2/cmd/golangci-lint
|
||||
|
||||
+40
-1
@@ -4,7 +4,9 @@
|
||||
|
||||
Create a `Version Release` issue to track the release process.
|
||||
|
||||
## Semantic Convention Generation
|
||||
## Semantic Convention Upgrade
|
||||
|
||||
### Semantic Convention Generation
|
||||
|
||||
New versions of the [OpenTelemetry Semantic Conventions] mean new versions of the `semconv` package need to be generated.
|
||||
The `semconv-generate` make target is used for this.
|
||||
@@ -22,6 +24,43 @@ make semconv-generate # Uses the exported TAG.
|
||||
This should create a new sub-package of [`semconv`](./semconv).
|
||||
Ensure things look correct before submitting a pull request to include the addition.
|
||||
|
||||
The `CHANGELOG.md` should also be updated to reflect the new changes:
|
||||
|
||||
```md
|
||||
- The `go.opentelemetry.io/otel/semconv/<NEW VERSION>` package. The package contains semantic conventions from the `<NEW VERSION>` version of the OpenTelemetry Semantic Conventions. See the [migration documentation](./semconv/<NEW VERSION>/MIGRATION.md) for information on how to upgrade from `go.opentelemetry.io/otel/semconv/<PREVIOUS VERSION>`. (#PR_NUMBER)
|
||||
```
|
||||
|
||||
> **Tip:** Change to the release and prior version to match the changes
|
||||
|
||||
### Update semconv imports
|
||||
|
||||
Once the new semconv module has been generated, update all semconv imports throughout the codebase to reference the new version:
|
||||
|
||||
```go
|
||||
// Before
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.37.0/otelconv"
|
||||
|
||||
|
||||
// After
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.39.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.39.0/otelconv"
|
||||
```
|
||||
|
||||
Once complete, run `make` to check for any compilation or test failures.
|
||||
|
||||
#### Handling attribute changes
|
||||
|
||||
Some semconv releases might add new attributes or impact attributes that are currently being used. Changes could stem from a simple renaming, to more complex changes like merging attributes and property values being changed.
|
||||
|
||||
One should update the code to the new attributes that supersede the impacted ones, hence sticking to the semantic conventions. However, legacy attributes might still be emitted in accordance to the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable.
|
||||
|
||||
For an example on how such migration might have to be tracked and performed, see issue [#7806](https://github.com/open-telemetry/opentelemetry-go/issues/7806).
|
||||
|
||||
### Go contrib linter update
|
||||
|
||||
Update [.golangci.yml](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/.golangci.yml) in [opentelemetry-go-contrib](https://github.com/open-telemetry/opentelemetry-go-contrib/) to mandate the new semconv version.
|
||||
|
||||
## Breaking changes validation
|
||||
|
||||
You can run `make gorelease` which runs [gorelease](https://pkg.go.dev/golang.org/x/exp/cmd/gorelease) to ensure that there are no unwanted changes made in the public API.
|
||||
|
||||
+2
-2
@@ -53,7 +53,7 @@ var (
|
||||
_ Encoder = &defaultAttrEncoder{}
|
||||
|
||||
// encoderIDCounter is for generating IDs for other attribute encoders.
|
||||
encoderIDCounter uint64
|
||||
encoderIDCounter atomic.Uint64
|
||||
|
||||
defaultEncoderOnce sync.Once
|
||||
defaultEncoderID = NewEncoderID()
|
||||
@@ -64,7 +64,7 @@ var (
|
||||
// once per each type of attribute encoder. Preferably in init() or in var
|
||||
// definition.
|
||||
func NewEncoderID() EncoderID {
|
||||
return EncoderID{value: atomic.AddUint64(&encoderIDCounter, 1)}
|
||||
return EncoderID{value: encoderIDCounter.Add(1)}
|
||||
}
|
||||
|
||||
// DefaultEncoder returns an attribute encoder that encodes attributes in such
|
||||
|
||||
+3
-1
@@ -27,6 +27,7 @@ const (
|
||||
int64SliceID uint64 = 3762322556277578591 // "_[]int64" (little endian)
|
||||
float64SliceID uint64 = 7308324551835016539 // "[]double" (little endian)
|
||||
stringSliceID uint64 = 7453010373645655387 // "[]string" (little endian)
|
||||
emptyID uint64 = 7305809155345288421 // "__empty_" (little endian)
|
||||
)
|
||||
|
||||
// hashKVs returns a new xxHash64 hash of kvs.
|
||||
@@ -80,7 +81,8 @@ func hashKV(h xxhash.Hash, kv KeyValue) xxhash.Hash {
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
h = h.String(rv.Index(i).String())
|
||||
}
|
||||
case INVALID:
|
||||
case EMPTY:
|
||||
h = h.Uint64(emptyID)
|
||||
default:
|
||||
// Logging is an alternative, but using the internal logger here
|
||||
// causes an import cycle so it is not done.
|
||||
|
||||
+50
-67
@@ -11,80 +11,63 @@ import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// BoolSliceValue converts a bool slice into an array with same elements as slice.
|
||||
func BoolSliceValue(v []bool) any {
|
||||
cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[bool]())).Elem()
|
||||
// sliceElem is the exact set of element types stored in attribute slice values.
|
||||
// Using a closed set prevents accidental instantiations for unsupported types.
|
||||
type sliceElem interface {
|
||||
bool | int64 | float64 | string
|
||||
}
|
||||
|
||||
// SliceValue converts a slice into an array with the same elements.
|
||||
func SliceValue[T sliceElem](v []T) any {
|
||||
// Keep only the common tiny-slice cases out of reflection. Extending this
|
||||
// much further increases code size for diminishing benefit while larger
|
||||
// slices still need the generic reflective path to preserve comparability.
|
||||
// This matches the short lengths that show up most often in local
|
||||
// benchmarks and semantic convention examples while leaving larger, less
|
||||
// predictable slices on the generic reflective path.
|
||||
switch len(v) {
|
||||
case 0:
|
||||
return [0]T{}
|
||||
case 1:
|
||||
return [1]T{v[0]}
|
||||
case 2:
|
||||
return [2]T{v[0], v[1]}
|
||||
case 3:
|
||||
return [3]T{v[0], v[1], v[2]}
|
||||
}
|
||||
|
||||
return sliceValueReflect(v)
|
||||
}
|
||||
|
||||
// AsSlice converts an array into a slice with the same elements.
|
||||
func AsSlice[T sliceElem](v any) []T {
|
||||
// Mirror the small fixed-array fast path used by SliceValue.
|
||||
switch a := v.(type) {
|
||||
case [0]T:
|
||||
return []T{}
|
||||
case [1]T:
|
||||
return []T{a[0]}
|
||||
case [2]T:
|
||||
return []T{a[0], a[1]}
|
||||
case [3]T:
|
||||
return []T{a[0], a[1], a[2]}
|
||||
}
|
||||
|
||||
return asSliceReflect[T](v)
|
||||
}
|
||||
|
||||
func sliceValueReflect[T sliceElem](v []T) any {
|
||||
cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[T]())).Elem()
|
||||
reflect.Copy(cp, reflect.ValueOf(v))
|
||||
return cp.Interface()
|
||||
}
|
||||
|
||||
// Int64SliceValue converts an int64 slice into an array with same elements as slice.
|
||||
func Int64SliceValue(v []int64) any {
|
||||
cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[int64]())).Elem()
|
||||
reflect.Copy(cp, reflect.ValueOf(v))
|
||||
return cp.Interface()
|
||||
}
|
||||
|
||||
// Float64SliceValue converts a float64 slice into an array with same elements as slice.
|
||||
func Float64SliceValue(v []float64) any {
|
||||
cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[float64]())).Elem()
|
||||
reflect.Copy(cp, reflect.ValueOf(v))
|
||||
return cp.Interface()
|
||||
}
|
||||
|
||||
// StringSliceValue converts a string slice into an array with same elements as slice.
|
||||
func StringSliceValue(v []string) any {
|
||||
cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[string]())).Elem()
|
||||
reflect.Copy(cp, reflect.ValueOf(v))
|
||||
return cp.Interface()
|
||||
}
|
||||
|
||||
// AsBoolSlice converts a bool array into a slice into with same elements as array.
|
||||
func AsBoolSlice(v any) []bool {
|
||||
func asSliceReflect[T sliceElem](v any) []T {
|
||||
rv := reflect.ValueOf(v)
|
||||
if rv.Type().Kind() != reflect.Array {
|
||||
if !rv.IsValid() || rv.Kind() != reflect.Array || rv.Type().Elem() != reflect.TypeFor[T]() {
|
||||
return nil
|
||||
}
|
||||
cpy := make([]bool, rv.Len())
|
||||
if len(cpy) > 0 {
|
||||
_ = reflect.Copy(reflect.ValueOf(cpy), rv)
|
||||
}
|
||||
return cpy
|
||||
}
|
||||
|
||||
// AsInt64Slice converts an int64 array into a slice into with same elements as array.
|
||||
func AsInt64Slice(v any) []int64 {
|
||||
rv := reflect.ValueOf(v)
|
||||
if rv.Type().Kind() != reflect.Array {
|
||||
return nil
|
||||
}
|
||||
cpy := make([]int64, rv.Len())
|
||||
if len(cpy) > 0 {
|
||||
_ = reflect.Copy(reflect.ValueOf(cpy), rv)
|
||||
}
|
||||
return cpy
|
||||
}
|
||||
|
||||
// AsFloat64Slice converts a float64 array into a slice into with same elements as array.
|
||||
func AsFloat64Slice(v any) []float64 {
|
||||
rv := reflect.ValueOf(v)
|
||||
if rv.Type().Kind() != reflect.Array {
|
||||
return nil
|
||||
}
|
||||
cpy := make([]float64, rv.Len())
|
||||
if len(cpy) > 0 {
|
||||
_ = reflect.Copy(reflect.ValueOf(cpy), rv)
|
||||
}
|
||||
return cpy
|
||||
}
|
||||
|
||||
// AsStringSlice converts a string array into a slice into with same elements as array.
|
||||
func AsStringSlice(v any) []string {
|
||||
rv := reflect.ValueOf(v)
|
||||
if rv.Type().Kind() != reflect.Array {
|
||||
return nil
|
||||
}
|
||||
cpy := make([]string, rv.Len())
|
||||
cpy := make([]T, rv.Len())
|
||||
if len(cpy) > 0 {
|
||||
_ = reflect.Copy(reflect.ValueOf(cpy), rv)
|
||||
}
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ type KeyValue struct {
|
||||
|
||||
// Valid reports whether kv is a valid OpenTelemetry attribute.
|
||||
func (kv KeyValue) Valid() bool {
|
||||
return kv.Key.Defined() && kv.Value.Type() != INVALID
|
||||
return kv.Key.Defined()
|
||||
}
|
||||
|
||||
// Bool creates a KeyValue with a BOOL Value type.
|
||||
|
||||
+3
-3
@@ -8,7 +8,7 @@ func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[INVALID-0]
|
||||
_ = x[EMPTY-0]
|
||||
_ = x[BOOL-1]
|
||||
_ = x[INT64-2]
|
||||
_ = x[FLOAT64-3]
|
||||
@@ -19,9 +19,9 @@ func _() {
|
||||
_ = x[STRINGSLICE-8]
|
||||
}
|
||||
|
||||
const _Type_name = "INVALIDBOOLINT64FLOAT64STRINGBOOLSLICEINT64SLICEFLOAT64SLICESTRINGSLICE"
|
||||
const _Type_name = "EMPTYBOOLINT64FLOAT64STRINGBOOLSLICEINT64SLICEFLOAT64SLICESTRINGSLICE"
|
||||
|
||||
var _Type_index = [...]uint8{0, 7, 11, 16, 23, 29, 38, 48, 60, 71}
|
||||
var _Type_index = [...]uint8{0, 5, 9, 14, 21, 27, 36, 46, 58, 69}
|
||||
|
||||
func (i Type) String() string {
|
||||
idx := int(i) - 0
|
||||
|
||||
+42
-19
@@ -6,7 +6,6 @@ package attribute // import "go.opentelemetry.io/otel/attribute"
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
attribute "go.opentelemetry.io/otel/attribute/internal"
|
||||
@@ -18,6 +17,8 @@ import (
|
||||
type Type int // nolint: revive // redefines builtin Type.
|
||||
|
||||
// Value represents the value part in key-value pairs.
|
||||
//
|
||||
// Note that the zero value is a valid empty value.
|
||||
type Value struct {
|
||||
vtype Type
|
||||
numeric uint64
|
||||
@@ -26,8 +27,8 @@ type Value struct {
|
||||
}
|
||||
|
||||
const (
|
||||
// INVALID is used for a Value with no value set.
|
||||
INVALID Type = iota
|
||||
// EMPTY is used for a Value with no value set.
|
||||
EMPTY Type = iota
|
||||
// BOOL is a boolean Type Value.
|
||||
BOOL
|
||||
// INT64 is a 64-bit signed integral Type Value.
|
||||
@@ -44,6 +45,10 @@ const (
|
||||
FLOAT64SLICE
|
||||
// STRINGSLICE is a slice of strings Type Value.
|
||||
STRINGSLICE
|
||||
// INVALID is used for a Value with no value set.
|
||||
//
|
||||
// Deprecated: Use EMPTY instead as an empty value is a valid value.
|
||||
INVALID = EMPTY
|
||||
)
|
||||
|
||||
// BoolValue creates a BOOL Value.
|
||||
@@ -56,7 +61,7 @@ func BoolValue(v bool) Value {
|
||||
|
||||
// BoolSliceValue creates a BOOLSLICE Value.
|
||||
func BoolSliceValue(v []bool) Value {
|
||||
return Value{vtype: BOOLSLICE, slice: attribute.BoolSliceValue(v)}
|
||||
return Value{vtype: BOOLSLICE, slice: attribute.SliceValue(v)}
|
||||
}
|
||||
|
||||
// IntValue creates an INT64 Value.
|
||||
@@ -64,16 +69,30 @@ func IntValue(v int) Value {
|
||||
return Int64Value(int64(v))
|
||||
}
|
||||
|
||||
// IntSliceValue creates an INTSLICE Value.
|
||||
// IntSliceValue creates an INT64SLICE Value.
|
||||
func IntSliceValue(v []int) Value {
|
||||
cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[int64]()))
|
||||
for i, val := range v {
|
||||
cp.Elem().Index(i).SetInt(int64(val))
|
||||
}
|
||||
return Value{
|
||||
vtype: INT64SLICE,
|
||||
slice: cp.Elem().Interface(),
|
||||
val := Value{vtype: INT64SLICE}
|
||||
|
||||
// Avoid the common tiny-slice cases from allocating a new slice.
|
||||
switch len(v) {
|
||||
case 0:
|
||||
val.slice = [0]int64{}
|
||||
case 1:
|
||||
val.slice = [1]int64{int64(v[0])}
|
||||
case 2:
|
||||
val.slice = [2]int64{int64(v[0]), int64(v[1])}
|
||||
case 3:
|
||||
val.slice = [3]int64{int64(v[0]), int64(v[1]), int64(v[2])}
|
||||
default:
|
||||
// Fallback to a new slice for larger slices.
|
||||
cp := make([]int64, len(v))
|
||||
for i, val := range v {
|
||||
cp[i] = int64(val)
|
||||
}
|
||||
val.slice = attribute.SliceValue(cp)
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
// Int64Value creates an INT64 Value.
|
||||
@@ -86,7 +105,7 @@ func Int64Value(v int64) Value {
|
||||
|
||||
// Int64SliceValue creates an INT64SLICE Value.
|
||||
func Int64SliceValue(v []int64) Value {
|
||||
return Value{vtype: INT64SLICE, slice: attribute.Int64SliceValue(v)}
|
||||
return Value{vtype: INT64SLICE, slice: attribute.SliceValue(v)}
|
||||
}
|
||||
|
||||
// Float64Value creates a FLOAT64 Value.
|
||||
@@ -99,7 +118,7 @@ func Float64Value(v float64) Value {
|
||||
|
||||
// Float64SliceValue creates a FLOAT64SLICE Value.
|
||||
func Float64SliceValue(v []float64) Value {
|
||||
return Value{vtype: FLOAT64SLICE, slice: attribute.Float64SliceValue(v)}
|
||||
return Value{vtype: FLOAT64SLICE, slice: attribute.SliceValue(v)}
|
||||
}
|
||||
|
||||
// StringValue creates a STRING Value.
|
||||
@@ -112,7 +131,7 @@ func StringValue(v string) Value {
|
||||
|
||||
// StringSliceValue creates a STRINGSLICE Value.
|
||||
func StringSliceValue(v []string) Value {
|
||||
return Value{vtype: STRINGSLICE, slice: attribute.StringSliceValue(v)}
|
||||
return Value{vtype: STRINGSLICE, slice: attribute.SliceValue(v)}
|
||||
}
|
||||
|
||||
// Type returns a type of the Value.
|
||||
@@ -136,7 +155,7 @@ func (v Value) AsBoolSlice() []bool {
|
||||
}
|
||||
|
||||
func (v Value) asBoolSlice() []bool {
|
||||
return attribute.AsBoolSlice(v.slice)
|
||||
return attribute.AsSlice[bool](v.slice)
|
||||
}
|
||||
|
||||
// AsInt64 returns the int64 value. Make sure that the Value's type is
|
||||
@@ -155,7 +174,7 @@ func (v Value) AsInt64Slice() []int64 {
|
||||
}
|
||||
|
||||
func (v Value) asInt64Slice() []int64 {
|
||||
return attribute.AsInt64Slice(v.slice)
|
||||
return attribute.AsSlice[int64](v.slice)
|
||||
}
|
||||
|
||||
// AsFloat64 returns the float64 value. Make sure that the Value's
|
||||
@@ -174,7 +193,7 @@ func (v Value) AsFloat64Slice() []float64 {
|
||||
}
|
||||
|
||||
func (v Value) asFloat64Slice() []float64 {
|
||||
return attribute.AsFloat64Slice(v.slice)
|
||||
return attribute.AsSlice[float64](v.slice)
|
||||
}
|
||||
|
||||
// AsString returns the string value. Make sure that the Value's type
|
||||
@@ -193,7 +212,7 @@ func (v Value) AsStringSlice() []string {
|
||||
}
|
||||
|
||||
func (v Value) asStringSlice() []string {
|
||||
return attribute.AsStringSlice(v.slice)
|
||||
return attribute.AsSlice[string](v.slice)
|
||||
}
|
||||
|
||||
type unknownValueType struct{}
|
||||
@@ -217,6 +236,8 @@ func (v Value) AsInterface() any {
|
||||
return v.stringly
|
||||
case STRINGSLICE:
|
||||
return v.asStringSlice()
|
||||
case EMPTY:
|
||||
return nil
|
||||
}
|
||||
return unknownValueType{}
|
||||
}
|
||||
@@ -252,6 +273,8 @@ func (v Value) Emit() string {
|
||||
return string(j)
|
||||
case STRING:
|
||||
return v.stringly
|
||||
case EMPTY:
|
||||
return ""
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# This is a renovate-friendly source of Docker images.
|
||||
FROM python:3.13.6-slim-bullseye@sha256:e98b521460ee75bca92175c16247bdf7275637a8faaeb2bcfa19d879ae5c4b9a AS python
|
||||
FROM otel/weaver:v0.21.2@sha256:2401de985c38bdb98b43918e2f43aa36b2afed4aa5669ac1c1de0a17301cd36d AS weaver
|
||||
FROM otel/weaver:v0.22.1@sha256:33ae522ae4b71c1c562563c1d81f46aa0f79f088a0873199143a1f11ac30e5c9 AS weaver
|
||||
FROM avtodev/markdown-lint:v1@sha256:6aeedc2f49138ce7a1cd0adffc1b1c0321b841dc2102408967d9301c031949ee AS markdown
|
||||
|
||||
+15
@@ -37,3 +37,18 @@ var Observability = newFeature(
|
||||
return "", false
|
||||
},
|
||||
)
|
||||
|
||||
// PerSeriesStartTimestamps is an experimental feature flag that determines if the SDK
|
||||
// uses the new Start Timestamps specification.
|
||||
//
|
||||
// To enable this feature set the OTEL_GO_X_PER_SERIES_START_TIMESTAMPS environment variable
|
||||
// to the case-insensitive string value of "true".
|
||||
var PerSeriesStartTimestamps = newFeature(
|
||||
[]string{"PER_SERIES_START_TIMESTAMPS"},
|
||||
func(v string) (bool, bool) {
|
||||
if strings.EqualFold(v, "true") {
|
||||
return true, true
|
||||
}
|
||||
return false, false
|
||||
},
|
||||
)
|
||||
|
||||
+8
@@ -193,3 +193,11 @@ func WithContainer() Option {
|
||||
func WithContainerID() Option {
|
||||
return WithDetectors(cgroupContainerIDDetector{})
|
||||
}
|
||||
|
||||
// WithService adds all the Service attributes to the configured Resource.
|
||||
func WithService() Option {
|
||||
return WithDetectors(
|
||||
defaultServiceInstanceIDDetector{},
|
||||
defaultServiceNameDetector{},
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user