TUN-6724: Migrate to sentry-go from raven-go
This commit is contained in:
parent
87bd36c924
commit
794e8e622f
|
@ -11,7 +11,7 @@ import (
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/getsentry/raven-go"
|
"github.com/getsentry/sentry-go"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
@ -202,7 +202,11 @@ func Commands() []*cli.Command {
|
||||||
|
|
||||||
// login pops up the browser window to do the actual login and JWT generation
|
// login pops up the browser window to do the actual login and JWT generation
|
||||||
func login(c *cli.Context) error {
|
func login(c *cli.Context) error {
|
||||||
if err := raven.SetDSN(sentryDSN); err != nil {
|
err := sentry.Init(sentry.ClientOptions{
|
||||||
|
Dsn: sentryDSN,
|
||||||
|
Release: c.App.Version,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,7 +255,11 @@ func ensureURLScheme(url string) string {
|
||||||
|
|
||||||
// curl provides a wrapper around curl, passing Access JWT along in request
|
// curl provides a wrapper around curl, passing Access JWT along in request
|
||||||
func curl(c *cli.Context) error {
|
func curl(c *cli.Context) error {
|
||||||
if err := raven.SetDSN(sentryDSN); err != nil {
|
err := sentry.Init(sentry.ClientOptions{
|
||||||
|
Dsn: sentryDSN,
|
||||||
|
Release: c.App.Version,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||||
|
@ -314,7 +322,11 @@ func run(cmd string, args ...string) error {
|
||||||
|
|
||||||
// token dumps provided token to stdout
|
// token dumps provided token to stdout
|
||||||
func generateToken(c *cli.Context) error {
|
func generateToken(c *cli.Context) error {
|
||||||
if err := raven.SetDSN(sentryDSN); err != nil {
|
err := sentry.Init(sentry.ClientOptions{
|
||||||
|
Dsn: sentryDSN,
|
||||||
|
Release: c.App.Version,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
appURL, err := url.Parse(ensureURLScheme(c.String("app")))
|
appURL, err := url.Parse(ensureURLScheme(c.String("app")))
|
||||||
|
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/getsentry/raven-go"
|
"github.com/getsentry/sentry-go"
|
||||||
homedir "github.com/mitchellh/go-homedir"
|
homedir "github.com/mitchellh/go-homedir"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
@ -50,7 +50,6 @@ var (
|
||||||
func main() {
|
func main() {
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
metrics.RegisterBuildInfo(BuildType, BuildTime, Version)
|
metrics.RegisterBuildInfo(BuildType, BuildTime, Version)
|
||||||
raven.SetRelease(Version)
|
|
||||||
maxprocs.Set()
|
maxprocs.Set()
|
||||||
bInfo := cliutil.GetBuildInfo(BuildType, Version)
|
bInfo := cliutil.GetBuildInfo(BuildType, Version)
|
||||||
|
|
||||||
|
@ -158,8 +157,10 @@ func action(graceShutdownC chan struct{}) cli.ActionFunc {
|
||||||
}
|
}
|
||||||
tags := make(map[string]string)
|
tags := make(map[string]string)
|
||||||
tags["hostname"] = c.String("hostname")
|
tags["hostname"] = c.String("hostname")
|
||||||
raven.SetTagsContext(tags)
|
func() {
|
||||||
raven.CapturePanic(func() { err = tunnel.TunnelCommand(c) }, nil)
|
defer sentry.Recover()
|
||||||
|
err = tunnel.TunnelCommand(c)
|
||||||
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
captureError(err)
|
captureError(err)
|
||||||
}
|
}
|
||||||
|
@ -187,7 +188,7 @@ func captureError(err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
raven.CaptureError(err, nil)
|
sentry.CaptureException(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// cloudflared was started without any flags
|
// cloudflared was started without any flags
|
||||||
|
|
|
@ -14,7 +14,7 @@ import (
|
||||||
|
|
||||||
"github.com/coreos/go-systemd/daemon"
|
"github.com/coreos/go-systemd/daemon"
|
||||||
"github.com/facebookgo/grace/gracenet"
|
"github.com/facebookgo/grace/gracenet"
|
||||||
"github.com/getsentry/raven-go"
|
"github.com/getsentry/sentry-go"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
homedir "github.com/mitchellh/go-homedir"
|
homedir "github.com/mitchellh/go-homedir"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -258,7 +258,13 @@ func StartServer(
|
||||||
namedTunnel *connection.NamedTunnelProperties,
|
namedTunnel *connection.NamedTunnelProperties,
|
||||||
log *zerolog.Logger,
|
log *zerolog.Logger,
|
||||||
) error {
|
) error {
|
||||||
_ = raven.SetDSN(sentryDSN)
|
err := sentry.Init(sentry.ClientOptions{
|
||||||
|
Dsn: sentryDSN,
|
||||||
|
Release: c.App.Version,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
listeners := gracenet.Net{}
|
listeners := gracenet.Net{}
|
||||||
errC := make(chan error)
|
errC := make(chan error)
|
||||||
|
|
13
go.mod
13
go.mod
|
@ -10,7 +10,8 @@ require (
|
||||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||||
github.com/facebookgo/grace v0.0.0-20180706040059-75cf19382434
|
github.com/facebookgo/grace v0.0.0-20180706040059-75cf19382434
|
||||||
github.com/fsnotify/fsnotify v1.4.9
|
github.com/fsnotify/fsnotify v1.4.9
|
||||||
github.com/getsentry/raven-go v0.0.0-20180517221441-ed7bcb39ff10
|
github.com/getsentry/raven-go v0.2.0
|
||||||
|
github.com/getsentry/sentry-go v0.16.0
|
||||||
github.com/gobwas/ws v1.0.4
|
github.com/gobwas/ws v1.0.4
|
||||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
|
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
|
||||||
github.com/google/gopacket v1.1.19
|
github.com/google/gopacket v1.1.19
|
||||||
|
@ -18,7 +19,7 @@ require (
|
||||||
github.com/gorilla/websocket v1.4.2
|
github.com/gorilla/websocket v1.4.2
|
||||||
github.com/json-iterator/go v1.1.12
|
github.com/json-iterator/go v1.1.12
|
||||||
github.com/lucas-clemente/quic-go v0.28.1
|
github.com/lucas-clemente/quic-go v0.28.1
|
||||||
github.com/mattn/go-colorable v0.1.8
|
github.com/mattn/go-colorable v0.1.13
|
||||||
github.com/miekg/dns v1.1.45
|
github.com/miekg/dns v1.1.45
|
||||||
github.com/mitchellh/go-homedir v1.1.0
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
|
@ -39,7 +40,7 @@ require (
|
||||||
golang.org/x/sync v0.1.0
|
golang.org/x/sync v0.1.0
|
||||||
golang.org/x/sys v0.3.0
|
golang.org/x/sys v0.3.0
|
||||||
golang.org/x/term v0.3.0
|
golang.org/x/term v0.3.0
|
||||||
google.golang.org/protobuf v1.28.0
|
google.golang.org/protobuf v1.28.1
|
||||||
gopkg.in/coreos/go-oidc.v2 v2.2.1
|
gopkg.in/coreos/go-oidc.v2 v2.2.1
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0
|
gopkg.in/square/go-jose.v2 v2.6.0
|
||||||
|
@ -48,10 +49,10 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
github.com/BurntSushi/toml v1.2.0 // indirect
|
||||||
github.com/apparentlymart/go-cidr v1.1.0 // indirect
|
github.com/apparentlymart/go-cidr v1.1.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
|
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
github.com/cheekybits/genny v1.0.0 // indirect
|
github.com/cheekybits/genny v1.0.0 // indirect
|
||||||
github.com/cloudflare/circl v1.2.1-0.20220809205628-0a9554f37a47 // indirect
|
github.com/cloudflare/circl v1.2.1-0.20220809205628-0a9554f37a47 // indirect
|
||||||
|
@ -76,7 +77,7 @@ require (
|
||||||
github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect
|
github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect
|
||||||
github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
|
github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
|
||||||
github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1 // indirect
|
github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
|
30
go.sum
30
go.sum
|
@ -77,8 +77,9 @@ github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935
|
||||||
github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc=
|
github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc=
|
||||||
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
|
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
|
||||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0=
|
||||||
|
github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
github.com/DataDog/datadog-go v4.4.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
github.com/DataDog/datadog-go v4.4.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||||
github.com/DataDog/gostackparse v0.5.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM=
|
github.com/DataDog/gostackparse v0.5.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM=
|
||||||
|
@ -112,8 +113,8 @@ github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7
|
||||||
github.com/bwesterb/go-ristretto v1.2.2/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
github.com/bwesterb/go-ristretto v1.2.2/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||||
github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 h1:JLaf/iINcLyjwbtTsCJjc6rtlASgHeIJPrB6QmwURnA=
|
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s=
|
||||||
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
|
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
|
||||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||||
|
@ -209,11 +210,14 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
|
||||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
|
github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
|
||||||
github.com/getsentry/raven-go v0.0.0-20180517221441-ed7bcb39ff10 h1:YO10pIIBftO/kkTFdWhctH96grJ7qiy7bMdiZcIvPKs=
|
github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
|
||||||
github.com/getsentry/raven-go v0.0.0-20180517221441-ed7bcb39ff10/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
||||||
|
github.com/getsentry/sentry-go v0.16.0 h1:owk+S+5XcgJLlGR/3+3s6N4d+uKwqYvh/eS0AIMjPWo=
|
||||||
|
github.com/getsentry/sentry-go v0.16.0/go.mod h1:ZXCloQLj0pG7mja5NK6NPf2V4A88YJ4pNlc2mOHwh6Y=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||||
|
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
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-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
@ -307,8 +311,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
|
||||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
@ -427,10 +431,10 @@ github.com/marten-seemann/qtls-go1-16 v0.1.5 h1:o9JrYPPco/Nukd/HpOHMHZoBDXQqoNtU
|
||||||
github.com/marten-seemann/qtls-go1-16 v0.1.5/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
|
github.com/marten-seemann/qtls-go1-16 v0.1.5/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
|
||||||
github.com/marten-seemann/qtls-go1-17 v0.1.2 h1:JADBlm0LYiVbuSySCHeY863dNkcpMmDR7s0bLKJeYlQ=
|
github.com/marten-seemann/qtls-go1-17 v0.1.2 h1:JADBlm0LYiVbuSySCHeY863dNkcpMmDR7s0bLKJeYlQ=
|
||||||
github.com/marten-seemann/qtls-go1-17 v0.1.2/go.mod h1:C2ekUKcDdz9SDWxec1N/MvcXBpaX9l3Nx67XaR84L5s=
|
github.com/marten-seemann/qtls-go1-17 v0.1.2/go.mod h1:C2ekUKcDdz9SDWxec1N/MvcXBpaX9l3Nx67XaR84L5s=
|
||||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||||
|
@ -489,6 +493,7 @@ github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ=
|
||||||
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||||
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||||
|
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
@ -810,7 +815,6 @@ golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
@ -868,6 +872,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||||
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
|
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
|
||||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
@ -1152,8 +1157,9 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
|
||||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||||
|
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
gopkg.in/DataDog/dd-trace-go.v1 v1.34.0/go.mod h1:HtrC65fyJ6lWazShCC9rlOeiTSZJ0XtZhkwjZM2WpC4=
|
gopkg.in/DataDog/dd-trace-go.v1 v1.34.0/go.mod h1:HtrC65fyJ6lWazShCC9rlOeiTSZJ0XtZhkwjZM2WpC4=
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/getsentry/raven-go"
|
"github.com/getsentry/sentry-go"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
@ -65,7 +65,7 @@ func (cr *CertReloader) LoadCert() error {
|
||||||
|
|
||||||
// Keep the old certificate if there's a problem reading the new one.
|
// Keep the old certificate if there's a problem reading the new one.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
raven.CaptureError(fmt.Errorf("Error parsing X509 key pair: %v", err), nil)
|
sentry.CaptureException(fmt.Errorf("Error parsing X509 key pair: %v", err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
cr.certificate = &cert
|
cr.certificate = &cert
|
||||||
|
|
|
@ -1,5 +1,2 @@
|
||||||
TAGS
|
/toml.test
|
||||||
tags
|
/toml-test
|
||||||
.*.swp
|
|
||||||
tomlcheck/tomlcheck
|
|
||||||
toml.test
|
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
language: go
|
|
||||||
go:
|
|
||||||
- 1.1
|
|
||||||
- 1.2
|
|
||||||
- 1.3
|
|
||||||
- 1.4
|
|
||||||
- 1.5
|
|
||||||
- 1.6
|
|
||||||
- tip
|
|
||||||
install:
|
|
||||||
- go install ./...
|
|
||||||
- go get github.com/BurntSushi/toml-test
|
|
||||||
script:
|
|
||||||
- export PATH="$PATH:$HOME/gopath/bin"
|
|
||||||
- make test
|
|
|
@ -1,3 +0,0 @@
|
||||||
Compatible with TOML version
|
|
||||||
[v0.4.0](https://github.com/toml-lang/toml/blob/v0.4.0/versions/en/toml-v0.4.0.md)
|
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
install:
|
|
||||||
go install ./...
|
|
||||||
|
|
||||||
test: install
|
|
||||||
go test -v
|
|
||||||
toml-test toml-test-decoder
|
|
||||||
toml-test -encoder toml-test-encoder
|
|
||||||
|
|
||||||
fmt:
|
|
||||||
gofmt -w *.go */*.go
|
|
||||||
colcheck *.go */*.go
|
|
||||||
|
|
||||||
tags:
|
|
||||||
find ./ -name '*.go' -print0 | xargs -0 gotags > TAGS
|
|
||||||
|
|
||||||
push:
|
|
||||||
git push origin master
|
|
||||||
git push github master
|
|
||||||
|
|
|
@ -1,46 +1,26 @@
|
||||||
## TOML parser and encoder for Go with reflection
|
|
||||||
|
|
||||||
TOML stands for Tom's Obvious, Minimal Language. This Go package provides a
|
TOML stands for Tom's Obvious, Minimal Language. This Go package provides a
|
||||||
reflection interface similar to Go's standard library `json` and `xml`
|
reflection interface similar to Go's standard library `json` and `xml` packages.
|
||||||
packages. This package also supports the `encoding.TextUnmarshaler` and
|
|
||||||
`encoding.TextMarshaler` interfaces so that you can define custom data
|
|
||||||
representations. (There is an example of this below.)
|
|
||||||
|
|
||||||
Spec: https://github.com/toml-lang/toml
|
Compatible with TOML version [v1.0.0](https://toml.io/en/v1.0.0).
|
||||||
|
|
||||||
Compatible with TOML version
|
Documentation: https://godocs.io/github.com/BurntSushi/toml
|
||||||
[v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md)
|
|
||||||
|
|
||||||
Documentation: https://godoc.org/github.com/BurntSushi/toml
|
See the [releases page](https://github.com/BurntSushi/toml/releases) for a
|
||||||
|
changelog; this information is also in the git tag annotations (e.g. `git show
|
||||||
|
v0.4.0`).
|
||||||
|
|
||||||
Installation:
|
This library requires Go 1.13 or newer; add it to your go.mod with:
|
||||||
|
|
||||||
```bash
|
% go get github.com/BurntSushi/toml@latest
|
||||||
go get github.com/BurntSushi/toml
|
|
||||||
```
|
|
||||||
|
|
||||||
Try the toml validator:
|
It also comes with a TOML validator CLI tool:
|
||||||
|
|
||||||
```bash
|
% go install github.com/BurntSushi/toml/cmd/tomlv@latest
|
||||||
go get github.com/BurntSushi/toml/cmd/tomlv
|
% tomlv some-toml-file.toml
|
||||||
tomlv some-toml-file.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
[![Build Status](https://travis-ci.org/BurntSushi/toml.svg?branch=master)](https://travis-ci.org/BurntSushi/toml) [![GoDoc](https://godoc.org/github.com/BurntSushi/toml?status.svg)](https://godoc.org/github.com/BurntSushi/toml)
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
|
|
||||||
This package passes all tests in
|
|
||||||
[toml-test](https://github.com/BurntSushi/toml-test) for both the decoder
|
|
||||||
and the encoder.
|
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
For the simplest example, consider some TOML file as just a list of keys and
|
||||||
This package works similarly to how the Go standard library handles `XML`
|
values:
|
||||||
and `JSON`. Namely, data is loaded into Go values via reflection.
|
|
||||||
|
|
||||||
For the simplest example, consider some TOML file as just a list of keys
|
|
||||||
and values:
|
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
Age = 25
|
Age = 25
|
||||||
|
@ -50,7 +30,7 @@ Perfection = [ 6, 28, 496, 8128 ]
|
||||||
DOB = 1987-07-05T05:45:00Z
|
DOB = 1987-07-05T05:45:00Z
|
||||||
```
|
```
|
||||||
|
|
||||||
Which could be defined in Go as:
|
Which can be decoded with:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@ -58,21 +38,15 @@ type Config struct {
|
||||||
Cats []string
|
Cats []string
|
||||||
Pi float64
|
Pi float64
|
||||||
Perfection []int
|
Perfection []int
|
||||||
DOB time.Time // requires `import time`
|
DOB time.Time
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
And then decoded with:
|
|
||||||
|
|
||||||
```go
|
|
||||||
var conf Config
|
var conf Config
|
||||||
if _, err := toml.Decode(tomlData, &conf); err != nil {
|
_, err := toml.Decode(tomlData, &conf)
|
||||||
// handle error
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also use struct tags if your struct field name doesn't map to a TOML
|
You can also use struct tags if your struct field name doesn't map to a TOML key
|
||||||
key value directly:
|
value directly:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
some_key_NAME = "wat"
|
some_key_NAME = "wat"
|
||||||
|
@ -84,135 +58,63 @@ type TOML struct {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using the `encoding.TextUnmarshaler` interface
|
Beware that like other decoders **only exported fields** are considered when
|
||||||
|
encoding and decoding; private fields are silently ignored.
|
||||||
|
|
||||||
Here's an example that automatically parses duration strings into
|
### Using the `Marshaler` and `encoding.TextUnmarshaler` interfaces
|
||||||
`time.Duration` values:
|
Here's an example that automatically parses values in a `mail.Address`:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[[song]]
|
contacts = [
|
||||||
name = "Thunder Road"
|
"Donald Duck <donald@duckburg.com>",
|
||||||
duration = "4m49s"
|
"Scrooge McDuck <scrooge@duckburg.com>",
|
||||||
|
|
||||||
[[song]]
|
|
||||||
name = "Stairway to Heaven"
|
|
||||||
duration = "8m03s"
|
|
||||||
```
|
|
||||||
|
|
||||||
Which can be decoded with:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type song struct {
|
|
||||||
Name string
|
|
||||||
Duration duration
|
|
||||||
}
|
|
||||||
type songs struct {
|
|
||||||
Song []song
|
|
||||||
}
|
|
||||||
var favorites songs
|
|
||||||
if _, err := toml.Decode(blob, &favorites); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range favorites.Song {
|
|
||||||
fmt.Printf("%s (%s)\n", s.Name, s.Duration)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
And you'll also need a `duration` type that satisfies the
|
|
||||||
`encoding.TextUnmarshaler` interface:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type duration struct {
|
|
||||||
time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *duration) UnmarshalText(text []byte) error {
|
|
||||||
var err error
|
|
||||||
d.Duration, err = time.ParseDuration(string(text))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### More complex usage
|
|
||||||
|
|
||||||
Here's an example of how to load the example from the official spec page:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# This is a TOML document. Boom.
|
|
||||||
|
|
||||||
title = "TOML Example"
|
|
||||||
|
|
||||||
[owner]
|
|
||||||
name = "Tom Preston-Werner"
|
|
||||||
organization = "GitHub"
|
|
||||||
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
|
|
||||||
dob = 1979-05-27T07:32:00Z # First class dates? Why not?
|
|
||||||
|
|
||||||
[database]
|
|
||||||
server = "192.168.1.1"
|
|
||||||
ports = [ 8001, 8001, 8002 ]
|
|
||||||
connection_max = 5000
|
|
||||||
enabled = true
|
|
||||||
|
|
||||||
[servers]
|
|
||||||
|
|
||||||
# You can indent as you please. Tabs or spaces. TOML don't care.
|
|
||||||
[servers.alpha]
|
|
||||||
ip = "10.0.0.1"
|
|
||||||
dc = "eqdc10"
|
|
||||||
|
|
||||||
[servers.beta]
|
|
||||||
ip = "10.0.0.2"
|
|
||||||
dc = "eqdc10"
|
|
||||||
|
|
||||||
[clients]
|
|
||||||
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
|
|
||||||
|
|
||||||
# Line breaks are OK when inside arrays
|
|
||||||
hosts = [
|
|
||||||
"alpha",
|
|
||||||
"omega"
|
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
And the corresponding Go types are:
|
Can be decoded with:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type tomlConfig struct {
|
// Create address type which satisfies the encoding.TextUnmarshaler interface.
|
||||||
Title string
|
type address struct {
|
||||||
Owner ownerInfo
|
*mail.Address
|
||||||
DB database `toml:"database"`
|
|
||||||
Servers map[string]server
|
|
||||||
Clients clients
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ownerInfo struct {
|
func (a *address) UnmarshalText(text []byte) error {
|
||||||
Name string
|
var err error
|
||||||
Org string `toml:"organization"`
|
a.Address, err = mail.ParseAddress(string(text))
|
||||||
Bio string
|
return err
|
||||||
DOB time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type database struct {
|
// Decode it.
|
||||||
Server string
|
func decode() {
|
||||||
Ports []int
|
blob := `
|
||||||
ConnMax int `toml:"connection_max"`
|
contacts = [
|
||||||
Enabled bool
|
"Donald Duck <donald@duckburg.com>",
|
||||||
|
"Scrooge McDuck <scrooge@duckburg.com>",
|
||||||
|
]
|
||||||
|
`
|
||||||
|
|
||||||
|
var contacts struct {
|
||||||
|
Contacts []address
|
||||||
}
|
}
|
||||||
|
|
||||||
type server struct {
|
_, err := toml.Decode(blob, &contacts)
|
||||||
IP string
|
if err != nil {
|
||||||
DC string
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
type clients struct {
|
for _, c := range contacts.Contacts {
|
||||||
Data [][]interface{}
|
fmt.Printf("%#v\n", c.Address)
|
||||||
Hosts []string
|
}
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// &mail.Address{Name:"Donald Duck", Address:"donald@duckburg.com"}
|
||||||
|
// &mail.Address{Name:"Scrooge McDuck", Address:"scrooge@duckburg.com"}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that a case insensitive match will be tried if an exact match can't be
|
To target TOML specifically you can implement `UnmarshalTOML` TOML interface in
|
||||||
found.
|
a similar way.
|
||||||
|
|
||||||
A working example of the above can be found in `_examples/example.{go,toml}`.
|
### More complex usage
|
||||||
|
See the [`_example/`](/_example) directory for a more complex example.
|
||||||
|
|
|
@ -1,54 +1,171 @@
|
||||||
package toml
|
package toml
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math"
|
"math"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func e(format string, args ...interface{}) error {
|
|
||||||
return fmt.Errorf("toml: "+format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshaler is the interface implemented by objects that can unmarshal a
|
// Unmarshaler is the interface implemented by objects that can unmarshal a
|
||||||
// TOML description of themselves.
|
// TOML description of themselves.
|
||||||
type Unmarshaler interface {
|
type Unmarshaler interface {
|
||||||
UnmarshalTOML(interface{}) error
|
UnmarshalTOML(interface{}) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmarshal decodes the contents of `p` in TOML format into a pointer `v`.
|
// Unmarshal decodes the contents of `data` in TOML format into a pointer `v`.
|
||||||
func Unmarshal(p []byte, v interface{}) error {
|
func Unmarshal(data []byte, v interface{}) error {
|
||||||
_, err := Decode(string(p), v)
|
_, err := NewDecoder(bytes.NewReader(data)).Decode(v)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decode the TOML data in to the pointer v.
|
||||||
|
//
|
||||||
|
// See the documentation on Decoder for a description of the decoding process.
|
||||||
|
func Decode(data string, v interface{}) (MetaData, error) {
|
||||||
|
return NewDecoder(strings.NewReader(data)).Decode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeFile is just like Decode, except it will automatically read the
|
||||||
|
// contents of the file at path and decode it for you.
|
||||||
|
func DecodeFile(path string, v interface{}) (MetaData, error) {
|
||||||
|
fp, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return MetaData{}, err
|
||||||
|
}
|
||||||
|
defer fp.Close()
|
||||||
|
return NewDecoder(fp).Decode(v)
|
||||||
|
}
|
||||||
|
|
||||||
// Primitive is a TOML value that hasn't been decoded into a Go value.
|
// Primitive is a TOML value that hasn't been decoded into a Go value.
|
||||||
// When using the various `Decode*` functions, the type `Primitive` may
|
|
||||||
// be given to any value, and its decoding will be delayed.
|
|
||||||
//
|
//
|
||||||
// A `Primitive` value can be decoded using the `PrimitiveDecode` function.
|
// This type can be used for any value, which will cause decoding to be delayed.
|
||||||
|
// You can use the PrimitiveDecode() function to "manually" decode these values.
|
||||||
//
|
//
|
||||||
// The underlying representation of a `Primitive` value is subject to change.
|
// NOTE: The underlying representation of a `Primitive` value is subject to
|
||||||
// Do not rely on it.
|
// change. Do not rely on it.
|
||||||
//
|
//
|
||||||
// N.B. Primitive values are still parsed, so using them will only avoid
|
// NOTE: Primitive values are still parsed, so using them will only avoid the
|
||||||
// the overhead of reflection. They can be useful when you don't know the
|
// overhead of reflection. They can be useful when you don't know the exact type
|
||||||
// exact type of TOML data until run time.
|
// of TOML data until runtime.
|
||||||
type Primitive struct {
|
type Primitive struct {
|
||||||
undecoded interface{}
|
undecoded interface{}
|
||||||
context Key
|
context Key
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEPRECATED!
|
// The significand precision for float32 and float64 is 24 and 53 bits; this is
|
||||||
|
// the range a natural number can be stored in a float without loss of data.
|
||||||
|
const (
|
||||||
|
maxSafeFloat32Int = 16777215 // 2^24-1
|
||||||
|
maxSafeFloat64Int = int64(9007199254740991) // 2^53-1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Decoder decodes TOML data.
|
||||||
//
|
//
|
||||||
// Use MetaData.PrimitiveDecode instead.
|
// TOML tables correspond to Go structs or maps (dealer's choice – they can be
|
||||||
func PrimitiveDecode(primValue Primitive, v interface{}) error {
|
// used interchangeably).
|
||||||
md := MetaData{decoded: make(map[string]bool)}
|
//
|
||||||
return md.unify(primValue.undecoded, rvalue(v))
|
// TOML table arrays correspond to either a slice of structs or a slice of maps.
|
||||||
|
//
|
||||||
|
// TOML datetimes correspond to Go time.Time values. Local datetimes are parsed
|
||||||
|
// in the local timezone.
|
||||||
|
//
|
||||||
|
// time.Duration types are treated as nanoseconds if the TOML value is an
|
||||||
|
// integer, or they're parsed with time.ParseDuration() if they're strings.
|
||||||
|
//
|
||||||
|
// All other TOML types (float, string, int, bool and array) correspond to the
|
||||||
|
// obvious Go types.
|
||||||
|
//
|
||||||
|
// An exception to the above rules is if a type implements the TextUnmarshaler
|
||||||
|
// interface, in which case any primitive TOML value (floats, strings, integers,
|
||||||
|
// booleans, datetimes) will be converted to a []byte and given to the value's
|
||||||
|
// UnmarshalText method. See the Unmarshaler example for a demonstration with
|
||||||
|
// email addresses.
|
||||||
|
//
|
||||||
|
// Key mapping
|
||||||
|
//
|
||||||
|
// TOML keys can map to either keys in a Go map or field names in a Go struct.
|
||||||
|
// The special `toml` struct tag can be used to map TOML keys to struct fields
|
||||||
|
// that don't match the key name exactly (see the example). A case insensitive
|
||||||
|
// match to struct names will be tried if an exact match can't be found.
|
||||||
|
//
|
||||||
|
// The mapping between TOML values and Go values is loose. That is, there may
|
||||||
|
// exist TOML values that cannot be placed into your representation, and there
|
||||||
|
// may be parts of your representation that do not correspond to TOML values.
|
||||||
|
// This loose mapping can be made stricter by using the IsDefined and/or
|
||||||
|
// Undecoded methods on the MetaData returned.
|
||||||
|
//
|
||||||
|
// This decoder does not handle cyclic types. Decode will not terminate if a
|
||||||
|
// cyclic type is passed.
|
||||||
|
type Decoder struct {
|
||||||
|
r io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDecoder creates a new Decoder.
|
||||||
|
func NewDecoder(r io.Reader) *Decoder {
|
||||||
|
return &Decoder{r: r}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
unmarshalToml = reflect.TypeOf((*Unmarshaler)(nil)).Elem()
|
||||||
|
unmarshalText = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
|
||||||
|
primitiveType = reflect.TypeOf((*Primitive)(nil)).Elem()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Decode TOML data in to the pointer `v`.
|
||||||
|
func (dec *Decoder) Decode(v interface{}) (MetaData, error) {
|
||||||
|
rv := reflect.ValueOf(v)
|
||||||
|
if rv.Kind() != reflect.Ptr {
|
||||||
|
s := "%q"
|
||||||
|
if reflect.TypeOf(v) == nil {
|
||||||
|
s = "%v"
|
||||||
|
}
|
||||||
|
|
||||||
|
return MetaData{}, fmt.Errorf("toml: cannot decode to non-pointer "+s, reflect.TypeOf(v))
|
||||||
|
}
|
||||||
|
if rv.IsNil() {
|
||||||
|
return MetaData{}, fmt.Errorf("toml: cannot decode to nil value of %q", reflect.TypeOf(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a supported type: struct, map, interface{}, or something
|
||||||
|
// that implements UnmarshalTOML or UnmarshalText.
|
||||||
|
rv = indirect(rv)
|
||||||
|
rt := rv.Type()
|
||||||
|
if rv.Kind() != reflect.Struct && rv.Kind() != reflect.Map &&
|
||||||
|
!(rv.Kind() == reflect.Interface && rv.NumMethod() == 0) &&
|
||||||
|
!rt.Implements(unmarshalToml) && !rt.Implements(unmarshalText) {
|
||||||
|
return MetaData{}, fmt.Errorf("toml: cannot decode to type %s", rt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: parser should read from io.Reader? Or at the very least, make it
|
||||||
|
// read from []byte rather than string
|
||||||
|
data, err := ioutil.ReadAll(dec.r)
|
||||||
|
if err != nil {
|
||||||
|
return MetaData{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := parse(string(data))
|
||||||
|
if err != nil {
|
||||||
|
return MetaData{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
md := MetaData{
|
||||||
|
mapping: p.mapping,
|
||||||
|
keyInfo: p.keyInfo,
|
||||||
|
keys: p.ordered,
|
||||||
|
decoded: make(map[string]struct{}, len(p.ordered)),
|
||||||
|
context: nil,
|
||||||
|
data: data,
|
||||||
|
}
|
||||||
|
return md, md.unify(p.mapping, rv)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrimitiveDecode is just like the other `Decode*` functions, except it
|
// PrimitiveDecode is just like the other `Decode*` functions, except it
|
||||||
|
@ -68,90 +185,15 @@ func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error {
|
||||||
return md.unify(primValue.undecoded, rvalue(v))
|
return md.unify(primValue.undecoded, rvalue(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode will decode the contents of `data` in TOML format into a pointer
|
|
||||||
// `v`.
|
|
||||||
//
|
|
||||||
// TOML hashes correspond to Go structs or maps. (Dealer's choice. They can be
|
|
||||||
// used interchangeably.)
|
|
||||||
//
|
|
||||||
// TOML arrays of tables correspond to either a slice of structs or a slice
|
|
||||||
// of maps.
|
|
||||||
//
|
|
||||||
// TOML datetimes correspond to Go `time.Time` values.
|
|
||||||
//
|
|
||||||
// All other TOML types (float, string, int, bool and array) correspond
|
|
||||||
// to the obvious Go types.
|
|
||||||
//
|
|
||||||
// An exception to the above rules is if a type implements the
|
|
||||||
// encoding.TextUnmarshaler interface. In this case, any primitive TOML value
|
|
||||||
// (floats, strings, integers, booleans and datetimes) will be converted to
|
|
||||||
// a byte string and given to the value's UnmarshalText method. See the
|
|
||||||
// Unmarshaler example for a demonstration with time duration strings.
|
|
||||||
//
|
|
||||||
// Key mapping
|
|
||||||
//
|
|
||||||
// TOML keys can map to either keys in a Go map or field names in a Go
|
|
||||||
// struct. The special `toml` struct tag may be used to map TOML keys to
|
|
||||||
// struct fields that don't match the key name exactly. (See the example.)
|
|
||||||
// A case insensitive match to struct names will be tried if an exact match
|
|
||||||
// can't be found.
|
|
||||||
//
|
|
||||||
// The mapping between TOML values and Go values is loose. That is, there
|
|
||||||
// may exist TOML values that cannot be placed into your representation, and
|
|
||||||
// there may be parts of your representation that do not correspond to
|
|
||||||
// TOML values. This loose mapping can be made stricter by using the IsDefined
|
|
||||||
// and/or Undecoded methods on the MetaData returned.
|
|
||||||
//
|
|
||||||
// This decoder will not handle cyclic types. If a cyclic type is passed,
|
|
||||||
// `Decode` will not terminate.
|
|
||||||
func Decode(data string, v interface{}) (MetaData, error) {
|
|
||||||
rv := reflect.ValueOf(v)
|
|
||||||
if rv.Kind() != reflect.Ptr {
|
|
||||||
return MetaData{}, e("Decode of non-pointer %s", reflect.TypeOf(v))
|
|
||||||
}
|
|
||||||
if rv.IsNil() {
|
|
||||||
return MetaData{}, e("Decode of nil %s", reflect.TypeOf(v))
|
|
||||||
}
|
|
||||||
p, err := parse(data)
|
|
||||||
if err != nil {
|
|
||||||
return MetaData{}, err
|
|
||||||
}
|
|
||||||
md := MetaData{
|
|
||||||
p.mapping, p.types, p.ordered,
|
|
||||||
make(map[string]bool, len(p.ordered)), nil,
|
|
||||||
}
|
|
||||||
return md, md.unify(p.mapping, indirect(rv))
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecodeFile is just like Decode, except it will automatically read the
|
|
||||||
// contents of the file at `fpath` and decode it for you.
|
|
||||||
func DecodeFile(fpath string, v interface{}) (MetaData, error) {
|
|
||||||
bs, err := ioutil.ReadFile(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return MetaData{}, err
|
|
||||||
}
|
|
||||||
return Decode(string(bs), v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecodeReader is just like Decode, except it will consume all bytes
|
|
||||||
// from the reader and decode it for you.
|
|
||||||
func DecodeReader(r io.Reader, v interface{}) (MetaData, error) {
|
|
||||||
bs, err := ioutil.ReadAll(r)
|
|
||||||
if err != nil {
|
|
||||||
return MetaData{}, err
|
|
||||||
}
|
|
||||||
return Decode(string(bs), v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// unify performs a sort of type unification based on the structure of `rv`,
|
// unify performs a sort of type unification based on the structure of `rv`,
|
||||||
// which is the client representation.
|
// which is the client representation.
|
||||||
//
|
//
|
||||||
// Any type mismatch produces an error. Finding a type that we don't know
|
// Any type mismatch produces an error. Finding a type that we don't know
|
||||||
// how to handle produces an unsupported type error.
|
// how to handle produces an unsupported type error.
|
||||||
func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
|
func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
|
||||||
|
|
||||||
// Special case. Look for a `Primitive` value.
|
// Special case. Look for a `Primitive` value.
|
||||||
if rv.Type() == reflect.TypeOf((*Primitive)(nil)).Elem() {
|
// TODO: #76 would make this superfluous after implemented.
|
||||||
|
if rv.Type() == primitiveType {
|
||||||
// Save the undecoded data and the key context into the primitive
|
// Save the undecoded data and the key context into the primitive
|
||||||
// value.
|
// value.
|
||||||
context := make(Key, len(md.context))
|
context := make(Key, len(md.context))
|
||||||
|
@ -163,36 +205,24 @@ func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special case. Unmarshaler Interface support.
|
rvi := rv.Interface()
|
||||||
if rv.CanAddr() {
|
if v, ok := rvi.(Unmarshaler); ok {
|
||||||
if v, ok := rv.Addr().Interface().(Unmarshaler); ok {
|
|
||||||
return v.UnmarshalTOML(data)
|
return v.UnmarshalTOML(data)
|
||||||
}
|
}
|
||||||
}
|
if v, ok := rvi.(encoding.TextUnmarshaler); ok {
|
||||||
|
|
||||||
// Special case. Handle time.Time values specifically.
|
|
||||||
// TODO: Remove this code when we decide to drop support for Go 1.1.
|
|
||||||
// This isn't necessary in Go 1.2 because time.Time satisfies the encoding
|
|
||||||
// interfaces.
|
|
||||||
if rv.Type().AssignableTo(rvalue(time.Time{}).Type()) {
|
|
||||||
return md.unifyDatetime(data, rv)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case. Look for a value satisfying the TextUnmarshaler interface.
|
|
||||||
if v, ok := rv.Interface().(TextUnmarshaler); ok {
|
|
||||||
return md.unifyText(data, v)
|
return md.unifyText(data, v)
|
||||||
}
|
}
|
||||||
// BUG(burntsushi)
|
|
||||||
|
// TODO:
|
||||||
// The behavior here is incorrect whenever a Go type satisfies the
|
// The behavior here is incorrect whenever a Go type satisfies the
|
||||||
// encoding.TextUnmarshaler interface but also corresponds to a TOML
|
// encoding.TextUnmarshaler interface but also corresponds to a TOML hash or
|
||||||
// hash or array. In particular, the unmarshaler should only be applied
|
// array. In particular, the unmarshaler should only be applied to primitive
|
||||||
// to primitive TOML values. But at this point, it will be applied to
|
// TOML values. But at this point, it will be applied to all kinds of values
|
||||||
// all kinds of values and produce an incorrect error whenever those values
|
// and produce an incorrect error whenever those values are hashes or arrays
|
||||||
// are hashes or arrays (including arrays of tables).
|
// (including arrays of tables).
|
||||||
|
|
||||||
k := rv.Kind()
|
k := rv.Kind()
|
||||||
|
|
||||||
// laziness
|
|
||||||
if k >= reflect.Int && k <= reflect.Uint64 {
|
if k >= reflect.Int && k <= reflect.Uint64 {
|
||||||
return md.unifyInt(data, rv)
|
return md.unifyInt(data, rv)
|
||||||
}
|
}
|
||||||
|
@ -218,17 +248,14 @@ func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
return md.unifyBool(data, rv)
|
return md.unifyBool(data, rv)
|
||||||
case reflect.Interface:
|
case reflect.Interface:
|
||||||
// we only support empty interfaces.
|
if rv.NumMethod() > 0 { // Only support empty interfaces are supported.
|
||||||
if rv.NumMethod() > 0 {
|
return md.e("unsupported type %s", rv.Type())
|
||||||
return e("unsupported type %s", rv.Type())
|
|
||||||
}
|
}
|
||||||
return md.unifyAnything(data, rv)
|
return md.unifyAnything(data, rv)
|
||||||
case reflect.Float32:
|
case reflect.Float32, reflect.Float64:
|
||||||
fallthrough
|
|
||||||
case reflect.Float64:
|
|
||||||
return md.unifyFloat64(data, rv)
|
return md.unifyFloat64(data, rv)
|
||||||
}
|
}
|
||||||
return e("unsupported type %s", rv.Kind())
|
return md.e("unsupported type %s", rv.Kind())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
|
func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
|
||||||
|
@ -237,7 +264,7 @@ func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
|
||||||
if mapping == nil {
|
if mapping == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return e("type mismatch for %s: expected table but found %T",
|
return md.e("type mismatch for %s: expected table but found %T",
|
||||||
rv.Type().String(), mapping)
|
rv.Type().String(), mapping)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,17 +286,18 @@ func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
|
||||||
for _, i := range f.index {
|
for _, i := range f.index {
|
||||||
subv = indirect(subv.Field(i))
|
subv = indirect(subv.Field(i))
|
||||||
}
|
}
|
||||||
|
|
||||||
if isUnifiable(subv) {
|
if isUnifiable(subv) {
|
||||||
md.decoded[md.context.add(key).String()] = true
|
md.decoded[md.context.add(key).String()] = struct{}{}
|
||||||
md.context = append(md.context, key)
|
md.context = append(md.context, key)
|
||||||
if err := md.unify(datum, subv); err != nil {
|
|
||||||
|
err := md.unify(datum, subv)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
md.context = md.context[0 : len(md.context)-1]
|
md.context = md.context[0 : len(md.context)-1]
|
||||||
} else if f.name != "" {
|
} else if f.name != "" {
|
||||||
// Bad user! No soup for you!
|
return md.e("cannot write unexported field %s.%s", rv.Type().String(), f.name)
|
||||||
return e("cannot write unexported field %s.%s",
|
|
||||||
rv.Type().String(), f.name)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -277,28 +305,43 @@ func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error {
|
func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error {
|
||||||
|
keyType := rv.Type().Key().Kind()
|
||||||
|
if keyType != reflect.String && keyType != reflect.Interface {
|
||||||
|
return fmt.Errorf("toml: cannot decode to a map with non-string key type (%s in %q)",
|
||||||
|
keyType, rv.Type())
|
||||||
|
}
|
||||||
|
|
||||||
tmap, ok := mapping.(map[string]interface{})
|
tmap, ok := mapping.(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
if tmap == nil {
|
if tmap == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return badtype("map", mapping)
|
return md.badtype("map", mapping)
|
||||||
}
|
}
|
||||||
if rv.IsNil() {
|
if rv.IsNil() {
|
||||||
rv.Set(reflect.MakeMap(rv.Type()))
|
rv.Set(reflect.MakeMap(rv.Type()))
|
||||||
}
|
}
|
||||||
for k, v := range tmap {
|
for k, v := range tmap {
|
||||||
md.decoded[md.context.add(k).String()] = true
|
md.decoded[md.context.add(k).String()] = struct{}{}
|
||||||
md.context = append(md.context, k)
|
md.context = append(md.context, k)
|
||||||
|
|
||||||
rvkey := indirect(reflect.New(rv.Type().Key()))
|
|
||||||
rvval := reflect.Indirect(reflect.New(rv.Type().Elem()))
|
rvval := reflect.Indirect(reflect.New(rv.Type().Elem()))
|
||||||
if err := md.unify(v, rvval); err != nil {
|
|
||||||
|
err := md.unify(v, indirect(rvval))
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
md.context = md.context[0 : len(md.context)-1]
|
md.context = md.context[0 : len(md.context)-1]
|
||||||
|
|
||||||
|
rvkey := indirect(reflect.New(rv.Type().Key()))
|
||||||
|
|
||||||
|
switch keyType {
|
||||||
|
case reflect.Interface:
|
||||||
|
rvkey.Set(reflect.ValueOf(k))
|
||||||
|
case reflect.String:
|
||||||
rvkey.SetString(k)
|
rvkey.SetString(k)
|
||||||
|
}
|
||||||
|
|
||||||
rv.SetMapIndex(rvkey, rvval)
|
rv.SetMapIndex(rvkey, rvval)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -310,12 +353,10 @@ func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error {
|
||||||
if !datav.IsValid() {
|
if !datav.IsValid() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return badtype("slice", data)
|
return md.badtype("slice", data)
|
||||||
}
|
}
|
||||||
sliceLen := datav.Len()
|
if l := datav.Len(); l != rv.Len() {
|
||||||
if sliceLen != rv.Len() {
|
return md.e("expected array length %d; got TOML array of length %d", rv.Len(), l)
|
||||||
return e("expected array length %d; got TOML array of length %d",
|
|
||||||
rv.Len(), sliceLen)
|
|
||||||
}
|
}
|
||||||
return md.unifySliceArray(datav, rv)
|
return md.unifySliceArray(datav, rv)
|
||||||
}
|
}
|
||||||
|
@ -326,7 +367,7 @@ func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error {
|
||||||
if !datav.IsValid() {
|
if !datav.IsValid() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return badtype("slice", data)
|
return md.badtype("slice", data)
|
||||||
}
|
}
|
||||||
n := datav.Len()
|
n := datav.Len()
|
||||||
if rv.IsNil() || rv.Cap() < n {
|
if rv.IsNil() || rv.Cap() < n {
|
||||||
|
@ -337,37 +378,45 @@ func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *MetaData) unifySliceArray(data, rv reflect.Value) error {
|
func (md *MetaData) unifySliceArray(data, rv reflect.Value) error {
|
||||||
sliceLen := data.Len()
|
l := data.Len()
|
||||||
for i := 0; i < sliceLen; i++ {
|
for i := 0; i < l; i++ {
|
||||||
v := data.Index(i).Interface()
|
err := md.unify(data.Index(i).Interface(), indirect(rv.Index(i)))
|
||||||
sliceval := indirect(rv.Index(i))
|
if err != nil {
|
||||||
if err := md.unify(v, sliceval); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *MetaData) unifyDatetime(data interface{}, rv reflect.Value) error {
|
func (md *MetaData) unifyString(data interface{}, rv reflect.Value) error {
|
||||||
if _, ok := data.(time.Time); ok {
|
_, ok := rv.Interface().(json.Number)
|
||||||
rv.Set(reflect.ValueOf(data))
|
if ok {
|
||||||
|
if i, ok := data.(int64); ok {
|
||||||
|
rv.SetString(strconv.FormatInt(i, 10))
|
||||||
|
} else if f, ok := data.(float64); ok {
|
||||||
|
rv.SetString(strconv.FormatFloat(f, 'f', -1, 64))
|
||||||
|
} else {
|
||||||
|
return md.badtype("string", data)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return badtype("time.Time", data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (md *MetaData) unifyString(data interface{}, rv reflect.Value) error {
|
|
||||||
if s, ok := data.(string); ok {
|
if s, ok := data.(string); ok {
|
||||||
rv.SetString(s)
|
rv.SetString(s)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return badtype("string", data)
|
return md.badtype("string", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error {
|
func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error {
|
||||||
|
rvk := rv.Kind()
|
||||||
|
|
||||||
if num, ok := data.(float64); ok {
|
if num, ok := data.(float64); ok {
|
||||||
switch rv.Kind() {
|
switch rvk {
|
||||||
case reflect.Float32:
|
case reflect.Float32:
|
||||||
|
if num < -math.MaxFloat32 || num > math.MaxFloat32 {
|
||||||
|
return md.parseErr(errParseRange{i: num, size: rvk.String()})
|
||||||
|
}
|
||||||
fallthrough
|
fallthrough
|
||||||
case reflect.Float64:
|
case reflect.Float64:
|
||||||
rv.SetFloat(num)
|
rv.SetFloat(num)
|
||||||
|
@ -376,62 +425,68 @@ func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return badtype("float", data)
|
|
||||||
|
if num, ok := data.(int64); ok {
|
||||||
|
if (rvk == reflect.Float32 && (num < -maxSafeFloat32Int || num > maxSafeFloat32Int)) ||
|
||||||
|
(rvk == reflect.Float64 && (num < -maxSafeFloat64Int || num > maxSafeFloat64Int)) {
|
||||||
|
return md.parseErr(errParseRange{i: num, size: rvk.String()})
|
||||||
|
}
|
||||||
|
rv.SetFloat(float64(num))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return md.badtype("float", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error {
|
func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error {
|
||||||
if num, ok := data.(int64); ok {
|
_, ok := rv.Interface().(time.Duration)
|
||||||
if rv.Kind() >= reflect.Int && rv.Kind() <= reflect.Int64 {
|
if ok {
|
||||||
switch rv.Kind() {
|
// Parse as string duration, and fall back to regular integer parsing
|
||||||
case reflect.Int, reflect.Int64:
|
// (as nanosecond) if this is not a string.
|
||||||
// No bounds checking necessary.
|
if s, ok := data.(string); ok {
|
||||||
case reflect.Int8:
|
dur, err := time.ParseDuration(s)
|
||||||
if num < math.MinInt8 || num > math.MaxInt8 {
|
if err != nil {
|
||||||
return e("value %d is out of range for int8", num)
|
return md.parseErr(errParseDuration{s})
|
||||||
}
|
}
|
||||||
case reflect.Int16:
|
rv.SetInt(int64(dur))
|
||||||
if num < math.MinInt16 || num > math.MaxInt16 {
|
return nil
|
||||||
return e("value %d is out of range for int16", num)
|
|
||||||
}
|
}
|
||||||
case reflect.Int32:
|
|
||||||
if num < math.MinInt32 || num > math.MaxInt32 {
|
|
||||||
return e("value %d is out of range for int32", num)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
num, ok := data.(int64)
|
||||||
|
if !ok {
|
||||||
|
return md.badtype("integer", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
rvk := rv.Kind()
|
||||||
|
switch {
|
||||||
|
case rvk >= reflect.Int && rvk <= reflect.Int64:
|
||||||
|
if (rvk == reflect.Int8 && (num < math.MinInt8 || num > math.MaxInt8)) ||
|
||||||
|
(rvk == reflect.Int16 && (num < math.MinInt16 || num > math.MaxInt16)) ||
|
||||||
|
(rvk == reflect.Int32 && (num < math.MinInt32 || num > math.MaxInt32)) {
|
||||||
|
return md.parseErr(errParseRange{i: num, size: rvk.String()})
|
||||||
}
|
}
|
||||||
rv.SetInt(num)
|
rv.SetInt(num)
|
||||||
} else if rv.Kind() >= reflect.Uint && rv.Kind() <= reflect.Uint64 {
|
case rvk >= reflect.Uint && rvk <= reflect.Uint64:
|
||||||
unum := uint64(num)
|
unum := uint64(num)
|
||||||
switch rv.Kind() {
|
if rvk == reflect.Uint8 && (num < 0 || unum > math.MaxUint8) ||
|
||||||
case reflect.Uint, reflect.Uint64:
|
rvk == reflect.Uint16 && (num < 0 || unum > math.MaxUint16) ||
|
||||||
// No bounds checking necessary.
|
rvk == reflect.Uint32 && (num < 0 || unum > math.MaxUint32) {
|
||||||
case reflect.Uint8:
|
return md.parseErr(errParseRange{i: num, size: rvk.String()})
|
||||||
if num < 0 || unum > math.MaxUint8 {
|
|
||||||
return e("value %d is out of range for uint8", num)
|
|
||||||
}
|
|
||||||
case reflect.Uint16:
|
|
||||||
if num < 0 || unum > math.MaxUint16 {
|
|
||||||
return e("value %d is out of range for uint16", num)
|
|
||||||
}
|
|
||||||
case reflect.Uint32:
|
|
||||||
if num < 0 || unum > math.MaxUint32 {
|
|
||||||
return e("value %d is out of range for uint32", num)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
rv.SetUint(unum)
|
rv.SetUint(unum)
|
||||||
} else {
|
default:
|
||||||
panic("unreachable")
|
panic("unreachable")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return badtype("integer", data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (md *MetaData) unifyBool(data interface{}, rv reflect.Value) error {
|
func (md *MetaData) unifyBool(data interface{}, rv reflect.Value) error {
|
||||||
if b, ok := data.(bool); ok {
|
if b, ok := data.(bool); ok {
|
||||||
rv.SetBool(b)
|
rv.SetBool(b)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return badtype("boolean", data)
|
return md.badtype("boolean", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *MetaData) unifyAnything(data interface{}, rv reflect.Value) error {
|
func (md *MetaData) unifyAnything(data interface{}, rv reflect.Value) error {
|
||||||
|
@ -439,10 +494,16 @@ func (md *MetaData) unifyAnything(data interface{}, rv reflect.Value) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *MetaData) unifyText(data interface{}, v TextUnmarshaler) error {
|
func (md *MetaData) unifyText(data interface{}, v encoding.TextUnmarshaler) error {
|
||||||
var s string
|
var s string
|
||||||
switch sdata := data.(type) {
|
switch sdata := data.(type) {
|
||||||
case TextMarshaler:
|
case Marshaler:
|
||||||
|
text, err := sdata.MarshalTOML()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s = string(text)
|
||||||
|
case encoding.TextMarshaler:
|
||||||
text, err := sdata.MarshalText()
|
text, err := sdata.MarshalText()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -459,7 +520,7 @@ func (md *MetaData) unifyText(data interface{}, v TextUnmarshaler) error {
|
||||||
case float64:
|
case float64:
|
||||||
s = fmt.Sprintf("%f", sdata)
|
s = fmt.Sprintf("%f", sdata)
|
||||||
default:
|
default:
|
||||||
return badtype("primitive (string-like)", data)
|
return md.badtype("primitive (string-like)", data)
|
||||||
}
|
}
|
||||||
if err := v.UnmarshalText([]byte(s)); err != nil {
|
if err := v.UnmarshalText([]byte(s)); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -467,22 +528,54 @@ func (md *MetaData) unifyText(data interface{}, v TextUnmarshaler) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (md *MetaData) badtype(dst string, data interface{}) error {
|
||||||
|
return md.e("incompatible types: TOML value has type %T; destination has type %s", data, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *MetaData) parseErr(err error) error {
|
||||||
|
k := md.context.String()
|
||||||
|
return ParseError{
|
||||||
|
LastKey: k,
|
||||||
|
Position: md.keyInfo[k].pos,
|
||||||
|
Line: md.keyInfo[k].pos.Line,
|
||||||
|
err: err,
|
||||||
|
input: string(md.data),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *MetaData) e(format string, args ...interface{}) error {
|
||||||
|
f := "toml: "
|
||||||
|
if len(md.context) > 0 {
|
||||||
|
f = fmt.Sprintf("toml: (last key %q): ", md.context)
|
||||||
|
p := md.keyInfo[md.context.String()].pos
|
||||||
|
if p.Line > 0 {
|
||||||
|
f = fmt.Sprintf("toml: line %d (last key %q): ", p.Line, md.context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf(f+format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
// rvalue returns a reflect.Value of `v`. All pointers are resolved.
|
// rvalue returns a reflect.Value of `v`. All pointers are resolved.
|
||||||
func rvalue(v interface{}) reflect.Value {
|
func rvalue(v interface{}) reflect.Value {
|
||||||
return indirect(reflect.ValueOf(v))
|
return indirect(reflect.ValueOf(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
// indirect returns the value pointed to by a pointer.
|
// indirect returns the value pointed to by a pointer.
|
||||||
// Pointers are followed until the value is not a pointer.
|
|
||||||
// New values are allocated for each nil pointer.
|
|
||||||
//
|
//
|
||||||
// An exception to this rule is if the value satisfies an interface of
|
// Pointers are followed until the value is not a pointer. New values are
|
||||||
// interest to us (like encoding.TextUnmarshaler).
|
// allocated for each nil pointer.
|
||||||
|
//
|
||||||
|
// An exception to this rule is if the value satisfies an interface of interest
|
||||||
|
// to us (like encoding.TextUnmarshaler).
|
||||||
func indirect(v reflect.Value) reflect.Value {
|
func indirect(v reflect.Value) reflect.Value {
|
||||||
if v.Kind() != reflect.Ptr {
|
if v.Kind() != reflect.Ptr {
|
||||||
if v.CanSet() {
|
if v.CanSet() {
|
||||||
pv := v.Addr()
|
pv := v.Addr()
|
||||||
if _, ok := pv.Interface().(TextUnmarshaler); ok {
|
pvi := pv.Interface()
|
||||||
|
if _, ok := pvi.(encoding.TextUnmarshaler); ok {
|
||||||
|
return pv
|
||||||
|
}
|
||||||
|
if _, ok := pvi.(Unmarshaler); ok {
|
||||||
return pv
|
return pv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -498,12 +591,12 @@ func isUnifiable(rv reflect.Value) bool {
|
||||||
if rv.CanSet() {
|
if rv.CanSet() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if _, ok := rv.Interface().(TextUnmarshaler); ok {
|
rvi := rv.Interface()
|
||||||
|
if _, ok := rvi.(encoding.TextUnmarshaler); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, ok := rvi.(Unmarshaler); ok {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func badtype(expected string, data interface{}) error {
|
|
||||||
return e("cannot load TOML value of type %T into a Go %s", data, expected)
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
//go:build go1.16
|
||||||
|
// +build go1.16
|
||||||
|
|
||||||
|
package toml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DecodeFS is just like Decode, except it will automatically read the contents
|
||||||
|
// of the file at `path` from a fs.FS instance.
|
||||||
|
func DecodeFS(fsys fs.FS, path string, v interface{}) (MetaData, error) {
|
||||||
|
fp, err := fsys.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return MetaData{}, err
|
||||||
|
}
|
||||||
|
defer fp.Close()
|
||||||
|
return NewDecoder(fp).Decode(v)
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package toml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Deprecated: use encoding.TextMarshaler
|
||||||
|
type TextMarshaler encoding.TextMarshaler
|
||||||
|
|
||||||
|
// Deprecated: use encoding.TextUnmarshaler
|
||||||
|
type TextUnmarshaler encoding.TextUnmarshaler
|
||||||
|
|
||||||
|
// Deprecated: use MetaData.PrimitiveDecode.
|
||||||
|
func PrimitiveDecode(primValue Primitive, v interface{}) error {
|
||||||
|
md := MetaData{decoded: make(map[string]struct{})}
|
||||||
|
return md.unify(primValue.undecoded, rvalue(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: use NewDecoder(reader).Decode(&value).
|
||||||
|
func DecodeReader(r io.Reader, v interface{}) (MetaData, error) { return NewDecoder(r).Decode(v) }
|
|
@ -1,27 +1,13 @@
|
||||||
/*
|
/*
|
||||||
Package toml provides facilities for decoding and encoding TOML configuration
|
Package toml implements decoding and encoding of TOML files.
|
||||||
files via reflection. There is also support for delaying decoding with
|
|
||||||
the Primitive type, and querying the set of keys in a TOML document with the
|
|
||||||
MetaData type.
|
|
||||||
|
|
||||||
The specification implemented: https://github.com/toml-lang/toml
|
This package supports TOML v1.0.0, as listed on https://toml.io
|
||||||
|
|
||||||
The sub-command github.com/BurntSushi/toml/cmd/tomlv can be used to verify
|
There is also support for delaying decoding with the Primitive type, and
|
||||||
whether a file is a valid TOML document. It can also be used to print the
|
querying the set of keys in a TOML document with the MetaData type.
|
||||||
type of each key in a TOML document.
|
|
||||||
|
|
||||||
Testing
|
The github.com/BurntSushi/toml/cmd/tomlv package implements a TOML validator,
|
||||||
|
and can be used to verify if TOML document is valid. It can also be used to
|
||||||
There are two important types of tests used for this package. The first is
|
print the type of each key.
|
||||||
contained inside '*_test.go' files and uses the standard Go unit testing
|
|
||||||
framework. These tests are primarily devoted to holistically testing the
|
|
||||||
decoder and encoder.
|
|
||||||
|
|
||||||
The second type of testing is used to verify the implementation's adherence
|
|
||||||
to the TOML specification. These tests have been factored into their own
|
|
||||||
project: https://github.com/BurntSushi/toml-test
|
|
||||||
|
|
||||||
The reason the tests are in a separate project is so that they can be used by
|
|
||||||
any implementation of TOML. Namely, it is language agnostic.
|
|
||||||
*/
|
*/
|
||||||
package toml
|
package toml
|
||||||
|
|
|
@ -2,57 +2,127 @@ package toml
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"encoding"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
type tomlEncodeError struct{ error }
|
type tomlEncodeError struct{ error }
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errArrayMixedElementTypes = errors.New(
|
errArrayNilElement = errors.New("toml: cannot encode array with nil element")
|
||||||
"toml: cannot encode array with mixed element types")
|
errNonString = errors.New("toml: cannot encode a map with non-string key type")
|
||||||
errArrayNilElement = errors.New(
|
errNoKey = errors.New("toml: top-level values must be Go maps or structs")
|
||||||
"toml: cannot encode array with nil element")
|
|
||||||
errNonString = errors.New(
|
|
||||||
"toml: cannot encode a map with non-string key type")
|
|
||||||
errAnonNonStruct = errors.New(
|
|
||||||
"toml: cannot encode an anonymous field that is not a struct")
|
|
||||||
errArrayNoTable = errors.New(
|
|
||||||
"toml: TOML array element cannot contain a table")
|
|
||||||
errNoKey = errors.New(
|
|
||||||
"toml: top-level values must be Go maps or structs")
|
|
||||||
errAnything = errors.New("") // used in testing
|
errAnything = errors.New("") // used in testing
|
||||||
)
|
)
|
||||||
|
|
||||||
var quotedReplacer = strings.NewReplacer(
|
var dblQuotedReplacer = strings.NewReplacer(
|
||||||
"\t", "\\t",
|
|
||||||
"\n", "\\n",
|
|
||||||
"\r", "\\r",
|
|
||||||
"\"", "\\\"",
|
"\"", "\\\"",
|
||||||
"\\", "\\\\",
|
"\\", "\\\\",
|
||||||
|
"\x00", `\u0000`,
|
||||||
|
"\x01", `\u0001`,
|
||||||
|
"\x02", `\u0002`,
|
||||||
|
"\x03", `\u0003`,
|
||||||
|
"\x04", `\u0004`,
|
||||||
|
"\x05", `\u0005`,
|
||||||
|
"\x06", `\u0006`,
|
||||||
|
"\x07", `\u0007`,
|
||||||
|
"\b", `\b`,
|
||||||
|
"\t", `\t`,
|
||||||
|
"\n", `\n`,
|
||||||
|
"\x0b", `\u000b`,
|
||||||
|
"\f", `\f`,
|
||||||
|
"\r", `\r`,
|
||||||
|
"\x0e", `\u000e`,
|
||||||
|
"\x0f", `\u000f`,
|
||||||
|
"\x10", `\u0010`,
|
||||||
|
"\x11", `\u0011`,
|
||||||
|
"\x12", `\u0012`,
|
||||||
|
"\x13", `\u0013`,
|
||||||
|
"\x14", `\u0014`,
|
||||||
|
"\x15", `\u0015`,
|
||||||
|
"\x16", `\u0016`,
|
||||||
|
"\x17", `\u0017`,
|
||||||
|
"\x18", `\u0018`,
|
||||||
|
"\x19", `\u0019`,
|
||||||
|
"\x1a", `\u001a`,
|
||||||
|
"\x1b", `\u001b`,
|
||||||
|
"\x1c", `\u001c`,
|
||||||
|
"\x1d", `\u001d`,
|
||||||
|
"\x1e", `\u001e`,
|
||||||
|
"\x1f", `\u001f`,
|
||||||
|
"\x7f", `\u007f`,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Encoder controls the encoding of Go values to a TOML document to some
|
var (
|
||||||
// io.Writer.
|
marshalToml = reflect.TypeOf((*Marshaler)(nil)).Elem()
|
||||||
//
|
marshalText = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
|
||||||
// The indentation level can be controlled with the Indent field.
|
timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
|
||||||
type Encoder struct {
|
)
|
||||||
// A single indentation level. By default it is two spaces.
|
|
||||||
Indent string
|
|
||||||
|
|
||||||
// hasWritten is whether we have written any output to w yet.
|
// Marshaler is the interface implemented by types that can marshal themselves
|
||||||
hasWritten bool
|
// into valid TOML.
|
||||||
w *bufio.Writer
|
type Marshaler interface {
|
||||||
|
MarshalTOML() ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEncoder returns a TOML encoder that encodes Go values to the io.Writer
|
// Encoder encodes a Go to a TOML document.
|
||||||
// given. By default, a single indentation level is 2 spaces.
|
//
|
||||||
|
// The mapping between Go values and TOML values should be precisely the same as
|
||||||
|
// for the Decode* functions.
|
||||||
|
//
|
||||||
|
// time.Time is encoded as a RFC 3339 string, and time.Duration as its string
|
||||||
|
// representation.
|
||||||
|
//
|
||||||
|
// The toml.Marshaler and encoder.TextMarshaler interfaces are supported to
|
||||||
|
// encoding the value as custom TOML.
|
||||||
|
//
|
||||||
|
// If you want to write arbitrary binary data then you will need to use
|
||||||
|
// something like base64 since TOML does not have any binary types.
|
||||||
|
//
|
||||||
|
// When encoding TOML hashes (Go maps or structs), keys without any sub-hashes
|
||||||
|
// are encoded first.
|
||||||
|
//
|
||||||
|
// Go maps will be sorted alphabetically by key for deterministic output.
|
||||||
|
//
|
||||||
|
// The toml struct tag can be used to provide the key name; if omitted the
|
||||||
|
// struct field name will be used. If the "omitempty" option is present the
|
||||||
|
// following value will be skipped:
|
||||||
|
//
|
||||||
|
// - arrays, slices, maps, and string with len of 0
|
||||||
|
// - struct with all zero values
|
||||||
|
// - bool false
|
||||||
|
//
|
||||||
|
// If omitzero is given all int and float types with a value of 0 will be
|
||||||
|
// skipped.
|
||||||
|
//
|
||||||
|
// Encoding Go values without a corresponding TOML representation will return an
|
||||||
|
// error. Examples of this includes maps with non-string keys, slices with nil
|
||||||
|
// elements, embedded non-struct types, and nested slices containing maps or
|
||||||
|
// structs. (e.g. [][]map[string]string is not allowed but []map[string]string
|
||||||
|
// is okay, as is []map[string][]string).
|
||||||
|
//
|
||||||
|
// NOTE: only exported keys are encoded due to the use of reflection. Unexported
|
||||||
|
// keys are silently discarded.
|
||||||
|
type Encoder struct {
|
||||||
|
// String to use for a single indentation level; default is two spaces.
|
||||||
|
Indent string
|
||||||
|
|
||||||
|
w *bufio.Writer
|
||||||
|
hasWritten bool // written any output to w yet?
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEncoder create a new Encoder.
|
||||||
func NewEncoder(w io.Writer) *Encoder {
|
func NewEncoder(w io.Writer) *Encoder {
|
||||||
return &Encoder{
|
return &Encoder{
|
||||||
w: bufio.NewWriter(w),
|
w: bufio.NewWriter(w),
|
||||||
|
@ -60,29 +130,10 @@ func NewEncoder(w io.Writer) *Encoder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode writes a TOML representation of the Go value to the underlying
|
// Encode writes a TOML representation of the Go value to the Encoder's writer.
|
||||||
// io.Writer. If the value given cannot be encoded to a valid TOML document,
|
|
||||||
// then an error is returned.
|
|
||||||
//
|
//
|
||||||
// The mapping between Go values and TOML values should be precisely the same
|
// An error is returned if the value given cannot be encoded to a valid TOML
|
||||||
// as for the Decode* functions. Similarly, the TextMarshaler interface is
|
// document.
|
||||||
// supported by encoding the resulting bytes as strings. (If you want to write
|
|
||||||
// arbitrary binary data then you will need to use something like base64 since
|
|
||||||
// TOML does not have any binary types.)
|
|
||||||
//
|
|
||||||
// When encoding TOML hashes (i.e., Go maps or structs), keys without any
|
|
||||||
// sub-hashes are encoded first.
|
|
||||||
//
|
|
||||||
// If a Go map is encoded, then its keys are sorted alphabetically for
|
|
||||||
// deterministic output. More control over this behavior may be provided if
|
|
||||||
// there is demand for it.
|
|
||||||
//
|
|
||||||
// Encoding Go values without a corresponding TOML representation---like map
|
|
||||||
// types with non-string keys---will cause an error to be returned. Similarly
|
|
||||||
// for mixed arrays/slices, arrays/slices with nil elements, embedded
|
|
||||||
// non-struct types and nested slices containing maps or structs.
|
|
||||||
// (e.g., [][]map[string]string is not allowed but []map[string]string is OK
|
|
||||||
// and so is []map[string][]string.)
|
|
||||||
func (enc *Encoder) Encode(v interface{}) error {
|
func (enc *Encoder) Encode(v interface{}) error {
|
||||||
rv := eindirect(reflect.ValueOf(v))
|
rv := eindirect(reflect.ValueOf(v))
|
||||||
if err := enc.safeEncode(Key([]string{}), rv); err != nil {
|
if err := enc.safeEncode(Key([]string{}), rv); err != nil {
|
||||||
|
@ -106,13 +157,15 @@ func (enc *Encoder) safeEncode(key Key, rv reflect.Value) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (enc *Encoder) encode(key Key, rv reflect.Value) {
|
func (enc *Encoder) encode(key Key, rv reflect.Value) {
|
||||||
// Special case. Time needs to be in ISO8601 format.
|
// If we can marshal the type to text, then we use that. This prevents the
|
||||||
// Special case. If we can marshal the type to text, then we used that.
|
// encoder for handling these types as generic structs (or whatever the
|
||||||
// Basically, this prevents the encoder for handling these types as
|
// underlying type of a TextMarshaler is).
|
||||||
// generic structs (or whatever the underlying type of a TextMarshaler is).
|
switch {
|
||||||
switch rv.Interface().(type) {
|
case isMarshaler(rv):
|
||||||
case time.Time, TextMarshaler:
|
enc.writeKeyValue(key, rv, false)
|
||||||
enc.keyEqElement(key, rv)
|
return
|
||||||
|
case rv.Type() == primitiveType: // TODO: #76 would make this superfluous after implemented.
|
||||||
|
enc.encode(key, reflect.ValueOf(rv.Interface().(Primitive).undecoded))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,12 +176,12 @@ func (enc *Encoder) encode(key Key, rv reflect.Value) {
|
||||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
|
||||||
reflect.Uint64,
|
reflect.Uint64,
|
||||||
reflect.Float32, reflect.Float64, reflect.String, reflect.Bool:
|
reflect.Float32, reflect.Float64, reflect.String, reflect.Bool:
|
||||||
enc.keyEqElement(key, rv)
|
enc.writeKeyValue(key, rv, false)
|
||||||
case reflect.Array, reflect.Slice:
|
case reflect.Array, reflect.Slice:
|
||||||
if typeEqual(tomlArrayHash, tomlTypeOfGo(rv)) {
|
if typeEqual(tomlArrayHash, tomlTypeOfGo(rv)) {
|
||||||
enc.eArrayOfTables(key, rv)
|
enc.eArrayOfTables(key, rv)
|
||||||
} else {
|
} else {
|
||||||
enc.keyEqElement(key, rv)
|
enc.writeKeyValue(key, rv, false)
|
||||||
}
|
}
|
||||||
case reflect.Interface:
|
case reflect.Interface:
|
||||||
if rv.IsNil() {
|
if rv.IsNil() {
|
||||||
|
@ -148,55 +201,114 @@ func (enc *Encoder) encode(key Key, rv reflect.Value) {
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
enc.eTable(key, rv)
|
enc.eTable(key, rv)
|
||||||
default:
|
default:
|
||||||
panic(e("unsupported type for key '%s': %s", key, k))
|
encPanic(fmt.Errorf("unsupported type for key '%s': %s", key, k))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eElement encodes any value that can be an array element (primitives and
|
// eElement encodes any value that can be an array element.
|
||||||
// arrays).
|
|
||||||
func (enc *Encoder) eElement(rv reflect.Value) {
|
func (enc *Encoder) eElement(rv reflect.Value) {
|
||||||
switch v := rv.Interface().(type) {
|
switch v := rv.Interface().(type) {
|
||||||
case time.Time:
|
case time.Time: // Using TextMarshaler adds extra quotes, which we don't want.
|
||||||
// Special case time.Time as a primitive. Has to come before
|
format := time.RFC3339Nano
|
||||||
// TextMarshaler below because time.Time implements
|
switch v.Location() {
|
||||||
// encoding.TextMarshaler, but we need to always use UTC.
|
case internal.LocalDatetime:
|
||||||
enc.wf(v.UTC().Format("2006-01-02T15:04:05Z"))
|
format = "2006-01-02T15:04:05.999999999"
|
||||||
|
case internal.LocalDate:
|
||||||
|
format = "2006-01-02"
|
||||||
|
case internal.LocalTime:
|
||||||
|
format = "15:04:05.999999999"
|
||||||
|
}
|
||||||
|
switch v.Location() {
|
||||||
|
default:
|
||||||
|
enc.wf(v.Format(format))
|
||||||
|
case internal.LocalDatetime, internal.LocalDate, internal.LocalTime:
|
||||||
|
enc.wf(v.In(time.UTC).Format(format))
|
||||||
|
}
|
||||||
return
|
return
|
||||||
case TextMarshaler:
|
case Marshaler:
|
||||||
// Special case. Use text marshaler if it's available for this value.
|
s, err := v.MarshalTOML()
|
||||||
if s, err := v.MarshalText(); err != nil {
|
if err != nil {
|
||||||
encPanic(err)
|
encPanic(err)
|
||||||
} else {
|
|
||||||
enc.writeQuoted(string(s))
|
|
||||||
}
|
}
|
||||||
|
if s == nil {
|
||||||
|
encPanic(errors.New("MarshalTOML returned nil and no error"))
|
||||||
|
}
|
||||||
|
enc.w.Write(s)
|
||||||
|
return
|
||||||
|
case encoding.TextMarshaler:
|
||||||
|
s, err := v.MarshalText()
|
||||||
|
if err != nil {
|
||||||
|
encPanic(err)
|
||||||
|
}
|
||||||
|
if s == nil {
|
||||||
|
encPanic(errors.New("MarshalText returned nil and no error"))
|
||||||
|
}
|
||||||
|
enc.writeQuoted(string(s))
|
||||||
|
return
|
||||||
|
case time.Duration:
|
||||||
|
enc.writeQuoted(v.String())
|
||||||
|
return
|
||||||
|
case json.Number:
|
||||||
|
n, _ := rv.Interface().(json.Number)
|
||||||
|
|
||||||
|
if n == "" { /// Useful zero value.
|
||||||
|
enc.w.WriteByte('0')
|
||||||
|
return
|
||||||
|
} else if v, err := n.Int64(); err == nil {
|
||||||
|
enc.eElement(reflect.ValueOf(v))
|
||||||
|
return
|
||||||
|
} else if v, err := n.Float64(); err == nil {
|
||||||
|
enc.eElement(reflect.ValueOf(v))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
encPanic(errors.New(fmt.Sprintf("Unable to convert \"%s\" to neither int64 nor float64", n)))
|
||||||
|
}
|
||||||
|
|
||||||
switch rv.Kind() {
|
switch rv.Kind() {
|
||||||
case reflect.Bool:
|
case reflect.Ptr:
|
||||||
enc.wf(strconv.FormatBool(rv.Bool()))
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
|
|
||||||
reflect.Int64:
|
|
||||||
enc.wf(strconv.FormatInt(rv.Int(), 10))
|
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16,
|
|
||||||
reflect.Uint32, reflect.Uint64:
|
|
||||||
enc.wf(strconv.FormatUint(rv.Uint(), 10))
|
|
||||||
case reflect.Float32:
|
|
||||||
enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 32)))
|
|
||||||
case reflect.Float64:
|
|
||||||
enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 64)))
|
|
||||||
case reflect.Array, reflect.Slice:
|
|
||||||
enc.eArrayOrSliceElement(rv)
|
|
||||||
case reflect.Interface:
|
|
||||||
enc.eElement(rv.Elem())
|
enc.eElement(rv.Elem())
|
||||||
|
return
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
enc.writeQuoted(rv.String())
|
enc.writeQuoted(rv.String())
|
||||||
|
case reflect.Bool:
|
||||||
|
enc.wf(strconv.FormatBool(rv.Bool()))
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
enc.wf(strconv.FormatInt(rv.Int(), 10))
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
enc.wf(strconv.FormatUint(rv.Uint(), 10))
|
||||||
|
case reflect.Float32:
|
||||||
|
f := rv.Float()
|
||||||
|
if math.IsNaN(f) {
|
||||||
|
enc.wf("nan")
|
||||||
|
} else if math.IsInf(f, 0) {
|
||||||
|
enc.wf("%cinf", map[bool]byte{true: '-', false: '+'}[math.Signbit(f)])
|
||||||
|
} else {
|
||||||
|
enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 32)))
|
||||||
|
}
|
||||||
|
case reflect.Float64:
|
||||||
|
f := rv.Float()
|
||||||
|
if math.IsNaN(f) {
|
||||||
|
enc.wf("nan")
|
||||||
|
} else if math.IsInf(f, 0) {
|
||||||
|
enc.wf("%cinf", map[bool]byte{true: '-', false: '+'}[math.Signbit(f)])
|
||||||
|
} else {
|
||||||
|
enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 64)))
|
||||||
|
}
|
||||||
|
case reflect.Array, reflect.Slice:
|
||||||
|
enc.eArrayOrSliceElement(rv)
|
||||||
|
case reflect.Struct:
|
||||||
|
enc.eStruct(nil, rv, true)
|
||||||
|
case reflect.Map:
|
||||||
|
enc.eMap(nil, rv, true)
|
||||||
|
case reflect.Interface:
|
||||||
|
enc.eElement(rv.Elem())
|
||||||
default:
|
default:
|
||||||
panic(e("unexpected primitive type: %s", rv.Kind()))
|
encPanic(fmt.Errorf("unexpected type: %T", rv.Interface()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// By the TOML spec, all floats must have a decimal with at least one
|
// By the TOML spec, all floats must have a decimal with at least one number on
|
||||||
// number on either side.
|
// either side.
|
||||||
func floatAddDecimal(fstr string) string {
|
func floatAddDecimal(fstr string) string {
|
||||||
if !strings.Contains(fstr, ".") {
|
if !strings.Contains(fstr, ".") {
|
||||||
return fstr + ".0"
|
return fstr + ".0"
|
||||||
|
@ -205,14 +317,14 @@ func floatAddDecimal(fstr string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (enc *Encoder) writeQuoted(s string) {
|
func (enc *Encoder) writeQuoted(s string) {
|
||||||
enc.wf("\"%s\"", quotedReplacer.Replace(s))
|
enc.wf("\"%s\"", dblQuotedReplacer.Replace(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) {
|
func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) {
|
||||||
length := rv.Len()
|
length := rv.Len()
|
||||||
enc.wf("[")
|
enc.wf("[")
|
||||||
for i := 0; i < length; i++ {
|
for i := 0; i < length; i++ {
|
||||||
elem := rv.Index(i)
|
elem := eindirect(rv.Index(i))
|
||||||
enc.eElement(elem)
|
enc.eElement(elem)
|
||||||
if i != length-1 {
|
if i != length-1 {
|
||||||
enc.wf(", ")
|
enc.wf(", ")
|
||||||
|
@ -226,44 +338,43 @@ func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
|
||||||
encPanic(errNoKey)
|
encPanic(errNoKey)
|
||||||
}
|
}
|
||||||
for i := 0; i < rv.Len(); i++ {
|
for i := 0; i < rv.Len(); i++ {
|
||||||
trv := rv.Index(i)
|
trv := eindirect(rv.Index(i))
|
||||||
if isNil(trv) {
|
if isNil(trv) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
panicIfInvalidKey(key)
|
|
||||||
enc.newline()
|
enc.newline()
|
||||||
enc.wf("%s[[%s]]", enc.indentStr(key), key.maybeQuotedAll())
|
enc.wf("%s[[%s]]", enc.indentStr(key), key)
|
||||||
enc.newline()
|
enc.newline()
|
||||||
enc.eMapOrStruct(key, trv)
|
enc.eMapOrStruct(key, trv, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (enc *Encoder) eTable(key Key, rv reflect.Value) {
|
func (enc *Encoder) eTable(key Key, rv reflect.Value) {
|
||||||
panicIfInvalidKey(key)
|
|
||||||
if len(key) == 1 {
|
if len(key) == 1 {
|
||||||
// Output an extra newline between top-level tables.
|
// Output an extra newline between top-level tables.
|
||||||
// (The newline isn't written if nothing else has been written though.)
|
// (The newline isn't written if nothing else has been written though.)
|
||||||
enc.newline()
|
enc.newline()
|
||||||
}
|
}
|
||||||
if len(key) > 0 {
|
if len(key) > 0 {
|
||||||
enc.wf("%s[%s]", enc.indentStr(key), key.maybeQuotedAll())
|
enc.wf("%s[%s]", enc.indentStr(key), key)
|
||||||
enc.newline()
|
enc.newline()
|
||||||
}
|
}
|
||||||
enc.eMapOrStruct(key, rv)
|
enc.eMapOrStruct(key, rv, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value) {
|
func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value, inline bool) {
|
||||||
switch rv := eindirect(rv); rv.Kind() {
|
switch rv.Kind() {
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
enc.eMap(key, rv)
|
enc.eMap(key, rv, inline)
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
enc.eStruct(key, rv)
|
enc.eStruct(key, rv, inline)
|
||||||
default:
|
default:
|
||||||
|
// Should never happen?
|
||||||
panic("eTable: unhandled reflect.Value Kind: " + rv.Kind().String())
|
panic("eTable: unhandled reflect.Value Kind: " + rv.Kind().String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (enc *Encoder) eMap(key Key, rv reflect.Value) {
|
func (enc *Encoder) eMap(key Key, rv reflect.Value, inline bool) {
|
||||||
rt := rv.Type()
|
rt := rv.Type()
|
||||||
if rt.Key().Kind() != reflect.String {
|
if rt.Key().Kind() != reflect.String {
|
||||||
encPanic(errNonString)
|
encPanic(errNonString)
|
||||||
|
@ -274,118 +385,179 @@ func (enc *Encoder) eMap(key Key, rv reflect.Value) {
|
||||||
var mapKeysDirect, mapKeysSub []string
|
var mapKeysDirect, mapKeysSub []string
|
||||||
for _, mapKey := range rv.MapKeys() {
|
for _, mapKey := range rv.MapKeys() {
|
||||||
k := mapKey.String()
|
k := mapKey.String()
|
||||||
if typeIsHash(tomlTypeOfGo(rv.MapIndex(mapKey))) {
|
if typeIsTable(tomlTypeOfGo(eindirect(rv.MapIndex(mapKey)))) {
|
||||||
mapKeysSub = append(mapKeysSub, k)
|
mapKeysSub = append(mapKeysSub, k)
|
||||||
} else {
|
} else {
|
||||||
mapKeysDirect = append(mapKeysDirect, k)
|
mapKeysDirect = append(mapKeysDirect, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var writeMapKeys = func(mapKeys []string) {
|
var writeMapKeys = func(mapKeys []string, trailC bool) {
|
||||||
sort.Strings(mapKeys)
|
sort.Strings(mapKeys)
|
||||||
for _, mapKey := range mapKeys {
|
for i, mapKey := range mapKeys {
|
||||||
mrv := rv.MapIndex(reflect.ValueOf(mapKey))
|
val := eindirect(rv.MapIndex(reflect.ValueOf(mapKey)))
|
||||||
if isNil(mrv) {
|
if isNil(val) {
|
||||||
// Don't write anything for nil fields.
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
enc.encode(key.add(mapKey), mrv)
|
|
||||||
|
if inline {
|
||||||
|
enc.writeKeyValue(Key{mapKey}, val, true)
|
||||||
|
if trailC || i != len(mapKeys)-1 {
|
||||||
|
enc.wf(", ")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
enc.encode(key.add(mapKey), val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
writeMapKeys(mapKeysDirect)
|
|
||||||
writeMapKeys(mapKeysSub)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (enc *Encoder) eStruct(key Key, rv reflect.Value) {
|
if inline {
|
||||||
|
enc.wf("{")
|
||||||
|
}
|
||||||
|
writeMapKeys(mapKeysDirect, len(mapKeysSub) > 0)
|
||||||
|
writeMapKeys(mapKeysSub, false)
|
||||||
|
if inline {
|
||||||
|
enc.wf("}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const is32Bit = (32 << (^uint(0) >> 63)) == 32
|
||||||
|
|
||||||
|
func pointerTo(t reflect.Type) reflect.Type {
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
return pointerTo(t.Elem())
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) {
|
||||||
// Write keys for fields directly under this key first, because if we write
|
// Write keys for fields directly under this key first, because if we write
|
||||||
// a field that creates a new table, then all keys under it will be in that
|
// a field that creates a new table then all keys under it will be in that
|
||||||
// table (not the one we're writing here).
|
// table (not the one we're writing here).
|
||||||
rt := rv.Type()
|
//
|
||||||
var fieldsDirect, fieldsSub [][]int
|
// Fields is a [][]int: for fieldsDirect this always has one entry (the
|
||||||
var addFields func(rt reflect.Type, rv reflect.Value, start []int)
|
// struct index). For fieldsSub it contains two entries: the parent field
|
||||||
|
// index from tv, and the field indexes for the fields of the sub.
|
||||||
|
var (
|
||||||
|
rt = rv.Type()
|
||||||
|
fieldsDirect, fieldsSub [][]int
|
||||||
|
addFields func(rt reflect.Type, rv reflect.Value, start []int)
|
||||||
|
)
|
||||||
addFields = func(rt reflect.Type, rv reflect.Value, start []int) {
|
addFields = func(rt reflect.Type, rv reflect.Value, start []int) {
|
||||||
for i := 0; i < rt.NumField(); i++ {
|
for i := 0; i < rt.NumField(); i++ {
|
||||||
f := rt.Field(i)
|
f := rt.Field(i)
|
||||||
// skip unexported fields
|
isEmbed := f.Anonymous && pointerTo(f.Type).Kind() == reflect.Struct
|
||||||
if f.PkgPath != "" && !f.Anonymous {
|
if f.PkgPath != "" && !isEmbed { /// Skip unexported fields.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
frv := rv.Field(i)
|
opts := getOptions(f.Tag)
|
||||||
if f.Anonymous {
|
if opts.skip {
|
||||||
t := f.Type
|
|
||||||
switch t.Kind() {
|
|
||||||
case reflect.Struct:
|
|
||||||
// Treat anonymous struct fields with
|
|
||||||
// tag names as though they are not
|
|
||||||
// anonymous, like encoding/json does.
|
|
||||||
if getOptions(f.Tag).name == "" {
|
|
||||||
addFields(t, frv, f.Index)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
case reflect.Ptr:
|
|
||||||
if t.Elem().Kind() == reflect.Struct &&
|
frv := eindirect(rv.Field(i))
|
||||||
getOptions(f.Tag).name == "" {
|
|
||||||
if !frv.IsNil() {
|
// Treat anonymous struct fields with tag names as though they are
|
||||||
addFields(t.Elem(), frv.Elem(), f.Index)
|
// not anonymous, like encoding/json does.
|
||||||
}
|
//
|
||||||
|
// Non-struct anonymous fields use the normal encoding logic.
|
||||||
|
if isEmbed {
|
||||||
|
if getOptions(f.Tag).name == "" && frv.Kind() == reflect.Struct {
|
||||||
|
addFields(frv.Type(), frv, append(start, f.Index...))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Fall through to the normal field encoding logic below
|
|
||||||
// for non-struct anonymous fields.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if typeIsHash(tomlTypeOfGo(frv)) {
|
if typeIsTable(tomlTypeOfGo(frv)) {
|
||||||
fieldsSub = append(fieldsSub, append(start, f.Index...))
|
fieldsSub = append(fieldsSub, append(start, f.Index...))
|
||||||
|
} else {
|
||||||
|
// Copy so it works correct on 32bit archs; not clear why this
|
||||||
|
// is needed. See #314, and https://www.reddit.com/r/golang/comments/pnx8v4
|
||||||
|
// This also works fine on 64bit, but 32bit archs are somewhat
|
||||||
|
// rare and this is a wee bit faster.
|
||||||
|
if is32Bit {
|
||||||
|
copyStart := make([]int, len(start))
|
||||||
|
copy(copyStart, start)
|
||||||
|
fieldsDirect = append(fieldsDirect, append(copyStart, f.Index...))
|
||||||
} else {
|
} else {
|
||||||
fieldsDirect = append(fieldsDirect, append(start, f.Index...))
|
fieldsDirect = append(fieldsDirect, append(start, f.Index...))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
addFields(rt, rv, nil)
|
addFields(rt, rv, nil)
|
||||||
|
|
||||||
var writeFields = func(fields [][]int) {
|
writeFields := func(fields [][]int) {
|
||||||
for _, fieldIndex := range fields {
|
for _, fieldIndex := range fields {
|
||||||
sft := rt.FieldByIndex(fieldIndex)
|
fieldType := rt.FieldByIndex(fieldIndex)
|
||||||
sf := rv.FieldByIndex(fieldIndex)
|
fieldVal := eindirect(rv.FieldByIndex(fieldIndex))
|
||||||
if isNil(sf) {
|
|
||||||
// Don't write anything for nil fields.
|
if isNil(fieldVal) { /// Don't write anything for nil fields.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := getOptions(sft.Tag)
|
opts := getOptions(fieldType.Tag)
|
||||||
if opts.skip {
|
if opts.skip {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
keyName := sft.Name
|
keyName := fieldType.Name
|
||||||
if opts.name != "" {
|
if opts.name != "" {
|
||||||
keyName = opts.name
|
keyName = opts.name
|
||||||
}
|
}
|
||||||
if opts.omitempty && isEmpty(sf) {
|
if opts.omitempty && isEmpty(fieldVal) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if opts.omitzero && isZero(sf) {
|
if opts.omitzero && isZero(fieldVal) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
enc.encode(key.add(keyName), sf)
|
if inline {
|
||||||
|
enc.writeKeyValue(Key{keyName}, fieldVal, true)
|
||||||
|
if fieldIndex[0] != len(fields)-1 {
|
||||||
|
enc.wf(", ")
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
enc.encode(key.add(keyName), fieldVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if inline {
|
||||||
|
enc.wf("{")
|
||||||
}
|
}
|
||||||
writeFields(fieldsDirect)
|
writeFields(fieldsDirect)
|
||||||
writeFields(fieldsSub)
|
writeFields(fieldsSub)
|
||||||
|
if inline {
|
||||||
|
enc.wf("}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tomlTypeName returns the TOML type name of the Go value's type. It is
|
// tomlTypeOfGo returns the TOML type name of the Go value's type.
|
||||||
// used to determine whether the types of array elements are mixed (which is
|
//
|
||||||
// forbidden). If the Go value is nil, then it is illegal for it to be an array
|
// It is used to determine whether the types of array elements are mixed (which
|
||||||
// element, and valueIsNil is returned as true.
|
// is forbidden). If the Go value is nil, then it is illegal for it to be an
|
||||||
|
// array element, and valueIsNil is returned as true.
|
||||||
// Returns the TOML type of a Go value. The type may be `nil`, which means
|
//
|
||||||
// no concrete TOML type could be found.
|
// The type may be `nil`, which means no concrete TOML type could be found.
|
||||||
func tomlTypeOfGo(rv reflect.Value) tomlType {
|
func tomlTypeOfGo(rv reflect.Value) tomlType {
|
||||||
if isNil(rv) || !rv.IsValid() {
|
if isNil(rv) || !rv.IsValid() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rv.Kind() == reflect.Struct {
|
||||||
|
if rv.Type() == timeType {
|
||||||
|
return tomlDatetime
|
||||||
|
}
|
||||||
|
if isMarshaler(rv) {
|
||||||
|
return tomlString
|
||||||
|
}
|
||||||
|
return tomlHash
|
||||||
|
}
|
||||||
|
|
||||||
|
if isMarshaler(rv) {
|
||||||
|
return tomlString
|
||||||
|
}
|
||||||
|
|
||||||
switch rv.Kind() {
|
switch rv.Kind() {
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
return tomlBool
|
return tomlBool
|
||||||
|
@ -397,7 +569,7 @@ func tomlTypeOfGo(rv reflect.Value) tomlType {
|
||||||
case reflect.Float32, reflect.Float64:
|
case reflect.Float32, reflect.Float64:
|
||||||
return tomlFloat
|
return tomlFloat
|
||||||
case reflect.Array, reflect.Slice:
|
case reflect.Array, reflect.Slice:
|
||||||
if typeEqual(tomlHash, tomlArrayType(rv)) {
|
if isTableArray(rv) {
|
||||||
return tomlArrayHash
|
return tomlArrayHash
|
||||||
}
|
}
|
||||||
return tomlArray
|
return tomlArray
|
||||||
|
@ -407,54 +579,35 @@ func tomlTypeOfGo(rv reflect.Value) tomlType {
|
||||||
return tomlString
|
return tomlString
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
return tomlHash
|
return tomlHash
|
||||||
case reflect.Struct:
|
|
||||||
switch rv.Interface().(type) {
|
|
||||||
case time.Time:
|
|
||||||
return tomlDatetime
|
|
||||||
case TextMarshaler:
|
|
||||||
return tomlString
|
|
||||||
default:
|
default:
|
||||||
return tomlHash
|
encPanic(errors.New("unsupported type: " + rv.Kind().String()))
|
||||||
}
|
panic("unreachable")
|
||||||
default:
|
|
||||||
panic("unexpected reflect.Kind: " + rv.Kind().String())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tomlArrayType returns the element type of a TOML array. The type returned
|
func isMarshaler(rv reflect.Value) bool {
|
||||||
// may be nil if it cannot be determined (e.g., a nil slice or a zero length
|
return rv.Type().Implements(marshalText) || rv.Type().Implements(marshalToml)
|
||||||
// slize). This function may also panic if it finds a type that cannot be
|
|
||||||
// expressed in TOML (such as nil elements, heterogeneous arrays or directly
|
|
||||||
// nested arrays of tables).
|
|
||||||
func tomlArrayType(rv reflect.Value) tomlType {
|
|
||||||
if isNil(rv) || !rv.IsValid() || rv.Len() == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
firstType := tomlTypeOfGo(rv.Index(0))
|
|
||||||
if firstType == nil {
|
// isTableArray reports if all entries in the array or slice are a table.
|
||||||
|
func isTableArray(arr reflect.Value) bool {
|
||||||
|
if isNil(arr) || !arr.IsValid() || arr.Len() == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := true
|
||||||
|
for i := 0; i < arr.Len(); i++ {
|
||||||
|
tt := tomlTypeOfGo(eindirect(arr.Index(i)))
|
||||||
|
// Don't allow nil.
|
||||||
|
if tt == nil {
|
||||||
encPanic(errArrayNilElement)
|
encPanic(errArrayNilElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
rvlen := rv.Len()
|
if ret && !typeEqual(tomlHash, tt) {
|
||||||
for i := 1; i < rvlen; i++ {
|
ret = false
|
||||||
elem := rv.Index(i)
|
|
||||||
switch elemType := tomlTypeOfGo(elem); {
|
|
||||||
case elemType == nil:
|
|
||||||
encPanic(errArrayNilElement)
|
|
||||||
case !typeEqual(firstType, elemType):
|
|
||||||
encPanic(errArrayMixedElementTypes)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If we have a nested array, then we must make sure that the nested
|
return ret
|
||||||
// array contains ONLY primitives.
|
|
||||||
// This checks arbitrarily nested arrays.
|
|
||||||
if typeEqual(firstType, tomlArray) || typeEqual(firstType, tomlArrayHash) {
|
|
||||||
nest := tomlArrayType(eindirect(rv.Index(0)))
|
|
||||||
if typeEqual(nest, tomlHash) || typeEqual(nest, tomlArrayHash) {
|
|
||||||
encPanic(errArrayNoTable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return firstType
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type tagOptions struct {
|
type tagOptions struct {
|
||||||
|
@ -499,6 +652,8 @@ func isEmpty(rv reflect.Value) bool {
|
||||||
switch rv.Kind() {
|
switch rv.Kind() {
|
||||||
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
|
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
|
||||||
return rv.Len() == 0
|
return rv.Len() == 0
|
||||||
|
case reflect.Struct:
|
||||||
|
return reflect.Zero(rv.Type()).Interface() == rv.Interface()
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
return !rv.Bool()
|
return !rv.Bool()
|
||||||
}
|
}
|
||||||
|
@ -511,18 +666,32 @@ func (enc *Encoder) newline() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (enc *Encoder) keyEqElement(key Key, val reflect.Value) {
|
// Write a key/value pair:
|
||||||
|
//
|
||||||
|
// key = <any value>
|
||||||
|
//
|
||||||
|
// This is also used for "k = v" in inline tables; so something like this will
|
||||||
|
// be written in three calls:
|
||||||
|
//
|
||||||
|
// ┌────────────────────┐
|
||||||
|
// │ ┌───┐ ┌─────┐│
|
||||||
|
// v v v v vv
|
||||||
|
// key = {k = v, k2 = v2}
|
||||||
|
//
|
||||||
|
func (enc *Encoder) writeKeyValue(key Key, val reflect.Value, inline bool) {
|
||||||
if len(key) == 0 {
|
if len(key) == 0 {
|
||||||
encPanic(errNoKey)
|
encPanic(errNoKey)
|
||||||
}
|
}
|
||||||
panicIfInvalidKey(key)
|
|
||||||
enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1))
|
enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1))
|
||||||
enc.eElement(val)
|
enc.eElement(val)
|
||||||
|
if !inline {
|
||||||
enc.newline()
|
enc.newline()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (enc *Encoder) wf(format string, v ...interface{}) {
|
func (enc *Encoder) wf(format string, v ...interface{}) {
|
||||||
if _, err := fmt.Fprintf(enc.w, format, v...); err != nil {
|
_, err := fmt.Fprintf(enc.w, format, v...)
|
||||||
|
if err != nil {
|
||||||
encPanic(err)
|
encPanic(err)
|
||||||
}
|
}
|
||||||
enc.hasWritten = true
|
enc.hasWritten = true
|
||||||
|
@ -536,13 +705,25 @@ func encPanic(err error) {
|
||||||
panic(tomlEncodeError{err})
|
panic(tomlEncodeError{err})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve any level of pointers to the actual value (e.g. **string → string).
|
||||||
func eindirect(v reflect.Value) reflect.Value {
|
func eindirect(v reflect.Value) reflect.Value {
|
||||||
switch v.Kind() {
|
if v.Kind() != reflect.Ptr && v.Kind() != reflect.Interface {
|
||||||
case reflect.Ptr, reflect.Interface:
|
if isMarshaler(v) {
|
||||||
return eindirect(v.Elem())
|
|
||||||
default:
|
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
if v.CanAddr() { /// Special case for marshalers; see #358.
|
||||||
|
if pv := v.Addr(); isMarshaler(pv) {
|
||||||
|
return pv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.IsNil() {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
return eindirect(v.Elem())
|
||||||
}
|
}
|
||||||
|
|
||||||
func isNil(rv reflect.Value) bool {
|
func isNil(rv reflect.Value) bool {
|
||||||
|
@ -553,16 +734,3 @@ func isNil(rv reflect.Value) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func panicIfInvalidKey(key Key) {
|
|
||||||
for _, k := range key {
|
|
||||||
if len(k) == 0 {
|
|
||||||
encPanic(e("Key '%s' is not a valid table name. Key names "+
|
|
||||||
"cannot be empty.", key.maybeQuotedAll()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isValidKeyName(s string) bool {
|
|
||||||
return len(s) != 0
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
// +build go1.2
|
|
||||||
|
|
||||||
package toml
|
|
||||||
|
|
||||||
// In order to support Go 1.1, we define our own TextMarshaler and
|
|
||||||
// TextUnmarshaler types. For Go 1.2+, we just alias them with the
|
|
||||||
// standard library interfaces.
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TextMarshaler is a synonym for encoding.TextMarshaler. It is defined here
|
|
||||||
// so that Go 1.1 can be supported.
|
|
||||||
type TextMarshaler encoding.TextMarshaler
|
|
||||||
|
|
||||||
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined
|
|
||||||
// here so that Go 1.1 can be supported.
|
|
||||||
type TextUnmarshaler encoding.TextUnmarshaler
|
|
|
@ -1,18 +0,0 @@
|
||||||
// +build !go1.2
|
|
||||||
|
|
||||||
package toml
|
|
||||||
|
|
||||||
// These interfaces were introduced in Go 1.2, so we add them manually when
|
|
||||||
// compiling for Go 1.1.
|
|
||||||
|
|
||||||
// TextMarshaler is a synonym for encoding.TextMarshaler. It is defined here
|
|
||||||
// so that Go 1.1 can be supported.
|
|
||||||
type TextMarshaler interface {
|
|
||||||
MarshalText() (text []byte, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined
|
|
||||||
// here so that Go 1.1 can be supported.
|
|
||||||
type TextUnmarshaler interface {
|
|
||||||
UnmarshalText(text []byte) error
|
|
||||||
}
|
|
|
@ -0,0 +1,276 @@
|
||||||
|
package toml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseError is returned when there is an error parsing the TOML syntax.
|
||||||
|
//
|
||||||
|
// For example invalid syntax, duplicate keys, etc.
|
||||||
|
//
|
||||||
|
// In addition to the error message itself, you can also print detailed location
|
||||||
|
// information with context by using ErrorWithPosition():
|
||||||
|
//
|
||||||
|
// toml: error: Key 'fruit' was already created and cannot be used as an array.
|
||||||
|
//
|
||||||
|
// At line 4, column 2-7:
|
||||||
|
//
|
||||||
|
// 2 | fruit = []
|
||||||
|
// 3 |
|
||||||
|
// 4 | [[fruit]] # Not allowed
|
||||||
|
// ^^^^^
|
||||||
|
//
|
||||||
|
// Furthermore, the ErrorWithUsage() can be used to print the above with some
|
||||||
|
// more detailed usage guidance:
|
||||||
|
//
|
||||||
|
// toml: error: newlines not allowed within inline tables
|
||||||
|
//
|
||||||
|
// At line 1, column 18:
|
||||||
|
//
|
||||||
|
// 1 | x = [{ key = 42 #
|
||||||
|
// ^
|
||||||
|
//
|
||||||
|
// Error help:
|
||||||
|
//
|
||||||
|
// Inline tables must always be on a single line:
|
||||||
|
//
|
||||||
|
// table = {key = 42, second = 43}
|
||||||
|
//
|
||||||
|
// It is invalid to split them over multiple lines like so:
|
||||||
|
//
|
||||||
|
// # INVALID
|
||||||
|
// table = {
|
||||||
|
// key = 42,
|
||||||
|
// second = 43
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Use regular for this:
|
||||||
|
//
|
||||||
|
// [table]
|
||||||
|
// key = 42
|
||||||
|
// second = 43
|
||||||
|
type ParseError struct {
|
||||||
|
Message string // Short technical message.
|
||||||
|
Usage string // Longer message with usage guidance; may be blank.
|
||||||
|
Position Position // Position of the error
|
||||||
|
LastKey string // Last parsed key, may be blank.
|
||||||
|
Line int // Line the error occurred. Deprecated: use Position.
|
||||||
|
|
||||||
|
err error
|
||||||
|
input string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position of an error.
|
||||||
|
type Position struct {
|
||||||
|
Line int // Line number, starting at 1.
|
||||||
|
Start int // Start of error, as byte offset starting at 0.
|
||||||
|
Len int // Lenght in bytes.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pe ParseError) Error() string {
|
||||||
|
msg := pe.Message
|
||||||
|
if msg == "" { // Error from errorf()
|
||||||
|
msg = pe.err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
if pe.LastKey == "" {
|
||||||
|
return fmt.Sprintf("toml: line %d: %s", pe.Position.Line, msg)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("toml: line %d (last key %q): %s",
|
||||||
|
pe.Position.Line, pe.LastKey, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorWithUsage() returns the error with detailed location context.
|
||||||
|
//
|
||||||
|
// See the documentation on ParseError.
|
||||||
|
func (pe ParseError) ErrorWithPosition() string {
|
||||||
|
if pe.input == "" { // Should never happen, but just in case.
|
||||||
|
return pe.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
lines = strings.Split(pe.input, "\n")
|
||||||
|
col = pe.column(lines)
|
||||||
|
b = new(strings.Builder)
|
||||||
|
)
|
||||||
|
|
||||||
|
msg := pe.Message
|
||||||
|
if msg == "" {
|
||||||
|
msg = pe.err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: don't show control characters as literals? This may not show up
|
||||||
|
// well everywhere.
|
||||||
|
|
||||||
|
if pe.Position.Len == 1 {
|
||||||
|
fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d:\n\n",
|
||||||
|
msg, pe.Position.Line, col+1)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d-%d:\n\n",
|
||||||
|
msg, pe.Position.Line, col, col+pe.Position.Len)
|
||||||
|
}
|
||||||
|
if pe.Position.Line > 2 {
|
||||||
|
fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-2, lines[pe.Position.Line-3])
|
||||||
|
}
|
||||||
|
if pe.Position.Line > 1 {
|
||||||
|
fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-1, lines[pe.Position.Line-2])
|
||||||
|
}
|
||||||
|
fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line, lines[pe.Position.Line-1])
|
||||||
|
fmt.Fprintf(b, "% 10s%s%s\n", "", strings.Repeat(" ", col), strings.Repeat("^", pe.Position.Len))
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorWithUsage() returns the error with detailed location context and usage
|
||||||
|
// guidance.
|
||||||
|
//
|
||||||
|
// See the documentation on ParseError.
|
||||||
|
func (pe ParseError) ErrorWithUsage() string {
|
||||||
|
m := pe.ErrorWithPosition()
|
||||||
|
if u, ok := pe.err.(interface{ Usage() string }); ok && u.Usage() != "" {
|
||||||
|
lines := strings.Split(strings.TrimSpace(u.Usage()), "\n")
|
||||||
|
for i := range lines {
|
||||||
|
if lines[i] != "" {
|
||||||
|
lines[i] = " " + lines[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m + "Error help:\n\n" + strings.Join(lines, "\n") + "\n"
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pe ParseError) column(lines []string) int {
|
||||||
|
var pos, col int
|
||||||
|
for i := range lines {
|
||||||
|
ll := len(lines[i]) + 1 // +1 for the removed newline
|
||||||
|
if pos+ll >= pe.Position.Start {
|
||||||
|
col = pe.Position.Start - pos
|
||||||
|
if col < 0 { // Should never happen, but just in case.
|
||||||
|
col = 0
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pos += ll
|
||||||
|
}
|
||||||
|
|
||||||
|
return col
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
errLexControl struct{ r rune }
|
||||||
|
errLexEscape struct{ r rune }
|
||||||
|
errLexUTF8 struct{ b byte }
|
||||||
|
errLexInvalidNum struct{ v string }
|
||||||
|
errLexInvalidDate struct{ v string }
|
||||||
|
errLexInlineTableNL struct{}
|
||||||
|
errLexStringNL struct{}
|
||||||
|
errParseRange struct {
|
||||||
|
i interface{} // int or float
|
||||||
|
size string // "int64", "uint16", etc.
|
||||||
|
}
|
||||||
|
errParseDuration struct{ d string }
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e errLexControl) Error() string {
|
||||||
|
return fmt.Sprintf("TOML files cannot contain control characters: '0x%02x'", e.r)
|
||||||
|
}
|
||||||
|
func (e errLexControl) Usage() string { return "" }
|
||||||
|
|
||||||
|
func (e errLexEscape) Error() string { return fmt.Sprintf(`invalid escape in string '\%c'`, e.r) }
|
||||||
|
func (e errLexEscape) Usage() string { return usageEscape }
|
||||||
|
func (e errLexUTF8) Error() string { return fmt.Sprintf("invalid UTF-8 byte: 0x%02x", e.b) }
|
||||||
|
func (e errLexUTF8) Usage() string { return "" }
|
||||||
|
func (e errLexInvalidNum) Error() string { return fmt.Sprintf("invalid number: %q", e.v) }
|
||||||
|
func (e errLexInvalidNum) Usage() string { return "" }
|
||||||
|
func (e errLexInvalidDate) Error() string { return fmt.Sprintf("invalid date: %q", e.v) }
|
||||||
|
func (e errLexInvalidDate) Usage() string { return "" }
|
||||||
|
func (e errLexInlineTableNL) Error() string { return "newlines not allowed within inline tables" }
|
||||||
|
func (e errLexInlineTableNL) Usage() string { return usageInlineNewline }
|
||||||
|
func (e errLexStringNL) Error() string { return "strings cannot contain newlines" }
|
||||||
|
func (e errLexStringNL) Usage() string { return usageStringNewline }
|
||||||
|
func (e errParseRange) Error() string { return fmt.Sprintf("%v is out of range for %s", e.i, e.size) }
|
||||||
|
func (e errParseRange) Usage() string { return usageIntOverflow }
|
||||||
|
func (e errParseDuration) Error() string { return fmt.Sprintf("invalid duration: %q", e.d) }
|
||||||
|
func (e errParseDuration) Usage() string { return usageDuration }
|
||||||
|
|
||||||
|
const usageEscape = `
|
||||||
|
A '\' inside a "-delimited string is interpreted as an escape character.
|
||||||
|
|
||||||
|
The following escape sequences are supported:
|
||||||
|
\b, \t, \n, \f, \r, \", \\, \uXXXX, and \UXXXXXXXX
|
||||||
|
|
||||||
|
To prevent a '\' from being recognized as an escape character, use either:
|
||||||
|
|
||||||
|
- a ' or '''-delimited string; escape characters aren't processed in them; or
|
||||||
|
- write two backslashes to get a single backslash: '\\'.
|
||||||
|
|
||||||
|
If you're trying to add a Windows path (e.g. "C:\Users\martin") then using '/'
|
||||||
|
instead of '\' will usually also work: "C:/Users/martin".
|
||||||
|
`
|
||||||
|
|
||||||
|
const usageInlineNewline = `
|
||||||
|
Inline tables must always be on a single line:
|
||||||
|
|
||||||
|
table = {key = 42, second = 43}
|
||||||
|
|
||||||
|
It is invalid to split them over multiple lines like so:
|
||||||
|
|
||||||
|
# INVALID
|
||||||
|
table = {
|
||||||
|
key = 42,
|
||||||
|
second = 43
|
||||||
|
}
|
||||||
|
|
||||||
|
Use regular for this:
|
||||||
|
|
||||||
|
[table]
|
||||||
|
key = 42
|
||||||
|
second = 43
|
||||||
|
`
|
||||||
|
|
||||||
|
const usageStringNewline = `
|
||||||
|
Strings must always be on a single line, and cannot span more than one line:
|
||||||
|
|
||||||
|
# INVALID
|
||||||
|
string = "Hello,
|
||||||
|
world!"
|
||||||
|
|
||||||
|
Instead use """ or ''' to split strings over multiple lines:
|
||||||
|
|
||||||
|
string = """Hello,
|
||||||
|
world!"""
|
||||||
|
`
|
||||||
|
|
||||||
|
const usageIntOverflow = `
|
||||||
|
This number is too large; this may be an error in the TOML, but it can also be a
|
||||||
|
bug in the program that uses too small of an integer.
|
||||||
|
|
||||||
|
The maximum and minimum values are:
|
||||||
|
|
||||||
|
size │ lowest │ highest
|
||||||
|
───────┼────────────────┼──────────
|
||||||
|
int8 │ -128 │ 127
|
||||||
|
int16 │ -32,768 │ 32,767
|
||||||
|
int32 │ -2,147,483,648 │ 2,147,483,647
|
||||||
|
int64 │ -9.2 × 10¹⁷ │ 9.2 × 10¹⁷
|
||||||
|
uint8 │ 0 │ 255
|
||||||
|
uint16 │ 0 │ 65535
|
||||||
|
uint32 │ 0 │ 4294967295
|
||||||
|
uint64 │ 0 │ 1.8 × 10¹⁸
|
||||||
|
|
||||||
|
int refers to int32 on 32-bit systems and int64 on 64-bit systems.
|
||||||
|
`
|
||||||
|
|
||||||
|
const usageDuration = `
|
||||||
|
A duration must be as "number<unit>", without any spaces. Valid units are:
|
||||||
|
|
||||||
|
ns nanoseconds (billionth of a second)
|
||||||
|
us, µs microseconds (millionth of a second)
|
||||||
|
ms milliseconds (thousands of a second)
|
||||||
|
s seconds
|
||||||
|
m minutes
|
||||||
|
h hours
|
||||||
|
|
||||||
|
You can combine multiple units; for example "5m10s" for 5 minutes and 10
|
||||||
|
seconds.
|
||||||
|
`
|
|
@ -0,0 +1,36 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Timezones used for local datetime, date, and time TOML types.
|
||||||
|
//
|
||||||
|
// The exact way times and dates without a timezone should be interpreted is not
|
||||||
|
// well-defined in the TOML specification and left to the implementation. These
|
||||||
|
// defaults to current local timezone offset of the computer, but this can be
|
||||||
|
// changed by changing these variables before decoding.
|
||||||
|
//
|
||||||
|
// TODO:
|
||||||
|
// Ideally we'd like to offer people the ability to configure the used timezone
|
||||||
|
// by setting Decoder.Timezone and Encoder.Timezone; however, this is a bit
|
||||||
|
// tricky: the reason we use three different variables for this is to support
|
||||||
|
// round-tripping – without these specific TZ names we wouldn't know which
|
||||||
|
// format to use.
|
||||||
|
//
|
||||||
|
// There isn't a good way to encode this right now though, and passing this sort
|
||||||
|
// of information also ties in to various related issues such as string format
|
||||||
|
// encoding, encoding of comments, etc.
|
||||||
|
//
|
||||||
|
// So, for the time being, just put this in internal until we can write a good
|
||||||
|
// comprehensive API for doing all of this.
|
||||||
|
//
|
||||||
|
// The reason they're exported is because they're referred from in e.g.
|
||||||
|
// internal/tag.
|
||||||
|
//
|
||||||
|
// Note that this behaviour is valid according to the TOML spec as the exact
|
||||||
|
// behaviour is left up to implementations.
|
||||||
|
var (
|
||||||
|
localOffset = func() int { _, o := time.Now().Zone(); return o }()
|
||||||
|
LocalDatetime = time.FixedZone("datetime-local", localOffset)
|
||||||
|
LocalDate = time.FixedZone("date-local", localOffset)
|
||||||
|
LocalTime = time.FixedZone("time-local", localOffset)
|
||||||
|
)
|
File diff suppressed because it is too large
Load Diff
124
vendor/github.com/BurntSushi/toml/decode_meta.go → vendor/github.com/BurntSushi/toml/meta.go
generated
vendored
124
vendor/github.com/BurntSushi/toml/decode_meta.go → vendor/github.com/BurntSushi/toml/meta.go
generated
vendored
|
@ -1,33 +1,40 @@
|
||||||
package toml
|
package toml
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// MetaData allows access to meta information about TOML data that may not
|
// MetaData allows access to meta information about TOML data that's not
|
||||||
// be inferrable via reflection. In particular, whether a key has been defined
|
// accessible otherwise.
|
||||||
// and the TOML type of a key.
|
//
|
||||||
|
// It allows checking if a key is defined in the TOML data, whether any keys
|
||||||
|
// were undecoded, and the TOML type of a key.
|
||||||
type MetaData struct {
|
type MetaData struct {
|
||||||
mapping map[string]interface{}
|
|
||||||
types map[string]tomlType
|
|
||||||
keys []Key
|
|
||||||
decoded map[string]bool
|
|
||||||
context Key // Used only during decoding.
|
context Key // Used only during decoding.
|
||||||
|
|
||||||
|
keyInfo map[string]keyInfo
|
||||||
|
mapping map[string]interface{}
|
||||||
|
keys []Key
|
||||||
|
decoded map[string]struct{}
|
||||||
|
data []byte // Input file; for errors.
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsDefined returns true if the key given exists in the TOML data. The key
|
// IsDefined reports if the key exists in the TOML data.
|
||||||
// should be specified hierarchially. e.g.,
|
|
||||||
//
|
//
|
||||||
// // access the TOML key 'a.b.c'
|
// The key should be specified hierarchically, for example to access the TOML
|
||||||
// IsDefined("a", "b", "c")
|
// key "a.b.c" you would use IsDefined("a", "b", "c"). Keys are case sensitive.
|
||||||
//
|
//
|
||||||
// IsDefined will return false if an empty key given. Keys are case sensitive.
|
// Returns false for an empty key.
|
||||||
func (md *MetaData) IsDefined(key ...string) bool {
|
func (md *MetaData) IsDefined(key ...string) bool {
|
||||||
if len(key) == 0 {
|
if len(key) == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
var hash map[string]interface{}
|
var (
|
||||||
var ok bool
|
hash map[string]interface{}
|
||||||
var hashOrVal interface{} = md.mapping
|
ok bool
|
||||||
|
hashOrVal interface{} = md.mapping
|
||||||
|
)
|
||||||
for _, k := range key {
|
for _, k := range key {
|
||||||
if hash, ok = hashOrVal.(map[string]interface{}); !ok {
|
if hash, ok = hashOrVal.(map[string]interface{}); !ok {
|
||||||
return false
|
return false
|
||||||
|
@ -41,58 +48,20 @@ func (md *MetaData) IsDefined(key ...string) bool {
|
||||||
|
|
||||||
// Type returns a string representation of the type of the key specified.
|
// Type returns a string representation of the type of the key specified.
|
||||||
//
|
//
|
||||||
// Type will return the empty string if given an empty key or a key that
|
// Type will return the empty string if given an empty key or a key that does
|
||||||
// does not exist. Keys are case sensitive.
|
// not exist. Keys are case sensitive.
|
||||||
func (md *MetaData) Type(key ...string) string {
|
func (md *MetaData) Type(key ...string) string {
|
||||||
fullkey := strings.Join(key, ".")
|
if ki, ok := md.keyInfo[Key(key).String()]; ok {
|
||||||
if typ, ok := md.types[fullkey]; ok {
|
return ki.tomlType.typeString()
|
||||||
return typ.typeString()
|
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Key is the type of any TOML key, including key groups. Use (MetaData).Keys
|
|
||||||
// to get values of this type.
|
|
||||||
type Key []string
|
|
||||||
|
|
||||||
func (k Key) String() string {
|
|
||||||
return strings.Join(k, ".")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k Key) maybeQuotedAll() string {
|
|
||||||
var ss []string
|
|
||||||
for i := range k {
|
|
||||||
ss = append(ss, k.maybeQuoted(i))
|
|
||||||
}
|
|
||||||
return strings.Join(ss, ".")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k Key) maybeQuoted(i int) string {
|
|
||||||
quote := false
|
|
||||||
for _, c := range k[i] {
|
|
||||||
if !isBareKeyChar(c) {
|
|
||||||
quote = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if quote {
|
|
||||||
return "\"" + strings.Replace(k[i], "\"", "\\\"", -1) + "\""
|
|
||||||
}
|
|
||||||
return k[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k Key) add(piece string) Key {
|
|
||||||
newKey := make(Key, len(k)+1)
|
|
||||||
copy(newKey, k)
|
|
||||||
newKey[len(k)] = piece
|
|
||||||
return newKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keys returns a slice of every key in the TOML data, including key groups.
|
// Keys returns a slice of every key in the TOML data, including key groups.
|
||||||
// Each key is itself a slice, where the first element is the top of the
|
|
||||||
// hierarchy and the last is the most specific.
|
|
||||||
//
|
//
|
||||||
// The list will have the same order as the keys appeared in the TOML data.
|
// Each key is itself a slice, where the first element is the top of the
|
||||||
|
// hierarchy and the last is the most specific. The list will have the same
|
||||||
|
// order as the keys appeared in the TOML data.
|
||||||
//
|
//
|
||||||
// All keys returned are non-empty.
|
// All keys returned are non-empty.
|
||||||
func (md *MetaData) Keys() []Key {
|
func (md *MetaData) Keys() []Key {
|
||||||
|
@ -113,9 +82,40 @@ func (md *MetaData) Keys() []Key {
|
||||||
func (md *MetaData) Undecoded() []Key {
|
func (md *MetaData) Undecoded() []Key {
|
||||||
undecoded := make([]Key, 0, len(md.keys))
|
undecoded := make([]Key, 0, len(md.keys))
|
||||||
for _, key := range md.keys {
|
for _, key := range md.keys {
|
||||||
if !md.decoded[key.String()] {
|
if _, ok := md.decoded[key.String()]; !ok {
|
||||||
undecoded = append(undecoded, key)
|
undecoded = append(undecoded, key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undecoded
|
return undecoded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Key represents any TOML key, including key groups. Use (MetaData).Keys to get
|
||||||
|
// values of this type.
|
||||||
|
type Key []string
|
||||||
|
|
||||||
|
func (k Key) String() string {
|
||||||
|
ss := make([]string, len(k))
|
||||||
|
for i := range k {
|
||||||
|
ss[i] = k.maybeQuoted(i)
|
||||||
|
}
|
||||||
|
return strings.Join(ss, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k Key) maybeQuoted(i int) string {
|
||||||
|
if k[i] == "" {
|
||||||
|
return `""`
|
||||||
|
}
|
||||||
|
for _, c := range k[i] {
|
||||||
|
if !isBareKeyChar(c) {
|
||||||
|
return `"` + dblQuotedReplacer.Replace(k[i]) + `"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return k[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k Key) add(piece string) Key {
|
||||||
|
newKey := make(Key, len(k)+1)
|
||||||
|
copy(newKey, k)
|
||||||
|
newKey[len(k)] = piece
|
||||||
|
return newKey
|
||||||
|
}
|
|
@ -5,54 +5,69 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
type parser struct {
|
type parser struct {
|
||||||
mapping map[string]interface{}
|
|
||||||
types map[string]tomlType
|
|
||||||
lx *lexer
|
lx *lexer
|
||||||
|
context Key // Full key for the current hash in scope.
|
||||||
|
currentKey string // Base key name for everything except hashes.
|
||||||
|
pos Position // Current position in the TOML file.
|
||||||
|
|
||||||
// A list of keys in the order that they appear in the TOML data.
|
ordered []Key // List of keys in the order that they appear in the TOML data.
|
||||||
ordered []Key
|
|
||||||
|
|
||||||
// the full key for the current hash in scope
|
keyInfo map[string]keyInfo // Map keyname → info about the TOML key.
|
||||||
context Key
|
mapping map[string]interface{} // Map keyname → key value.
|
||||||
|
implicits map[string]struct{} // Record implicit keys (e.g. "key.group.names").
|
||||||
// the base key name for everything except hashes
|
|
||||||
currentKey string
|
|
||||||
|
|
||||||
// rough approximation of line number
|
|
||||||
approxLine int
|
|
||||||
|
|
||||||
// A map of 'key.group.names' to whether they were created implicitly.
|
|
||||||
implicits map[string]bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type parseError string
|
type keyInfo struct {
|
||||||
|
pos Position
|
||||||
func (pe parseError) Error() string {
|
tomlType tomlType
|
||||||
return string(pe)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parse(data string) (p *parser, err error) {
|
func parse(data string) (p *parser, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
var ok bool
|
if pErr, ok := r.(ParseError); ok {
|
||||||
if err, ok = r.(parseError); ok {
|
pErr.input = data
|
||||||
|
err = pErr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
panic(r)
|
panic(r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Read over BOM; do this here as the lexer calls utf8.DecodeRuneInString()
|
||||||
|
// which mangles stuff.
|
||||||
|
if strings.HasPrefix(data, "\xff\xfe") || strings.HasPrefix(data, "\xfe\xff") {
|
||||||
|
data = data[2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Examine first few bytes for NULL bytes; this probably means it's a UTF-16
|
||||||
|
// file (second byte in surrogate pair being NULL). Again, do this here to
|
||||||
|
// avoid having to deal with UTF-8/16 stuff in the lexer.
|
||||||
|
ex := 6
|
||||||
|
if len(data) < 6 {
|
||||||
|
ex = len(data)
|
||||||
|
}
|
||||||
|
if i := strings.IndexRune(data[:ex], 0); i > -1 {
|
||||||
|
return nil, ParseError{
|
||||||
|
Message: "files cannot contain NULL bytes; probably using UTF-16; TOML files must be UTF-8",
|
||||||
|
Position: Position{Line: 1, Start: i, Len: 1},
|
||||||
|
Line: 1,
|
||||||
|
input: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
p = &parser{
|
p = &parser{
|
||||||
|
keyInfo: make(map[string]keyInfo),
|
||||||
mapping: make(map[string]interface{}),
|
mapping: make(map[string]interface{}),
|
||||||
types: make(map[string]tomlType),
|
|
||||||
lx: lex(data),
|
lx: lex(data),
|
||||||
ordered: make([]Key, 0),
|
ordered: make([]Key, 0),
|
||||||
implicits: make(map[string]bool),
|
implicits: make(map[string]struct{}),
|
||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
item := p.next()
|
item := p.next()
|
||||||
|
@ -65,17 +80,54 @@ func parse(data string) (p *parser, err error) {
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *parser) panicErr(it item, err error) {
|
||||||
|
panic(ParseError{
|
||||||
|
err: err,
|
||||||
|
Position: it.pos,
|
||||||
|
Line: it.pos.Len,
|
||||||
|
LastKey: p.current(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) panicItemf(it item, format string, v ...interface{}) {
|
||||||
|
panic(ParseError{
|
||||||
|
Message: fmt.Sprintf(format, v...),
|
||||||
|
Position: it.pos,
|
||||||
|
Line: it.pos.Len,
|
||||||
|
LastKey: p.current(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (p *parser) panicf(format string, v ...interface{}) {
|
func (p *parser) panicf(format string, v ...interface{}) {
|
||||||
msg := fmt.Sprintf("Near line %d (last key parsed '%s'): %s",
|
panic(ParseError{
|
||||||
p.approxLine, p.current(), fmt.Sprintf(format, v...))
|
Message: fmt.Sprintf(format, v...),
|
||||||
panic(parseError(msg))
|
Position: p.pos,
|
||||||
|
Line: p.pos.Line,
|
||||||
|
LastKey: p.current(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) next() item {
|
func (p *parser) next() item {
|
||||||
it := p.lx.nextItem()
|
it := p.lx.nextItem()
|
||||||
|
//fmt.Printf("ITEM %-18s line %-3d │ %q\n", it.typ, it.pos.Line, it.val)
|
||||||
if it.typ == itemError {
|
if it.typ == itemError {
|
||||||
p.panicf("%s", it.val)
|
if it.err != nil {
|
||||||
|
panic(ParseError{
|
||||||
|
Position: it.pos,
|
||||||
|
Line: it.pos.Line,
|
||||||
|
LastKey: p.current(),
|
||||||
|
err: it.err,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.panicItemf(it, "%s", it.val)
|
||||||
|
}
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) nextPos() item {
|
||||||
|
it := p.next()
|
||||||
|
p.pos = it.pos
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,44 +149,60 @@ func (p *parser) assertEqual(expected, got itemType) {
|
||||||
|
|
||||||
func (p *parser) topLevel(item item) {
|
func (p *parser) topLevel(item item) {
|
||||||
switch item.typ {
|
switch item.typ {
|
||||||
case itemCommentStart:
|
case itemCommentStart: // # ..
|
||||||
p.approxLine = item.line
|
|
||||||
p.expect(itemText)
|
p.expect(itemText)
|
||||||
case itemTableStart:
|
case itemTableStart: // [ .. ]
|
||||||
kg := p.next()
|
name := p.nextPos()
|
||||||
p.approxLine = kg.line
|
|
||||||
|
|
||||||
var key Key
|
var key Key
|
||||||
for ; kg.typ != itemTableEnd && kg.typ != itemEOF; kg = p.next() {
|
for ; name.typ != itemTableEnd && name.typ != itemEOF; name = p.next() {
|
||||||
key = append(key, p.keyString(kg))
|
key = append(key, p.keyString(name))
|
||||||
}
|
}
|
||||||
p.assertEqual(itemTableEnd, kg.typ)
|
p.assertEqual(itemTableEnd, name.typ)
|
||||||
|
|
||||||
p.establishContext(key, false)
|
p.addContext(key, false)
|
||||||
p.setType("", tomlHash)
|
p.setType("", tomlHash, item.pos)
|
||||||
p.ordered = append(p.ordered, key)
|
p.ordered = append(p.ordered, key)
|
||||||
case itemArrayTableStart:
|
case itemArrayTableStart: // [[ .. ]]
|
||||||
kg := p.next()
|
name := p.nextPos()
|
||||||
p.approxLine = kg.line
|
|
||||||
|
|
||||||
var key Key
|
var key Key
|
||||||
for ; kg.typ != itemArrayTableEnd && kg.typ != itemEOF; kg = p.next() {
|
for ; name.typ != itemArrayTableEnd && name.typ != itemEOF; name = p.next() {
|
||||||
key = append(key, p.keyString(kg))
|
key = append(key, p.keyString(name))
|
||||||
}
|
}
|
||||||
p.assertEqual(itemArrayTableEnd, kg.typ)
|
p.assertEqual(itemArrayTableEnd, name.typ)
|
||||||
|
|
||||||
p.establishContext(key, true)
|
p.addContext(key, true)
|
||||||
p.setType("", tomlArrayHash)
|
p.setType("", tomlArrayHash, item.pos)
|
||||||
p.ordered = append(p.ordered, key)
|
p.ordered = append(p.ordered, key)
|
||||||
case itemKeyStart:
|
case itemKeyStart: // key = ..
|
||||||
kname := p.next()
|
outerContext := p.context
|
||||||
p.approxLine = kname.line
|
/// Read all the key parts (e.g. 'a' and 'b' in 'a.b')
|
||||||
p.currentKey = p.keyString(kname)
|
k := p.nextPos()
|
||||||
|
var key Key
|
||||||
|
for ; k.typ != itemKeyEnd && k.typ != itemEOF; k = p.next() {
|
||||||
|
key = append(key, p.keyString(k))
|
||||||
|
}
|
||||||
|
p.assertEqual(itemKeyEnd, k.typ)
|
||||||
|
|
||||||
val, typ := p.value(p.next())
|
/// The current key is the last part.
|
||||||
p.setValue(p.currentKey, val)
|
p.currentKey = key[len(key)-1]
|
||||||
p.setType(p.currentKey, typ)
|
|
||||||
|
/// All the other parts (if any) are the context; need to set each part
|
||||||
|
/// as implicit.
|
||||||
|
context := key[:len(key)-1]
|
||||||
|
for i := range context {
|
||||||
|
p.addImplicitContext(append(p.context, context[i:i+1]...))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set value.
|
||||||
|
vItem := p.next()
|
||||||
|
val, typ := p.value(vItem, false)
|
||||||
|
p.set(p.currentKey, val, typ, vItem.pos)
|
||||||
p.ordered = append(p.ordered, p.context.add(p.currentKey))
|
p.ordered = append(p.ordered, p.context.add(p.currentKey))
|
||||||
|
|
||||||
|
/// Remove the context we added (preserving any context from [tbl] lines).
|
||||||
|
p.context = outerContext
|
||||||
p.currentKey = ""
|
p.currentKey = ""
|
||||||
default:
|
default:
|
||||||
p.bug("Unexpected type at top level: %s", item.typ)
|
p.bug("Unexpected type at top level: %s", item.typ)
|
||||||
|
@ -148,59 +216,81 @@ func (p *parser) keyString(it item) string {
|
||||||
return it.val
|
return it.val
|
||||||
case itemString, itemMultilineString,
|
case itemString, itemMultilineString,
|
||||||
itemRawString, itemRawMultilineString:
|
itemRawString, itemRawMultilineString:
|
||||||
s, _ := p.value(it)
|
s, _ := p.value(it, false)
|
||||||
return s.(string)
|
return s.(string)
|
||||||
default:
|
default:
|
||||||
p.bug("Unexpected key type: %s", it.typ)
|
p.bug("Unexpected key type: %s", it.typ)
|
||||||
|
}
|
||||||
panic("unreachable")
|
panic("unreachable")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
var datetimeRepl = strings.NewReplacer(
|
||||||
|
"z", "Z",
|
||||||
|
"t", "T",
|
||||||
|
" ", "T")
|
||||||
|
|
||||||
// value translates an expected value from the lexer into a Go value wrapped
|
// value translates an expected value from the lexer into a Go value wrapped
|
||||||
// as an empty interface.
|
// as an empty interface.
|
||||||
func (p *parser) value(it item) (interface{}, tomlType) {
|
func (p *parser) value(it item, parentIsArray bool) (interface{}, tomlType) {
|
||||||
switch it.typ {
|
switch it.typ {
|
||||||
case itemString:
|
case itemString:
|
||||||
return p.replaceEscapes(it.val), p.typeOfPrimitive(it)
|
return p.replaceEscapes(it, it.val), p.typeOfPrimitive(it)
|
||||||
case itemMultilineString:
|
case itemMultilineString:
|
||||||
trimmed := stripFirstNewline(stripEscapedWhitespace(it.val))
|
return p.replaceEscapes(it, stripFirstNewline(p.stripEscapedNewlines(it.val))), p.typeOfPrimitive(it)
|
||||||
return p.replaceEscapes(trimmed), p.typeOfPrimitive(it)
|
|
||||||
case itemRawString:
|
case itemRawString:
|
||||||
return it.val, p.typeOfPrimitive(it)
|
return it.val, p.typeOfPrimitive(it)
|
||||||
case itemRawMultilineString:
|
case itemRawMultilineString:
|
||||||
return stripFirstNewline(it.val), p.typeOfPrimitive(it)
|
return stripFirstNewline(it.val), p.typeOfPrimitive(it)
|
||||||
|
case itemInteger:
|
||||||
|
return p.valueInteger(it)
|
||||||
|
case itemFloat:
|
||||||
|
return p.valueFloat(it)
|
||||||
case itemBool:
|
case itemBool:
|
||||||
switch it.val {
|
switch it.val {
|
||||||
case "true":
|
case "true":
|
||||||
return true, p.typeOfPrimitive(it)
|
return true, p.typeOfPrimitive(it)
|
||||||
case "false":
|
case "false":
|
||||||
return false, p.typeOfPrimitive(it)
|
return false, p.typeOfPrimitive(it)
|
||||||
}
|
default:
|
||||||
p.bug("Expected boolean value, but got '%s'.", it.val)
|
p.bug("Expected boolean value, but got '%s'.", it.val)
|
||||||
case itemInteger:
|
|
||||||
if !numUnderscoresOK(it.val) {
|
|
||||||
p.panicf("Invalid integer %q: underscores must be surrounded by digits",
|
|
||||||
it.val)
|
|
||||||
}
|
}
|
||||||
val := strings.Replace(it.val, "_", "", -1)
|
case itemDatetime:
|
||||||
num, err := strconv.ParseInt(val, 10, 64)
|
return p.valueDatetime(it)
|
||||||
|
case itemArray:
|
||||||
|
return p.valueArray(it)
|
||||||
|
case itemInlineTableStart:
|
||||||
|
return p.valueInlineTable(it, parentIsArray)
|
||||||
|
default:
|
||||||
|
p.bug("Unexpected value type: %s", it.typ)
|
||||||
|
}
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) valueInteger(it item) (interface{}, tomlType) {
|
||||||
|
if !numUnderscoresOK(it.val) {
|
||||||
|
p.panicItemf(it, "Invalid integer %q: underscores must be surrounded by digits", it.val)
|
||||||
|
}
|
||||||
|
if numHasLeadingZero(it.val) {
|
||||||
|
p.panicItemf(it, "Invalid integer %q: cannot have leading zeroes", it.val)
|
||||||
|
}
|
||||||
|
|
||||||
|
num, err := strconv.ParseInt(it.val, 0, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Distinguish integer values. Normally, it'd be a bug if the lexer
|
// Distinguish integer values. Normally, it'd be a bug if the lexer
|
||||||
// provides an invalid integer, but it's possible that the number is
|
// provides an invalid integer, but it's possible that the number is
|
||||||
// out of range of valid values (which the lexer cannot determine).
|
// out of range of valid values (which the lexer cannot determine).
|
||||||
// So mark the former as a bug but the latter as a legitimate user
|
// So mark the former as a bug but the latter as a legitimate user
|
||||||
// error.
|
// error.
|
||||||
if e, ok := err.(*strconv.NumError); ok &&
|
if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange {
|
||||||
e.Err == strconv.ErrRange {
|
p.panicErr(it, errParseRange{i: it.val, size: "int64"})
|
||||||
|
|
||||||
p.panicf("Integer '%s' is out of the range of 64-bit "+
|
|
||||||
"signed integers.", it.val)
|
|
||||||
} else {
|
} else {
|
||||||
p.bug("Expected integer value, but got '%s'.", it.val)
|
p.bug("Expected integer value, but got '%s'.", it.val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return num, p.typeOfPrimitive(it)
|
return num, p.typeOfPrimitive(it)
|
||||||
case itemFloat:
|
}
|
||||||
|
|
||||||
|
func (p *parser) valueFloat(it item) (interface{}, tomlType) {
|
||||||
parts := strings.FieldsFunc(it.val, func(r rune) bool {
|
parts := strings.FieldsFunc(it.val, func(r rune) bool {
|
||||||
switch r {
|
switch r {
|
||||||
case '.', 'e', 'E':
|
case '.', 'e', 'E':
|
||||||
|
@ -210,66 +300,95 @@ func (p *parser) value(it item) (interface{}, tomlType) {
|
||||||
})
|
})
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
if !numUnderscoresOK(part) {
|
if !numUnderscoresOK(part) {
|
||||||
p.panicf("Invalid float %q: underscores must be "+
|
p.panicItemf(it, "Invalid float %q: underscores must be surrounded by digits", it.val)
|
||||||
"surrounded by digits", it.val)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(parts) > 0 && numHasLeadingZero(parts[0]) {
|
||||||
|
p.panicItemf(it, "Invalid float %q: cannot have leading zeroes", it.val)
|
||||||
|
}
|
||||||
if !numPeriodsOK(it.val) {
|
if !numPeriodsOK(it.val) {
|
||||||
// As a special case, numbers like '123.' or '1.e2',
|
// As a special case, numbers like '123.' or '1.e2',
|
||||||
// which are valid as far as Go/strconv are concerned,
|
// which are valid as far as Go/strconv are concerned,
|
||||||
// must be rejected because TOML says that a fractional
|
// must be rejected because TOML says that a fractional
|
||||||
// part consists of '.' followed by 1+ digits.
|
// part consists of '.' followed by 1+ digits.
|
||||||
p.panicf("Invalid float %q: '.' must be followed "+
|
p.panicItemf(it, "Invalid float %q: '.' must be followed by one or more digits", it.val)
|
||||||
"by one or more digits", it.val)
|
|
||||||
}
|
}
|
||||||
val := strings.Replace(it.val, "_", "", -1)
|
val := strings.Replace(it.val, "_", "", -1)
|
||||||
|
if val == "+nan" || val == "-nan" { // Go doesn't support this, but TOML spec does.
|
||||||
|
val = "nan"
|
||||||
|
}
|
||||||
num, err := strconv.ParseFloat(val, 64)
|
num, err := strconv.ParseFloat(val, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if e, ok := err.(*strconv.NumError); ok &&
|
if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange {
|
||||||
e.Err == strconv.ErrRange {
|
p.panicErr(it, errParseRange{i: it.val, size: "float64"})
|
||||||
|
|
||||||
p.panicf("Float '%s' is out of the range of 64-bit "+
|
|
||||||
"IEEE-754 floating-point numbers.", it.val)
|
|
||||||
} else {
|
} else {
|
||||||
p.panicf("Invalid float value: %q", it.val)
|
p.panicItemf(it, "Invalid float value: %q", it.val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return num, p.typeOfPrimitive(it)
|
return num, p.typeOfPrimitive(it)
|
||||||
case itemDatetime:
|
}
|
||||||
var t time.Time
|
|
||||||
var ok bool
|
var dtTypes = []struct {
|
||||||
var err error
|
fmt string
|
||||||
for _, format := range []string{
|
zone *time.Location
|
||||||
"2006-01-02T15:04:05Z07:00",
|
|
||||||
"2006-01-02T15:04:05",
|
|
||||||
"2006-01-02",
|
|
||||||
}{
|
}{
|
||||||
t, err = time.ParseInLocation(format, it.val, time.Local)
|
{time.RFC3339Nano, time.Local},
|
||||||
|
{"2006-01-02T15:04:05.999999999", internal.LocalDatetime},
|
||||||
|
{"2006-01-02", internal.LocalDate},
|
||||||
|
{"15:04:05.999999999", internal.LocalTime},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) valueDatetime(it item) (interface{}, tomlType) {
|
||||||
|
it.val = datetimeRepl.Replace(it.val)
|
||||||
|
var (
|
||||||
|
t time.Time
|
||||||
|
ok bool
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
for _, dt := range dtTypes {
|
||||||
|
t, err = time.ParseInLocation(dt.fmt, it.val, dt.zone)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
ok = true
|
ok = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
p.panicf("Invalid TOML Datetime: %q.", it.val)
|
p.panicItemf(it, "Invalid TOML Datetime: %q.", it.val)
|
||||||
}
|
}
|
||||||
return t, p.typeOfPrimitive(it)
|
return t, p.typeOfPrimitive(it)
|
||||||
case itemArray:
|
}
|
||||||
array := make([]interface{}, 0)
|
|
||||||
types := make([]tomlType, 0)
|
|
||||||
|
|
||||||
|
func (p *parser) valueArray(it item) (interface{}, tomlType) {
|
||||||
|
p.setType(p.currentKey, tomlArray, it.pos)
|
||||||
|
|
||||||
|
var (
|
||||||
|
types []tomlType
|
||||||
|
|
||||||
|
// Initialize to a non-nil empty slice. This makes it consistent with
|
||||||
|
// how S = [] decodes into a non-nil slice inside something like struct
|
||||||
|
// { S []string }. See #338
|
||||||
|
array = []interface{}{}
|
||||||
|
)
|
||||||
for it = p.next(); it.typ != itemArrayEnd; it = p.next() {
|
for it = p.next(); it.typ != itemArrayEnd; it = p.next() {
|
||||||
if it.typ == itemCommentStart {
|
if it.typ == itemCommentStart {
|
||||||
p.expect(itemText)
|
p.expect(itemText)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
val, typ := p.value(it)
|
val, typ := p.value(it, true)
|
||||||
array = append(array, val)
|
array = append(array, val)
|
||||||
types = append(types, typ)
|
types = append(types, typ)
|
||||||
|
|
||||||
|
// XXX: types isn't used here, we need it to record the accurate type
|
||||||
|
// information.
|
||||||
|
//
|
||||||
|
// Not entirely sure how to best store this; could use "key[0]",
|
||||||
|
// "key[1]" notation, or maybe store it on the Array type?
|
||||||
}
|
}
|
||||||
return array, p.typeOfArray(types)
|
return array, tomlArray
|
||||||
case itemInlineTableStart:
|
}
|
||||||
|
|
||||||
|
func (p *parser) valueInlineTable(it item, parentIsArray bool) (interface{}, tomlType) {
|
||||||
var (
|
var (
|
||||||
hash = make(map[string]interface{})
|
hash = make(map[string]interface{})
|
||||||
outerContext = p.context
|
outerContext = p.context
|
||||||
|
@ -277,51 +396,81 @@ func (p *parser) value(it item) (interface{}, tomlType) {
|
||||||
)
|
)
|
||||||
|
|
||||||
p.context = append(p.context, p.currentKey)
|
p.context = append(p.context, p.currentKey)
|
||||||
|
prevContext := p.context
|
||||||
p.currentKey = ""
|
p.currentKey = ""
|
||||||
|
|
||||||
|
p.addImplicit(p.context)
|
||||||
|
p.addContext(p.context, parentIsArray)
|
||||||
|
|
||||||
|
/// Loop over all table key/value pairs.
|
||||||
for it := p.next(); it.typ != itemInlineTableEnd; it = p.next() {
|
for it := p.next(); it.typ != itemInlineTableEnd; it = p.next() {
|
||||||
if it.typ != itemKeyStart {
|
|
||||||
p.bug("Expected key start but instead found %q, around line %d",
|
|
||||||
it.val, p.approxLine)
|
|
||||||
}
|
|
||||||
if it.typ == itemCommentStart {
|
if it.typ == itemCommentStart {
|
||||||
p.expect(itemText)
|
p.expect(itemText)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// retrieve key
|
/// Read all key parts.
|
||||||
k := p.next()
|
k := p.nextPos()
|
||||||
p.approxLine = k.line
|
var key Key
|
||||||
kname := p.keyString(k)
|
for ; k.typ != itemKeyEnd && k.typ != itemEOF; k = p.next() {
|
||||||
|
key = append(key, p.keyString(k))
|
||||||
|
}
|
||||||
|
p.assertEqual(itemKeyEnd, k.typ)
|
||||||
|
|
||||||
// retrieve value
|
/// The current key is the last part.
|
||||||
p.currentKey = kname
|
p.currentKey = key[len(key)-1]
|
||||||
val, typ := p.value(p.next())
|
|
||||||
// make sure we keep metadata up to date
|
/// All the other parts (if any) are the context; need to set each part
|
||||||
p.setType(kname, typ)
|
/// as implicit.
|
||||||
|
context := key[:len(key)-1]
|
||||||
|
for i := range context {
|
||||||
|
p.addImplicitContext(append(p.context, context[i:i+1]...))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the value.
|
||||||
|
val, typ := p.value(p.next(), false)
|
||||||
|
p.set(p.currentKey, val, typ, it.pos)
|
||||||
p.ordered = append(p.ordered, p.context.add(p.currentKey))
|
p.ordered = append(p.ordered, p.context.add(p.currentKey))
|
||||||
hash[kname] = val
|
hash[p.currentKey] = val
|
||||||
|
|
||||||
|
/// Restore context.
|
||||||
|
p.context = prevContext
|
||||||
}
|
}
|
||||||
p.context = outerContext
|
p.context = outerContext
|
||||||
p.currentKey = outerKey
|
p.currentKey = outerKey
|
||||||
return hash, tomlHash
|
return hash, tomlHash
|
||||||
}
|
}
|
||||||
p.bug("Unexpected value type: %s", it.typ)
|
|
||||||
panic("unreachable")
|
// numHasLeadingZero checks if this number has leading zeroes, allowing for '0',
|
||||||
|
// +/- signs, and base prefixes.
|
||||||
|
func numHasLeadingZero(s string) bool {
|
||||||
|
if len(s) > 1 && s[0] == '0' && !(s[1] == 'b' || s[1] == 'o' || s[1] == 'x') { // Allow 0b, 0o, 0x
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if len(s) > 2 && (s[0] == '-' || s[0] == '+') && s[1] == '0' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// numUnderscoresOK checks whether each underscore in s is surrounded by
|
// numUnderscoresOK checks whether each underscore in s is surrounded by
|
||||||
// characters that are not underscores.
|
// characters that are not underscores.
|
||||||
func numUnderscoresOK(s string) bool {
|
func numUnderscoresOK(s string) bool {
|
||||||
|
switch s {
|
||||||
|
case "nan", "+nan", "-nan", "inf", "-inf", "+inf":
|
||||||
|
return true
|
||||||
|
}
|
||||||
accept := false
|
accept := false
|
||||||
for _, r := range s {
|
for _, r := range s {
|
||||||
if r == '_' {
|
if r == '_' {
|
||||||
if !accept {
|
if !accept {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
accept = false
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
accept = true
|
|
||||||
|
// isHexadecimal is a superset of all the permissable characters
|
||||||
|
// surrounding an underscore.
|
||||||
|
accept = isHexadecimal(r)
|
||||||
}
|
}
|
||||||
return accept
|
return accept
|
||||||
}
|
}
|
||||||
|
@ -338,13 +487,12 @@ func numPeriodsOK(s string) bool {
|
||||||
return !period
|
return !period
|
||||||
}
|
}
|
||||||
|
|
||||||
// establishContext sets the current context of the parser,
|
// Set the current context of the parser, where the context is either a hash or
|
||||||
// where the context is either a hash or an array of hashes. Which one is
|
// an array of hashes, depending on the value of the `array` parameter.
|
||||||
// set depends on the value of the `array` parameter.
|
|
||||||
//
|
//
|
||||||
// Establishing the context also makes sure that the key isn't a duplicate, and
|
// Establishing the context also makes sure that the key isn't a duplicate, and
|
||||||
// will create implicit hashes automatically.
|
// will create implicit hashes automatically.
|
||||||
func (p *parser) establishContext(key Key, array bool) {
|
func (p *parser) addContext(key Key, array bool) {
|
||||||
var ok bool
|
var ok bool
|
||||||
|
|
||||||
// Always start at the top level and drill down for our context.
|
// Always start at the top level and drill down for our context.
|
||||||
|
@ -383,7 +531,7 @@ func (p *parser) establishContext(key Key, array bool) {
|
||||||
// list of tables for it.
|
// list of tables for it.
|
||||||
k := key[len(key)-1]
|
k := key[len(key)-1]
|
||||||
if _, ok := hashContext[k]; !ok {
|
if _, ok := hashContext[k]; !ok {
|
||||||
hashContext[k] = make([]map[string]interface{}, 0, 5)
|
hashContext[k] = make([]map[string]interface{}, 0, 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a new table. But make sure the key hasn't already been used
|
// Add a new table. But make sure the key hasn't already been used
|
||||||
|
@ -391,8 +539,7 @@ func (p *parser) establishContext(key Key, array bool) {
|
||||||
if hash, ok := hashContext[k].([]map[string]interface{}); ok {
|
if hash, ok := hashContext[k].([]map[string]interface{}); ok {
|
||||||
hashContext[k] = append(hash, make(map[string]interface{}))
|
hashContext[k] = append(hash, make(map[string]interface{}))
|
||||||
} else {
|
} else {
|
||||||
p.panicf("Key '%s' was already created and cannot be used as "+
|
p.panicf("Key '%s' was already created and cannot be used as an array.", key)
|
||||||
"an array.", keyContext)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
p.setValue(key[len(key)-1], make(map[string]interface{}))
|
p.setValue(key[len(key)-1], make(map[string]interface{}))
|
||||||
|
@ -400,15 +547,23 @@ func (p *parser) establishContext(key Key, array bool) {
|
||||||
p.context = append(p.context, key[len(key)-1])
|
p.context = append(p.context, key[len(key)-1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set calls setValue and setType.
|
||||||
|
func (p *parser) set(key string, val interface{}, typ tomlType, pos Position) {
|
||||||
|
p.setValue(key, val)
|
||||||
|
p.setType(key, typ, pos)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// setValue sets the given key to the given value in the current context.
|
// setValue sets the given key to the given value in the current context.
|
||||||
// It will make sure that the key hasn't already been defined, account for
|
// It will make sure that the key hasn't already been defined, account for
|
||||||
// implicit key groups.
|
// implicit key groups.
|
||||||
func (p *parser) setValue(key string, value interface{}) {
|
func (p *parser) setValue(key string, value interface{}) {
|
||||||
var tmpHash interface{}
|
var (
|
||||||
var ok bool
|
tmpHash interface{}
|
||||||
|
ok bool
|
||||||
hash := p.mapping
|
hash = p.mapping
|
||||||
keyContext := make(Key, 0)
|
keyContext Key
|
||||||
|
)
|
||||||
for _, k := range p.context {
|
for _, k := range p.context {
|
||||||
keyContext = append(keyContext, k)
|
keyContext = append(keyContext, k)
|
||||||
if tmpHash, ok = hash[k]; !ok {
|
if tmpHash, ok = hash[k]; !ok {
|
||||||
|
@ -422,24 +577,26 @@ func (p *parser) setValue(key string, value interface{}) {
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
hash = t
|
hash = t
|
||||||
default:
|
default:
|
||||||
p.bug("Expected hash to have type 'map[string]interface{}', but "+
|
p.panicf("Key '%s' has already been defined.", keyContext)
|
||||||
"it has '%T' instead.", tmpHash)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keyContext = append(keyContext, key)
|
keyContext = append(keyContext, key)
|
||||||
|
|
||||||
if _, ok := hash[key]; ok {
|
if _, ok := hash[key]; ok {
|
||||||
// Typically, if the given key has already been set, then we have
|
// Normally redefining keys isn't allowed, but the key could have been
|
||||||
// to raise an error since duplicate keys are disallowed. However,
|
// defined implicitly and it's allowed to be redefined concretely. (See
|
||||||
// it's possible that a key was previously defined implicitly. In this
|
// the `valid/implicit-and-explicit-after.toml` in toml-test)
|
||||||
// case, it is allowed to be redefined concretely. (See the
|
|
||||||
// `tests/valid/implicit-and-explicit-after.toml` test in `toml-test`.)
|
|
||||||
//
|
//
|
||||||
// But we have to make sure to stop marking it as an implicit. (So that
|
// But we have to make sure to stop marking it as an implicit. (So that
|
||||||
// another redefinition provokes an error.)
|
// another redefinition provokes an error.)
|
||||||
//
|
//
|
||||||
// Note that since it has already been defined (as a hash), we don't
|
// Note that since it has already been defined (as a hash), we don't
|
||||||
// want to overwrite it. So our business is done.
|
// want to overwrite it. So our business is done.
|
||||||
|
if p.isArray(keyContext) {
|
||||||
|
p.removeImplicit(keyContext)
|
||||||
|
hash[key] = value
|
||||||
|
return
|
||||||
|
}
|
||||||
if p.isImplicit(keyContext) {
|
if p.isImplicit(keyContext) {
|
||||||
p.removeImplicit(keyContext)
|
p.removeImplicit(keyContext)
|
||||||
return
|
return
|
||||||
|
@ -449,40 +606,39 @@ func (p *parser) setValue(key string, value interface{}) {
|
||||||
// key, which is *always* wrong.
|
// key, which is *always* wrong.
|
||||||
p.panicf("Key '%s' has already been defined.", keyContext)
|
p.panicf("Key '%s' has already been defined.", keyContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
hash[key] = value
|
hash[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
// setType sets the type of a particular value at a given key.
|
// setType sets the type of a particular value at a given key. It should be
|
||||||
// It should be called immediately AFTER setValue.
|
// called immediately AFTER setValue.
|
||||||
//
|
//
|
||||||
// Note that if `key` is empty, then the type given will be applied to the
|
// Note that if `key` is empty, then the type given will be applied to the
|
||||||
// current context (which is either a table or an array of tables).
|
// current context (which is either a table or an array of tables).
|
||||||
func (p *parser) setType(key string, typ tomlType) {
|
func (p *parser) setType(key string, typ tomlType, pos Position) {
|
||||||
keyContext := make(Key, 0, len(p.context)+1)
|
keyContext := make(Key, 0, len(p.context)+1)
|
||||||
for _, k := range p.context {
|
keyContext = append(keyContext, p.context...)
|
||||||
keyContext = append(keyContext, k)
|
|
||||||
}
|
|
||||||
if len(key) > 0 { // allow type setting for hashes
|
if len(key) > 0 { // allow type setting for hashes
|
||||||
keyContext = append(keyContext, key)
|
keyContext = append(keyContext, key)
|
||||||
}
|
}
|
||||||
p.types[keyContext.String()] = typ
|
// Special case to make empty keys ("" = 1) work.
|
||||||
|
// Without it it will set "" rather than `""`.
|
||||||
|
// TODO: why is this needed? And why is this only needed here?
|
||||||
|
if len(keyContext) == 0 {
|
||||||
|
keyContext = Key{""}
|
||||||
|
}
|
||||||
|
p.keyInfo[keyContext.String()] = keyInfo{tomlType: typ, pos: pos}
|
||||||
}
|
}
|
||||||
|
|
||||||
// addImplicit sets the given Key as having been created implicitly.
|
// Implicit keys need to be created when tables are implied in "a.b.c.d = 1" and
|
||||||
func (p *parser) addImplicit(key Key) {
|
// "[a.b.c]" (the "a", "b", and "c" hashes are never created explicitly).
|
||||||
p.implicits[key.String()] = true
|
func (p *parser) addImplicit(key Key) { p.implicits[key.String()] = struct{}{} }
|
||||||
}
|
func (p *parser) removeImplicit(key Key) { delete(p.implicits, key.String()) }
|
||||||
|
func (p *parser) isImplicit(key Key) bool { _, ok := p.implicits[key.String()]; return ok }
|
||||||
// removeImplicit stops tagging the given key as having been implicitly
|
func (p *parser) isArray(key Key) bool { return p.keyInfo[key.String()].tomlType == tomlArray }
|
||||||
// created.
|
func (p *parser) addImplicitContext(key Key) {
|
||||||
func (p *parser) removeImplicit(key Key) {
|
p.addImplicit(key)
|
||||||
p.implicits[key.String()] = false
|
p.addContext(key, false)
|
||||||
}
|
|
||||||
|
|
||||||
// isImplicit returns true if the key group pointed to by the key was created
|
|
||||||
// implicitly.
|
|
||||||
func (p *parser) isImplicit(key Key) bool {
|
|
||||||
return p.implicits[key.String()]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// current returns the full key name of the current context.
|
// current returns the full key name of the current context.
|
||||||
|
@ -497,24 +653,62 @@ func (p *parser) current() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func stripFirstNewline(s string) string {
|
func stripFirstNewline(s string) string {
|
||||||
if len(s) == 0 || s[0] != '\n' {
|
if len(s) > 0 && s[0] == '\n' {
|
||||||
return s
|
|
||||||
}
|
|
||||||
return s[1:]
|
return s[1:]
|
||||||
}
|
}
|
||||||
|
if len(s) > 1 && s[0] == '\r' && s[1] == '\n' {
|
||||||
func stripEscapedWhitespace(s string) string {
|
return s[2:]
|
||||||
esc := strings.Split(s, "\\\n")
|
|
||||||
if len(esc) > 1 {
|
|
||||||
for i := 1; i < len(esc); i++ {
|
|
||||||
esc[i] = strings.TrimLeftFunc(esc[i], unicode.IsSpace)
|
|
||||||
}
|
}
|
||||||
}
|
return s
|
||||||
return strings.Join(esc, "")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) replaceEscapes(str string) string {
|
// Remove newlines inside triple-quoted strings if a line ends with "\".
|
||||||
var replaced []rune
|
func (p *parser) stripEscapedNewlines(s string) string {
|
||||||
|
split := strings.Split(s, "\n")
|
||||||
|
if len(split) < 1 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
escNL := false // Keep track of the last non-blank line was escaped.
|
||||||
|
for i, line := range split {
|
||||||
|
line = strings.TrimRight(line, " \t\r")
|
||||||
|
|
||||||
|
if len(line) == 0 || line[len(line)-1] != '\\' {
|
||||||
|
split[i] = strings.TrimRight(split[i], "\r")
|
||||||
|
if !escNL && i != len(split)-1 {
|
||||||
|
split[i] += "\n"
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
escBS := true
|
||||||
|
for j := len(line) - 1; j >= 0 && line[j] == '\\'; j-- {
|
||||||
|
escBS = !escBS
|
||||||
|
}
|
||||||
|
if escNL {
|
||||||
|
line = strings.TrimLeft(line, " \t\r")
|
||||||
|
}
|
||||||
|
escNL = !escBS
|
||||||
|
|
||||||
|
if escBS {
|
||||||
|
split[i] += "\n"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == len(split)-1 {
|
||||||
|
p.panicf("invalid escape: '\\ '")
|
||||||
|
}
|
||||||
|
|
||||||
|
split[i] = line[:len(line)-1] // Remove \
|
||||||
|
if len(split)-1 > i {
|
||||||
|
split[i+1] = strings.TrimLeft(split[i+1], " \t\r")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(split, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) replaceEscapes(it item, str string) string {
|
||||||
|
replaced := make([]rune, 0, len(str))
|
||||||
s := []byte(str)
|
s := []byte(str)
|
||||||
r := 0
|
r := 0
|
||||||
for r < len(s) {
|
for r < len(s) {
|
||||||
|
@ -532,7 +726,8 @@ func (p *parser) replaceEscapes(str string) string {
|
||||||
switch s[r] {
|
switch s[r] {
|
||||||
default:
|
default:
|
||||||
p.bug("Expected valid escape code after \\, but got %q.", s[r])
|
p.bug("Expected valid escape code after \\, but got %q.", s[r])
|
||||||
return ""
|
case ' ', '\t':
|
||||||
|
p.panicItemf(it, "invalid escape: '\\%c'", s[r])
|
||||||
case 'b':
|
case 'b':
|
||||||
replaced = append(replaced, rune(0x0008))
|
replaced = append(replaced, rune(0x0008))
|
||||||
r += 1
|
r += 1
|
||||||
|
@ -558,14 +753,14 @@ func (p *parser) replaceEscapes(str string) string {
|
||||||
// At this point, we know we have a Unicode escape of the form
|
// At this point, we know we have a Unicode escape of the form
|
||||||
// `uXXXX` at [r, r+5). (Because the lexer guarantees this
|
// `uXXXX` at [r, r+5). (Because the lexer guarantees this
|
||||||
// for us.)
|
// for us.)
|
||||||
escaped := p.asciiEscapeToUnicode(s[r+1 : r+5])
|
escaped := p.asciiEscapeToUnicode(it, s[r+1:r+5])
|
||||||
replaced = append(replaced, escaped)
|
replaced = append(replaced, escaped)
|
||||||
r += 5
|
r += 5
|
||||||
case 'U':
|
case 'U':
|
||||||
// At this point, we know we have a Unicode escape of the form
|
// At this point, we know we have a Unicode escape of the form
|
||||||
// `uXXXX` at [r, r+9). (Because the lexer guarantees this
|
// `uXXXX` at [r, r+9). (Because the lexer guarantees this
|
||||||
// for us.)
|
// for us.)
|
||||||
escaped := p.asciiEscapeToUnicode(s[r+1 : r+9])
|
escaped := p.asciiEscapeToUnicode(it, s[r+1:r+9])
|
||||||
replaced = append(replaced, escaped)
|
replaced = append(replaced, escaped)
|
||||||
r += 9
|
r += 9
|
||||||
}
|
}
|
||||||
|
@ -573,20 +768,14 @@ func (p *parser) replaceEscapes(str string) string {
|
||||||
return string(replaced)
|
return string(replaced)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) asciiEscapeToUnicode(bs []byte) rune {
|
func (p *parser) asciiEscapeToUnicode(it item, bs []byte) rune {
|
||||||
s := string(bs)
|
s := string(bs)
|
||||||
hex, err := strconv.ParseUint(strings.ToLower(s), 16, 32)
|
hex, err := strconv.ParseUint(strings.ToLower(s), 16, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.bug("Could not parse '%s' as a hexadecimal number, but the "+
|
p.bug("Could not parse '%s' as a hexadecimal number, but the lexer claims it's OK: %s", s, err)
|
||||||
"lexer claims it's OK: %s", s, err)
|
|
||||||
}
|
}
|
||||||
if !utf8.ValidRune(rune(hex)) {
|
if !utf8.ValidRune(rune(hex)) {
|
||||||
p.panicf("Escaped character '\\u%s' is not valid UTF-8.", s)
|
p.panicItemf(it, "Escaped character '\\u%s' is not valid UTF-8.", s)
|
||||||
}
|
}
|
||||||
return rune(hex)
|
return rune(hex)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isStringType(ty itemType) bool {
|
|
||||||
return ty == itemString || ty == itemMultilineString ||
|
|
||||||
ty == itemRawString || ty == itemRawMultilineString
|
|
||||||
}
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
au BufWritePost *.go silent!make tags > /dev/null 2>&1
|
|
|
@ -70,8 +70,8 @@ func typeFields(t reflect.Type) []field {
|
||||||
next := []field{{typ: t}}
|
next := []field{{typ: t}}
|
||||||
|
|
||||||
// Count of queued names for current level and the next.
|
// Count of queued names for current level and the next.
|
||||||
count := map[reflect.Type]int{}
|
var count map[reflect.Type]int
|
||||||
nextCount := map[reflect.Type]int{}
|
var nextCount map[reflect.Type]int
|
||||||
|
|
||||||
// Types already visited at an earlier level.
|
// Types already visited at an earlier level.
|
||||||
visited := map[reflect.Type]bool{}
|
visited := map[reflect.Type]bool{}
|
||||||
|
|
|
@ -16,7 +16,7 @@ func typeEqual(t1, t2 tomlType) bool {
|
||||||
return t1.typeString() == t2.typeString()
|
return t1.typeString() == t2.typeString()
|
||||||
}
|
}
|
||||||
|
|
||||||
func typeIsHash(t tomlType) bool {
|
func typeIsTable(t tomlType) bool {
|
||||||
return typeEqual(t, tomlHash) || typeEqual(t, tomlArrayHash)
|
return typeEqual(t, tomlHash) || typeEqual(t, tomlArrayHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,24 +68,3 @@ func (p *parser) typeOfPrimitive(lexItem item) tomlType {
|
||||||
p.bug("Cannot infer primitive type of lex item '%s'.", lexItem)
|
p.bug("Cannot infer primitive type of lex item '%s'.", lexItem)
|
||||||
panic("unreachable")
|
panic("unreachable")
|
||||||
}
|
}
|
||||||
|
|
||||||
// typeOfArray returns a tomlType for an array given a list of types of its
|
|
||||||
// values.
|
|
||||||
//
|
|
||||||
// In the current spec, if an array is homogeneous, then its type is always
|
|
||||||
// "Array". If the array is not homogeneous, an error is generated.
|
|
||||||
func (p *parser) typeOfArray(types []tomlType) tomlType {
|
|
||||||
// Empty arrays are cool.
|
|
||||||
if len(types) == 0 {
|
|
||||||
return tomlArray
|
|
||||||
}
|
|
||||||
|
|
||||||
theType := types[0]
|
|
||||||
for _, t := range types[1:] {
|
|
||||||
if !typeEqual(theType, t) {
|
|
||||||
p.panicf("Array contains values of type '%s' and '%s', but "+
|
|
||||||
"arrays must be homogeneous.", theType, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tomlArray
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
---
|
|
||||||
language: go
|
|
||||||
go:
|
|
||||||
- tip
|
|
||||||
- 1.12.x
|
|
||||||
- 1.11.x
|
|
||||||
- 1.10.x
|
|
||||||
- 1.9.x
|
|
||||||
- 1.8.x
|
|
||||||
sudo: false
|
|
||||||
|
|
||||||
# Forks will use that path for checkout
|
|
||||||
go_import_path: github.com/certifi/gocertifi
|
|
|
@ -59,16 +59,11 @@ Import as follows:
|
||||||
import "github.com/certifi/gocertifi"
|
import "github.com/certifi/gocertifi"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Errors
|
|
||||||
|
|
||||||
```go
|
|
||||||
var ErrParseFailed = errors.New("gocertifi: error when parsing certificates")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Functions
|
### Functions
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func CACerts() (*x509.CertPool, error)
|
func CACerts() (*x509.CertPool, error)
|
||||||
```
|
```
|
||||||
CACerts builds an X.509 certificate pool containing the Mozilla CA Certificate
|
CACerts builds an X.509 certificate pool containing the Mozilla CA Certificate
|
||||||
bundle. Returns nil on error along with an appropriate error code.
|
bundle. This can't actually error and always returns successfully with `nil`
|
||||||
|
as the error. This will be replaced in `v2` to only return the `CertPool`.
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,5 @@
|
||||||
*.test
|
*.test
|
||||||
|
*.out
|
||||||
example/example
|
example/example
|
||||||
|
/xunit.xml
|
||||||
docs/_build
|
/coverage.xml
|
||||||
docs/doctrees
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
[submodule "docs/_sentryext"]
|
|
||||||
path = docs/_sentryext
|
|
||||||
url = https://github.com/getsentry/sentry-doc-support
|
|
|
@ -1,26 +1,41 @@
|
||||||
sudo: false
|
sudo: false
|
||||||
language: go
|
language: go
|
||||||
go:
|
go:
|
||||||
- "1.2"
|
|
||||||
- "1.3"
|
|
||||||
- 1.4.x
|
|
||||||
- 1.5.x
|
|
||||||
- 1.6.x
|
|
||||||
- 1.7.x
|
- 1.7.x
|
||||||
- 1.8.x
|
- 1.8.x
|
||||||
- 1.9.x
|
- 1.9.x
|
||||||
- 1.10.x
|
- 1.10.x
|
||||||
|
- 1.11.x
|
||||||
- tip
|
- tip
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
- go install -race std
|
- go install -race std
|
||||||
- go get golang.org/x/tools/cmd/cover
|
- go get golang.org/x/tools/cmd/cover
|
||||||
|
- go get github.com/tebeka/go2xunit
|
||||||
|
- go get github.com/t-yuki/gocover-cobertura
|
||||||
- go get -v ./...
|
- go get -v ./...
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- go test -v -race ./...
|
- go test -v -race ./... | tee gotest.out
|
||||||
- go test -v -cover ./...
|
- $GOPATH/bin/go2xunit -fail -input gotest.out -output xunit.xml
|
||||||
|
- go test -v -coverprofile=coverage.txt -covermode count .
|
||||||
|
- $GOPATH/bin/gocover-cobertura < coverage.txt > coverage.xml
|
||||||
|
|
||||||
|
after_script:
|
||||||
|
- npm install -g @zeus-ci/cli
|
||||||
|
- zeus upload -t "application/x-cobertura+xml" coverage.xml
|
||||||
|
- zeus upload -t "application/x-xunit+xml" xunit.xml
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- go: tip
|
- go: tip
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
webhooks:
|
||||||
|
urls:
|
||||||
|
- https://zeus.ci/hooks/cd949996-d30a-11e8-ba53-0a580a28042d/public/provider/travis/webhook
|
||||||
|
on_success: always
|
||||||
|
on_failure: always
|
||||||
|
on_start: always
|
||||||
|
on_cancel: always
|
||||||
|
on_error: always
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
# raven [![Build Status](https://travis-ci.org/getsentry/raven-go.png?branch=master)](https://travis-ci.org/getsentry/raven-go)
|
# raven
|
||||||
|
|
||||||
raven is a Go client for the [Sentry](https://github.com/getsentry/sentry)
|
[![Build Status](https://api.travis-ci.org/getsentry/raven-go.svg?branch=master)](https://travis-ci.org/getsentry/raven-go)
|
||||||
|
[![Go Report Card](https://goreportcard.com/badge/github.com/getsentry/raven-go)](https://goreportcard.com/report/github.com/getsentry/raven-go)
|
||||||
|
[![GoDoc](https://godoc.org/github.com/getsentry/raven-go?status.svg)](https://godoc.org/github.com/getsentry/raven-go)
|
||||||
|
|
||||||
|
raven is the official Go SDK for the [Sentry](https://github.com/getsentry/sentry)
|
||||||
event/error logging system.
|
event/error logging system.
|
||||||
|
|
||||||
- [**API Documentation**](https://godoc.org/github.com/getsentry/raven-go)
|
- [**API Documentation**](https://godoc.org/github.com/getsentry/raven-go)
|
||||||
|
@ -11,3 +15,5 @@ event/error logging system.
|
||||||
```text
|
```text
|
||||||
go get github.com/getsentry/raven-go
|
go get github.com/getsentry/raven-go
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note: Go 1.7 and newer are supported.
|
||||||
|
|
|
@ -945,7 +945,7 @@ func (t *HTTPTransport) Send(url, authHeader string, packet *Packet) error {
|
||||||
io.Copy(ioutil.Discard, res.Body)
|
io.Copy(ioutil.Discard, res.Body)
|
||||||
res.Body.Close()
|
res.Body.Close()
|
||||||
if res.StatusCode != 200 {
|
if res.StatusCode != 200 {
|
||||||
return fmt.Errorf("raven: got http status %d", res.StatusCode)
|
return fmt.Errorf("raven: got http status %d - x-sentry-error: %s", res.StatusCode, res.Header.Get("X-Sentry-Error"))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,17 +69,31 @@ func (h *Http) Class() string { return "request" }
|
||||||
// ...
|
// ...
|
||||||
// }))
|
// }))
|
||||||
func RecoveryHandler(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
func RecoveryHandler(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return Recoverer(http.HandlerFunc(handler)).ServeHTTP
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recovery handler to wrap the stdlib net/http Mux.
|
||||||
|
// Example:
|
||||||
|
// mux := http.NewServeMux
|
||||||
|
// ...
|
||||||
|
// http.Handle("/", raven.Recoverer(mux))
|
||||||
|
func Recoverer(handler http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if rval := recover(); rval != nil {
|
if rval := recover(); rval != nil {
|
||||||
debug.PrintStack()
|
debug.PrintStack()
|
||||||
rvalStr := fmt.Sprint(rval)
|
rvalStr := fmt.Sprint(rval)
|
||||||
packet := NewPacket(rvalStr, NewException(errors.New(rvalStr), GetOrNewStacktrace(rval.(error), 2, 3, nil)), NewHttp(r))
|
var packet *Packet
|
||||||
|
if err, ok := rval.(error); ok {
|
||||||
|
packet = NewPacket(rvalStr, NewException(errors.New(rvalStr), GetOrNewStacktrace(err, 2, 3, nil)), NewHttp(r))
|
||||||
|
} else {
|
||||||
|
packet = NewPacket(rvalStr, NewException(errors.New(rvalStr), NewStacktrace(2, 3, nil)), NewHttp(r))
|
||||||
|
}
|
||||||
Capture(packet, nil)
|
Capture(packet, nil)
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
handler(w, r)
|
handler.ServeHTTP(w, r)
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,14 +61,17 @@ func GetOrNewStacktrace(err error, skip int, context int, appPackagePrefixes []s
|
||||||
for _, f := range stacktracer.StackTrace() {
|
for _, f := range stacktracer.StackTrace() {
|
||||||
pc := uintptr(f) - 1
|
pc := uintptr(f) - 1
|
||||||
fn := runtime.FuncForPC(pc)
|
fn := runtime.FuncForPC(pc)
|
||||||
|
var fName string
|
||||||
var file string
|
var file string
|
||||||
var line int
|
var line int
|
||||||
if fn != nil {
|
if fn != nil {
|
||||||
file, line = fn.FileLine(pc)
|
file, line = fn.FileLine(pc)
|
||||||
|
fName = fn.Name()
|
||||||
} else {
|
} else {
|
||||||
file = "unknown"
|
file = "unknown"
|
||||||
|
fName = "unknown"
|
||||||
}
|
}
|
||||||
frame := NewStacktraceFrame(pc, file, line, context, appPackagePrefixes)
|
frame := NewStacktraceFrame(pc, fName, file, line, context, appPackagePrefixes)
|
||||||
if frame != nil {
|
if frame != nil {
|
||||||
frames = append([]*StacktraceFrame{frame}, frames...)
|
frames = append([]*StacktraceFrame{frame}, frames...)
|
||||||
}
|
}
|
||||||
|
@ -89,16 +92,29 @@ func GetOrNewStacktrace(err error, skip int, context int, appPackagePrefixes []s
|
||||||
// be considered "in app".
|
// be considered "in app".
|
||||||
func NewStacktrace(skip int, context int, appPackagePrefixes []string) *Stacktrace {
|
func NewStacktrace(skip int, context int, appPackagePrefixes []string) *Stacktrace {
|
||||||
var frames []*StacktraceFrame
|
var frames []*StacktraceFrame
|
||||||
for i := 1 + skip; ; i++ {
|
|
||||||
pc, file, line, ok := runtime.Caller(i)
|
callerPcs := make([]uintptr, 100)
|
||||||
if !ok {
|
numCallers := runtime.Callers(skip+2, callerPcs)
|
||||||
break
|
|
||||||
|
// If there are no callers, the entire stacktrace is nil
|
||||||
|
if numCallers == 0 {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
frame := NewStacktraceFrame(pc, file, line, context, appPackagePrefixes)
|
|
||||||
|
callersFrames := runtime.CallersFrames(callerPcs)
|
||||||
|
|
||||||
|
for {
|
||||||
|
fr, more := callersFrames.Next()
|
||||||
|
if fr.Func != nil {
|
||||||
|
frame := NewStacktraceFrame(fr.PC, fr.Function, fr.File, fr.Line, context, appPackagePrefixes)
|
||||||
if frame != nil {
|
if frame != nil {
|
||||||
frames = append(frames, frame)
|
frames = append(frames, frame)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !more {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
// If there are no frames, the entire stacktrace is nil
|
// If there are no frames, the entire stacktrace is nil
|
||||||
if len(frames) == 0 {
|
if len(frames) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
@ -122,9 +138,9 @@ func NewStacktrace(skip int, context int, appPackagePrefixes []string) *Stacktra
|
||||||
//
|
//
|
||||||
// appPackagePrefixes is a list of prefixes used to check whether a package should
|
// appPackagePrefixes is a list of prefixes used to check whether a package should
|
||||||
// be considered "in app".
|
// be considered "in app".
|
||||||
func NewStacktraceFrame(pc uintptr, file string, line, context int, appPackagePrefixes []string) *StacktraceFrame {
|
func NewStacktraceFrame(pc uintptr, fName, file string, line, context int, appPackagePrefixes []string) *StacktraceFrame {
|
||||||
frame := &StacktraceFrame{AbsolutePath: file, Filename: trimPath(file), Lineno: line, InApp: false}
|
frame := &StacktraceFrame{AbsolutePath: file, Filename: trimPath(file), Lineno: line, InApp: false}
|
||||||
frame.Module, frame.Function = functionName(pc)
|
frame.Module, frame.Function = functionName(fName)
|
||||||
|
|
||||||
// `runtime.goexit` is effectively a placeholder that comes from
|
// `runtime.goexit` is effectively a placeholder that comes from
|
||||||
// runtime/asm_amd64.s and is meaningless.
|
// runtime/asm_amd64.s and is meaningless.
|
||||||
|
@ -143,7 +159,7 @@ func NewStacktraceFrame(pc uintptr, file string, line, context int, appPackagePr
|
||||||
}
|
}
|
||||||
|
|
||||||
if context > 0 {
|
if context > 0 {
|
||||||
contextLines, lineIdx := fileContext(file, line, context)
|
contextLines, lineIdx := sourceCodeLoader.Load(file, line, context)
|
||||||
if len(contextLines) > 0 {
|
if len(contextLines) > 0 {
|
||||||
for i, line := range contextLines {
|
for i, line := range contextLines {
|
||||||
switch {
|
switch {
|
||||||
|
@ -157,7 +173,7 @@ func NewStacktraceFrame(pc uintptr, file string, line, context int, appPackagePr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if context == -1 {
|
} else if context == -1 {
|
||||||
contextLine, _ := fileContext(file, line, 0)
|
contextLine, _ := sourceCodeLoader.Load(file, line, 0)
|
||||||
if len(contextLine) > 0 {
|
if len(contextLine) > 0 {
|
||||||
frame.ContextLine = string(contextLine[0])
|
frame.ContextLine = string(contextLine[0])
|
||||||
}
|
}
|
||||||
|
@ -166,12 +182,8 @@ func NewStacktraceFrame(pc uintptr, file string, line, context int, appPackagePr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the name of the package and function containing the PC.
|
// Retrieve the name of the package and function containing the PC.
|
||||||
func functionName(pc uintptr) (pack string, name string) {
|
func functionName(fName string) (pack string, name string) {
|
||||||
fn := runtime.FuncForPC(pc)
|
name = fName
|
||||||
if fn == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
name = fn.Name()
|
|
||||||
// We get this:
|
// We get this:
|
||||||
// runtime/debug.*T·ptrmethod
|
// runtime/debug.*T·ptrmethod
|
||||||
// and want this:
|
// and want this:
|
||||||
|
@ -185,24 +197,36 @@ func functionName(pc uintptr) (pack string, name string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileCacheLock sync.Mutex
|
type SourceCodeLoader interface {
|
||||||
var fileCache = make(map[string][][]byte)
|
Load(filename string, line, context int) ([][]byte, int)
|
||||||
|
}
|
||||||
|
|
||||||
func fileContext(filename string, line, context int) ([][]byte, int) {
|
var sourceCodeLoader SourceCodeLoader = &fsLoader{cache: make(map[string][][]byte)}
|
||||||
fileCacheLock.Lock()
|
|
||||||
defer fileCacheLock.Unlock()
|
func SetSourceCodeLoader(loader SourceCodeLoader) {
|
||||||
lines, ok := fileCache[filename]
|
sourceCodeLoader = loader
|
||||||
|
}
|
||||||
|
|
||||||
|
type fsLoader struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
cache map[string][][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *fsLoader) Load(filename string, line, context int) ([][]byte, int) {
|
||||||
|
fs.mu.Lock()
|
||||||
|
defer fs.mu.Unlock()
|
||||||
|
lines, ok := fs.cache[filename]
|
||||||
if !ok {
|
if !ok {
|
||||||
data, err := ioutil.ReadFile(filename)
|
data, err := ioutil.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// cache errors as nil slice: code below handles it correctly
|
// cache errors as nil slice: code below handles it correctly
|
||||||
// otherwise when missing the source or running as a different user, we try
|
// otherwise when missing the source or running as a different user, we try
|
||||||
// reading the file on each error which is unnecessary
|
// reading the file on each error which is unnecessary
|
||||||
fileCache[filename] = nil
|
fs.cache[filename] = nil
|
||||||
return nil, 0
|
return nil, 0
|
||||||
}
|
}
|
||||||
lines = bytes.Split(data, []byte{'\n'})
|
lines = bytes.Split(data, []byte{'\n'})
|
||||||
fileCache[filename] = lines
|
fs.cache[filename] = lines
|
||||||
}
|
}
|
||||||
|
|
||||||
if lines == nil {
|
if lines == nil {
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
minVersion: 0.23.1
|
||||||
|
changelogPolicy: simple
|
||||||
|
artifactProvider:
|
||||||
|
name: none
|
||||||
|
targets:
|
||||||
|
- name: github
|
||||||
|
includeNames: /none/
|
||||||
|
tagPrefix: v
|
||||||
|
- name: registry
|
||||||
|
sdks:
|
||||||
|
github:getsentry/sentry-go:
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Tell Git to use LF for line endings on all platforms.
|
||||||
|
# Required to have correct test data on Windows.
|
||||||
|
# https://github.com/mvdan/github-actions-golang#caveats
|
||||||
|
# https://github.com/actions/checkout/issues/135#issuecomment-613361104
|
||||||
|
* text eol=lf
|
|
@ -0,0 +1,10 @@
|
||||||
|
coverage.txt
|
||||||
|
|
||||||
|
# Just my personal way of tracking stuff — Kamil
|
||||||
|
FIXME.md
|
||||||
|
TODO.md
|
||||||
|
!NOTES.md
|
||||||
|
|
||||||
|
# IDE system files
|
||||||
|
.idea
|
||||||
|
.vscode
|
|
@ -0,0 +1,49 @@
|
||||||
|
linters:
|
||||||
|
disable-all: true
|
||||||
|
enable:
|
||||||
|
- bodyclose
|
||||||
|
- deadcode
|
||||||
|
- depguard
|
||||||
|
- dogsled
|
||||||
|
- dupl
|
||||||
|
- errcheck
|
||||||
|
- exportloopref
|
||||||
|
- gochecknoinits
|
||||||
|
- goconst
|
||||||
|
- gocritic
|
||||||
|
- gocyclo
|
||||||
|
- godot
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
- gosec
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- misspell
|
||||||
|
- nakedret
|
||||||
|
- prealloc
|
||||||
|
- revive
|
||||||
|
- staticcheck
|
||||||
|
- structcheck
|
||||||
|
- typecheck
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- unused
|
||||||
|
- varcheck
|
||||||
|
- whitespace
|
||||||
|
issues:
|
||||||
|
exclude-rules:
|
||||||
|
- path: _test\.go
|
||||||
|
linters:
|
||||||
|
- prealloc
|
||||||
|
- path: _test\.go
|
||||||
|
text: "G306:"
|
||||||
|
linters:
|
||||||
|
- gosec
|
||||||
|
- path: errors_test\.go
|
||||||
|
linters:
|
||||||
|
- unused
|
||||||
|
- path: http/example_test\.go
|
||||||
|
linters:
|
||||||
|
- errcheck
|
||||||
|
- bodyclose
|
|
@ -0,0 +1,400 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 0.16.0
|
||||||
|
|
||||||
|
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.16.0.
|
||||||
|
Due to ongoing work towards a stable API for `v1.0.0`, we sadly had to include **two breaking changes** in this release.
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- Add `EnableTracing`, a boolean option flag to enable performance monitoring (`false` by default).
|
||||||
|
- If you're using `TracesSampleRate` or `TracesSampler`, this option is **required** to enable performance monitoring.
|
||||||
|
|
||||||
|
```go
|
||||||
|
sentry.Init(sentry.ClientOptions{
|
||||||
|
EnableTracing: true,
|
||||||
|
TracesSampleRate: 1.0,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
- Unify TracesSampler [#498](https://github.com/getsentry/sentry-go/pull/498)
|
||||||
|
- `TracesSampler` was changed to a callback that must return a `float64` between `0.0` and `1.0`.
|
||||||
|
|
||||||
|
For example, you can apply a sample rate of `1.0` (100%) to all `/api` transactions, and a sample rate of `0.5` (50%) to all other transactions.
|
||||||
|
You can read more about this in our [SDK docs](https://docs.sentry.io/platforms/go/configuration/filtering/#using-sampling-to-filter-transaction-events).
|
||||||
|
|
||||||
|
```go
|
||||||
|
sentry.Init(sentry.ClientOptions{
|
||||||
|
TracesSampler: sentry.TracesSampler(func(ctx sentry.SamplingContext) float64 {
|
||||||
|
hub := sentry.GetHubFromContext(ctx.Span.Context())
|
||||||
|
name := hub.Scope().Transaction()
|
||||||
|
|
||||||
|
if strings.HasPrefix(name, "GET /api") {
|
||||||
|
return 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0.5
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Send errors logged with [Logrus](https://github.com/sirupsen/logrus) to Sentry.
|
||||||
|
- Have a look at our [logrus examples](https://github.com/getsentry/sentry-go/blob/master/example/logrus/main.go) on how to use the integration.
|
||||||
|
- Add support for Dynamic Sampling [#491](https://github.com/getsentry/sentry-go/pull/491)
|
||||||
|
- You can read more about Dynamic Sampling in our [product docs](https://docs.sentry.io/product/data-management-settings/dynamic-sampling/).
|
||||||
|
- Add detailed logging about the reason transactions are being dropped.
|
||||||
|
- You can enable SDK logging via `sentry.ClientOptions.Debug: true`.
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Do not clone the hub when calling `StartTransaction` [#505](https://github.com/getsentry/sentry-go/pull/505)
|
||||||
|
- Fixes [#502](https://github.com/getsentry/sentry-go/issues/502)
|
||||||
|
|
||||||
|
## 0.15.0
|
||||||
|
|
||||||
|
- fix: Scope values should not override Event values (#446)
|
||||||
|
- feat: Make maximum amount of spans configurable (#460)
|
||||||
|
- feat: Add a method to start a transaction (#482)
|
||||||
|
- feat: Extend User interface by adding Data, Name and Segment (#483)
|
||||||
|
- feat: Add ClientOptions.SendDefaultPII (#485)
|
||||||
|
|
||||||
|
## 0.14.0
|
||||||
|
|
||||||
|
- feat: Add function to continue from trace string (#434)
|
||||||
|
- feat: Add `max-depth` options (#428)
|
||||||
|
- *[breaking]* ref: Use a `Context` type mapping to a `map[string]interface{}` for all event contexts (#444)
|
||||||
|
- *[breaking]* ref: Replace deprecated `ioutil` pkg with `os` & `io` (#454)
|
||||||
|
- ref: Optimize `stacktrace.go` from size and speed (#467)
|
||||||
|
- ci: Test against `go1.19` and `go1.18`, drop `go1.16` and `go1.15` support (#432, #477)
|
||||||
|
- deps: Dependency update to fix CVEs (#462, #464, #477)
|
||||||
|
|
||||||
|
_NOTE:_ This version drops support for Go 1.16 and Go 1.15. The currently supported Go versions are the last 3 stable releases: 1.19, 1.18 and 1.17.
|
||||||
|
|
||||||
|
## v0.13.0
|
||||||
|
|
||||||
|
- ref: Change DSN ProjectID to be a string (#420)
|
||||||
|
- fix: When extracting PCs from stack frames, try the `PC` field (#393)
|
||||||
|
- build: Bump gin-gonic/gin from v1.4.0 to v1.7.7 (#412)
|
||||||
|
- build: Bump Go version in go.mod (#410)
|
||||||
|
- ci: Bump golangci-lint version in GH workflow (#419)
|
||||||
|
- ci: Update GraphQL config with appropriate permissions (#417)
|
||||||
|
- ci: ci: Add craft release automation (#422)
|
||||||
|
|
||||||
|
## v0.12.0
|
||||||
|
|
||||||
|
- feat: Automatic Release detection (#363, #369, #386, #400)
|
||||||
|
- fix: Do not change Hub.lastEventID for transactions (#379)
|
||||||
|
- fix: Do not clear LastEventID when events are dropped (#382)
|
||||||
|
- Updates to documentation (#366, #385)
|
||||||
|
|
||||||
|
_NOTE:_
|
||||||
|
This version drops support for Go 1.14, however no changes have been made that would make the SDK not work with Go 1.14. The currently supported Go versions are the last 3 stable releases: 1.15, 1.16 and 1.17.
|
||||||
|
There are two behavior changes related to `LastEventID`, both of which were intended to align the behavior of the Sentry Go SDK with other Sentry SDKs.
|
||||||
|
The new [automatic release detection feature](https://github.com/getsentry/sentry-go/issues/335) makes it easier to use Sentry and separate events per release without requiring extra work from users. We intend to improve this functionality in a future release by utilizing information that will be available in runtime starting with Go 1.18. The tracking issue is [#401](https://github.com/getsentry/sentry-go/issues/401).
|
||||||
|
|
||||||
|
## v0.11.0
|
||||||
|
|
||||||
|
- feat(transports): Category-based Rate Limiting ([#354](https://github.com/getsentry/sentry-go/pull/354))
|
||||||
|
- feat(transports): Report User-Agent identifying SDK ([#357](https://github.com/getsentry/sentry-go/pull/357))
|
||||||
|
- fix(scope): Include event processors in clone ([#349](https://github.com/getsentry/sentry-go/pull/349))
|
||||||
|
- Improvements to `go doc` documentation ([#344](https://github.com/getsentry/sentry-go/pull/344), [#350](https://github.com/getsentry/sentry-go/pull/350), [#351](https://github.com/getsentry/sentry-go/pull/351))
|
||||||
|
- Miscellaneous changes to our testing infrastructure with GitHub Actions
|
||||||
|
([57123a40](https://github.com/getsentry/sentry-go/commit/57123a409be55f61b1d5a6da93c176c55a399ad0), [#128](https://github.com/getsentry/sentry-go/pull/128), [#338](https://github.com/getsentry/sentry-go/pull/338), [#345](https://github.com/getsentry/sentry-go/pull/345), [#346](https://github.com/getsentry/sentry-go/pull/346), [#352](https://github.com/getsentry/sentry-go/pull/352), [#353](https://github.com/getsentry/sentry-go/pull/353), [#355](https://github.com/getsentry/sentry-go/pull/355))
|
||||||
|
|
||||||
|
_NOTE:_
|
||||||
|
This version drops support for Go 1.13. The currently supported Go versions are the last 3 stable releases: 1.14, 1.15 and 1.16.
|
||||||
|
Users of the tracing functionality (`StartSpan`, etc) should upgrade to this version to benefit from separate rate limits for errors and transactions.
|
||||||
|
There are no breaking changes and upgrading should be a smooth experience for all users.
|
||||||
|
|
||||||
|
## v0.10.0
|
||||||
|
|
||||||
|
- feat: Debug connection reuse (#323)
|
||||||
|
- fix: Send root span data as `Event.Extra` (#329)
|
||||||
|
- fix: Do not double sample transactions (#328)
|
||||||
|
- fix: Do not override trace context of transactions (#327)
|
||||||
|
- fix: Drain and close API response bodies (#322)
|
||||||
|
- ci: Run tests against Go tip (#319)
|
||||||
|
- ci: Move away from Travis in favor of GitHub Actions (#314) (#321)
|
||||||
|
|
||||||
|
## v0.9.0
|
||||||
|
|
||||||
|
- feat: Initial tracing and performance monitoring support (#285)
|
||||||
|
- doc: Revamp sentryhttp documentation (#304)
|
||||||
|
- fix: Hub.PopScope never empties the scope stack (#300)
|
||||||
|
- ref: Report Event.Timestamp in local time (#299)
|
||||||
|
- ref: Report Breadcrumb.Timestamp in local time (#299)
|
||||||
|
|
||||||
|
_NOTE:_
|
||||||
|
This version introduces support for [Sentry's Performance Monitoring](https://docs.sentry.io/platforms/go/performance/).
|
||||||
|
The new tracing capabilities are beta, and we plan to expand them on future versions. Feedback is welcome, please open new issues on GitHub.
|
||||||
|
The `sentryhttp` package got better API docs, an [updated usage example](https://github.com/getsentry/sentry-go/tree/master/example/http) and support for creating automatic transactions as part of Performance Monitoring.
|
||||||
|
|
||||||
|
## v0.8.0
|
||||||
|
|
||||||
|
- build: Bump required version of Iris (#296)
|
||||||
|
- fix: avoid unnecessary allocation in Client.processEvent (#293)
|
||||||
|
- doc: Remove deprecation of sentryhttp.HandleFunc (#284)
|
||||||
|
- ref: Update sentryhttp example (#283)
|
||||||
|
- doc: Improve documentation of sentryhttp package (#282)
|
||||||
|
- doc: Clarify SampleRate documentation (#279)
|
||||||
|
- fix: Remove RawStacktrace (#278)
|
||||||
|
- docs: Add example of custom HTTP transport
|
||||||
|
- ci: Test against go1.15, drop go1.12 support (#271)
|
||||||
|
|
||||||
|
_NOTE:_
|
||||||
|
This version comes with a few updates. Some examples and documentation have been
|
||||||
|
improved. We've bumped the supported version of the Iris framework to avoid
|
||||||
|
LGPL-licensed modules in the module dependency graph.
|
||||||
|
The `Exception.RawStacktrace` and `Thread.RawStacktrace` fields have been
|
||||||
|
removed to conform to Sentry's ingestion protocol, only `Exception.Stacktrace`
|
||||||
|
and `Thread.Stacktrace` should appear in user code.
|
||||||
|
|
||||||
|
## v0.7.0
|
||||||
|
|
||||||
|
- feat: Include original error when event cannot be encoded as JSON (#258)
|
||||||
|
- feat: Use Hub from request context when available (#217, #259)
|
||||||
|
- feat: Extract stack frames from golang.org/x/xerrors (#262)
|
||||||
|
- feat: Make Environment Integration preserve existing context data (#261)
|
||||||
|
- feat: Recover and RecoverWithContext with arbitrary types (#268)
|
||||||
|
- feat: Report bad usage of CaptureMessage and CaptureEvent (#269)
|
||||||
|
- feat: Send debug logging to stderr by default (#266)
|
||||||
|
- feat: Several improvements to documentation (#223, #245, #250, #265)
|
||||||
|
- feat: Example of Recover followed by panic (#241, #247)
|
||||||
|
- feat: Add Transactions and Spans (to support OpenTelemetry Sentry Exporter) (#235, #243, #254)
|
||||||
|
- fix: Set either Frame.Filename or Frame.AbsPath (#233)
|
||||||
|
- fix: Clone requestBody to new Scope (#244)
|
||||||
|
- fix: Synchronize access and mutation of Hub.lastEventID (#264)
|
||||||
|
- fix: Avoid repeated syscalls in prepareEvent (#256)
|
||||||
|
- fix: Do not allocate new RNG for every event (#256)
|
||||||
|
- fix: Remove stale replace directive in go.mod (#255)
|
||||||
|
- fix(http): Deprecate HandleFunc, remove duplication (#260)
|
||||||
|
|
||||||
|
_NOTE:_
|
||||||
|
This version comes packed with several fixes and improvements and no breaking
|
||||||
|
changes.
|
||||||
|
Notably, there is a change in how the SDK reports file names in stack traces
|
||||||
|
that should resolve any ambiguity when looking at stack traces and using the
|
||||||
|
Suspect Commits feature.
|
||||||
|
We recommend all users to upgrade.
|
||||||
|
|
||||||
|
## v0.6.1
|
||||||
|
|
||||||
|
- fix: Use NewEvent to init Event struct (#220)
|
||||||
|
|
||||||
|
_NOTE:_
|
||||||
|
A change introduced in v0.6.0 with the intent of avoiding allocations made a
|
||||||
|
pattern used in official examples break in certain circumstances (attempting
|
||||||
|
to write to a nil map).
|
||||||
|
This release reverts the change such that maps in the Event struct are always
|
||||||
|
allocated.
|
||||||
|
|
||||||
|
## v0.6.0
|
||||||
|
|
||||||
|
- feat: Read module dependencies from runtime/debug (#199)
|
||||||
|
- feat: Support chained errors using Unwrap (#206)
|
||||||
|
- feat: Report chain of errors when available (#185)
|
||||||
|
- **[breaking]** fix: Accept http.RoundTripper to customize transport (#205)
|
||||||
|
Before the SDK accepted a concrete value of type `*http.Transport` in
|
||||||
|
`ClientOptions`, now it accepts any value implementing the `http.RoundTripper`
|
||||||
|
interface. Note that `*http.Transport` implements `http.RoundTripper`, so most
|
||||||
|
code bases will continue to work unchanged.
|
||||||
|
Users of custom transport gain the ability to pass in other implementations of
|
||||||
|
`http.RoundTripper` and may be able to simplify their code bases.
|
||||||
|
- fix: Do not panic when scope event processor drops event (#192)
|
||||||
|
- **[breaking]** fix: Use time.Time for timestamps (#191)
|
||||||
|
Users of sentry-go typically do not need to manipulate timestamps manually.
|
||||||
|
For those who do, the field type changed from `int64` to `time.Time`, which
|
||||||
|
should be more convenient to use. The recommended way to get the current time
|
||||||
|
is `time.Now().UTC()`.
|
||||||
|
- fix: Report usage error including stack trace (#189)
|
||||||
|
- feat: Add Exception.ThreadID field (#183)
|
||||||
|
- ci: Test against Go 1.14, drop 1.11 (#170)
|
||||||
|
- feat: Limit reading bytes from request bodies (#168)
|
||||||
|
- **[breaking]** fix: Rename fasthttp integration package sentryhttp => sentryfasthttp
|
||||||
|
The current recommendation is to use a named import, in which case existing
|
||||||
|
code should not require any change:
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/getsentry/sentry-go"
|
||||||
|
sentryfasthttp "github.com/getsentry/sentry-go/fasthttp"
|
||||||
|
"github.com/valyala/fasthttp"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
_NOTE:_
|
||||||
|
This version includes some new features and a few breaking changes, none of
|
||||||
|
which should pose troubles with upgrading. Most code bases should be able to
|
||||||
|
upgrade without any changes.
|
||||||
|
|
||||||
|
## v0.5.1
|
||||||
|
|
||||||
|
- fix: Ignore err.Cause() when it is nil (#160)
|
||||||
|
|
||||||
|
## v0.5.0
|
||||||
|
|
||||||
|
- fix: Synchronize access to HTTPTransport.disabledUntil (#158)
|
||||||
|
- docs: Update Flush documentation (#153)
|
||||||
|
- fix: HTTPTransport.Flush panic and data race (#140)
|
||||||
|
|
||||||
|
_NOTE:_
|
||||||
|
This version changes the implementation of the default transport, modifying the
|
||||||
|
behavior of `sentry.Flush`. The previous behavior was to wait until there were
|
||||||
|
no buffered events; new concurrent events kept `Flush` from returning. The new
|
||||||
|
behavior is to wait until the last event prior to the call to `Flush` has been
|
||||||
|
sent or the timeout; new concurrent events have no effect. The new behavior is
|
||||||
|
inline with the [Unified API
|
||||||
|
Guidelines](https://docs.sentry.io/development/sdk-dev/unified-api/).
|
||||||
|
|
||||||
|
We have updated the documentation and examples to clarify that `Flush` is meant
|
||||||
|
to be called typically only once before program termination, to wait for
|
||||||
|
in-flight events to be sent to Sentry. Calling `Flush` after every event is not
|
||||||
|
recommended, as it introduces unnecessary latency to the surrounding function.
|
||||||
|
Please verify the usage of `sentry.Flush` in your code base.
|
||||||
|
|
||||||
|
## v0.4.0
|
||||||
|
|
||||||
|
- fix(stacktrace): Correctly report package names (#127)
|
||||||
|
- fix(stacktrace): Do not rely on AbsPath of files (#123)
|
||||||
|
- build: Require github.com/ugorji/go@v1.1.7 (#110)
|
||||||
|
- fix: Correctly store last event id (#99)
|
||||||
|
- fix: Include request body in event payload (#94)
|
||||||
|
- build: Reset go.mod version to 1.11 (#109)
|
||||||
|
- fix: Eliminate data race in modules integration (#105)
|
||||||
|
- feat: Add support for path prefixes in the DSN (#102)
|
||||||
|
- feat: Add HTTPClient option (#86)
|
||||||
|
- feat: Extract correct type and value from top-most error (#85)
|
||||||
|
- feat: Check for broken pipe errors in Gin integration (#82)
|
||||||
|
- fix: Client.CaptureMessage accept nil EventModifier (#72)
|
||||||
|
|
||||||
|
## v0.3.1
|
||||||
|
|
||||||
|
- feat: Send extra information exposed by the Go runtime (#76)
|
||||||
|
- fix: Handle new lines in module integration (#65)
|
||||||
|
- fix: Make sure that cache is locked when updating for contextifyFramesIntegration
|
||||||
|
- ref: Update Iris integration and example to version 12
|
||||||
|
- misc: Remove indirect dependencies in order to move them to separate go.mod files
|
||||||
|
|
||||||
|
## v0.3.0
|
||||||
|
|
||||||
|
- feat: Retry event marshaling without contextual data if the first pass fails
|
||||||
|
- fix: Include `url.Parse` error in `DsnParseError`
|
||||||
|
- fix: Make more `Scope` methods safe for concurrency
|
||||||
|
- fix: Synchronize concurrent access to `Hub.client`
|
||||||
|
- ref: Remove mutex from `Scope` exported API
|
||||||
|
- ref: Remove mutex from `Hub` exported API
|
||||||
|
- ref: Compile regexps for `filterFrames` only once
|
||||||
|
- ref: Change `SampleRate` type to `float64`
|
||||||
|
- doc: `Scope.Clear` not safe for concurrent use
|
||||||
|
- ci: Test sentry-go with `go1.13`, drop `go1.10`
|
||||||
|
|
||||||
|
_NOTE:_
|
||||||
|
This version removes some of the internal APIs that landed publicly (namely `Hub/Scope` mutex structs) and may require (but shouldn't) some changes to your code.
|
||||||
|
It's not done through major version update, as we are still in `0.x` stage.
|
||||||
|
|
||||||
|
## v0.2.1
|
||||||
|
|
||||||
|
- fix: Run `Contextify` integration on `Threads` as well
|
||||||
|
|
||||||
|
## v0.2.0
|
||||||
|
|
||||||
|
- feat: Add `SetTransaction()` method on the `Scope`
|
||||||
|
- feat: `fasthttp` framework support with `sentryfasthttp` package
|
||||||
|
- fix: Add `RWMutex` locks to internal `Hub` and `Scope` changes
|
||||||
|
|
||||||
|
## v0.1.3
|
||||||
|
|
||||||
|
- feat: Move frames context reading into `contextifyFramesIntegration` (#28)
|
||||||
|
|
||||||
|
_NOTE:_
|
||||||
|
In case of any performance issues due to source contexts IO, you can let us know and turn off the integration in the meantime with:
|
||||||
|
|
||||||
|
```go
|
||||||
|
sentry.Init(sentry.ClientOptions{
|
||||||
|
Integrations: func(integrations []sentry.Integration) []sentry.Integration {
|
||||||
|
var filteredIntegrations []sentry.Integration
|
||||||
|
for _, integration := range integrations {
|
||||||
|
if integration.Name() == "ContextifyFrames" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filteredIntegrations = append(filteredIntegrations, integration)
|
||||||
|
}
|
||||||
|
return filteredIntegrations
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## v0.1.2
|
||||||
|
|
||||||
|
- feat: Better source code location resolution and more useful inapp frames (#26)
|
||||||
|
- feat: Use `noopTransport` when no `Dsn` provided (#27)
|
||||||
|
- ref: Allow empty `Dsn` instead of returning an error (#22)
|
||||||
|
- fix: Use `NewScope` instead of literal struct inside a `scope.Clear` call (#24)
|
||||||
|
- fix: Add to `WaitGroup` before the request is put inside a buffer (#25)
|
||||||
|
|
||||||
|
## v0.1.1
|
||||||
|
|
||||||
|
- fix: Check for initialized `Client` in `AddBreadcrumbs` (#20)
|
||||||
|
- build: Bump version when releasing with Craft (#19)
|
||||||
|
|
||||||
|
## v0.1.0
|
||||||
|
|
||||||
|
- First stable release! \o/
|
||||||
|
|
||||||
|
## v0.0.1-beta.5
|
||||||
|
|
||||||
|
- feat: **[breaking]** Add `NewHTTPTransport` and `NewHTTPSyncTransport` which accepts all transport options
|
||||||
|
- feat: New `HTTPSyncTransport` that blocks after each call
|
||||||
|
- feat: New `Echo` integration
|
||||||
|
- ref: **[breaking]** Remove `BufferSize` option from `ClientOptions` and move it to `HTTPTransport` instead
|
||||||
|
- ref: Export default `HTTPTransport`
|
||||||
|
- ref: Export `net/http` integration handler
|
||||||
|
- ref: Set `Request` instantly in the package handlers, not in `recoverWithSentry` so it can be accessed later on
|
||||||
|
- ci: Add craft config
|
||||||
|
|
||||||
|
## v0.0.1-beta.4
|
||||||
|
|
||||||
|
- feat: `IgnoreErrors` client option and corresponding integration
|
||||||
|
- ref: Reworked `net/http` integration, wrote better example and complete readme
|
||||||
|
- ref: Reworked `Gin` integration, wrote better example and complete readme
|
||||||
|
- ref: Reworked `Iris` integration, wrote better example and complete readme
|
||||||
|
- ref: Reworked `Negroni` integration, wrote better example and complete readme
|
||||||
|
- ref: Reworked `Martini` integration, wrote better example and complete readme
|
||||||
|
- ref: Remove `Handle()` from frameworks handlers and return it directly from New
|
||||||
|
|
||||||
|
## v0.0.1-beta.3
|
||||||
|
|
||||||
|
- feat: `Iris` framework support with `sentryiris` package
|
||||||
|
- feat: `Gin` framework support with `sentrygin` package
|
||||||
|
- feat: `Martini` framework support with `sentrymartini` package
|
||||||
|
- feat: `Negroni` framework support with `sentrynegroni` package
|
||||||
|
- feat: Add `Hub.Clone()` for easier frameworks integration
|
||||||
|
- feat: Return `EventID` from `Recovery` methods
|
||||||
|
- feat: Add `NewScope` and `NewEvent` functions and use them in the whole codebase
|
||||||
|
- feat: Add `AddEventProcessor` to the `Client`
|
||||||
|
- fix: Operate on requests body copy instead of the original
|
||||||
|
- ref: Try to read source files from the root directory, based on the filename as well, to make it work on AWS Lambda
|
||||||
|
- ref: Remove `gocertifi` dependence and document how to provide your own certificates
|
||||||
|
- ref: **[breaking]** Remove `Decorate` and `DecorateFunc` methods in favor of `sentryhttp` package
|
||||||
|
- ref: **[breaking]** Allow for integrations to live on the client, by passing client instance in `SetupOnce` method
|
||||||
|
- ref: **[breaking]** Remove `GetIntegration` from the `Hub`
|
||||||
|
- ref: **[breaking]** Remove `GlobalEventProcessors` getter from the public API
|
||||||
|
|
||||||
|
## v0.0.1-beta.2
|
||||||
|
|
||||||
|
- feat: Add `AttachStacktrace` client option to include stacktrace for messages
|
||||||
|
- feat: Add `BufferSize` client option to configure transport buffer size
|
||||||
|
- feat: Add `SetRequest` method on a `Scope` to control `Request` context data
|
||||||
|
- feat: Add `FromHTTPRequest` for `Request` type for easier extraction
|
||||||
|
- ref: Extract `Request` information more accurately
|
||||||
|
- fix: Attach `ServerName`, `Release`, `Dist`, `Environment` options to the event
|
||||||
|
- fix: Don't log events dropped due to full transport buffer as sent
|
||||||
|
- fix: Don't panic and create an appropriate event when called `CaptureException` or `Recover` with `nil` value
|
||||||
|
|
||||||
|
## v0.0.1-beta
|
||||||
|
|
||||||
|
- Initial release
|
|
@ -0,0 +1,96 @@
|
||||||
|
# Contributing to sentry-go
|
||||||
|
|
||||||
|
Hey, thank you if you're reading this, we welcome your contribution!
|
||||||
|
|
||||||
|
## Sending a Pull Request
|
||||||
|
|
||||||
|
Please help us save time when reviewing your PR by following this simple
|
||||||
|
process:
|
||||||
|
|
||||||
|
1. Is your PR a simple typo fix? Read no further, **click that green "Create
|
||||||
|
pull request" button**!
|
||||||
|
|
||||||
|
2. For more complex PRs that involve behavior changes or new APIs, please
|
||||||
|
consider [opening an **issue**][new-issue] describing the problem you're
|
||||||
|
trying to solve if there's not one already.
|
||||||
|
|
||||||
|
A PR is often one specific solution to a problem and sometimes talking about
|
||||||
|
the problem unfolds new possible solutions. Remember we will be responsible
|
||||||
|
for maintaining the changes later.
|
||||||
|
|
||||||
|
3. Fixing a bug and changing a behavior? Please add automated tests to prevent
|
||||||
|
future regression.
|
||||||
|
|
||||||
|
4. Practice writing good commit messages. We have [commit
|
||||||
|
guidelines][commit-guide].
|
||||||
|
|
||||||
|
5. We have [guidelines for PR submitters][pr-guide]. A short summary:
|
||||||
|
|
||||||
|
- Good PR descriptions are very helpful and most of the time they include
|
||||||
|
**why** something is done and why done in this particular way. Also list
|
||||||
|
other possible solutions that were considered and discarded.
|
||||||
|
- Be your own first reviewer. Make sure your code compiles and passes the
|
||||||
|
existing tests.
|
||||||
|
|
||||||
|
[new-issue]: https://github.com/getsentry/sentry-go/issues/new/choose
|
||||||
|
[commit-guide]: https://develop.sentry.dev/code-review/#commit-guidelines
|
||||||
|
[pr-guide]: https://develop.sentry.dev/code-review/#guidelines-for-submitters
|
||||||
|
|
||||||
|
Please also read through our [SDK Development docs](https://develop.sentry.dev/sdk/).
|
||||||
|
It contains information about SDK features, expected payloads and best practices for
|
||||||
|
contributing to Sentry SDKs.
|
||||||
|
|
||||||
|
## Community
|
||||||
|
|
||||||
|
The public-facing channels for support and development of Sentry SDKs can be found on [Discord](https://discord.gg/Ww9hbqr).
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ go test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Watch mode
|
||||||
|
|
||||||
|
Use: https://github.com/cespare/reflex
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ reflex -g '*.go' -d "none" -- sh -c 'printf "\n"; go test'
|
||||||
|
```
|
||||||
|
|
||||||
|
### With data race detection
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ go test -race
|
||||||
|
```
|
||||||
|
|
||||||
|
### Coverage
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ go test -race -coverprofile=coverage.txt -covermode=atomic && go tool cover -html coverage.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Linting
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ golangci-lint run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Release
|
||||||
|
|
||||||
|
1. Update `CHANGELOG.md` with new version in `vX.X.X` format title and list of changes.
|
||||||
|
|
||||||
|
The command below can be used to get a list of changes since the last tag, with the format used in `CHANGELOG.md`:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ git log --no-merges --format=%s $(git describe --abbrev=0).. | sed 's/^/- /'
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Commit with `misc: vX.X.X changelog` commit message and push to `master`.
|
||||||
|
|
||||||
|
3. Let [`craft`](https://github.com/getsentry/craft) do the rest:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ craft prepare X.X.X
|
||||||
|
$ craft publish X.X.X
|
||||||
|
```
|
|
@ -0,0 +1,9 @@
|
||||||
|
Copyright (c) 2019 Sentry (https://sentry.io) and individual contributors.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,3 @@
|
||||||
|
# `raven-go` to `sentry-go` Migration Guide
|
||||||
|
|
||||||
|
A [`raven-go` to `sentry-go` migration guide](https://docs.sentry.io/platforms/go/migration/) is available at the official Sentry documentation site.
|
|
@ -0,0 +1,105 @@
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://sentry.io/?utm_source=github&utm_medium=logo" target="_blank">
|
||||||
|
<picture>
|
||||||
|
<source srcset="https://sentry-brand.storage.googleapis.com/sentry-logo-white.png" media="(prefers-color-scheme: dark)" />
|
||||||
|
<source srcset="https://sentry-brand.storage.googleapis.com/sentry-logo-black.png" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" />
|
||||||
|
<img src="https://sentry-brand.storage.googleapis.com/sentry-logo-black.png" alt="Sentry" width="280">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
# Official Sentry SDK for Go
|
||||||
|
|
||||||
|
[![Build Status](https://github.com/getsentry/sentry-go/workflows/go-workflow/badge.svg)](https://github.com/getsentry/sentry-go/actions?query=workflow%3Ago-workflow)
|
||||||
|
[![Go Report Card](https://goreportcard.com/badge/github.com/getsentry/sentry-go)](https://goreportcard.com/report/github.com/getsentry/sentry-go)
|
||||||
|
[![Discord](https://img.shields.io/discord/621778831602221064)](https://discord.gg/Ww9hbqr)
|
||||||
|
[![GoDoc](https://godoc.org/github.com/getsentry/sentry-go?status.svg)](https://godoc.org/github.com/getsentry/sentry-go)
|
||||||
|
[![go.dev](https://img.shields.io/badge/go.dev-pkg-007d9c.svg?style=flat)](https://pkg.go.dev/github.com/getsentry/sentry-go)
|
||||||
|
|
||||||
|
`sentry-go` provides a Sentry client implementation for the Go programming
|
||||||
|
language. This is the next generation of the Go SDK for [Sentry](https://sentry.io/),
|
||||||
|
intended to replace the `raven-go` package.
|
||||||
|
|
||||||
|
> Looking for the old `raven-go` SDK documentation? See the Legacy client section [here](https://docs.sentry.io/clients/go/).
|
||||||
|
> If you want to start using `sentry-go` instead, check out the [migration guide](https://docs.sentry.io/platforms/go/migration/).
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
The only requirement is a Go compiler.
|
||||||
|
|
||||||
|
We verify this package against the 3 most recent releases of Go. Those are the
|
||||||
|
supported versions. The exact versions are defined in
|
||||||
|
[`GitHub workflow`](.github/workflows/test.yml).
|
||||||
|
|
||||||
|
In addition, we run tests against the current master branch of the Go toolchain,
|
||||||
|
though support for this configuration is best-effort.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
`sentry-go` can be installed like any other Go library through `go get`:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ go get github.com/getsentry/sentry-go@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Check out the [list of released versions](https://pkg.go.dev/github.com/getsentry/sentry-go?tab=versions).
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
To use `sentry-go`, you’ll need to import the `sentry-go` package and initialize
|
||||||
|
it with your DSN and other [options](https://pkg.go.dev/github.com/getsentry/sentry-go#ClientOptions).
|
||||||
|
|
||||||
|
If not specified in the SDK initialization, the
|
||||||
|
[DSN](https://docs.sentry.io/product/sentry-basics/dsn-explainer/),
|
||||||
|
[Release](https://docs.sentry.io/product/releases/) and
|
||||||
|
[Environment](https://docs.sentry.io/product/sentry-basics/environments/)
|
||||||
|
are read from the environment variables `SENTRY_DSN`, `SENTRY_RELEASE` and
|
||||||
|
`SENTRY_ENVIRONMENT`, respectively.
|
||||||
|
|
||||||
|
More on this in the [Configuration section of the official Sentry Go SDK documentation](https://docs.sentry.io/platforms/go/configuration/).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The SDK supports reporting errors and tracking application performance.
|
||||||
|
|
||||||
|
To get started, have a look at one of our [examples](example/):
|
||||||
|
- [Basic error instrumentation](example/basic/main.go)
|
||||||
|
- [Error and tracing for HTTP servers](example/http/main.go)
|
||||||
|
|
||||||
|
We also provide a [complete API reference](https://pkg.go.dev/github.com/getsentry/sentry-go).
|
||||||
|
|
||||||
|
For more detailed information about how to get the most out of `sentry-go`,
|
||||||
|
checkout the official documentation:
|
||||||
|
|
||||||
|
- [Sentry Go SDK documentation](https://docs.sentry.io/platforms/go/)
|
||||||
|
- Guides:
|
||||||
|
- [net/http](https://docs.sentry.io/platforms/go/guides/http/)
|
||||||
|
- [echo](https://docs.sentry.io/platforms/go/guides/echo/)
|
||||||
|
- [fasthttp](https://docs.sentry.io/platforms/go/guides/fasthttp/)
|
||||||
|
- [gin](https://docs.sentry.io/platforms/go/guides/gin/)
|
||||||
|
- [iris](https://docs.sentry.io/platforms/go/guides/iris/)
|
||||||
|
- [martini](https://docs.sentry.io/platforms/go/guides/martini/)
|
||||||
|
- [negroni](https://docs.sentry.io/platforms/go/guides/negroni/)
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Bug Tracker](https://github.com/getsentry/sentry-go/issues)
|
||||||
|
- [GitHub Project](https://github.com/getsentry/sentry-go)
|
||||||
|
- [![GoDoc](https://godoc.org/github.com/getsentry/sentry-go?status.svg)](https://godoc.org/github.com/getsentry/sentry-go)
|
||||||
|
- [![go.dev](https://img.shields.io/badge/go.dev-pkg-007d9c.svg?style=flat)](https://pkg.go.dev/github.com/getsentry/sentry-go)
|
||||||
|
- [![Documentation](https://img.shields.io/badge/documentation-sentry.io-green.svg)](https://docs.sentry.io/platforms/go/)
|
||||||
|
- [![Discussions](https://img.shields.io/github/discussions/getsentry/sentry-go.svg)](https://github.com/getsentry/sentry-go/discussions)
|
||||||
|
- [![Discord](https://img.shields.io/discord/621778831602221064)](https://discord.gg/Ww9hbqr)
|
||||||
|
- [![Stack Overflow](https://img.shields.io/badge/stack%20overflow-sentry-green.svg)](http://stackoverflow.com/questions/tagged/sentry)
|
||||||
|
- [![Twitter Follow](https://img.shields.io/twitter/follow/getsentry?label=getsentry&style=social)](https://twitter.com/intent/follow?screen_name=getsentry)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under
|
||||||
|
[The 2-Clause BSD License](https://opensource.org/licenses/BSD-2-Clause), see
|
||||||
|
[`LICENSE`](LICENSE).
|
||||||
|
|
||||||
|
## Community
|
||||||
|
|
||||||
|
Join Sentry's [`#go` channel on Discord](https://discord.gg/Ww9hbqr) to get
|
||||||
|
involved and help us improve the SDK!
|
|
@ -0,0 +1,682 @@
|
||||||
|
package sentry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/getsentry/sentry-go/internal/debug"
|
||||||
|
)
|
||||||
|
|
||||||
|
// maxErrorDepth is the maximum number of errors reported in a chain of errors.
|
||||||
|
// This protects the SDK from an arbitrarily long chain of wrapped errors.
|
||||||
|
//
|
||||||
|
// An additional consideration is that arguably reporting a long chain of errors
|
||||||
|
// is of little use when debugging production errors with Sentry. The Sentry UI
|
||||||
|
// is not optimized for long chains either. The top-level error together with a
|
||||||
|
// stack trace is often the most useful information.
|
||||||
|
const maxErrorDepth = 10
|
||||||
|
|
||||||
|
// defaultMaxSpans limits the default number of recorded spans per transaction. The limit is
|
||||||
|
// meant to bound memory usage and prevent too large transaction events that
|
||||||
|
// would be rejected by Sentry.
|
||||||
|
const defaultMaxSpans = 1000
|
||||||
|
|
||||||
|
// hostname is the host name reported by the kernel. It is precomputed once to
|
||||||
|
// avoid syscalls when capturing events.
|
||||||
|
//
|
||||||
|
// The error is ignored because retrieving the host name is best-effort. If the
|
||||||
|
// error is non-nil, there is nothing to do other than retrying. We choose not
|
||||||
|
// to retry for now.
|
||||||
|
var hostname, _ = os.Hostname()
|
||||||
|
|
||||||
|
// lockedRand is a random number generator safe for concurrent use. Its API is
|
||||||
|
// intentionally limited and it is not meant as a full replacement for a
|
||||||
|
// rand.Rand.
|
||||||
|
type lockedRand struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
r *rand.Rand
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64 returns a pseudo-random number in [0.0,1.0).
|
||||||
|
func (r *lockedRand) Float64() float64 {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
return r.r.Float64()
|
||||||
|
}
|
||||||
|
|
||||||
|
// rng is the internal random number generator.
|
||||||
|
//
|
||||||
|
// We do not use the global functions from math/rand because, while they are
|
||||||
|
// safe for concurrent use, any package in a build could change the seed and
|
||||||
|
// affect the generated numbers, for instance making them deterministic. On the
|
||||||
|
// other hand, the source returned from rand.NewSource is not safe for
|
||||||
|
// concurrent use, so we need to couple its use with a sync.Mutex.
|
||||||
|
var rng = &lockedRand{
|
||||||
|
// #nosec G404 -- We are fine using transparent, non-secure value here.
|
||||||
|
r: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||||
|
}
|
||||||
|
|
||||||
|
// usageError is used to report to Sentry an SDK usage error.
|
||||||
|
//
|
||||||
|
// It is not exported because it is never returned by any function or method in
|
||||||
|
// the exported API.
|
||||||
|
type usageError struct {
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger is an instance of log.Logger that is use to provide debug information about running Sentry Client
|
||||||
|
// can be enabled by either using Logger.SetOutput directly or with Debug client option.
|
||||||
|
var Logger = log.New(io.Discard, "[Sentry] ", log.LstdFlags)
|
||||||
|
|
||||||
|
// EventProcessor is a function that processes an event.
|
||||||
|
// Event processors are used to change an event before it is sent to Sentry.
|
||||||
|
type EventProcessor func(event *Event, hint *EventHint) *Event
|
||||||
|
|
||||||
|
// EventModifier is the interface that wraps the ApplyToEvent method.
|
||||||
|
//
|
||||||
|
// ApplyToEvent changes an event based on external data and/or
|
||||||
|
// an event hint.
|
||||||
|
type EventModifier interface {
|
||||||
|
ApplyToEvent(event *Event, hint *EventHint) *Event
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalEventProcessors []EventProcessor
|
||||||
|
|
||||||
|
// AddGlobalEventProcessor adds processor to the global list of event
|
||||||
|
// processors. Global event processors apply to all events.
|
||||||
|
//
|
||||||
|
// AddGlobalEventProcessor is deprecated. Most users will prefer to initialize
|
||||||
|
// the SDK with Init and provide a ClientOptions.BeforeSend function or use
|
||||||
|
// Scope.AddEventProcessor instead.
|
||||||
|
func AddGlobalEventProcessor(processor EventProcessor) {
|
||||||
|
globalEventProcessors = append(globalEventProcessors, processor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Integration allows for registering a functions that modify or discard captured events.
|
||||||
|
type Integration interface {
|
||||||
|
Name() string
|
||||||
|
SetupOnce(client *Client)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientOptions that configures a SDK Client.
|
||||||
|
type ClientOptions struct {
|
||||||
|
// The DSN to use. If the DSN is not set, the client is effectively
|
||||||
|
// disabled.
|
||||||
|
Dsn string
|
||||||
|
// In debug mode, the debug information is printed to stdout to help you
|
||||||
|
// understand what sentry is doing.
|
||||||
|
Debug bool
|
||||||
|
// Configures whether SDK should generate and attach stacktraces to pure
|
||||||
|
// capture message calls.
|
||||||
|
AttachStacktrace bool
|
||||||
|
// The sample rate for event submission in the range [0.0, 1.0]. By default,
|
||||||
|
// all events are sent. Thus, as a historical special case, the sample rate
|
||||||
|
// 0.0 is treated as if it was 1.0. To drop all events, set the DSN to the
|
||||||
|
// empty string.
|
||||||
|
SampleRate float64
|
||||||
|
// Enable performance tracing.
|
||||||
|
EnableTracing bool
|
||||||
|
// The sample rate for sampling traces in the range [0.0, 1.0].
|
||||||
|
TracesSampleRate float64
|
||||||
|
// Used to customize the sampling of traces, overrides TracesSampleRate.
|
||||||
|
TracesSampler TracesSampler
|
||||||
|
// List of regexp strings that will be used to match against event's message
|
||||||
|
// and if applicable, caught errors type and value.
|
||||||
|
// If the match is found, then a whole event will be dropped.
|
||||||
|
IgnoreErrors []string
|
||||||
|
// If this flag is enabled, certain personally identifiable information (PII) is added by active integrations.
|
||||||
|
// By default, no such data is sent.
|
||||||
|
SendDefaultPII bool
|
||||||
|
// BeforeSend is called before error events are sent to Sentry.
|
||||||
|
// Use it to mutate the event or return nil to discard the event.
|
||||||
|
// See EventProcessor if you need to mutate transactions.
|
||||||
|
BeforeSend func(event *Event, hint *EventHint) *Event
|
||||||
|
// Before breadcrumb add callback.
|
||||||
|
BeforeBreadcrumb func(breadcrumb *Breadcrumb, hint *BreadcrumbHint) *Breadcrumb
|
||||||
|
// Integrations to be installed on the current Client, receives default
|
||||||
|
// integrations.
|
||||||
|
Integrations func([]Integration) []Integration
|
||||||
|
// io.Writer implementation that should be used with the Debug mode.
|
||||||
|
DebugWriter io.Writer
|
||||||
|
// The transport to use. Defaults to HTTPTransport.
|
||||||
|
Transport Transport
|
||||||
|
// The server name to be reported.
|
||||||
|
ServerName string
|
||||||
|
// The release to be sent with events.
|
||||||
|
//
|
||||||
|
// Some Sentry features are built around releases, and, thus, reporting
|
||||||
|
// events with a non-empty release improves the product experience. See
|
||||||
|
// https://docs.sentry.io/product/releases/.
|
||||||
|
//
|
||||||
|
// If Release is not set, the SDK will try to derive a default value
|
||||||
|
// from environment variables or the Git repository in the working
|
||||||
|
// directory.
|
||||||
|
//
|
||||||
|
// If you distribute a compiled binary, it is recommended to set the
|
||||||
|
// Release value explicitly at build time. As an example, you can use:
|
||||||
|
//
|
||||||
|
// go build -ldflags='-X main.release=VALUE'
|
||||||
|
//
|
||||||
|
// That will set the value of a predeclared variable 'release' in the
|
||||||
|
// 'main' package to 'VALUE'. Then, use that variable when initializing
|
||||||
|
// the SDK:
|
||||||
|
//
|
||||||
|
// sentry.Init(ClientOptions{Release: release})
|
||||||
|
//
|
||||||
|
// See https://golang.org/cmd/go/ and https://golang.org/cmd/link/ for
|
||||||
|
// the official documentation of -ldflags and -X, respectively.
|
||||||
|
Release string
|
||||||
|
// The dist to be sent with events.
|
||||||
|
Dist string
|
||||||
|
// The environment to be sent with events.
|
||||||
|
Environment string
|
||||||
|
// Maximum number of breadcrumbs
|
||||||
|
// when MaxBreadcrumbs is negative then ignore breadcrumbs.
|
||||||
|
MaxBreadcrumbs int
|
||||||
|
// Maximum number of spans.
|
||||||
|
//
|
||||||
|
// See https://develop.sentry.dev/sdk/envelopes/#size-limits for size limits
|
||||||
|
// applied during event ingestion. Events that exceed these limits might get dropped.
|
||||||
|
MaxSpans int
|
||||||
|
// An optional pointer to http.Client that will be used with a default
|
||||||
|
// HTTPTransport. Using your own client will make HTTPTransport, HTTPProxy,
|
||||||
|
// HTTPSProxy and CaCerts options ignored.
|
||||||
|
HTTPClient *http.Client
|
||||||
|
// An optional pointer to http.Transport that will be used with a default
|
||||||
|
// HTTPTransport. Using your own transport will make HTTPProxy, HTTPSProxy
|
||||||
|
// and CaCerts options ignored.
|
||||||
|
HTTPTransport http.RoundTripper
|
||||||
|
// An optional HTTP proxy to use.
|
||||||
|
// This will default to the HTTP_PROXY environment variable.
|
||||||
|
HTTPProxy string
|
||||||
|
// An optional HTTPS proxy to use.
|
||||||
|
// This will default to the HTTPS_PROXY environment variable.
|
||||||
|
// HTTPS_PROXY takes precedence over HTTP_PROXY for https requests.
|
||||||
|
HTTPSProxy string
|
||||||
|
// An optional set of SSL certificates to use.
|
||||||
|
CaCerts *x509.CertPool
|
||||||
|
// MaxErrorDepth is the maximum number of errors reported in a chain of errors.
|
||||||
|
// This protects the SDK from an arbitrarily long chain of wrapped errors.
|
||||||
|
//
|
||||||
|
// An additional consideration is that arguably reporting a long chain of errors
|
||||||
|
// is of little use when debugging production errors with Sentry. The Sentry UI
|
||||||
|
// is not optimized for long chains either. The top-level error together with a
|
||||||
|
// stack trace is often the most useful information.
|
||||||
|
MaxErrorDepth int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client is the underlying processor that is used by the main API and Hub
|
||||||
|
// instances. It must be created with NewClient.
|
||||||
|
type Client struct {
|
||||||
|
options ClientOptions
|
||||||
|
dsn *Dsn
|
||||||
|
eventProcessors []EventProcessor
|
||||||
|
integrations []Integration
|
||||||
|
// Transport is read-only. Replacing the transport of an existing client is
|
||||||
|
// not supported, create a new client instead.
|
||||||
|
Transport Transport
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates and returns an instance of Client configured using
|
||||||
|
// ClientOptions.
|
||||||
|
//
|
||||||
|
// Most users will not create clients directly. Instead, initialize the SDK with
|
||||||
|
// Init and use the package-level functions (for simple programs that run on a
|
||||||
|
// single goroutine) or hub methods (for concurrent programs, for example web
|
||||||
|
// servers).
|
||||||
|
func NewClient(options ClientOptions) (*Client, error) {
|
||||||
|
if options.Debug {
|
||||||
|
debugWriter := options.DebugWriter
|
||||||
|
if debugWriter == nil {
|
||||||
|
debugWriter = os.Stderr
|
||||||
|
}
|
||||||
|
Logger.SetOutput(debugWriter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Dsn == "" {
|
||||||
|
options.Dsn = os.Getenv("SENTRY_DSN")
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Release == "" {
|
||||||
|
options.Release = defaultRelease()
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Environment == "" {
|
||||||
|
options.Environment = os.Getenv("SENTRY_ENVIRONMENT")
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.MaxErrorDepth == 0 {
|
||||||
|
options.MaxErrorDepth = maxErrorDepth
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.MaxSpans == 0 {
|
||||||
|
options.MaxSpans = defaultMaxSpans
|
||||||
|
}
|
||||||
|
|
||||||
|
// SENTRYGODEBUG is a comma-separated list of key=value pairs (similar
|
||||||
|
// to GODEBUG). It is not a supported feature: recognized debug options
|
||||||
|
// may change any time.
|
||||||
|
//
|
||||||
|
// The intended public is SDK developers. It is orthogonal to
|
||||||
|
// options.Debug, which is also available for SDK users.
|
||||||
|
dbg := strings.Split(os.Getenv("SENTRYGODEBUG"), ",")
|
||||||
|
sort.Strings(dbg)
|
||||||
|
// dbgOpt returns true when the given debug option is enabled, for
|
||||||
|
// example SENTRYGODEBUG=someopt=1.
|
||||||
|
dbgOpt := func(opt string) bool {
|
||||||
|
s := opt + "=1"
|
||||||
|
return dbg[sort.SearchStrings(dbg, s)%len(dbg)] == s
|
||||||
|
}
|
||||||
|
if dbgOpt("httpdump") || dbgOpt("httptrace") {
|
||||||
|
options.HTTPTransport = &debug.Transport{
|
||||||
|
RoundTripper: http.DefaultTransport,
|
||||||
|
Output: os.Stderr,
|
||||||
|
Dump: dbgOpt("httpdump"),
|
||||||
|
Trace: dbgOpt("httptrace"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dsn *Dsn
|
||||||
|
if options.Dsn != "" {
|
||||||
|
var err error
|
||||||
|
dsn, err = NewDsn(options.Dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client := Client{
|
||||||
|
options: options,
|
||||||
|
dsn: dsn,
|
||||||
|
}
|
||||||
|
|
||||||
|
client.setupTransport()
|
||||||
|
client.setupIntegrations()
|
||||||
|
|
||||||
|
return &client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) setupTransport() {
|
||||||
|
opts := client.options
|
||||||
|
transport := opts.Transport
|
||||||
|
|
||||||
|
if transport == nil {
|
||||||
|
if opts.Dsn == "" {
|
||||||
|
transport = new(noopTransport)
|
||||||
|
} else {
|
||||||
|
httpTransport := NewHTTPTransport()
|
||||||
|
// When tracing is enabled, use larger buffer to
|
||||||
|
// accommodate more concurrent events.
|
||||||
|
// TODO(tracing): consider using separate buffers per
|
||||||
|
// event type.
|
||||||
|
if opts.EnableTracing {
|
||||||
|
httpTransport.BufferSize = 1000
|
||||||
|
}
|
||||||
|
transport = httpTransport
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transport.Configure(opts)
|
||||||
|
client.Transport = transport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) setupIntegrations() {
|
||||||
|
integrations := []Integration{
|
||||||
|
new(contextifyFramesIntegration),
|
||||||
|
new(environmentIntegration),
|
||||||
|
new(modulesIntegration),
|
||||||
|
new(ignoreErrorsIntegration),
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.options.Integrations != nil {
|
||||||
|
integrations = client.options.Integrations(integrations)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, integration := range integrations {
|
||||||
|
if client.integrationAlreadyInstalled(integration.Name()) {
|
||||||
|
Logger.Printf("Integration %s is already installed\n", integration.Name())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
client.integrations = append(client.integrations, integration)
|
||||||
|
integration.SetupOnce(client)
|
||||||
|
Logger.Printf("Integration installed: %s\n", integration.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(client.integrations, func(i, j int) bool {
|
||||||
|
return client.integrations[i].Name() < client.integrations[j].Name()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddEventProcessor adds an event processor to the client. It must not be
|
||||||
|
// called from concurrent goroutines. Most users will prefer to use
|
||||||
|
// ClientOptions.BeforeSend or Scope.AddEventProcessor instead.
|
||||||
|
//
|
||||||
|
// Note that typical programs have only a single client created by Init and the
|
||||||
|
// client is shared among multiple hubs, one per goroutine, such that adding an
|
||||||
|
// event processor to the client affects all hubs that share the client.
|
||||||
|
func (client *Client) AddEventProcessor(processor EventProcessor) {
|
||||||
|
client.eventProcessors = append(client.eventProcessors, processor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options return ClientOptions for the current Client.
|
||||||
|
func (client Client) Options() ClientOptions {
|
||||||
|
return client.options
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureMessage captures an arbitrary message.
|
||||||
|
func (client *Client) CaptureMessage(message string, hint *EventHint, scope EventModifier) *EventID {
|
||||||
|
event := client.eventFromMessage(message, LevelInfo)
|
||||||
|
return client.CaptureEvent(event, hint, scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureException captures an error.
|
||||||
|
func (client *Client) CaptureException(exception error, hint *EventHint, scope EventModifier) *EventID {
|
||||||
|
event := client.eventFromException(exception, LevelError)
|
||||||
|
return client.CaptureEvent(event, hint, scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureEvent captures an event on the currently active client if any.
|
||||||
|
//
|
||||||
|
// The event must already be assembled. Typically code would instead use
|
||||||
|
// the utility methods like CaptureException. The return value is the
|
||||||
|
// event ID. In case Sentry is disabled or event was dropped, the return value will be nil.
|
||||||
|
func (client *Client) CaptureEvent(event *Event, hint *EventHint, scope EventModifier) *EventID {
|
||||||
|
return client.processEvent(event, hint, scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recover captures a panic.
|
||||||
|
// Returns EventID if successfully, or nil if there's no error to recover from.
|
||||||
|
func (client *Client) Recover(err interface{}, hint *EventHint, scope EventModifier) *EventID {
|
||||||
|
if err == nil {
|
||||||
|
err = recover()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normally we would not pass a nil Context, but RecoverWithContext doesn't
|
||||||
|
// use the Context for communicating deadline nor cancelation. All it does
|
||||||
|
// is store the Context in the EventHint and there nil means the Context is
|
||||||
|
// not available.
|
||||||
|
// nolint: staticcheck
|
||||||
|
return client.RecoverWithContext(nil, err, hint, scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecoverWithContext captures a panic and passes relevant context object.
|
||||||
|
// Returns EventID if successfully, or nil if there's no error to recover from.
|
||||||
|
func (client *Client) RecoverWithContext(
|
||||||
|
ctx context.Context,
|
||||||
|
err interface{},
|
||||||
|
hint *EventHint,
|
||||||
|
scope EventModifier,
|
||||||
|
) *EventID {
|
||||||
|
if err == nil {
|
||||||
|
err = recover()
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx != nil {
|
||||||
|
if hint == nil {
|
||||||
|
hint = &EventHint{}
|
||||||
|
}
|
||||||
|
if hint.Context == nil {
|
||||||
|
hint.Context = ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var event *Event
|
||||||
|
switch err := err.(type) {
|
||||||
|
case error:
|
||||||
|
event = client.eventFromException(err, LevelFatal)
|
||||||
|
case string:
|
||||||
|
event = client.eventFromMessage(err, LevelFatal)
|
||||||
|
default:
|
||||||
|
event = client.eventFromMessage(fmt.Sprintf("%#v", err), LevelFatal)
|
||||||
|
}
|
||||||
|
return client.CaptureEvent(event, hint, scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush waits until the underlying Transport sends any buffered events to the
|
||||||
|
// Sentry server, blocking for at most the given timeout. It returns false if
|
||||||
|
// the timeout was reached. In that case, some events may not have been sent.
|
||||||
|
//
|
||||||
|
// Flush should be called before terminating the program to avoid
|
||||||
|
// unintentionally dropping events.
|
||||||
|
//
|
||||||
|
// Do not call Flush indiscriminately after every call to CaptureEvent,
|
||||||
|
// CaptureException or CaptureMessage. Instead, to have the SDK send events over
|
||||||
|
// the network synchronously, configure it to use the HTTPSyncTransport in the
|
||||||
|
// call to Init.
|
||||||
|
func (client *Client) Flush(timeout time.Duration) bool {
|
||||||
|
return client.Transport.Flush(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) eventFromMessage(message string, level Level) *Event {
|
||||||
|
if message == "" {
|
||||||
|
err := usageError{fmt.Errorf("%s called with empty message", callerFunctionName())}
|
||||||
|
return client.eventFromException(err, level)
|
||||||
|
}
|
||||||
|
event := NewEvent()
|
||||||
|
event.Level = level
|
||||||
|
event.Message = message
|
||||||
|
|
||||||
|
if client.Options().AttachStacktrace {
|
||||||
|
event.Threads = []Thread{{
|
||||||
|
Stacktrace: NewStacktrace(),
|
||||||
|
Crashed: false,
|
||||||
|
Current: true,
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) eventFromException(exception error, level Level) *Event {
|
||||||
|
err := exception
|
||||||
|
if err == nil {
|
||||||
|
err = usageError{fmt.Errorf("%s called with nil error", callerFunctionName())}
|
||||||
|
}
|
||||||
|
|
||||||
|
event := NewEvent()
|
||||||
|
event.Level = level
|
||||||
|
|
||||||
|
for i := 0; i < client.options.MaxErrorDepth && err != nil; i++ {
|
||||||
|
event.Exception = append(event.Exception, Exception{
|
||||||
|
Value: err.Error(),
|
||||||
|
Type: reflect.TypeOf(err).String(),
|
||||||
|
Stacktrace: ExtractStacktrace(err),
|
||||||
|
})
|
||||||
|
switch previous := err.(type) {
|
||||||
|
case interface{ Unwrap() error }:
|
||||||
|
err = previous.Unwrap()
|
||||||
|
case interface{ Cause() error }:
|
||||||
|
err = previous.Cause()
|
||||||
|
default:
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a trace of the current stack to the most recent error in a chain if
|
||||||
|
// it doesn't have a stack trace yet.
|
||||||
|
// We only add to the most recent error to avoid duplication and because the
|
||||||
|
// current stack is most likely unrelated to errors deeper in the chain.
|
||||||
|
if event.Exception[0].Stacktrace == nil {
|
||||||
|
event.Exception[0].Stacktrace = NewStacktrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
// event.Exception should be sorted such that the most recent error is last.
|
||||||
|
reverse(event.Exception)
|
||||||
|
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
// reverse reverses the slice a in place.
|
||||||
|
func reverse(a []Exception) {
|
||||||
|
for i := len(a)/2 - 1; i >= 0; i-- {
|
||||||
|
opp := len(a) - 1 - i
|
||||||
|
a[i], a[opp] = a[opp], a[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) processEvent(event *Event, hint *EventHint, scope EventModifier) *EventID {
|
||||||
|
if event == nil {
|
||||||
|
err := usageError{fmt.Errorf("%s called with nil event", callerFunctionName())}
|
||||||
|
return client.CaptureException(err, hint, scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
options := client.Options()
|
||||||
|
|
||||||
|
// The default error event sample rate for all SDKs is 1.0 (send all).
|
||||||
|
//
|
||||||
|
// In Go, the zero value (default) for float64 is 0.0, which means that
|
||||||
|
// constructing a client with NewClient(ClientOptions{}), or, equivalently,
|
||||||
|
// initializing the SDK with Init(ClientOptions{}) without an explicit
|
||||||
|
// SampleRate would drop all events.
|
||||||
|
//
|
||||||
|
// To retain the desired default behavior, we exceptionally flip SampleRate
|
||||||
|
// from 0.0 to 1.0 here. Setting the sample rate to 0.0 is not very useful
|
||||||
|
// anyway, and the same end result can be achieved in many other ways like
|
||||||
|
// not initializing the SDK, setting the DSN to the empty string or using an
|
||||||
|
// event processor that always returns nil.
|
||||||
|
//
|
||||||
|
// An alternative API could be such that default options don't need to be
|
||||||
|
// the same as Go's zero values, for example using the Functional Options
|
||||||
|
// pattern. That would either require a breaking change if we want to reuse
|
||||||
|
// the obvious NewClient name, or a new function as an alternative
|
||||||
|
// constructor.
|
||||||
|
if options.SampleRate == 0.0 {
|
||||||
|
options.SampleRate = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transactions are sampled by options.TracesSampleRate or
|
||||||
|
// options.TracesSampler when they are started. All other events
|
||||||
|
// (errors, messages) are sampled here.
|
||||||
|
if event.Type != transactionType && !sample(options.SampleRate) {
|
||||||
|
Logger.Println("Event dropped due to SampleRate hit.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if event = client.prepareEvent(event, hint, scope); event == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// As per spec, transactions do not go through BeforeSend.
|
||||||
|
if event.Type != transactionType && options.BeforeSend != nil {
|
||||||
|
if hint == nil {
|
||||||
|
hint = &EventHint{}
|
||||||
|
}
|
||||||
|
if event = options.BeforeSend(event, hint); event == nil {
|
||||||
|
Logger.Println("Event dropped due to BeforeSend callback.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Transport.SendEvent(event)
|
||||||
|
|
||||||
|
return &event.EventID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventModifier) *Event {
|
||||||
|
if event.EventID == "" {
|
||||||
|
event.EventID = EventID(uuid())
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Timestamp.IsZero() {
|
||||||
|
event.Timestamp = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Level == "" {
|
||||||
|
event.Level = LevelInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.ServerName == "" {
|
||||||
|
event.ServerName = client.Options().ServerName
|
||||||
|
|
||||||
|
if event.ServerName == "" {
|
||||||
|
event.ServerName = hostname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Release == "" {
|
||||||
|
event.Release = client.Options().Release
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Dist == "" {
|
||||||
|
event.Dist = client.Options().Dist
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Environment == "" {
|
||||||
|
event.Environment = client.Options().Environment
|
||||||
|
}
|
||||||
|
|
||||||
|
event.Platform = "go"
|
||||||
|
event.Sdk = SdkInfo{
|
||||||
|
Name: SDKIdentifier,
|
||||||
|
Version: SDKVersion,
|
||||||
|
Integrations: client.listIntegrations(),
|
||||||
|
Packages: []SdkPackage{{
|
||||||
|
Name: "sentry-go",
|
||||||
|
Version: SDKVersion,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
if scope != nil {
|
||||||
|
event = scope.ApplyToEvent(event, hint)
|
||||||
|
if event == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, processor := range client.eventProcessors {
|
||||||
|
id := event.EventID
|
||||||
|
event = processor(event, hint)
|
||||||
|
if event == nil {
|
||||||
|
Logger.Printf("Event dropped by one of the Client EventProcessors: %s\n", id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, processor := range globalEventProcessors {
|
||||||
|
id := event.EventID
|
||||||
|
event = processor(event, hint)
|
||||||
|
if event == nil {
|
||||||
|
Logger.Printf("Event dropped by one of the Global EventProcessors: %s\n", id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client Client) listIntegrations() []string {
|
||||||
|
integrations := make([]string, len(client.integrations))
|
||||||
|
for i, integration := range client.integrations {
|
||||||
|
integrations[i] = integration.Name()
|
||||||
|
}
|
||||||
|
return integrations
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client Client) integrationAlreadyInstalled(name string) bool {
|
||||||
|
for _, integration := range client.integrations {
|
||||||
|
if integration.Name() == name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// sample returns true with the given probability, which must be in the range
|
||||||
|
// [0.0, 1.0].
|
||||||
|
func sample(probability float64) bool {
|
||||||
|
return rng.Float64() < probability
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
Package sentry is the official Sentry SDK for Go.
|
||||||
|
|
||||||
|
Use it to report errors and track application performance through distributed
|
||||||
|
tracing.
|
||||||
|
|
||||||
|
For more information about Sentry and SDK features please have a look at the
|
||||||
|
documentation site https://docs.sentry.io/platforms/go/.
|
||||||
|
|
||||||
|
# Basic Usage
|
||||||
|
|
||||||
|
The first step is to initialize the SDK, providing at a minimum the DSN of your
|
||||||
|
Sentry project. This step is accomplished through a call to sentry.Init.
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := sentry.Init(...)
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
A more detailed yet simple example is available at
|
||||||
|
https://github.com/getsentry/sentry-go/blob/master/example/basic/main.go.
|
||||||
|
|
||||||
|
# Error Reporting
|
||||||
|
|
||||||
|
The Capture* functions report messages and errors to Sentry.
|
||||||
|
|
||||||
|
sentry.CaptureMessage(...)
|
||||||
|
sentry.CaptureException(...)
|
||||||
|
sentry.CaptureEvent(...)
|
||||||
|
|
||||||
|
Use similarly named functions in the Hub for concurrent programs like web
|
||||||
|
servers.
|
||||||
|
|
||||||
|
# Performance Monitoring
|
||||||
|
|
||||||
|
You can use Sentry to monitor your application's performance. More information
|
||||||
|
on the product page https://docs.sentry.io/product/performance/.
|
||||||
|
|
||||||
|
The StartSpan function creates new spans.
|
||||||
|
|
||||||
|
span := sentry.StartSpan(ctx, "operation")
|
||||||
|
...
|
||||||
|
span.Finish()
|
||||||
|
|
||||||
|
# Integrations
|
||||||
|
|
||||||
|
The SDK has support for several Go frameworks, available as subpackages.
|
||||||
|
|
||||||
|
# Getting Support
|
||||||
|
|
||||||
|
For paid Sentry.io accounts, head out to https://sentry.io/support.
|
||||||
|
|
||||||
|
For all users, support channels include:
|
||||||
|
|
||||||
|
Forum: https://forum.sentry.io
|
||||||
|
Discord: https://discord.gg/Ww9hbqr (#go channel)
|
||||||
|
|
||||||
|
If you found an issue with the SDK, please report through
|
||||||
|
https://github.com/getsentry/sentry-go/issues/new/choose.
|
||||||
|
|
||||||
|
For responsibly disclosing a security issue, please follow the steps in
|
||||||
|
https://sentry.io/security/#vulnerability-disclosure.
|
||||||
|
*/
|
||||||
|
package sentry
|
|
@ -0,0 +1,204 @@
|
||||||
|
package sentry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type scheme string
|
||||||
|
|
||||||
|
const (
|
||||||
|
schemeHTTP scheme = "http"
|
||||||
|
schemeHTTPS scheme = "https"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (scheme scheme) defaultPort() int {
|
||||||
|
switch scheme {
|
||||||
|
case schemeHTTPS:
|
||||||
|
return 443
|
||||||
|
case schemeHTTP:
|
||||||
|
return 80
|
||||||
|
default:
|
||||||
|
return 80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DsnParseError represents an error that occurs if a Sentry
|
||||||
|
// DSN cannot be parsed.
|
||||||
|
type DsnParseError struct {
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e DsnParseError) Error() string {
|
||||||
|
return "[Sentry] DsnParseError: " + e.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dsn is used as the remote address source to client transport.
|
||||||
|
type Dsn struct {
|
||||||
|
scheme scheme
|
||||||
|
publicKey string
|
||||||
|
secretKey string
|
||||||
|
host string
|
||||||
|
port int
|
||||||
|
path string
|
||||||
|
projectID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDsn creates a Dsn by parsing rawURL. Most users will never call this
|
||||||
|
// function directly. It is provided for use in custom Transport
|
||||||
|
// implementations.
|
||||||
|
func NewDsn(rawURL string) (*Dsn, error) {
|
||||||
|
// Parse
|
||||||
|
parsedURL, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &DsnParseError{fmt.Sprintf("invalid url: %v", err)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scheme
|
||||||
|
var scheme scheme
|
||||||
|
switch parsedURL.Scheme {
|
||||||
|
case "http":
|
||||||
|
scheme = schemeHTTP
|
||||||
|
case "https":
|
||||||
|
scheme = schemeHTTPS
|
||||||
|
default:
|
||||||
|
return nil, &DsnParseError{"invalid scheme"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicKey
|
||||||
|
publicKey := parsedURL.User.Username()
|
||||||
|
if publicKey == "" {
|
||||||
|
return nil, &DsnParseError{"empty username"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecretKey
|
||||||
|
var secretKey string
|
||||||
|
if parsedSecretKey, ok := parsedURL.User.Password(); ok {
|
||||||
|
secretKey = parsedSecretKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host
|
||||||
|
host := parsedURL.Hostname()
|
||||||
|
if host == "" {
|
||||||
|
return nil, &DsnParseError{"empty host"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port
|
||||||
|
var port int
|
||||||
|
if parsedURL.Port() != "" {
|
||||||
|
parsedPort, err := strconv.Atoi(parsedURL.Port())
|
||||||
|
if err != nil {
|
||||||
|
return nil, &DsnParseError{"invalid port"}
|
||||||
|
}
|
||||||
|
port = parsedPort
|
||||||
|
} else {
|
||||||
|
port = scheme.defaultPort()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProjectID
|
||||||
|
if parsedURL.Path == "" || parsedURL.Path == "/" {
|
||||||
|
return nil, &DsnParseError{"empty project id"}
|
||||||
|
}
|
||||||
|
pathSegments := strings.Split(parsedURL.Path[1:], "/")
|
||||||
|
projectID := pathSegments[len(pathSegments)-1]
|
||||||
|
|
||||||
|
if projectID == "" {
|
||||||
|
return nil, &DsnParseError{"empty project id"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path
|
||||||
|
var path string
|
||||||
|
if len(pathSegments) > 1 {
|
||||||
|
path = "/" + strings.Join(pathSegments[0:len(pathSegments)-1], "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Dsn{
|
||||||
|
scheme: scheme,
|
||||||
|
publicKey: publicKey,
|
||||||
|
secretKey: secretKey,
|
||||||
|
host: host,
|
||||||
|
port: port,
|
||||||
|
path: path,
|
||||||
|
projectID: projectID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String formats Dsn struct into a valid string url.
|
||||||
|
func (dsn Dsn) String() string {
|
||||||
|
var url string
|
||||||
|
url += fmt.Sprintf("%s://%s", dsn.scheme, dsn.publicKey)
|
||||||
|
if dsn.secretKey != "" {
|
||||||
|
url += fmt.Sprintf(":%s", dsn.secretKey)
|
||||||
|
}
|
||||||
|
url += fmt.Sprintf("@%s", dsn.host)
|
||||||
|
if dsn.port != dsn.scheme.defaultPort() {
|
||||||
|
url += fmt.Sprintf(":%d", dsn.port)
|
||||||
|
}
|
||||||
|
if dsn.path != "" {
|
||||||
|
url += dsn.path
|
||||||
|
}
|
||||||
|
url += fmt.Sprintf("/%s", dsn.projectID)
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreAPIURL returns the URL of the store endpoint of the project associated
|
||||||
|
// with the DSN.
|
||||||
|
func (dsn Dsn) StoreAPIURL() *url.URL {
|
||||||
|
return dsn.getAPIURL("store")
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnvelopeAPIURL returns the URL of the envelope endpoint of the project
|
||||||
|
// associated with the DSN.
|
||||||
|
func (dsn Dsn) EnvelopeAPIURL() *url.URL {
|
||||||
|
return dsn.getAPIURL("envelope")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dsn Dsn) getAPIURL(s string) *url.URL {
|
||||||
|
var rawURL string
|
||||||
|
rawURL += fmt.Sprintf("%s://%s", dsn.scheme, dsn.host)
|
||||||
|
if dsn.port != dsn.scheme.defaultPort() {
|
||||||
|
rawURL += fmt.Sprintf(":%d", dsn.port)
|
||||||
|
}
|
||||||
|
if dsn.path != "" {
|
||||||
|
rawURL += dsn.path
|
||||||
|
}
|
||||||
|
rawURL += fmt.Sprintf("/api/%s/%s/", dsn.projectID, s)
|
||||||
|
parsedURL, _ := url.Parse(rawURL)
|
||||||
|
return parsedURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestHeaders returns all the necessary headers that have to be used in the transport.
|
||||||
|
func (dsn Dsn) RequestHeaders() map[string]string {
|
||||||
|
auth := fmt.Sprintf("Sentry sentry_version=%s, sentry_timestamp=%d, "+
|
||||||
|
"sentry_client=sentry.go/%s, sentry_key=%s", apiVersion, time.Now().Unix(), Version, dsn.publicKey)
|
||||||
|
|
||||||
|
if dsn.secretKey != "" {
|
||||||
|
auth = fmt.Sprintf("%s, sentry_secret=%s", auth, dsn.secretKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Sentry-Auth": auth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON converts the Dsn struct to JSON.
|
||||||
|
func (dsn Dsn) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(dsn.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON converts JSON data to the Dsn struct.
|
||||||
|
func (dsn *Dsn) UnmarshalJSON(data []byte) error {
|
||||||
|
var str string
|
||||||
|
_ = json.Unmarshal(data, &str)
|
||||||
|
newDsn, err := NewDsn(str)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*dsn = *newDsn
|
||||||
|
return nil
|
||||||
|
}
|
110
vendor/github.com/getsentry/sentry-go/dynamic_sampling_context.go
generated
vendored
Normal file
110
vendor/github.com/getsentry/sentry-go/dynamic_sampling_context.go
generated
vendored
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
package sentry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/getsentry/sentry-go/internal/otel/baggage"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sentryPrefix = "sentry-"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DynamicSamplingContext holds information about the current event that can be used to make dynamic sampling decisions.
|
||||||
|
type DynamicSamplingContext struct {
|
||||||
|
Entries map[string]string
|
||||||
|
Frozen bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func DynamicSamplingContextFromHeader(header []byte) (DynamicSamplingContext, error) {
|
||||||
|
bag, err := baggage.Parse(string(header))
|
||||||
|
if err != nil {
|
||||||
|
return DynamicSamplingContext{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := map[string]string{}
|
||||||
|
for _, member := range bag.Members() {
|
||||||
|
// We only store baggage members if their key starts with "sentry-".
|
||||||
|
if k, v := member.Key(), member.Value(); strings.HasPrefix(k, sentryPrefix) {
|
||||||
|
entries[strings.TrimPrefix(k, sentryPrefix)] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DynamicSamplingContext{
|
||||||
|
Entries: entries,
|
||||||
|
Frozen: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DynamicSamplingContextFromTransaction(span *Span) DynamicSamplingContext {
|
||||||
|
entries := map[string]string{}
|
||||||
|
|
||||||
|
hub := hubFromContext(span.Context())
|
||||||
|
scope := hub.Scope()
|
||||||
|
client := hub.Client()
|
||||||
|
options := client.Options()
|
||||||
|
|
||||||
|
if traceID := span.TraceID.String(); traceID != "" {
|
||||||
|
entries["trace_id"] = traceID
|
||||||
|
}
|
||||||
|
if sampleRate := span.sampleRate; sampleRate != 0 {
|
||||||
|
entries["sample_rate"] = strconv.FormatFloat(sampleRate, 'f', -1, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dsn := client.dsn; dsn != nil {
|
||||||
|
if publicKey := dsn.publicKey; publicKey != "" {
|
||||||
|
entries["public_key"] = publicKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if release := options.Release; release != "" {
|
||||||
|
entries["release"] = release
|
||||||
|
}
|
||||||
|
if environment := options.Environment; environment != "" {
|
||||||
|
entries["environment"] = environment
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include the transaction name if it's of good quality (not empty and not SourceURL)
|
||||||
|
if span.Source != "" && span.Source != SourceURL {
|
||||||
|
if transactionName := scope.Transaction(); transactionName != "" {
|
||||||
|
entries["transaction"] = transactionName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if userSegment := scope.user.Segment; userSegment != "" {
|
||||||
|
entries["user_segment"] = userSegment
|
||||||
|
}
|
||||||
|
|
||||||
|
return DynamicSamplingContext{
|
||||||
|
Entries: entries,
|
||||||
|
Frozen: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DynamicSamplingContext) HasEntries() bool {
|
||||||
|
return len(d.Entries) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DynamicSamplingContext) IsFrozen() bool {
|
||||||
|
return d.Frozen
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DynamicSamplingContext) String() string {
|
||||||
|
members := []baggage.Member{}
|
||||||
|
for k, entry := range d.Entries {
|
||||||
|
member, err := baggage.NewMember(sentryPrefix+k, entry)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
members = append(members, member)
|
||||||
|
}
|
||||||
|
if len(members) > 0 {
|
||||||
|
baggage, err := baggage.New(members...)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return baggage.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
|
@ -0,0 +1,384 @@
|
||||||
|
package sentry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contextKey int
|
||||||
|
|
||||||
|
// Keys used to store values in a Context. Use with Context.Value to access
|
||||||
|
// values stored by the SDK.
|
||||||
|
const (
|
||||||
|
// HubContextKey is the key used to store the current Hub.
|
||||||
|
HubContextKey = contextKey(1)
|
||||||
|
// RequestContextKey is the key used to store the current http.Request.
|
||||||
|
RequestContextKey = contextKey(2)
|
||||||
|
)
|
||||||
|
|
||||||
|
// defaultMaxBreadcrumbs is the default maximum number of breadcrumbs added to
|
||||||
|
// an event. Can be overwritten with the maxBreadcrumbs option.
|
||||||
|
const defaultMaxBreadcrumbs = 30
|
||||||
|
|
||||||
|
// maxBreadcrumbs is the absolute maximum number of breadcrumbs added to an
|
||||||
|
// event. The maxBreadcrumbs option cannot be set higher than this value.
|
||||||
|
const maxBreadcrumbs = 100
|
||||||
|
|
||||||
|
// currentHub is the initial Hub with no Client bound and an empty Scope.
|
||||||
|
var currentHub = NewHub(nil, NewScope())
|
||||||
|
|
||||||
|
// Hub is the central object that manages scopes and clients.
|
||||||
|
//
|
||||||
|
// This can be used to capture events and manage the scope.
|
||||||
|
// The default hub that is available automatically.
|
||||||
|
//
|
||||||
|
// In most situations developers do not need to interface the hub. Instead
|
||||||
|
// toplevel convenience functions are exposed that will automatically dispatch
|
||||||
|
// to global (CurrentHub) hub. In some situations this might not be
|
||||||
|
// possible in which case it might become necessary to manually work with the
|
||||||
|
// hub. This is for instance the case when working with async code.
|
||||||
|
type Hub struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
stack *stack
|
||||||
|
lastEventID EventID
|
||||||
|
}
|
||||||
|
|
||||||
|
type layer struct {
|
||||||
|
// mu protects concurrent reads and writes to client.
|
||||||
|
mu sync.RWMutex
|
||||||
|
client *Client
|
||||||
|
// scope is read-only, not protected by mu.
|
||||||
|
scope *Scope
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client returns the layer's client. Safe for concurrent use.
|
||||||
|
func (l *layer) Client() *Client {
|
||||||
|
l.mu.RLock()
|
||||||
|
defer l.mu.RUnlock()
|
||||||
|
return l.client
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetClient sets the layer's client. Safe for concurrent use.
|
||||||
|
func (l *layer) SetClient(c *Client) {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
l.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
type stack []*layer
|
||||||
|
|
||||||
|
// NewHub returns an instance of a Hub with provided Client and Scope bound.
|
||||||
|
func NewHub(client *Client, scope *Scope) *Hub {
|
||||||
|
hub := Hub{
|
||||||
|
stack: &stack{{
|
||||||
|
client: client,
|
||||||
|
scope: scope,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
return &hub
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentHub returns an instance of previously initialized Hub stored in the global namespace.
|
||||||
|
func CurrentHub() *Hub {
|
||||||
|
return currentHub
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastEventID returns the ID of the last event (error or message) captured
|
||||||
|
// through the hub and sent to the underlying transport.
|
||||||
|
//
|
||||||
|
// Transactions and events dropped by sampling or event processors do not change
|
||||||
|
// the last event ID.
|
||||||
|
//
|
||||||
|
// LastEventID is a convenience method to cover use cases in which errors are
|
||||||
|
// captured indirectly and the ID is needed. For example, it can be used as part
|
||||||
|
// of an HTTP middleware to log the ID of the last error, if any.
|
||||||
|
//
|
||||||
|
// For more flexibility, consider instead using the ClientOptions.BeforeSend
|
||||||
|
// function or event processors.
|
||||||
|
func (hub *Hub) LastEventID() EventID {
|
||||||
|
hub.mu.RLock()
|
||||||
|
defer hub.mu.RUnlock()
|
||||||
|
|
||||||
|
return hub.lastEventID
|
||||||
|
}
|
||||||
|
|
||||||
|
// stackTop returns the top layer of the hub stack. Valid hubs always have at
|
||||||
|
// least one layer, therefore stackTop always return a non-nil pointer.
|
||||||
|
func (hub *Hub) stackTop() *layer {
|
||||||
|
hub.mu.RLock()
|
||||||
|
defer hub.mu.RUnlock()
|
||||||
|
|
||||||
|
stack := hub.stack
|
||||||
|
stackLen := len(*stack)
|
||||||
|
top := (*stack)[stackLen-1]
|
||||||
|
return top
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone returns a copy of the current Hub with top-most scope and client copied over.
|
||||||
|
func (hub *Hub) Clone() *Hub {
|
||||||
|
top := hub.stackTop()
|
||||||
|
scope := top.scope
|
||||||
|
if scope != nil {
|
||||||
|
scope = scope.Clone()
|
||||||
|
}
|
||||||
|
return NewHub(top.Client(), scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scope returns top-level Scope of the current Hub or nil if no Scope is bound.
|
||||||
|
func (hub *Hub) Scope() *Scope {
|
||||||
|
top := hub.stackTop()
|
||||||
|
return top.scope
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client returns top-level Client of the current Hub or nil if no Client is bound.
|
||||||
|
func (hub *Hub) Client() *Client {
|
||||||
|
top := hub.stackTop()
|
||||||
|
return top.Client()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushScope pushes a new scope for the current Hub and reuses previously bound Client.
|
||||||
|
func (hub *Hub) PushScope() *Scope {
|
||||||
|
top := hub.stackTop()
|
||||||
|
|
||||||
|
var scope *Scope
|
||||||
|
if top.scope != nil {
|
||||||
|
scope = top.scope.Clone()
|
||||||
|
} else {
|
||||||
|
scope = NewScope()
|
||||||
|
}
|
||||||
|
|
||||||
|
hub.mu.Lock()
|
||||||
|
defer hub.mu.Unlock()
|
||||||
|
|
||||||
|
*hub.stack = append(*hub.stack, &layer{
|
||||||
|
client: top.Client(),
|
||||||
|
scope: scope,
|
||||||
|
})
|
||||||
|
|
||||||
|
return scope
|
||||||
|
}
|
||||||
|
|
||||||
|
// PopScope drops the most recent scope.
|
||||||
|
//
|
||||||
|
// Calls to PopScope must be coordinated with PushScope. For most cases, using
|
||||||
|
// WithScope should be more convenient.
|
||||||
|
//
|
||||||
|
// Calls to PopScope that do not match previous calls to PushScope are silently
|
||||||
|
// ignored.
|
||||||
|
func (hub *Hub) PopScope() {
|
||||||
|
hub.mu.Lock()
|
||||||
|
defer hub.mu.Unlock()
|
||||||
|
|
||||||
|
stack := *hub.stack
|
||||||
|
stackLen := len(stack)
|
||||||
|
if stackLen > 1 {
|
||||||
|
// Never pop the last item off the stack, the stack should always have
|
||||||
|
// at least one item.
|
||||||
|
*hub.stack = stack[0 : stackLen-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindClient binds a new Client for the current Hub.
|
||||||
|
func (hub *Hub) BindClient(client *Client) {
|
||||||
|
top := hub.stackTop()
|
||||||
|
top.SetClient(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithScope runs f in an isolated temporary scope.
|
||||||
|
//
|
||||||
|
// It is useful when extra data should be sent with a single capture call, for
|
||||||
|
// instance a different level or tags.
|
||||||
|
//
|
||||||
|
// The scope passed to f starts as a clone of the current scope and can be
|
||||||
|
// freely modified without affecting the current scope.
|
||||||
|
//
|
||||||
|
// It is a shorthand for PushScope followed by PopScope.
|
||||||
|
func (hub *Hub) WithScope(f func(scope *Scope)) {
|
||||||
|
scope := hub.PushScope()
|
||||||
|
defer hub.PopScope()
|
||||||
|
f(scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigureScope runs f in the current scope.
|
||||||
|
//
|
||||||
|
// It is useful to set data that applies to all events that share the current
|
||||||
|
// scope.
|
||||||
|
//
|
||||||
|
// Modifying the scope affects all references to the current scope.
|
||||||
|
//
|
||||||
|
// See also WithScope for making isolated temporary changes.
|
||||||
|
func (hub *Hub) ConfigureScope(f func(scope *Scope)) {
|
||||||
|
scope := hub.Scope()
|
||||||
|
f(scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureEvent calls the method of a same name on currently bound Client instance
|
||||||
|
// passing it a top-level Scope.
|
||||||
|
// Returns EventID if successfully, or nil if there's no Scope or Client available.
|
||||||
|
func (hub *Hub) CaptureEvent(event *Event) *EventID {
|
||||||
|
client, scope := hub.Client(), hub.Scope()
|
||||||
|
if client == nil || scope == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
eventID := client.CaptureEvent(event, nil, scope)
|
||||||
|
|
||||||
|
if event.Type != transactionType && eventID != nil {
|
||||||
|
hub.mu.Lock()
|
||||||
|
hub.lastEventID = *eventID
|
||||||
|
hub.mu.Unlock()
|
||||||
|
}
|
||||||
|
return eventID
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureMessage calls the method of a same name on currently bound Client instance
|
||||||
|
// passing it a top-level Scope.
|
||||||
|
// Returns EventID if successfully, or nil if there's no Scope or Client available.
|
||||||
|
func (hub *Hub) CaptureMessage(message string) *EventID {
|
||||||
|
client, scope := hub.Client(), hub.Scope()
|
||||||
|
if client == nil || scope == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
eventID := client.CaptureMessage(message, nil, scope)
|
||||||
|
|
||||||
|
if eventID != nil {
|
||||||
|
hub.mu.Lock()
|
||||||
|
hub.lastEventID = *eventID
|
||||||
|
hub.mu.Unlock()
|
||||||
|
}
|
||||||
|
return eventID
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureException calls the method of a same name on currently bound Client instance
|
||||||
|
// passing it a top-level Scope.
|
||||||
|
// Returns EventID if successfully, or nil if there's no Scope or Client available.
|
||||||
|
func (hub *Hub) CaptureException(exception error) *EventID {
|
||||||
|
client, scope := hub.Client(), hub.Scope()
|
||||||
|
if client == nil || scope == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
eventID := client.CaptureException(exception, &EventHint{OriginalException: exception}, scope)
|
||||||
|
|
||||||
|
if eventID != nil {
|
||||||
|
hub.mu.Lock()
|
||||||
|
hub.lastEventID = *eventID
|
||||||
|
hub.mu.Unlock()
|
||||||
|
}
|
||||||
|
return eventID
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddBreadcrumb records a new breadcrumb.
|
||||||
|
//
|
||||||
|
// The total number of breadcrumbs that can be recorded are limited by the
|
||||||
|
// configuration on the client.
|
||||||
|
func (hub *Hub) AddBreadcrumb(breadcrumb *Breadcrumb, hint *BreadcrumbHint) {
|
||||||
|
client := hub.Client()
|
||||||
|
|
||||||
|
// If there's no client, just store it on the scope straight away
|
||||||
|
if client == nil {
|
||||||
|
hub.Scope().AddBreadcrumb(breadcrumb, maxBreadcrumbs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
options := client.Options()
|
||||||
|
max := options.MaxBreadcrumbs
|
||||||
|
if max < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.BeforeBreadcrumb != nil {
|
||||||
|
if hint == nil {
|
||||||
|
hint = &BreadcrumbHint{}
|
||||||
|
}
|
||||||
|
if breadcrumb = options.BeforeBreadcrumb(breadcrumb, hint); breadcrumb == nil {
|
||||||
|
Logger.Println("breadcrumb dropped due to BeforeBreadcrumb callback.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if max == 0 {
|
||||||
|
max = defaultMaxBreadcrumbs
|
||||||
|
} else if max > maxBreadcrumbs {
|
||||||
|
max = maxBreadcrumbs
|
||||||
|
}
|
||||||
|
|
||||||
|
hub.Scope().AddBreadcrumb(breadcrumb, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recover calls the method of a same name on currently bound Client instance
|
||||||
|
// passing it a top-level Scope.
|
||||||
|
// Returns EventID if successfully, or nil if there's no Scope or Client available.
|
||||||
|
func (hub *Hub) Recover(err interface{}) *EventID {
|
||||||
|
if err == nil {
|
||||||
|
err = recover()
|
||||||
|
}
|
||||||
|
client, scope := hub.Client(), hub.Scope()
|
||||||
|
if client == nil || scope == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return client.Recover(err, &EventHint{RecoveredException: err}, scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecoverWithContext calls the method of a same name on currently bound Client instance
|
||||||
|
// passing it a top-level Scope.
|
||||||
|
// Returns EventID if successfully, or nil if there's no Scope or Client available.
|
||||||
|
func (hub *Hub) RecoverWithContext(ctx context.Context, err interface{}) *EventID {
|
||||||
|
if err == nil {
|
||||||
|
err = recover()
|
||||||
|
}
|
||||||
|
client, scope := hub.Client(), hub.Scope()
|
||||||
|
if client == nil || scope == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return client.RecoverWithContext(ctx, err, &EventHint{RecoveredException: err}, scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush waits until the underlying Transport sends any buffered events to the
|
||||||
|
// Sentry server, blocking for at most the given timeout. It returns false if
|
||||||
|
// the timeout was reached. In that case, some events may not have been sent.
|
||||||
|
//
|
||||||
|
// Flush should be called before terminating the program to avoid
|
||||||
|
// unintentionally dropping events.
|
||||||
|
//
|
||||||
|
// Do not call Flush indiscriminately after every call to CaptureEvent,
|
||||||
|
// CaptureException or CaptureMessage. Instead, to have the SDK send events over
|
||||||
|
// the network synchronously, configure it to use the HTTPSyncTransport in the
|
||||||
|
// call to Init.
|
||||||
|
func (hub *Hub) Flush(timeout time.Duration) bool {
|
||||||
|
client := hub.Client()
|
||||||
|
|
||||||
|
if client == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.Flush(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasHubOnContext checks whether Hub instance is bound to a given Context struct.
|
||||||
|
func HasHubOnContext(ctx context.Context) bool {
|
||||||
|
_, ok := ctx.Value(HubContextKey).(*Hub)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHubFromContext tries to retrieve Hub instance from the given Context struct
|
||||||
|
// or return nil if one is not found.
|
||||||
|
func GetHubFromContext(ctx context.Context) *Hub {
|
||||||
|
if hub, ok := ctx.Value(HubContextKey).(*Hub); ok {
|
||||||
|
return hub
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hubFromContext returns either a hub stored in the context or the current hub.
|
||||||
|
// The return value is guaranteed to be non-nil, unlike GetHubFromContext.
|
||||||
|
func hubFromContext(ctx context.Context) *Hub {
|
||||||
|
if hub, ok := ctx.Value(HubContextKey).(*Hub); ok {
|
||||||
|
return hub
|
||||||
|
}
|
||||||
|
return currentHub
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHubOnContext stores given Hub instance on the Context struct and returns a new Context.
|
||||||
|
func SetHubOnContext(ctx context.Context, hub *Hub) context.Context {
|
||||||
|
return context.WithValue(ctx, HubContextKey, hub)
|
||||||
|
}
|
|
@ -0,0 +1,293 @@
|
||||||
|
package sentry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// Modules Integration
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
type modulesIntegration struct {
|
||||||
|
once sync.Once
|
||||||
|
modules map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mi *modulesIntegration) Name() string {
|
||||||
|
return "Modules"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mi *modulesIntegration) SetupOnce(client *Client) {
|
||||||
|
client.AddEventProcessor(mi.processor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mi *modulesIntegration) processor(event *Event, hint *EventHint) *Event {
|
||||||
|
if len(event.Modules) == 0 {
|
||||||
|
mi.once.Do(func() {
|
||||||
|
info, ok := debug.ReadBuildInfo()
|
||||||
|
if !ok {
|
||||||
|
Logger.Print("The Modules integration is not available in binaries built without module support.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mi.modules = extractModules(info)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
event.Modules = mi.modules
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractModules(info *debug.BuildInfo) map[string]string {
|
||||||
|
modules := map[string]string{
|
||||||
|
info.Main.Path: info.Main.Version,
|
||||||
|
}
|
||||||
|
for _, dep := range info.Deps {
|
||||||
|
ver := dep.Version
|
||||||
|
if dep.Replace != nil {
|
||||||
|
ver += fmt.Sprintf(" => %s %s", dep.Replace.Path, dep.Replace.Version)
|
||||||
|
}
|
||||||
|
modules[dep.Path] = strings.TrimSuffix(ver, " ")
|
||||||
|
}
|
||||||
|
return modules
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// Environment Integration
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
type environmentIntegration struct{}
|
||||||
|
|
||||||
|
func (ei *environmentIntegration) Name() string {
|
||||||
|
return "Environment"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ei *environmentIntegration) SetupOnce(client *Client) {
|
||||||
|
client.AddEventProcessor(ei.processor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ei *environmentIntegration) processor(event *Event, hint *EventHint) *Event {
|
||||||
|
// Initialize maps as necessary.
|
||||||
|
contextNames := []string{"device", "os", "runtime"}
|
||||||
|
if event.Contexts == nil {
|
||||||
|
event.Contexts = make(map[string]Context, len(contextNames))
|
||||||
|
}
|
||||||
|
for _, name := range contextNames {
|
||||||
|
if event.Contexts[name] == nil {
|
||||||
|
event.Contexts[name] = make(Context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set contextual information preserving existing data. For each context, if
|
||||||
|
// the existing value is not of type map[string]interface{}, then no
|
||||||
|
// additional information is added.
|
||||||
|
if deviceContext, ok := event.Contexts["device"]; ok {
|
||||||
|
if _, ok := deviceContext["arch"]; !ok {
|
||||||
|
deviceContext["arch"] = runtime.GOARCH
|
||||||
|
}
|
||||||
|
if _, ok := deviceContext["num_cpu"]; !ok {
|
||||||
|
deviceContext["num_cpu"] = runtime.NumCPU()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if osContext, ok := event.Contexts["os"]; ok {
|
||||||
|
if _, ok := osContext["name"]; !ok {
|
||||||
|
osContext["name"] = runtime.GOOS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if runtimeContext, ok := event.Contexts["runtime"]; ok {
|
||||||
|
if _, ok := runtimeContext["name"]; !ok {
|
||||||
|
runtimeContext["name"] = "go"
|
||||||
|
}
|
||||||
|
if _, ok := runtimeContext["version"]; !ok {
|
||||||
|
runtimeContext["version"] = runtime.Version()
|
||||||
|
}
|
||||||
|
if _, ok := runtimeContext["go_numroutines"]; !ok {
|
||||||
|
runtimeContext["go_numroutines"] = runtime.NumGoroutine()
|
||||||
|
}
|
||||||
|
if _, ok := runtimeContext["go_maxprocs"]; !ok {
|
||||||
|
runtimeContext["go_maxprocs"] = runtime.GOMAXPROCS(0)
|
||||||
|
}
|
||||||
|
if _, ok := runtimeContext["go_numcgocalls"]; !ok {
|
||||||
|
runtimeContext["go_numcgocalls"] = runtime.NumCgoCall()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// Ignore Errors Integration
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
type ignoreErrorsIntegration struct {
|
||||||
|
ignoreErrors []*regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (iei *ignoreErrorsIntegration) Name() string {
|
||||||
|
return "IgnoreErrors"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (iei *ignoreErrorsIntegration) SetupOnce(client *Client) {
|
||||||
|
iei.ignoreErrors = transformStringsIntoRegexps(client.Options().IgnoreErrors)
|
||||||
|
client.AddEventProcessor(iei.processor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (iei *ignoreErrorsIntegration) processor(event *Event, hint *EventHint) *Event {
|
||||||
|
suspects := getIgnoreErrorsSuspects(event)
|
||||||
|
|
||||||
|
for _, suspect := range suspects {
|
||||||
|
for _, pattern := range iei.ignoreErrors {
|
||||||
|
if pattern.Match([]byte(suspect)) {
|
||||||
|
Logger.Printf("Event dropped due to being matched by `IgnoreErrors` option."+
|
||||||
|
"| Value matched: %s | Filter used: %s", suspect, pattern)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformStringsIntoRegexps(strings []string) []*regexp.Regexp {
|
||||||
|
var exprs []*regexp.Regexp
|
||||||
|
|
||||||
|
for _, s := range strings {
|
||||||
|
r, err := regexp.Compile(s)
|
||||||
|
if err == nil {
|
||||||
|
exprs = append(exprs, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return exprs
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIgnoreErrorsSuspects(event *Event) []string {
|
||||||
|
suspects := []string{}
|
||||||
|
|
||||||
|
if event.Message != "" {
|
||||||
|
suspects = append(suspects, event.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ex := range event.Exception {
|
||||||
|
suspects = append(suspects, ex.Type, ex.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return suspects
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// Contextify Frames Integration
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
type contextifyFramesIntegration struct {
|
||||||
|
sr sourceReader
|
||||||
|
contextLines int
|
||||||
|
cachedLocations sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfi *contextifyFramesIntegration) Name() string {
|
||||||
|
return "ContextifyFrames"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfi *contextifyFramesIntegration) SetupOnce(client *Client) {
|
||||||
|
cfi.sr = newSourceReader()
|
||||||
|
cfi.contextLines = 5
|
||||||
|
|
||||||
|
client.AddEventProcessor(cfi.processor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfi *contextifyFramesIntegration) processor(event *Event, hint *EventHint) *Event {
|
||||||
|
// Range over all exceptions
|
||||||
|
for _, ex := range event.Exception {
|
||||||
|
// If it has no stacktrace, just bail out
|
||||||
|
if ex.Stacktrace == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it does, it should have frames, so try to contextify them
|
||||||
|
ex.Stacktrace.Frames = cfi.contextify(ex.Stacktrace.Frames)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range over all threads
|
||||||
|
for _, th := range event.Threads {
|
||||||
|
// If it has no stacktrace, just bail out
|
||||||
|
if th.Stacktrace == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it does, it should have frames, so try to contextify them
|
||||||
|
th.Stacktrace.Frames = cfi.contextify(th.Stacktrace.Frames)
|
||||||
|
}
|
||||||
|
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfi *contextifyFramesIntegration) contextify(frames []Frame) []Frame {
|
||||||
|
contextifiedFrames := make([]Frame, 0, len(frames))
|
||||||
|
|
||||||
|
for _, frame := range frames {
|
||||||
|
if !frame.InApp {
|
||||||
|
contextifiedFrames = append(contextifiedFrames, frame)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var path string
|
||||||
|
|
||||||
|
if cachedPath, ok := cfi.cachedLocations.Load(frame.AbsPath); ok {
|
||||||
|
if p, ok := cachedPath.(string); ok {
|
||||||
|
path = p
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Optimize for happy path here
|
||||||
|
if fileExists(frame.AbsPath) {
|
||||||
|
path = frame.AbsPath
|
||||||
|
} else {
|
||||||
|
path = cfi.findNearbySourceCodeLocation(frame.AbsPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if path == "" {
|
||||||
|
contextifiedFrames = append(contextifiedFrames, frame)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
lines, contextLine := cfi.sr.readContextLines(path, frame.Lineno, cfi.contextLines)
|
||||||
|
contextifiedFrames = append(contextifiedFrames, cfi.addContextLinesToFrame(frame, lines, contextLine))
|
||||||
|
}
|
||||||
|
|
||||||
|
return contextifiedFrames
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfi *contextifyFramesIntegration) findNearbySourceCodeLocation(originalPath string) string {
|
||||||
|
trimmedPath := strings.TrimPrefix(originalPath, "/")
|
||||||
|
components := strings.Split(trimmedPath, "/")
|
||||||
|
|
||||||
|
for len(components) > 0 {
|
||||||
|
components = components[1:]
|
||||||
|
possibleLocation := strings.Join(components, "/")
|
||||||
|
|
||||||
|
if fileExists(possibleLocation) {
|
||||||
|
cfi.cachedLocations.Store(originalPath, possibleLocation)
|
||||||
|
return possibleLocation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfi.cachedLocations.Store(originalPath, "")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfi *contextifyFramesIntegration) addContextLinesToFrame(frame Frame, lines [][]byte, contextLine int) Frame {
|
||||||
|
for i, line := range lines {
|
||||||
|
switch {
|
||||||
|
case i < contextLine:
|
||||||
|
frame.PreContext = append(frame.PreContext, string(line))
|
||||||
|
case i == contextLine:
|
||||||
|
frame.ContextLine = string(line)
|
||||||
|
default:
|
||||||
|
frame.PostContext = append(frame.PostContext, string(line))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return frame
|
||||||
|
}
|
|
@ -0,0 +1,392 @@
|
||||||
|
package sentry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Protocol Docs (kinda)
|
||||||
|
// https://github.com/getsentry/rust-sentry-types/blob/master/src/protocol/v7.rs
|
||||||
|
|
||||||
|
// transactionType is the type of a transaction event.
|
||||||
|
const transactionType = "transaction"
|
||||||
|
|
||||||
|
// Level marks the severity of the event.
|
||||||
|
type Level string
|
||||||
|
|
||||||
|
// Describes the severity of the event.
|
||||||
|
const (
|
||||||
|
LevelDebug Level = "debug"
|
||||||
|
LevelInfo Level = "info"
|
||||||
|
LevelWarning Level = "warning"
|
||||||
|
LevelError Level = "error"
|
||||||
|
LevelFatal Level = "fatal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getSensitiveHeaders() map[string]bool {
|
||||||
|
return map[string]bool{
|
||||||
|
"Authorization": true,
|
||||||
|
"Cookie": true,
|
||||||
|
"X-Forwarded-For": true,
|
||||||
|
"X-Real-Ip": true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SdkInfo contains all metadata about about the SDK being used.
|
||||||
|
type SdkInfo struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
Integrations []string `json:"integrations,omitempty"`
|
||||||
|
Packages []SdkPackage `json:"packages,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SdkPackage describes a package that was installed.
|
||||||
|
type SdkPackage struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This type could be more useful, as map of interface{} is too generic
|
||||||
|
// and requires a lot of type assertions in beforeBreadcrumb calls
|
||||||
|
// plus it could just be map[string]interface{} then.
|
||||||
|
|
||||||
|
// BreadcrumbHint contains information that can be associated with a Breadcrumb.
|
||||||
|
type BreadcrumbHint map[string]interface{}
|
||||||
|
|
||||||
|
// Breadcrumb specifies an application event that occurred before a Sentry event.
|
||||||
|
// An event may contain one or more breadcrumbs.
|
||||||
|
type Breadcrumb struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Category string `json:"category,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Data map[string]interface{} `json:"data,omitempty"`
|
||||||
|
Level Level `json:"level,omitempty"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: provide constants for known breadcrumb types.
|
||||||
|
// See https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types.
|
||||||
|
|
||||||
|
// MarshalJSON converts the Breadcrumb struct to JSON.
|
||||||
|
func (b *Breadcrumb) MarshalJSON() ([]byte, error) {
|
||||||
|
// We want to omit time.Time zero values, otherwise the server will try to
|
||||||
|
// interpret dates too far in the past. However, encoding/json doesn't
|
||||||
|
// support the "omitempty" option for struct types. See
|
||||||
|
// https://golang.org/issues/11939.
|
||||||
|
//
|
||||||
|
// We overcome the limitation and achieve what we want by shadowing fields
|
||||||
|
// and a few type tricks.
|
||||||
|
|
||||||
|
// breadcrumb aliases Breadcrumb to allow calling json.Marshal without an
|
||||||
|
// infinite loop. It preserves all fields while none of the attached
|
||||||
|
// methods.
|
||||||
|
type breadcrumb Breadcrumb
|
||||||
|
|
||||||
|
if b.Timestamp.IsZero() {
|
||||||
|
return json.Marshal(struct {
|
||||||
|
// Embed all of the fields of Breadcrumb.
|
||||||
|
*breadcrumb
|
||||||
|
// Timestamp shadows the original Timestamp field and is meant to
|
||||||
|
// remain nil, triggering the omitempty behavior.
|
||||||
|
Timestamp json.RawMessage `json:"timestamp,omitempty"`
|
||||||
|
}{breadcrumb: (*breadcrumb)(b)})
|
||||||
|
}
|
||||||
|
return json.Marshal((*breadcrumb)(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
// User describes the user associated with an Event. If this is used, at least
|
||||||
|
// an ID or an IP address should be provided.
|
||||||
|
type User struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
IPAddress string `json:"ip_address,omitempty"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Segment string `json:"segment,omitempty"`
|
||||||
|
Data map[string]string `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u User) IsEmpty() bool {
|
||||||
|
if len(u.ID) > 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(u.Email) > 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(u.IPAddress) > 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(u.Username) > 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(u.Name) > 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(u.Segment) > 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(u.Data) > 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request contains information on a HTTP request related to the event.
|
||||||
|
type Request struct {
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
Method string `json:"method,omitempty"`
|
||||||
|
Data string `json:"data,omitempty"`
|
||||||
|
QueryString string `json:"query_string,omitempty"`
|
||||||
|
Cookies string `json:"cookies,omitempty"`
|
||||||
|
Headers map[string]string `json:"headers,omitempty"`
|
||||||
|
Env map[string]string `json:"env,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRequest returns a new Sentry Request from the given http.Request.
|
||||||
|
//
|
||||||
|
// NewRequest avoids operations that depend on network access. In particular, it
|
||||||
|
// does not read r.Body.
|
||||||
|
func NewRequest(r *http.Request) *Request {
|
||||||
|
protocol := schemeHTTP
|
||||||
|
if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
|
||||||
|
protocol = schemeHTTPS
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf("%s://%s%s", protocol, r.Host, r.URL.Path)
|
||||||
|
|
||||||
|
var cookies string
|
||||||
|
var env map[string]string
|
||||||
|
headers := map[string]string{}
|
||||||
|
|
||||||
|
if client := CurrentHub().Client(); client != nil {
|
||||||
|
if client.Options().SendDefaultPII {
|
||||||
|
// We read only the first Cookie header because of the specification:
|
||||||
|
// https://tools.ietf.org/html/rfc6265#section-5.4
|
||||||
|
// When the user agent generates an HTTP request, the user agent MUST NOT
|
||||||
|
// attach more than one Cookie header field.
|
||||||
|
cookies = r.Header.Get("Cookie")
|
||||||
|
|
||||||
|
for k, v := range r.Header {
|
||||||
|
headers[k] = strings.Join(v, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
if addr, port, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||||
|
env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sensitiveHeaders := getSensitiveHeaders()
|
||||||
|
for k, v := range r.Header {
|
||||||
|
if _, ok := sensitiveHeaders[k]; !ok {
|
||||||
|
headers[k] = strings.Join(v, ",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers["Host"] = r.Host
|
||||||
|
|
||||||
|
return &Request{
|
||||||
|
URL: url,
|
||||||
|
Method: r.Method,
|
||||||
|
QueryString: r.URL.RawQuery,
|
||||||
|
Cookies: cookies,
|
||||||
|
Headers: headers,
|
||||||
|
Env: env,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exception specifies an error that occurred.
|
||||||
|
type Exception struct {
|
||||||
|
Type string `json:"type,omitempty"` // used as the main issue title
|
||||||
|
Value string `json:"value,omitempty"` // used as the main issue subtitle
|
||||||
|
Module string `json:"module,omitempty"`
|
||||||
|
ThreadID string `json:"thread_id,omitempty"`
|
||||||
|
Stacktrace *Stacktrace `json:"stacktrace,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SDKMetaData is a struct to stash data which is needed at some point in the SDK's event processing pipeline
|
||||||
|
// but which shouldn't get send to Sentry.
|
||||||
|
type SDKMetaData struct {
|
||||||
|
dsc DynamicSamplingContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains information about how the name of the transaction was determined.
|
||||||
|
type TransactionInfo struct {
|
||||||
|
Source TransactionSource `json:"source,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventID is a hexadecimal string representing a unique uuid4 for an Event.
|
||||||
|
// An EventID must be 32 characters long, lowercase and not have any dashes.
|
||||||
|
type EventID string
|
||||||
|
|
||||||
|
type Context = map[string]interface{}
|
||||||
|
|
||||||
|
// Event is the fundamental data structure that is sent to Sentry.
|
||||||
|
type Event struct {
|
||||||
|
Breadcrumbs []*Breadcrumb `json:"breadcrumbs,omitempty"`
|
||||||
|
Contexts map[string]Context `json:"contexts,omitempty"`
|
||||||
|
Dist string `json:"dist,omitempty"`
|
||||||
|
Environment string `json:"environment,omitempty"`
|
||||||
|
EventID EventID `json:"event_id,omitempty"`
|
||||||
|
Extra map[string]interface{} `json:"extra,omitempty"`
|
||||||
|
Fingerprint []string `json:"fingerprint,omitempty"`
|
||||||
|
Level Level `json:"level,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Platform string `json:"platform,omitempty"`
|
||||||
|
Release string `json:"release,omitempty"`
|
||||||
|
Sdk SdkInfo `json:"sdk,omitempty"`
|
||||||
|
ServerName string `json:"server_name,omitempty"`
|
||||||
|
Threads []Thread `json:"threads,omitempty"`
|
||||||
|
Tags map[string]string `json:"tags,omitempty"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Transaction string `json:"transaction,omitempty"`
|
||||||
|
User User `json:"user,omitempty"`
|
||||||
|
Logger string `json:"logger,omitempty"`
|
||||||
|
Modules map[string]string `json:"modules,omitempty"`
|
||||||
|
Request *Request `json:"request,omitempty"`
|
||||||
|
Exception []Exception `json:"exception,omitempty"`
|
||||||
|
|
||||||
|
// The fields below are only relevant for transactions.
|
||||||
|
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
StartTime time.Time `json:"start_timestamp"`
|
||||||
|
Spans []*Span `json:"spans,omitempty"`
|
||||||
|
TransactionInfo *TransactionInfo `json:"transaction_info,omitempty"`
|
||||||
|
|
||||||
|
// The fields below are not part of the final JSON payload.
|
||||||
|
|
||||||
|
sdkMetaData SDKMetaData
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Event.Contexts map[string]interface{} => map[string]EventContext,
|
||||||
|
// to prevent accidentally storing T when we mean *T.
|
||||||
|
// For example, the TraceContext must be stored as *TraceContext to pick up the
|
||||||
|
// MarshalJSON method (and avoid copying).
|
||||||
|
// type EventContext interface{ EventContext() }
|
||||||
|
|
||||||
|
// MarshalJSON converts the Event struct to JSON.
|
||||||
|
func (e *Event) MarshalJSON() ([]byte, error) {
|
||||||
|
// We want to omit time.Time zero values, otherwise the server will try to
|
||||||
|
// interpret dates too far in the past. However, encoding/json doesn't
|
||||||
|
// support the "omitempty" option for struct types. See
|
||||||
|
// https://golang.org/issues/11939.
|
||||||
|
//
|
||||||
|
// We overcome the limitation and achieve what we want by shadowing fields
|
||||||
|
// and a few type tricks.
|
||||||
|
if e.Type == transactionType {
|
||||||
|
return e.transactionMarshalJSON()
|
||||||
|
}
|
||||||
|
return e.defaultMarshalJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Event) defaultMarshalJSON() ([]byte, error) {
|
||||||
|
// event aliases Event to allow calling json.Marshal without an infinite
|
||||||
|
// loop. It preserves all fields while none of the attached methods.
|
||||||
|
type event Event
|
||||||
|
|
||||||
|
// errorEvent is like Event with shadowed fields for customizing JSON
|
||||||
|
// marshaling.
|
||||||
|
type errorEvent struct {
|
||||||
|
*event
|
||||||
|
|
||||||
|
// Timestamp shadows the original Timestamp field. It allows us to
|
||||||
|
// include the timestamp when non-zero and omit it otherwise.
|
||||||
|
Timestamp json.RawMessage `json:"timestamp,omitempty"`
|
||||||
|
|
||||||
|
// The fields below are not part of error events and only make sense to
|
||||||
|
// be sent for transactions. They shadow the respective fields in Event
|
||||||
|
// and are meant to remain nil, triggering the omitempty behavior.
|
||||||
|
|
||||||
|
Type json.RawMessage `json:"type,omitempty"`
|
||||||
|
StartTime json.RawMessage `json:"start_timestamp,omitempty"`
|
||||||
|
Spans json.RawMessage `json:"spans,omitempty"`
|
||||||
|
TransactionInfo json.RawMessage `json:"transaction_info,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
x := errorEvent{event: (*event)(e)}
|
||||||
|
if !e.Timestamp.IsZero() {
|
||||||
|
b, err := e.Timestamp.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
x.Timestamp = b
|
||||||
|
}
|
||||||
|
return json.Marshal(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Event) transactionMarshalJSON() ([]byte, error) {
|
||||||
|
// event aliases Event to allow calling json.Marshal without an infinite
|
||||||
|
// loop. It preserves all fields while none of the attached methods.
|
||||||
|
type event Event
|
||||||
|
|
||||||
|
// transactionEvent is like Event with shadowed fields for customizing JSON
|
||||||
|
// marshaling.
|
||||||
|
type transactionEvent struct {
|
||||||
|
*event
|
||||||
|
|
||||||
|
// The fields below shadow the respective fields in Event. They allow us
|
||||||
|
// to include timestamps when non-zero and omit them otherwise.
|
||||||
|
|
||||||
|
StartTime json.RawMessage `json:"start_timestamp,omitempty"`
|
||||||
|
Timestamp json.RawMessage `json:"timestamp,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
x := transactionEvent{event: (*event)(e)}
|
||||||
|
if !e.Timestamp.IsZero() {
|
||||||
|
b, err := e.Timestamp.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
x.Timestamp = b
|
||||||
|
}
|
||||||
|
if !e.StartTime.IsZero() {
|
||||||
|
b, err := e.StartTime.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
x.StartTime = b
|
||||||
|
}
|
||||||
|
return json.Marshal(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEvent creates a new Event.
|
||||||
|
func NewEvent() *Event {
|
||||||
|
event := Event{
|
||||||
|
Contexts: make(map[string]Context),
|
||||||
|
Extra: make(map[string]interface{}),
|
||||||
|
Tags: make(map[string]string),
|
||||||
|
Modules: make(map[string]string),
|
||||||
|
}
|
||||||
|
return &event
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread specifies threads that were running at the time of an event.
|
||||||
|
type Thread struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Stacktrace *Stacktrace `json:"stacktrace,omitempty"`
|
||||||
|
Crashed bool `json:"crashed,omitempty"`
|
||||||
|
Current bool `json:"current,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventHint contains information that can be associated with an Event.
|
||||||
|
type EventHint struct {
|
||||||
|
Data interface{}
|
||||||
|
EventID string
|
||||||
|
OriginalException error
|
||||||
|
RecoveredException interface{}
|
||||||
|
Context context.Context
|
||||||
|
Request *http.Request
|
||||||
|
Response *http.Response
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptrace"
|
||||||
|
"net/http/httputil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Transport implements http.RoundTripper and can be used to wrap other HTTP
|
||||||
|
// transports for debugging, normally http.DefaultTransport.
|
||||||
|
type Transport struct {
|
||||||
|
http.RoundTripper
|
||||||
|
Output io.Writer
|
||||||
|
// Dump controls whether to dump HTTP request and responses.
|
||||||
|
Dump bool
|
||||||
|
// Trace enables usage of net/http/httptrace.
|
||||||
|
Trace bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if t.Dump {
|
||||||
|
b, err := httputil.DumpRequestOut(req, true)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
_, err = buf.Write(ensureTrailingNewline(b))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.Trace {
|
||||||
|
trace := &httptrace.ClientTrace{
|
||||||
|
DNSDone: func(di httptrace.DNSDoneInfo) {
|
||||||
|
fmt.Fprintf(&buf, "* DNS %v → %v\n", req.Host, di.Addrs)
|
||||||
|
},
|
||||||
|
GotConn: func(ci httptrace.GotConnInfo) {
|
||||||
|
fmt.Fprintf(&buf, "* Connection local=%v remote=%v", ci.Conn.LocalAddr(), ci.Conn.RemoteAddr())
|
||||||
|
if ci.Reused {
|
||||||
|
fmt.Fprint(&buf, " (reused)")
|
||||||
|
}
|
||||||
|
if ci.WasIdle {
|
||||||
|
fmt.Fprintf(&buf, " (idle %v)", ci.IdleTime)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(&buf)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
|
||||||
|
}
|
||||||
|
resp, err := t.RoundTripper.RoundTrip(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if t.Dump {
|
||||||
|
b, err := httputil.DumpResponse(resp, true)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
_, err = buf.Write(ensureTrailingNewline(b))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = io.Copy(t.Output, &buf)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureTrailingNewline(b []byte) []byte {
|
||||||
|
if len(b) > 0 && b[len(b)-1] != '\n' {
|
||||||
|
b = append(b, '\n')
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
573
vendor/github.com/getsentry/sentry-go/internal/otel/baggage/baggage.go
generated
vendored
Normal file
573
vendor/github.com/getsentry/sentry-go/internal/otel/baggage/baggage.go
generated
vendored
Normal file
|
@ -0,0 +1,573 @@
|
||||||
|
// This file was vendored in unmodified from
|
||||||
|
// https://github.com/open-telemetry/opentelemetry-go/blob/c21b6b6bb31a2f74edd06e262f1690f3f6ea3d5c/baggage/baggage.go
|
||||||
|
//
|
||||||
|
// # Copyright The OpenTelemetry Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package baggage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/getsentry/sentry-go/internal/otel/baggage/internal/baggage"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxMembers = 180
|
||||||
|
maxBytesPerMembers = 4096
|
||||||
|
maxBytesPerBaggageString = 8192
|
||||||
|
|
||||||
|
listDelimiter = ","
|
||||||
|
keyValueDelimiter = "="
|
||||||
|
propertyDelimiter = ";"
|
||||||
|
|
||||||
|
keyDef = `([\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5a\x5e-\x7a\x7c\x7e]+)`
|
||||||
|
valueDef = `([\x21\x23-\x2b\x2d-\x3a\x3c-\x5B\x5D-\x7e]*)`
|
||||||
|
keyValueDef = `\s*` + keyDef + `\s*` + keyValueDelimiter + `\s*` + valueDef + `\s*`
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
keyRe = regexp.MustCompile(`^` + keyDef + `$`)
|
||||||
|
valueRe = regexp.MustCompile(`^` + valueDef + `$`)
|
||||||
|
propertyRe = regexp.MustCompile(`^(?:\s*` + keyDef + `\s*|` + keyValueDef + `)$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errInvalidKey = errors.New("invalid key")
|
||||||
|
errInvalidValue = errors.New("invalid value")
|
||||||
|
errInvalidProperty = errors.New("invalid baggage list-member property")
|
||||||
|
errInvalidMember = errors.New("invalid baggage list-member")
|
||||||
|
errMemberNumber = errors.New("too many list-members in baggage-string")
|
||||||
|
errMemberBytes = errors.New("list-member too large")
|
||||||
|
errBaggageBytes = errors.New("baggage-string too large")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Property is an additional metadata entry for a baggage list-member.
|
||||||
|
type Property struct {
|
||||||
|
key, value string
|
||||||
|
|
||||||
|
// hasValue indicates if a zero-value value means the property does not
|
||||||
|
// have a value or if it was the zero-value.
|
||||||
|
hasValue bool
|
||||||
|
|
||||||
|
// hasData indicates whether the created property contains data or not.
|
||||||
|
// Properties that do not contain data are invalid with no other check
|
||||||
|
// required.
|
||||||
|
hasData bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKeyProperty returns a new Property for key.
|
||||||
|
//
|
||||||
|
// If key is invalid, an error will be returned.
|
||||||
|
func NewKeyProperty(key string) (Property, error) {
|
||||||
|
if !keyRe.MatchString(key) {
|
||||||
|
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
p := Property{key: key, hasData: true}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKeyValueProperty returns a new Property for key with value.
|
||||||
|
//
|
||||||
|
// If key or value are invalid, an error will be returned.
|
||||||
|
func NewKeyValueProperty(key, value string) (Property, error) {
|
||||||
|
if !keyRe.MatchString(key) {
|
||||||
|
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, key)
|
||||||
|
}
|
||||||
|
if !valueRe.MatchString(value) {
|
||||||
|
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidValue, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
p := Property{
|
||||||
|
key: key,
|
||||||
|
value: value,
|
||||||
|
hasValue: true,
|
||||||
|
hasData: true,
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInvalidProperty() Property {
|
||||||
|
return Property{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseProperty attempts to decode a Property from the passed string. It
|
||||||
|
// returns an error if the input is invalid according to the W3C Baggage
|
||||||
|
// specification.
|
||||||
|
func parseProperty(property string) (Property, error) {
|
||||||
|
if property == "" {
|
||||||
|
return newInvalidProperty(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
match := propertyRe.FindStringSubmatch(property)
|
||||||
|
if len(match) != 4 {
|
||||||
|
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidProperty, property)
|
||||||
|
}
|
||||||
|
|
||||||
|
p := Property{hasData: true}
|
||||||
|
if match[1] != "" {
|
||||||
|
p.key = match[1]
|
||||||
|
} else {
|
||||||
|
p.key = match[2]
|
||||||
|
p.value = match[3]
|
||||||
|
p.hasValue = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate ensures p conforms to the W3C Baggage specification, returning an
|
||||||
|
// error otherwise.
|
||||||
|
func (p Property) validate() error {
|
||||||
|
errFunc := func(err error) error {
|
||||||
|
return fmt.Errorf("invalid property: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.hasData {
|
||||||
|
return errFunc(fmt.Errorf("%w: %q", errInvalidProperty, p))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !keyRe.MatchString(p.key) {
|
||||||
|
return errFunc(fmt.Errorf("%w: %q", errInvalidKey, p.key))
|
||||||
|
}
|
||||||
|
if p.hasValue && !valueRe.MatchString(p.value) {
|
||||||
|
return errFunc(fmt.Errorf("%w: %q", errInvalidValue, p.value))
|
||||||
|
}
|
||||||
|
if !p.hasValue && p.value != "" {
|
||||||
|
return errFunc(errors.New("inconsistent value"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key returns the Property key.
|
||||||
|
func (p Property) Key() string {
|
||||||
|
return p.key
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value returns the Property value. Additionally, a boolean value is returned
|
||||||
|
// indicating if the returned value is the empty if the Property has a value
|
||||||
|
// that is empty or if the value is not set.
|
||||||
|
func (p Property) Value() (string, bool) {
|
||||||
|
return p.value, p.hasValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// String encodes Property into a string compliant with the W3C Baggage
|
||||||
|
// specification.
|
||||||
|
func (p Property) String() string {
|
||||||
|
if p.hasValue {
|
||||||
|
return fmt.Sprintf("%s%s%v", p.key, keyValueDelimiter, p.value)
|
||||||
|
}
|
||||||
|
return p.key
|
||||||
|
}
|
||||||
|
|
||||||
|
type properties []Property
|
||||||
|
|
||||||
|
func fromInternalProperties(iProps []baggage.Property) properties {
|
||||||
|
if len(iProps) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
props := make(properties, len(iProps))
|
||||||
|
for i, p := range iProps {
|
||||||
|
props[i] = Property{
|
||||||
|
key: p.Key,
|
||||||
|
value: p.Value,
|
||||||
|
hasValue: p.HasValue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return props
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p properties) asInternal() []baggage.Property {
|
||||||
|
if len(p) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
iProps := make([]baggage.Property, len(p))
|
||||||
|
for i, prop := range p {
|
||||||
|
iProps[i] = baggage.Property{
|
||||||
|
Key: prop.key,
|
||||||
|
Value: prop.value,
|
||||||
|
HasValue: prop.hasValue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return iProps
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p properties) Copy() properties {
|
||||||
|
if len(p) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
props := make(properties, len(p))
|
||||||
|
copy(props, p)
|
||||||
|
return props
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate ensures each Property in p conforms to the W3C Baggage
|
||||||
|
// specification, returning an error otherwise.
|
||||||
|
func (p properties) validate() error {
|
||||||
|
for _, prop := range p {
|
||||||
|
if err := prop.validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String encodes properties into a string compliant with the W3C Baggage
|
||||||
|
// specification.
|
||||||
|
func (p properties) String() string {
|
||||||
|
props := make([]string, len(p))
|
||||||
|
for i, prop := range p {
|
||||||
|
props[i] = prop.String()
|
||||||
|
}
|
||||||
|
return strings.Join(props, propertyDelimiter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Member is a list-member of a baggage-string as defined by the W3C Baggage
|
||||||
|
// specification.
|
||||||
|
type Member struct {
|
||||||
|
key, value string
|
||||||
|
properties properties
|
||||||
|
|
||||||
|
// hasData indicates whether the created property contains data or not.
|
||||||
|
// Properties that do not contain data are invalid with no other check
|
||||||
|
// required.
|
||||||
|
hasData bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMember returns a new Member from the passed arguments. The key will be
|
||||||
|
// used directly while the value will be url decoded after validation. An error
|
||||||
|
// is returned if the created Member would be invalid according to the W3C
|
||||||
|
// Baggage specification.
|
||||||
|
func NewMember(key, value string, props ...Property) (Member, error) {
|
||||||
|
m := Member{
|
||||||
|
key: key,
|
||||||
|
value: value,
|
||||||
|
properties: properties(props).Copy(),
|
||||||
|
hasData: true,
|
||||||
|
}
|
||||||
|
if err := m.validate(); err != nil {
|
||||||
|
return newInvalidMember(), err
|
||||||
|
}
|
||||||
|
decodedValue, err := url.QueryUnescape(value)
|
||||||
|
if err != nil {
|
||||||
|
return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidValue, value)
|
||||||
|
}
|
||||||
|
m.value = decodedValue
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInvalidMember() Member {
|
||||||
|
return Member{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseMember attempts to decode a Member from the passed string. It returns
|
||||||
|
// an error if the input is invalid according to the W3C Baggage
|
||||||
|
// specification.
|
||||||
|
func parseMember(member string) (Member, error) {
|
||||||
|
if n := len(member); n > maxBytesPerMembers {
|
||||||
|
return newInvalidMember(), fmt.Errorf("%w: %d", errMemberBytes, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
key, value string
|
||||||
|
props properties
|
||||||
|
)
|
||||||
|
|
||||||
|
parts := strings.SplitN(member, propertyDelimiter, 2)
|
||||||
|
switch len(parts) {
|
||||||
|
case 2:
|
||||||
|
// Parse the member properties.
|
||||||
|
for _, pStr := range strings.Split(parts[1], propertyDelimiter) {
|
||||||
|
p, err := parseProperty(pStr)
|
||||||
|
if err != nil {
|
||||||
|
return newInvalidMember(), err
|
||||||
|
}
|
||||||
|
props = append(props, p)
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
case 1:
|
||||||
|
// Parse the member key/value pair.
|
||||||
|
|
||||||
|
// Take into account a value can contain equal signs (=).
|
||||||
|
kv := strings.SplitN(parts[0], keyValueDelimiter, 2)
|
||||||
|
if len(kv) != 2 {
|
||||||
|
return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidMember, member)
|
||||||
|
}
|
||||||
|
// "Leading and trailing whitespaces are allowed but MUST be trimmed
|
||||||
|
// when converting the header into a data structure."
|
||||||
|
key = strings.TrimSpace(kv[0])
|
||||||
|
var err error
|
||||||
|
value, err = url.QueryUnescape(strings.TrimSpace(kv[1]))
|
||||||
|
if err != nil {
|
||||||
|
return newInvalidMember(), fmt.Errorf("%w: %q", err, value)
|
||||||
|
}
|
||||||
|
if !keyRe.MatchString(key) {
|
||||||
|
return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidKey, key)
|
||||||
|
}
|
||||||
|
if !valueRe.MatchString(value) {
|
||||||
|
return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidValue, value)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// This should never happen unless a developer has changed the string
|
||||||
|
// splitting somehow. Panic instead of failing silently and allowing
|
||||||
|
// the bug to slip past the CI checks.
|
||||||
|
panic("failed to parse baggage member")
|
||||||
|
}
|
||||||
|
|
||||||
|
return Member{key: key, value: value, properties: props, hasData: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate ensures m conforms to the W3C Baggage specification.
|
||||||
|
// A key is just an ASCII string, but a value must be URL encoded UTF-8,
|
||||||
|
// returning an error otherwise.
|
||||||
|
func (m Member) validate() error {
|
||||||
|
if !m.hasData {
|
||||||
|
return fmt.Errorf("%w: %q", errInvalidMember, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !keyRe.MatchString(m.key) {
|
||||||
|
return fmt.Errorf("%w: %q", errInvalidKey, m.key)
|
||||||
|
}
|
||||||
|
if !valueRe.MatchString(m.value) {
|
||||||
|
return fmt.Errorf("%w: %q", errInvalidValue, m.value)
|
||||||
|
}
|
||||||
|
return m.properties.validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key returns the Member key.
|
||||||
|
func (m Member) Key() string { return m.key }
|
||||||
|
|
||||||
|
// Value returns the Member value.
|
||||||
|
func (m Member) Value() string { return m.value }
|
||||||
|
|
||||||
|
// Properties returns a copy of the Member properties.
|
||||||
|
func (m Member) Properties() []Property { return m.properties.Copy() }
|
||||||
|
|
||||||
|
// String encodes Member into a string compliant with the W3C Baggage
|
||||||
|
// specification.
|
||||||
|
func (m Member) String() string {
|
||||||
|
// A key is just an ASCII string, but a value is URL encoded UTF-8.
|
||||||
|
s := fmt.Sprintf("%s%s%s", m.key, keyValueDelimiter, url.QueryEscape(m.value))
|
||||||
|
if len(m.properties) > 0 {
|
||||||
|
s = fmt.Sprintf("%s%s%s", s, propertyDelimiter, m.properties.String())
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Baggage is a list of baggage members representing the baggage-string as
|
||||||
|
// defined by the W3C Baggage specification.
|
||||||
|
type Baggage struct { //nolint:golint
|
||||||
|
list baggage.List
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new valid Baggage. It returns an error if it results in a
|
||||||
|
// Baggage exceeding limits set in that specification.
|
||||||
|
//
|
||||||
|
// It expects all the provided members to have already been validated.
|
||||||
|
func New(members ...Member) (Baggage, error) {
|
||||||
|
if len(members) == 0 {
|
||||||
|
return Baggage{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b := make(baggage.List)
|
||||||
|
for _, m := range members {
|
||||||
|
if !m.hasData {
|
||||||
|
return Baggage{}, errInvalidMember
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenTelemetry resolves duplicates by last-one-wins.
|
||||||
|
b[m.key] = baggage.Item{
|
||||||
|
Value: m.value,
|
||||||
|
Properties: m.properties.asInternal(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check member numbers after deduplication.
|
||||||
|
if len(b) > maxMembers {
|
||||||
|
return Baggage{}, errMemberNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
bag := Baggage{b}
|
||||||
|
if n := len(bag.String()); n > maxBytesPerBaggageString {
|
||||||
|
return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse attempts to decode a baggage-string from the passed string. It
|
||||||
|
// returns an error if the input is invalid according to the W3C Baggage
|
||||||
|
// specification.
|
||||||
|
//
|
||||||
|
// If there are duplicate list-members contained in baggage, the last one
|
||||||
|
// defined (reading left-to-right) will be the only one kept. This diverges
|
||||||
|
// from the W3C Baggage specification which allows duplicate list-members, but
|
||||||
|
// conforms to the OpenTelemetry Baggage specification.
|
||||||
|
func Parse(bStr string) (Baggage, error) {
|
||||||
|
if bStr == "" {
|
||||||
|
return Baggage{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if n := len(bStr); n > maxBytesPerBaggageString {
|
||||||
|
return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
b := make(baggage.List)
|
||||||
|
for _, memberStr := range strings.Split(bStr, listDelimiter) {
|
||||||
|
m, err := parseMember(memberStr)
|
||||||
|
if err != nil {
|
||||||
|
return Baggage{}, err
|
||||||
|
}
|
||||||
|
// OpenTelemetry resolves duplicates by last-one-wins.
|
||||||
|
b[m.key] = baggage.Item{
|
||||||
|
Value: m.value,
|
||||||
|
Properties: m.properties.asInternal(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenTelemetry does not allow for duplicate list-members, but the W3C
|
||||||
|
// specification does. Now that we have deduplicated, ensure the baggage
|
||||||
|
// does not exceed list-member limits.
|
||||||
|
if len(b) > maxMembers {
|
||||||
|
return Baggage{}, errMemberNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
return Baggage{b}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Member returns the baggage list-member identified by key.
|
||||||
|
//
|
||||||
|
// If there is no list-member matching the passed key the returned Member will
|
||||||
|
// be a zero-value Member.
|
||||||
|
// The returned member is not validated, as we assume the validation happened
|
||||||
|
// when it was added to the Baggage.
|
||||||
|
func (b Baggage) Member(key string) Member {
|
||||||
|
v, ok := b.list[key]
|
||||||
|
if !ok {
|
||||||
|
// We do not need to worry about distinguishing between the situation
|
||||||
|
// where a zero-valued Member is included in the Baggage because a
|
||||||
|
// zero-valued Member is invalid according to the W3C Baggage
|
||||||
|
// specification (it has an empty key).
|
||||||
|
return newInvalidMember()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Member{
|
||||||
|
key: key,
|
||||||
|
value: v.Value,
|
||||||
|
properties: fromInternalProperties(v.Properties),
|
||||||
|
hasData: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Members returns all the baggage list-members.
|
||||||
|
// The order of the returned list-members does not have significance.
|
||||||
|
//
|
||||||
|
// The returned members are not validated, as we assume the validation happened
|
||||||
|
// when they were added to the Baggage.
|
||||||
|
func (b Baggage) Members() []Member {
|
||||||
|
if len(b.list) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
members := make([]Member, 0, len(b.list))
|
||||||
|
for k, v := range b.list {
|
||||||
|
members = append(members, Member{
|
||||||
|
key: k,
|
||||||
|
value: v.Value,
|
||||||
|
properties: fromInternalProperties(v.Properties),
|
||||||
|
hasData: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return members
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMember returns a copy the Baggage with the member included. If the
|
||||||
|
// baggage contains a Member with the same key the existing Member is
|
||||||
|
// replaced.
|
||||||
|
//
|
||||||
|
// If member is invalid according to the W3C Baggage specification, an error
|
||||||
|
// is returned with the original Baggage.
|
||||||
|
func (b Baggage) SetMember(member Member) (Baggage, error) {
|
||||||
|
if !member.hasData {
|
||||||
|
return b, errInvalidMember
|
||||||
|
}
|
||||||
|
|
||||||
|
n := len(b.list)
|
||||||
|
if _, ok := b.list[member.key]; !ok {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
list := make(baggage.List, n)
|
||||||
|
|
||||||
|
for k, v := range b.list {
|
||||||
|
// Do not copy if we are just going to overwrite.
|
||||||
|
if k == member.key {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
list[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
list[member.key] = baggage.Item{
|
||||||
|
Value: member.value,
|
||||||
|
Properties: member.properties.asInternal(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return Baggage{list: list}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMember returns a copy of the Baggage with the list-member identified
|
||||||
|
// by key removed.
|
||||||
|
func (b Baggage) DeleteMember(key string) Baggage {
|
||||||
|
n := len(b.list)
|
||||||
|
if _, ok := b.list[key]; ok {
|
||||||
|
n--
|
||||||
|
}
|
||||||
|
list := make(baggage.List, n)
|
||||||
|
|
||||||
|
for k, v := range b.list {
|
||||||
|
if k == key {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
list[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return Baggage{list: list}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of list-members in the Baggage.
|
||||||
|
func (b Baggage) Len() int {
|
||||||
|
return len(b.list)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String encodes Baggage into a string compliant with the W3C Baggage
|
||||||
|
// specification. The returned string will be invalid if the Baggage contains
|
||||||
|
// any invalid list-members.
|
||||||
|
func (b Baggage) String() string {
|
||||||
|
members := make([]string, 0, len(b.list))
|
||||||
|
for k, v := range b.list {
|
||||||
|
members = append(members, Member{
|
||||||
|
key: k,
|
||||||
|
value: v.Value,
|
||||||
|
properties: fromInternalProperties(v.Properties),
|
||||||
|
}.String())
|
||||||
|
}
|
||||||
|
return strings.Join(members, listDelimiter)
|
||||||
|
}
|
46
vendor/github.com/getsentry/sentry-go/internal/otel/baggage/internal/baggage/baggage.go
generated
vendored
Normal file
46
vendor/github.com/getsentry/sentry-go/internal/otel/baggage/internal/baggage/baggage.go
generated
vendored
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
// This file was vendored in unmodified from
|
||||||
|
// https://github.com/open-telemetry/opentelemetry-go/blob/c21b6b6bb31a2f74edd06e262f1690f3f6ea3d5c/internal/baggage/baggage.go
|
||||||
|
//
|
||||||
|
// Copyright The OpenTelemetry Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package baggage provides base types and functionality to store and retrieve
|
||||||
|
baggage in Go context. This package exists because the OpenTracing bridge to
|
||||||
|
OpenTelemetry needs to synchronize state whenever baggage for a context is
|
||||||
|
modified and that context contains an OpenTracing span. If it were not for
|
||||||
|
this need this package would not need to exist and the
|
||||||
|
`go.opentelemetry.io/otel/baggage` package would be the singular place where
|
||||||
|
W3C baggage is handled.
|
||||||
|
*/
|
||||||
|
package baggage
|
||||||
|
|
||||||
|
// List is the collection of baggage members. The W3C allows for duplicates,
|
||||||
|
// but OpenTelemetry does not, therefore, this is represented as a map.
|
||||||
|
type List map[string]Item
|
||||||
|
|
||||||
|
// Item is the value and metadata properties part of a list-member.
|
||||||
|
type Item struct {
|
||||||
|
Value string
|
||||||
|
Properties []Property
|
||||||
|
}
|
||||||
|
|
||||||
|
// Property is a metadata entry for a list-member.
|
||||||
|
type Property struct {
|
||||||
|
Key, Value string
|
||||||
|
|
||||||
|
// HasValue indicates if a zero-value value means the property does not
|
||||||
|
// have a value or if it was the zero-value.
|
||||||
|
HasValue bool
|
||||||
|
}
|
46
vendor/github.com/getsentry/sentry-go/internal/ratelimit/category.go
generated
vendored
Normal file
46
vendor/github.com/getsentry/sentry-go/internal/ratelimit/category.go
generated
vendored
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package ratelimit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/text/cases"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reference:
|
||||||
|
// https://github.com/getsentry/relay/blob/0424a2e017d193a93918053c90cdae9472d164bf/relay-common/src/constants.rs#L116-L127
|
||||||
|
|
||||||
|
// Category classifies supported payload types that can be ingested by Sentry
|
||||||
|
// and, therefore, rate limited.
|
||||||
|
type Category string
|
||||||
|
|
||||||
|
// Known rate limit categories. As a special case, the CategoryAll applies to
|
||||||
|
// all known payload types.
|
||||||
|
const (
|
||||||
|
CategoryAll Category = ""
|
||||||
|
CategoryError Category = "error"
|
||||||
|
CategoryTransaction Category = "transaction"
|
||||||
|
)
|
||||||
|
|
||||||
|
// knownCategories is the set of currently known categories. Other categories
|
||||||
|
// are ignored for the purpose of rate-limiting.
|
||||||
|
var knownCategories = map[Category]struct{}{
|
||||||
|
CategoryAll: {},
|
||||||
|
CategoryError: {},
|
||||||
|
CategoryTransaction: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the category formatted for debugging.
|
||||||
|
func (c Category) String() string {
|
||||||
|
switch c {
|
||||||
|
case "":
|
||||||
|
return "CategoryAll"
|
||||||
|
default:
|
||||||
|
caser := cases.Title(language.English)
|
||||||
|
rv := "Category"
|
||||||
|
for _, w := range strings.Fields(string(c)) {
|
||||||
|
rv += caser.String(w)
|
||||||
|
}
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
}
|
22
vendor/github.com/getsentry/sentry-go/internal/ratelimit/deadline.go
generated
vendored
Normal file
22
vendor/github.com/getsentry/sentry-go/internal/ratelimit/deadline.go
generated
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package ratelimit
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// A Deadline is a time instant when a rate limit expires.
|
||||||
|
type Deadline time.Time
|
||||||
|
|
||||||
|
// After reports whether the deadline d is after other.
|
||||||
|
func (d Deadline) After(other Deadline) bool {
|
||||||
|
return time.Time(d).After(time.Time(other))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal reports whether d and e represent the same deadline.
|
||||||
|
func (d Deadline) Equal(e Deadline) bool {
|
||||||
|
return time.Time(d).Equal(time.Time(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the deadline formatted for debugging.
|
||||||
|
func (d Deadline) String() string {
|
||||||
|
// Like time.Time.String, but without the monotonic clock reading.
|
||||||
|
return time.Time(d).Round(0).String()
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
// Package ratelimit provides tools to work with rate limits imposed by Sentry's
|
||||||
|
// data ingestion pipeline.
|
||||||
|
package ratelimit
|
|
@ -0,0 +1,64 @@
|
||||||
|
package ratelimit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Map maps categories to rate limit deadlines.
|
||||||
|
//
|
||||||
|
// A rate limit is in effect for a given category if either the category's
|
||||||
|
// deadline or the deadline for the special CategoryAll has not yet expired.
|
||||||
|
//
|
||||||
|
// Use IsRateLimited to check whether a category is rate-limited.
|
||||||
|
type Map map[Category]Deadline
|
||||||
|
|
||||||
|
// IsRateLimited returns true if the category is currently rate limited.
|
||||||
|
func (m Map) IsRateLimited(c Category) bool {
|
||||||
|
return m.isRateLimited(c, time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Map) isRateLimited(c Category, now time.Time) bool {
|
||||||
|
return m.Deadline(c).After(Deadline(now))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deadline returns the deadline when the rate limit for the given category or
|
||||||
|
// the special CategoryAll expire, whichever is furthest into the future.
|
||||||
|
func (m Map) Deadline(c Category) Deadline {
|
||||||
|
categoryDeadline := m[c]
|
||||||
|
allDeadline := m[CategoryAll]
|
||||||
|
if categoryDeadline.After(allDeadline) {
|
||||||
|
return categoryDeadline
|
||||||
|
}
|
||||||
|
return allDeadline
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge merges the other map into m.
|
||||||
|
//
|
||||||
|
// If a category appears in both maps, the deadline that is furthest into the
|
||||||
|
// future is preserved.
|
||||||
|
func (m Map) Merge(other Map) {
|
||||||
|
for c, d := range other {
|
||||||
|
if d.After(m[c]) {
|
||||||
|
m[c] = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromResponse returns a rate limit map from an HTTP response.
|
||||||
|
func FromResponse(r *http.Response) Map {
|
||||||
|
return fromResponse(r, time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromResponse(r *http.Response, now time.Time) Map {
|
||||||
|
s := r.Header.Get("X-Sentry-Rate-Limits")
|
||||||
|
if s != "" {
|
||||||
|
return parseXSentryRateLimits(s, now)
|
||||||
|
}
|
||||||
|
if r.StatusCode == http.StatusTooManyRequests {
|
||||||
|
s := r.Header.Get("Retry-After")
|
||||||
|
deadline, _ := parseRetryAfter(s, now)
|
||||||
|
return Map{CategoryAll: deadline}
|
||||||
|
}
|
||||||
|
return Map{}
|
||||||
|
}
|
76
vendor/github.com/getsentry/sentry-go/internal/ratelimit/rate_limits.go
generated
vendored
Normal file
76
vendor/github.com/getsentry/sentry-go/internal/ratelimit/rate_limits.go
generated
vendored
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package ratelimit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errInvalidXSRLRetryAfter = errors.New("invalid retry-after value")
|
||||||
|
|
||||||
|
// parseXSentryRateLimits returns a RateLimits map by parsing an input string in
|
||||||
|
// the format of the X-Sentry-Rate-Limits header.
|
||||||
|
//
|
||||||
|
// Example
|
||||||
|
//
|
||||||
|
// X-Sentry-Rate-Limits: 60:transaction, 2700:default;error;security
|
||||||
|
//
|
||||||
|
// This will rate limit transactions for the next 60 seconds and errors for the
|
||||||
|
// next 2700 seconds.
|
||||||
|
//
|
||||||
|
// Limits for unknown categories are ignored.
|
||||||
|
func parseXSentryRateLimits(s string, now time.Time) Map {
|
||||||
|
// https://github.com/getsentry/relay/blob/0424a2e017d193a93918053c90cdae9472d164bf/relay-server/src/utils/rate_limits.rs#L44-L82
|
||||||
|
m := make(Map, len(knownCategories))
|
||||||
|
for _, limit := range strings.Split(s, ",") {
|
||||||
|
limit = strings.TrimSpace(limit)
|
||||||
|
if limit == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
components := strings.Split(limit, ":")
|
||||||
|
if len(components) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
retryAfter, err := parseXSRLRetryAfter(strings.TrimSpace(components[0]), now)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
categories := ""
|
||||||
|
if len(components) > 1 {
|
||||||
|
categories = components[1]
|
||||||
|
}
|
||||||
|
for _, category := range strings.Split(categories, ";") {
|
||||||
|
c := Category(strings.ToLower(strings.TrimSpace(category)))
|
||||||
|
if _, ok := knownCategories[c]; !ok {
|
||||||
|
// skip unknown categories, keep m small
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// always keep the deadline furthest into the future
|
||||||
|
if retryAfter.After(m[c]) {
|
||||||
|
m[c] = retryAfter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseXSRLRetryAfter parses a string into a retry-after rate limit deadline.
|
||||||
|
//
|
||||||
|
// Valid input is a number, possibly signed and possibly floating-point,
|
||||||
|
// indicating the number of seconds to wait before sending another request.
|
||||||
|
// Negative values are treated as zero. Fractional values are rounded to the
|
||||||
|
// next integer.
|
||||||
|
func parseXSRLRetryAfter(s string, now time.Time) (Deadline, error) {
|
||||||
|
// https://github.com/getsentry/relay/blob/0424a2e017d193a93918053c90cdae9472d164bf/relay-quotas/src/rate_limit.rs#L88-L96
|
||||||
|
f, err := strconv.ParseFloat(s, 64)
|
||||||
|
if err != nil {
|
||||||
|
return Deadline{}, errInvalidXSRLRetryAfter
|
||||||
|
}
|
||||||
|
d := time.Duration(math.Ceil(math.Max(f, 0.0))) * time.Second
|
||||||
|
if d < 0 {
|
||||||
|
d = 0
|
||||||
|
}
|
||||||
|
return Deadline(now.Add(d)), nil
|
||||||
|
}
|
40
vendor/github.com/getsentry/sentry-go/internal/ratelimit/retry_after.go
generated
vendored
Normal file
40
vendor/github.com/getsentry/sentry-go/internal/ratelimit/retry_after.go
generated
vendored
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package ratelimit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultRetryAfter = 1 * time.Minute
|
||||||
|
|
||||||
|
var errInvalidRetryAfter = errors.New("invalid input")
|
||||||
|
|
||||||
|
// parseRetryAfter parses a string s as in the standard Retry-After HTTP header
|
||||||
|
// and returns a deadline until when requests are rate limited and therefore new
|
||||||
|
// requests should not be sent. The input may be either a date or a non-negative
|
||||||
|
// integer number of seconds.
|
||||||
|
//
|
||||||
|
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
||||||
|
//
|
||||||
|
// parseRetryAfter always returns a usable deadline, even in case of an error.
|
||||||
|
//
|
||||||
|
// This is the original rate limiting mechanism used by Sentry, superseeded by
|
||||||
|
// the X-Sentry-Rate-Limits response header.
|
||||||
|
func parseRetryAfter(s string, now time.Time) (Deadline, error) {
|
||||||
|
if s == "" {
|
||||||
|
goto invalid
|
||||||
|
}
|
||||||
|
if n, err := strconv.Atoi(s); err == nil {
|
||||||
|
if n < 0 {
|
||||||
|
goto invalid
|
||||||
|
}
|
||||||
|
d := time.Duration(n) * time.Second
|
||||||
|
return Deadline(now.Add(d)), nil
|
||||||
|
}
|
||||||
|
if date, err := time.Parse(time.RFC1123, s); err == nil {
|
||||||
|
return Deadline(date), nil
|
||||||
|
}
|
||||||
|
invalid:
|
||||||
|
return Deadline(now.Add(defaultRetryAfter)), errInvalidRetryAfter
|
||||||
|
}
|
|
@ -0,0 +1,428 @@
|
||||||
|
package sentry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Scope holds contextual data for the current scope.
|
||||||
|
//
|
||||||
|
// The scope is an object that can cloned efficiently and stores data that is
|
||||||
|
// locally relevant to an event. For instance the scope will hold recorded
|
||||||
|
// breadcrumbs and similar information.
|
||||||
|
//
|
||||||
|
// The scope can be interacted with in two ways. First, the scope is routinely
|
||||||
|
// updated with information by functions such as AddBreadcrumb which will modify
|
||||||
|
// the current scope. Second, the current scope can be configured through the
|
||||||
|
// ConfigureScope function or Hub method of the same name.
|
||||||
|
//
|
||||||
|
// The scope is meant to be modified but not inspected directly. When preparing
|
||||||
|
// an event for reporting, the current client adds information from the current
|
||||||
|
// scope into the event.
|
||||||
|
type Scope struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
breadcrumbs []*Breadcrumb
|
||||||
|
user User
|
||||||
|
tags map[string]string
|
||||||
|
contexts map[string]Context
|
||||||
|
extra map[string]interface{}
|
||||||
|
fingerprint []string
|
||||||
|
level Level
|
||||||
|
transaction string
|
||||||
|
request *http.Request
|
||||||
|
// requestBody holds a reference to the original request.Body.
|
||||||
|
requestBody interface {
|
||||||
|
// Bytes returns bytes from the original body, lazily buffered as the
|
||||||
|
// original body is read.
|
||||||
|
Bytes() []byte
|
||||||
|
// Overflow returns true if the body is larger than the maximum buffer
|
||||||
|
// size.
|
||||||
|
Overflow() bool
|
||||||
|
}
|
||||||
|
eventProcessors []EventProcessor
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewScope creates a new Scope.
|
||||||
|
func NewScope() *Scope {
|
||||||
|
scope := Scope{
|
||||||
|
breadcrumbs: make([]*Breadcrumb, 0),
|
||||||
|
tags: make(map[string]string),
|
||||||
|
contexts: make(map[string]Context),
|
||||||
|
extra: make(map[string]interface{}),
|
||||||
|
fingerprint: make([]string, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &scope
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddBreadcrumb adds new breadcrumb to the current scope
|
||||||
|
// and optionally throws the old one if limit is reached.
|
||||||
|
func (scope *Scope) AddBreadcrumb(breadcrumb *Breadcrumb, limit int) {
|
||||||
|
if breadcrumb.Timestamp.IsZero() {
|
||||||
|
breadcrumb.Timestamp = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.mu.Lock()
|
||||||
|
defer scope.mu.Unlock()
|
||||||
|
|
||||||
|
scope.breadcrumbs = append(scope.breadcrumbs, breadcrumb)
|
||||||
|
if len(scope.breadcrumbs) > limit {
|
||||||
|
scope.breadcrumbs = scope.breadcrumbs[1 : limit+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearBreadcrumbs clears all breadcrumbs from the current scope.
|
||||||
|
func (scope *Scope) ClearBreadcrumbs() {
|
||||||
|
scope.mu.Lock()
|
||||||
|
defer scope.mu.Unlock()
|
||||||
|
|
||||||
|
scope.breadcrumbs = []*Breadcrumb{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUser sets the user for the current scope.
|
||||||
|
func (scope *Scope) SetUser(user User) {
|
||||||
|
scope.mu.Lock()
|
||||||
|
defer scope.mu.Unlock()
|
||||||
|
|
||||||
|
scope.user = user
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRequest sets the request for the current scope.
|
||||||
|
func (scope *Scope) SetRequest(r *http.Request) {
|
||||||
|
scope.mu.Lock()
|
||||||
|
defer scope.mu.Unlock()
|
||||||
|
|
||||||
|
scope.request = r
|
||||||
|
|
||||||
|
if r == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't buffer request body if we know it is oversized.
|
||||||
|
if r.ContentLength > maxRequestBodyBytes {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Don't buffer if there is no body.
|
||||||
|
if r.Body == nil || r.Body == http.NoBody {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf := &limitedBuffer{Capacity: maxRequestBodyBytes}
|
||||||
|
r.Body = readCloser{
|
||||||
|
Reader: io.TeeReader(r.Body, buf),
|
||||||
|
Closer: r.Body,
|
||||||
|
}
|
||||||
|
scope.requestBody = buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRequestBody sets the request body for the current scope.
|
||||||
|
//
|
||||||
|
// This method should only be called when the body bytes are already available
|
||||||
|
// in memory. Typically, the request body is buffered lazily from the
|
||||||
|
// Request.Body from SetRequest.
|
||||||
|
func (scope *Scope) SetRequestBody(b []byte) {
|
||||||
|
scope.mu.Lock()
|
||||||
|
defer scope.mu.Unlock()
|
||||||
|
|
||||||
|
capacity := maxRequestBodyBytes
|
||||||
|
overflow := false
|
||||||
|
if len(b) > capacity {
|
||||||
|
overflow = true
|
||||||
|
b = b[:capacity]
|
||||||
|
}
|
||||||
|
scope.requestBody = &limitedBuffer{
|
||||||
|
Capacity: capacity,
|
||||||
|
Buffer: *bytes.NewBuffer(b),
|
||||||
|
overflow: overflow,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// maxRequestBodyBytes is the default maximum request body size to send to
|
||||||
|
// Sentry.
|
||||||
|
const maxRequestBodyBytes = 10 * 1024
|
||||||
|
|
||||||
|
// A limitedBuffer is like a bytes.Buffer, but limited to store at most Capacity
|
||||||
|
// bytes. Any writes past the capacity are silently discarded, similar to
|
||||||
|
// io.Discard.
|
||||||
|
type limitedBuffer struct {
|
||||||
|
Capacity int
|
||||||
|
|
||||||
|
bytes.Buffer
|
||||||
|
overflow bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write implements io.Writer.
|
||||||
|
func (b *limitedBuffer) Write(p []byte) (n int, err error) {
|
||||||
|
// Silently ignore writes after overflow.
|
||||||
|
if b.overflow {
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
left := b.Capacity - b.Len()
|
||||||
|
if left < 0 {
|
||||||
|
left = 0
|
||||||
|
}
|
||||||
|
if len(p) > left {
|
||||||
|
b.overflow = true
|
||||||
|
p = p[:left]
|
||||||
|
}
|
||||||
|
return b.Buffer.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overflow returns true if the limitedBuffer discarded bytes written to it.
|
||||||
|
func (b *limitedBuffer) Overflow() bool {
|
||||||
|
return b.overflow
|
||||||
|
}
|
||||||
|
|
||||||
|
// readCloser combines an io.Reader and an io.Closer to implement io.ReadCloser.
|
||||||
|
type readCloser struct {
|
||||||
|
io.Reader
|
||||||
|
io.Closer
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTag adds a tag to the current scope.
|
||||||
|
func (scope *Scope) SetTag(key, value string) {
|
||||||
|
scope.mu.Lock()
|
||||||
|
defer scope.mu.Unlock()
|
||||||
|
|
||||||
|
scope.tags[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTags assigns multiple tags to the current scope.
|
||||||
|
func (scope *Scope) SetTags(tags map[string]string) {
|
||||||
|
scope.mu.Lock()
|
||||||
|
defer scope.mu.Unlock()
|
||||||
|
|
||||||
|
for k, v := range tags {
|
||||||
|
scope.tags[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveTag removes a tag from the current scope.
|
||||||
|
func (scope *Scope) RemoveTag(key string) {
|
||||||
|
scope.mu.Lock()
|
||||||
|
defer scope.mu.Unlock()
|
||||||
|
|
||||||
|
delete(scope.tags, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContext adds a context to the current scope.
|
||||||
|
func (scope *Scope) SetContext(key string, value Context) {
|
||||||
|
scope.mu.Lock()
|
||||||
|
defer scope.mu.Unlock()
|
||||||
|
|
||||||
|
scope.contexts[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContexts assigns multiple contexts to the current scope.
|
||||||
|
func (scope *Scope) SetContexts(contexts map[string]Context) {
|
||||||
|
scope.mu.Lock()
|
||||||
|
defer scope.mu.Unlock()
|
||||||
|
|
||||||
|
for k, v := range contexts {
|
||||||
|
scope.contexts[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveContext removes a context from the current scope.
|
||||||
|
func (scope *Scope) RemoveContext(key string) {
|
||||||
|
scope.mu.Lock()
|
||||||
|
defer scope.mu.Unlock()
|
||||||
|
|
||||||
|
delete(scope.contexts, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetExtra adds an extra to the current scope.
|
||||||
|
func (scope *Scope) SetExtra(key string, value interface{}) {
|
||||||
|
scope.mu.Lock()
|
||||||
|
defer scope.mu.Unlock()
|
||||||
|
|
||||||
|
scope.extra[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetExtras assigns multiple extras to the current scope.
|
||||||
|
func (scope *Scope) SetExtras(extra map[string]interface{}) {
|
||||||
|
scope.mu.Lock()
|
||||||
|
defer scope.mu.Unlock()
|
||||||
|
|
||||||
|
for k, v := range extra {
|
||||||
|
scope.extra[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveExtra removes a extra from the current scope.
|
||||||
|
func (scope *Scope) RemoveExtra(key string) {
|
||||||
|
scope.mu.Lock()
|
||||||
|
defer scope.mu.Unlock()
|
||||||
|
|
||||||
|
delete(scope.extra, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFingerprint sets new fingerprint for the current scope.
|
||||||
|
func (scope *Scope) SetFingerprint(fingerprint []string) {
|
||||||
|
scope.mu.Lock()
|
||||||
|
defer scope.mu.Unlock()
|
||||||
|
|
||||||
|
scope.fingerprint = fingerprint
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLevel sets new level for the current scope.
|
||||||
|
func (scope *Scope) SetLevel(level Level) {
|
||||||
|
scope.mu.Lock()
|
||||||
|
defer scope.mu.Unlock()
|
||||||
|
|
||||||
|
scope.level = level
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTransaction sets the transaction name for the current transaction.
|
||||||
|
func (scope *Scope) SetTransaction(name string) {
|
||||||
|
scope.mu.Lock()
|
||||||
|
defer scope.mu.Unlock()
|
||||||
|
|
||||||
|
scope.transaction = name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction returns the transaction name for the current transaction.
|
||||||
|
func (scope *Scope) Transaction() (name string) {
|
||||||
|
scope.mu.RLock()
|
||||||
|
defer scope.mu.RUnlock()
|
||||||
|
|
||||||
|
return scope.transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone returns a copy of the current scope with all data copied over.
|
||||||
|
func (scope *Scope) Clone() *Scope {
|
||||||
|
scope.mu.RLock()
|
||||||
|
defer scope.mu.RUnlock()
|
||||||
|
|
||||||
|
clone := NewScope()
|
||||||
|
clone.user = scope.user
|
||||||
|
clone.breadcrumbs = make([]*Breadcrumb, len(scope.breadcrumbs))
|
||||||
|
copy(clone.breadcrumbs, scope.breadcrumbs)
|
||||||
|
for key, value := range scope.tags {
|
||||||
|
clone.tags[key] = value
|
||||||
|
}
|
||||||
|
for key, value := range scope.contexts {
|
||||||
|
clone.contexts[key] = value
|
||||||
|
}
|
||||||
|
for key, value := range scope.extra {
|
||||||
|
clone.extra[key] = value
|
||||||
|
}
|
||||||
|
clone.fingerprint = make([]string, len(scope.fingerprint))
|
||||||
|
copy(clone.fingerprint, scope.fingerprint)
|
||||||
|
clone.level = scope.level
|
||||||
|
clone.transaction = scope.transaction
|
||||||
|
clone.request = scope.request
|
||||||
|
clone.requestBody = scope.requestBody
|
||||||
|
clone.eventProcessors = scope.eventProcessors
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear removes the data from the current scope. Not safe for concurrent use.
|
||||||
|
func (scope *Scope) Clear() {
|
||||||
|
*scope = *NewScope()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddEventProcessor adds an event processor to the current scope.
|
||||||
|
func (scope *Scope) AddEventProcessor(processor EventProcessor) {
|
||||||
|
scope.mu.Lock()
|
||||||
|
defer scope.mu.Unlock()
|
||||||
|
|
||||||
|
scope.eventProcessors = append(scope.eventProcessors, processor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyToEvent takes the data from the current scope and attaches it to the event.
|
||||||
|
func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint) *Event {
|
||||||
|
scope.mu.RLock()
|
||||||
|
defer scope.mu.RUnlock()
|
||||||
|
|
||||||
|
if len(scope.breadcrumbs) > 0 {
|
||||||
|
event.Breadcrumbs = append(event.Breadcrumbs, scope.breadcrumbs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(scope.tags) > 0 {
|
||||||
|
if event.Tags == nil {
|
||||||
|
event.Tags = make(map[string]string, len(scope.tags))
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range scope.tags {
|
||||||
|
event.Tags[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(scope.contexts) > 0 {
|
||||||
|
if event.Contexts == nil {
|
||||||
|
event.Contexts = make(map[string]Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range scope.contexts {
|
||||||
|
if key == "trace" && event.Type == transactionType {
|
||||||
|
// Do not override trace context of
|
||||||
|
// transactions, otherwise it breaks the
|
||||||
|
// transaction event representation.
|
||||||
|
// For error events, the trace context is used
|
||||||
|
// to link errors and traces/spans in Sentry.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we are not overwriting event fields
|
||||||
|
if _, ok := event.Contexts[key]; !ok {
|
||||||
|
event.Contexts[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(scope.extra) > 0 {
|
||||||
|
if event.Extra == nil {
|
||||||
|
event.Extra = make(map[string]interface{}, len(scope.extra))
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range scope.extra {
|
||||||
|
event.Extra[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.User.IsEmpty() {
|
||||||
|
event.User = scope.user
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(event.Fingerprint) == 0 {
|
||||||
|
event.Fingerprint = append(event.Fingerprint, scope.fingerprint...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if scope.level != "" {
|
||||||
|
event.Level = scope.level
|
||||||
|
}
|
||||||
|
|
||||||
|
if scope.transaction != "" {
|
||||||
|
event.Transaction = scope.transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Request == nil && scope.request != nil {
|
||||||
|
event.Request = NewRequest(scope.request)
|
||||||
|
// NOTE: The SDK does not attempt to send partial request body data.
|
||||||
|
//
|
||||||
|
// The reason being that Sentry's ingest pipeline and UI are optimized
|
||||||
|
// to show structured data. Additionally, tooling around PII scrubbing
|
||||||
|
// relies on structured data; truncated request bodies would create
|
||||||
|
// invalid payloads that are more prone to leaking PII data.
|
||||||
|
//
|
||||||
|
// Users can still send more data along their events if they want to,
|
||||||
|
// for example using Event.Extra.
|
||||||
|
if scope.requestBody != nil && !scope.requestBody.Overflow() {
|
||||||
|
event.Request.Data = string(scope.requestBody.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, processor := range scope.eventProcessors {
|
||||||
|
id := event.EventID
|
||||||
|
event = processor(event, hint)
|
||||||
|
if event == nil {
|
||||||
|
Logger.Printf("Event dropped by one of the Scope EventProcessors: %s\n", id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return event
|
||||||
|
}
|
|
@ -0,0 +1,136 @@
|
||||||
|
package sentry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Deprecated: Use SDKVersion instead.
|
||||||
|
const Version = SDKVersion
|
||||||
|
|
||||||
|
// Version is the version of the SDK.
|
||||||
|
const SDKVersion = "0.16.0"
|
||||||
|
|
||||||
|
// The identifier of the SDK.
|
||||||
|
const SDKIdentifier = "sentry.go"
|
||||||
|
|
||||||
|
// apiVersion is the minimum version of the Sentry API compatible with the
|
||||||
|
// sentry-go SDK.
|
||||||
|
const apiVersion = "7"
|
||||||
|
|
||||||
|
// userAgent is the User-Agent of outgoing HTTP requests.
|
||||||
|
const userAgent = "sentry-go/" + SDKVersion
|
||||||
|
|
||||||
|
// Init initializes the SDK with options. The returned error is non-nil if
|
||||||
|
// options is invalid, for instance if a malformed DSN is provided.
|
||||||
|
func Init(options ClientOptions) error {
|
||||||
|
hub := CurrentHub()
|
||||||
|
client, err := NewClient(options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hub.BindClient(client)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddBreadcrumb records a new breadcrumb.
|
||||||
|
//
|
||||||
|
// The total number of breadcrumbs that can be recorded are limited by the
|
||||||
|
// configuration on the client.
|
||||||
|
func AddBreadcrumb(breadcrumb *Breadcrumb) {
|
||||||
|
hub := CurrentHub()
|
||||||
|
hub.AddBreadcrumb(breadcrumb, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureMessage captures an arbitrary message.
|
||||||
|
func CaptureMessage(message string) *EventID {
|
||||||
|
hub := CurrentHub()
|
||||||
|
return hub.CaptureMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureException captures an error.
|
||||||
|
func CaptureException(exception error) *EventID {
|
||||||
|
hub := CurrentHub()
|
||||||
|
return hub.CaptureException(exception)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureEvent captures an event on the currently active client if any.
|
||||||
|
//
|
||||||
|
// The event must already be assembled. Typically code would instead use
|
||||||
|
// the utility methods like CaptureException. The return value is the
|
||||||
|
// event ID. In case Sentry is disabled or event was dropped, the return value will be nil.
|
||||||
|
func CaptureEvent(event *Event) *EventID {
|
||||||
|
hub := CurrentHub()
|
||||||
|
return hub.CaptureEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recover captures a panic.
|
||||||
|
func Recover() *EventID {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
hub := CurrentHub()
|
||||||
|
return hub.Recover(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecoverWithContext captures a panic and passes relevant context object.
|
||||||
|
func RecoverWithContext(ctx context.Context) *EventID {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
var hub *Hub
|
||||||
|
|
||||||
|
if HasHubOnContext(ctx) {
|
||||||
|
hub = GetHubFromContext(ctx)
|
||||||
|
} else {
|
||||||
|
hub = CurrentHub()
|
||||||
|
}
|
||||||
|
|
||||||
|
return hub.RecoverWithContext(ctx, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithScope is a shorthand for CurrentHub().WithScope.
|
||||||
|
func WithScope(f func(scope *Scope)) {
|
||||||
|
hub := CurrentHub()
|
||||||
|
hub.WithScope(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigureScope is a shorthand for CurrentHub().ConfigureScope.
|
||||||
|
func ConfigureScope(f func(scope *Scope)) {
|
||||||
|
hub := CurrentHub()
|
||||||
|
hub.ConfigureScope(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushScope is a shorthand for CurrentHub().PushScope.
|
||||||
|
func PushScope() {
|
||||||
|
hub := CurrentHub()
|
||||||
|
hub.PushScope()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PopScope is a shorthand for CurrentHub().PopScope.
|
||||||
|
func PopScope() {
|
||||||
|
hub := CurrentHub()
|
||||||
|
hub.PopScope()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush waits until the underlying Transport sends any buffered events to the
|
||||||
|
// Sentry server, blocking for at most the given timeout. It returns false if
|
||||||
|
// the timeout was reached. In that case, some events may not have been sent.
|
||||||
|
//
|
||||||
|
// Flush should be called before terminating the program to avoid
|
||||||
|
// unintentionally dropping events.
|
||||||
|
//
|
||||||
|
// Do not call Flush indiscriminately after every call to CaptureEvent,
|
||||||
|
// CaptureException or CaptureMessage. Instead, to have the SDK send events over
|
||||||
|
// the network synchronously, configure it to use the HTTPSyncTransport in the
|
||||||
|
// call to Init.
|
||||||
|
func Flush(timeout time.Duration) bool {
|
||||||
|
hub := CurrentHub()
|
||||||
|
return hub.Flush(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastEventID returns an ID of last captured event.
|
||||||
|
func LastEventID() EventID {
|
||||||
|
hub := CurrentHub()
|
||||||
|
return hub.LastEventID()
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package sentry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sourceReader struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
cache map[string][][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSourceReader() sourceReader {
|
||||||
|
return sourceReader{
|
||||||
|
cache: make(map[string][][]byte),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sr *sourceReader) readContextLines(filename string, line, context int) ([][]byte, int) {
|
||||||
|
sr.mu.Lock()
|
||||||
|
defer sr.mu.Unlock()
|
||||||
|
|
||||||
|
lines, ok := sr.cache[filename]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
data, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
sr.cache[filename] = nil
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
lines = bytes.Split(data, []byte{'\n'})
|
||||||
|
sr.cache[filename] = lines
|
||||||
|
}
|
||||||
|
|
||||||
|
return sr.calculateContextLines(lines, line, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sr *sourceReader) calculateContextLines(lines [][]byte, line, context int) ([][]byte, int) {
|
||||||
|
// Stacktrace lines are 1-indexed, slices are 0-indexed
|
||||||
|
line--
|
||||||
|
|
||||||
|
// contextLine points to a line that caused an issue itself, in relation to
|
||||||
|
// returned slice.
|
||||||
|
contextLine := context
|
||||||
|
|
||||||
|
if lines == nil || line >= len(lines) || line < 0 {
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if context < 0 {
|
||||||
|
context = 0
|
||||||
|
contextLine = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
start := line - context
|
||||||
|
|
||||||
|
if start < 0 {
|
||||||
|
contextLine += start
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
end := line + context + 1
|
||||||
|
|
||||||
|
if end > len(lines) {
|
||||||
|
end = len(lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines[start:end], contextLine
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package sentry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A spanRecorder stores a span tree that makes up a transaction. Safe for
|
||||||
|
// concurrent use. It is okay to add child spans from multiple goroutines.
|
||||||
|
type spanRecorder struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
spans []*Span
|
||||||
|
overflowOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
// record stores a span. The first stored span is assumed to be the root of a
|
||||||
|
// span tree.
|
||||||
|
func (r *spanRecorder) record(s *Span) {
|
||||||
|
maxSpans := defaultMaxSpans
|
||||||
|
if client := CurrentHub().Client(); client != nil {
|
||||||
|
maxSpans = client.Options().MaxSpans
|
||||||
|
}
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
if len(r.spans) >= maxSpans {
|
||||||
|
r.overflowOnce.Do(func() {
|
||||||
|
root := r.spans[0]
|
||||||
|
Logger.Printf("Too many spans: dropping spans from transaction with TraceID=%s SpanID=%s limit=%d",
|
||||||
|
root.TraceID, root.SpanID, maxSpans)
|
||||||
|
})
|
||||||
|
// TODO(tracing): mark the transaction event in some way to
|
||||||
|
// communicate that spans were dropped.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.spans = append(r.spans, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// root returns the first recorded span. Returns nil if none have been recorded.
|
||||||
|
func (r *spanRecorder) root() *Span {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
if len(r.spans) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return r.spans[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// children returns a list of all recorded spans, except the root. Returns nil
|
||||||
|
// if there are no children.
|
||||||
|
func (r *spanRecorder) children() []*Span {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
if len(r.spans) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return r.spans[1:]
|
||||||
|
}
|
|
@ -0,0 +1,343 @@
|
||||||
|
package sentry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go/build"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const unknown string = "unknown"
|
||||||
|
|
||||||
|
// The module download is split into two parts: downloading the go.mod and downloading the actual code.
|
||||||
|
// If you have dependencies only needed for tests, then they will show up in your go.mod,
|
||||||
|
// and go get will download their go.mods, but it will not download their code.
|
||||||
|
// The test-only dependencies get downloaded only when you need it, such as the first time you run go test.
|
||||||
|
//
|
||||||
|
// https://github.com/golang/go/issues/26913#issuecomment-411976222
|
||||||
|
|
||||||
|
// Stacktrace holds information about the frames of the stack.
|
||||||
|
type Stacktrace struct {
|
||||||
|
Frames []Frame `json:"frames,omitempty"`
|
||||||
|
FramesOmitted []uint `json:"frames_omitted,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStacktrace creates a stacktrace using runtime.Callers.
|
||||||
|
func NewStacktrace() *Stacktrace {
|
||||||
|
pcs := make([]uintptr, 100)
|
||||||
|
n := runtime.Callers(1, pcs)
|
||||||
|
|
||||||
|
if n == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
frames := extractFrames(pcs[:n])
|
||||||
|
frames = filterFrames(frames)
|
||||||
|
|
||||||
|
stacktrace := Stacktrace{
|
||||||
|
Frames: frames,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &stacktrace
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Make it configurable so that anyone can provide their own implementation?
|
||||||
|
// Use of reflection allows us to not have a hard dependency on any given
|
||||||
|
// package, so we don't have to import it.
|
||||||
|
|
||||||
|
// ExtractStacktrace creates a new Stacktrace based on the given error.
|
||||||
|
func ExtractStacktrace(err error) *Stacktrace {
|
||||||
|
method := extractReflectedStacktraceMethod(err)
|
||||||
|
|
||||||
|
var pcs []uintptr
|
||||||
|
|
||||||
|
if method.IsValid() {
|
||||||
|
pcs = extractPcs(method)
|
||||||
|
} else {
|
||||||
|
pcs = extractXErrorsPC(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pcs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
frames := extractFrames(pcs)
|
||||||
|
frames = filterFrames(frames)
|
||||||
|
|
||||||
|
stacktrace := Stacktrace{
|
||||||
|
Frames: frames,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &stacktrace
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractReflectedStacktraceMethod(err error) reflect.Value {
|
||||||
|
errValue := reflect.ValueOf(err)
|
||||||
|
|
||||||
|
// https://github.com/go-errors/errors
|
||||||
|
methodStackFrames := errValue.MethodByName("StackFrames")
|
||||||
|
if methodStackFrames.IsValid() {
|
||||||
|
return methodStackFrames
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/pkg/errors
|
||||||
|
methodStackTrace := errValue.MethodByName("StackTrace")
|
||||||
|
if methodStackTrace.IsValid() {
|
||||||
|
return methodStackTrace
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/pingcap/errors
|
||||||
|
methodGetStackTracer := errValue.MethodByName("GetStackTracer")
|
||||||
|
if methodGetStackTracer.IsValid() {
|
||||||
|
stacktracer := methodGetStackTracer.Call(nil)[0]
|
||||||
|
stacktracerStackTrace := reflect.ValueOf(stacktracer).MethodByName("StackTrace")
|
||||||
|
|
||||||
|
if stacktracerStackTrace.IsValid() {
|
||||||
|
return stacktracerStackTrace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reflect.Value{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractPcs(method reflect.Value) []uintptr {
|
||||||
|
var pcs []uintptr
|
||||||
|
|
||||||
|
stacktrace := method.Call(nil)[0]
|
||||||
|
|
||||||
|
if stacktrace.Kind() != reflect.Slice {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < stacktrace.Len(); i++ {
|
||||||
|
pc := stacktrace.Index(i)
|
||||||
|
|
||||||
|
switch pc.Kind() {
|
||||||
|
case reflect.Uintptr:
|
||||||
|
pcs = append(pcs, uintptr(pc.Uint()))
|
||||||
|
case reflect.Struct:
|
||||||
|
for _, fieldName := range []string{"ProgramCounter", "PC"} {
|
||||||
|
field := pc.FieldByName(fieldName)
|
||||||
|
if !field.IsValid() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if field.Kind() == reflect.Uintptr {
|
||||||
|
pcs = append(pcs, uintptr(field.Uint()))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pcs
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractXErrorsPC extracts program counters from error values compatible with
|
||||||
|
// the error types from golang.org/x/xerrors.
|
||||||
|
//
|
||||||
|
// It returns nil if err is not compatible with errors from that package or if
|
||||||
|
// no program counters are stored in err.
|
||||||
|
func extractXErrorsPC(err error) []uintptr {
|
||||||
|
// This implementation uses the reflect package to avoid a hard dependency
|
||||||
|
// on third-party packages.
|
||||||
|
|
||||||
|
// We don't know if err matches the expected type. For simplicity, instead
|
||||||
|
// of trying to account for all possible ways things can go wrong, some
|
||||||
|
// assumptions are made and if they are violated the code will panic. We
|
||||||
|
// recover from any panic and ignore it, returning nil.
|
||||||
|
//nolint: errcheck
|
||||||
|
defer func() { recover() }()
|
||||||
|
|
||||||
|
field := reflect.ValueOf(err).Elem().FieldByName("frame") // type Frame struct{ frames [3]uintptr }
|
||||||
|
field = field.FieldByName("frames")
|
||||||
|
field = field.Slice(1, field.Len()) // drop first pc pointing to xerrors.New
|
||||||
|
pc := make([]uintptr, field.Len())
|
||||||
|
for i := 0; i < field.Len(); i++ {
|
||||||
|
pc[i] = uintptr(field.Index(i).Uint())
|
||||||
|
}
|
||||||
|
return pc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frame represents a function call and it's metadata. Frames are associated
|
||||||
|
// with a Stacktrace.
|
||||||
|
type Frame struct {
|
||||||
|
Function string `json:"function,omitempty"`
|
||||||
|
Symbol string `json:"symbol,omitempty"`
|
||||||
|
// Module is, despite the name, the Sentry protocol equivalent of a Go
|
||||||
|
// package's import path.
|
||||||
|
Module string `json:"module,omitempty"`
|
||||||
|
// Package is not used for Go stack trace frames. In other platforms it
|
||||||
|
// refers to a container where the Module can be found. For example, a
|
||||||
|
// Java JAR, a .NET Assembly, or a native dynamic library.
|
||||||
|
// It exists for completeness, allowing the construction and reporting
|
||||||
|
// of custom event payloads.
|
||||||
|
Package string `json:"package,omitempty"`
|
||||||
|
Filename string `json:"filename,omitempty"`
|
||||||
|
AbsPath string `json:"abs_path,omitempty"`
|
||||||
|
Lineno int `json:"lineno,omitempty"`
|
||||||
|
Colno int `json:"colno,omitempty"`
|
||||||
|
PreContext []string `json:"pre_context,omitempty"`
|
||||||
|
ContextLine string `json:"context_line,omitempty"`
|
||||||
|
PostContext []string `json:"post_context,omitempty"`
|
||||||
|
InApp bool `json:"in_app,omitempty"`
|
||||||
|
Vars map[string]interface{} `json:"vars,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFrame assembles a stacktrace frame out of runtime.Frame.
|
||||||
|
func NewFrame(f runtime.Frame) Frame {
|
||||||
|
var abspath, relpath string
|
||||||
|
// NOTE: f.File paths historically use forward slash as path separator even
|
||||||
|
// on Windows, though this is not yet documented, see
|
||||||
|
// https://golang.org/issues/3335. In any case, filepath.IsAbs can work with
|
||||||
|
// paths with either slash or backslash on Windows.
|
||||||
|
switch {
|
||||||
|
case f.File == "":
|
||||||
|
relpath = unknown
|
||||||
|
// Leave abspath as the empty string to be omitted when serializing
|
||||||
|
// event as JSON.
|
||||||
|
abspath = ""
|
||||||
|
case filepath.IsAbs(f.File):
|
||||||
|
abspath = f.File
|
||||||
|
// TODO: in the general case, it is not trivial to come up with a
|
||||||
|
// "project relative" path with the data we have in run time.
|
||||||
|
// We shall not use filepath.Base because it creates ambiguous paths and
|
||||||
|
// affects the "Suspect Commits" feature.
|
||||||
|
// For now, leave relpath empty to be omitted when serializing the event
|
||||||
|
// as JSON. Improve this later.
|
||||||
|
relpath = ""
|
||||||
|
default:
|
||||||
|
// f.File is a relative path. This may happen when the binary is built
|
||||||
|
// with the -trimpath flag.
|
||||||
|
relpath = f.File
|
||||||
|
// Omit abspath when serializing the event as JSON.
|
||||||
|
abspath = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function := f.Function
|
||||||
|
var pkg string
|
||||||
|
|
||||||
|
if function != "" {
|
||||||
|
pkg, function = splitQualifiedFunctionName(function)
|
||||||
|
}
|
||||||
|
|
||||||
|
frame := Frame{
|
||||||
|
AbsPath: abspath,
|
||||||
|
Filename: relpath,
|
||||||
|
Lineno: f.Line,
|
||||||
|
Module: pkg,
|
||||||
|
Function: function,
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.InApp = isInAppFrame(frame)
|
||||||
|
|
||||||
|
return frame
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitQualifiedFunctionName splits a package path-qualified function name into
|
||||||
|
// package name and function name. Such qualified names are found in
|
||||||
|
// runtime.Frame.Function values.
|
||||||
|
func splitQualifiedFunctionName(name string) (pkg string, fun string) {
|
||||||
|
pkg = packageName(name)
|
||||||
|
fun = strings.TrimPrefix(name, pkg+".")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractFrames(pcs []uintptr) []Frame {
|
||||||
|
var frames = make([]Frame, 0, len(pcs))
|
||||||
|
callersFrames := runtime.CallersFrames(pcs)
|
||||||
|
|
||||||
|
for {
|
||||||
|
callerFrame, more := callersFrames.Next()
|
||||||
|
|
||||||
|
frames = append(frames, NewFrame(callerFrame))
|
||||||
|
|
||||||
|
if !more {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reverse
|
||||||
|
for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
frames[i], frames[j] = frames[j], frames[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterFrames filters out stack frames that are not meant to be reported to
|
||||||
|
// Sentry. Those are frames internal to the SDK or Go.
|
||||||
|
func filterFrames(frames []Frame) []Frame {
|
||||||
|
if len(frames) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// reuse
|
||||||
|
filteredFrames := frames[:0]
|
||||||
|
|
||||||
|
for _, frame := range frames {
|
||||||
|
// Skip Go internal frames.
|
||||||
|
if frame.Module == "runtime" || frame.Module == "testing" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Skip Sentry internal frames, except for frames in _test packages (for
|
||||||
|
// testing).
|
||||||
|
if strings.HasPrefix(frame.Module, "github.com/getsentry/sentry-go") &&
|
||||||
|
!strings.HasSuffix(frame.Module, "_test") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filteredFrames = append(filteredFrames, frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredFrames
|
||||||
|
}
|
||||||
|
|
||||||
|
func isInAppFrame(frame Frame) bool {
|
||||||
|
if strings.HasPrefix(frame.AbsPath, build.Default.GOROOT) ||
|
||||||
|
strings.Contains(frame.Module, "vendor") ||
|
||||||
|
strings.Contains(frame.Module, "third_party") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func callerFunctionName() string {
|
||||||
|
pcs := make([]uintptr, 1)
|
||||||
|
runtime.Callers(3, pcs)
|
||||||
|
callersFrames := runtime.CallersFrames(pcs)
|
||||||
|
callerFrame, _ := callersFrames.Next()
|
||||||
|
return baseName(callerFrame.Function)
|
||||||
|
}
|
||||||
|
|
||||||
|
// packageName returns the package part of the symbol name, or the empty string
|
||||||
|
// if there is none.
|
||||||
|
// It replicates https://golang.org/pkg/debug/gosym/#Sym.PackageName, avoiding a
|
||||||
|
// dependency on debug/gosym.
|
||||||
|
func packageName(name string) string {
|
||||||
|
// A prefix of "type." and "go." is a compiler-generated symbol that doesn't belong to any package.
|
||||||
|
// See variable reservedimports in cmd/compile/internal/gc/subr.go
|
||||||
|
if strings.HasPrefix(name, "go.") || strings.HasPrefix(name, "type.") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
pathend := strings.LastIndex(name, "/")
|
||||||
|
if pathend < 0 {
|
||||||
|
pathend = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if i := strings.Index(name[pathend:], "."); i != -1 {
|
||||||
|
return name[:pathend+i]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// baseName returns the symbol name without the package or receiver name.
|
||||||
|
// It replicates https://golang.org/pkg/debug/gosym/#Sym.BaseName, avoiding a
|
||||||
|
// dependency on debug/gosym.
|
||||||
|
func baseName(name string) string {
|
||||||
|
if i := strings.LastIndex(name, "."); i != -1 {
|
||||||
|
return name[i+1:]
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package sentry
|
||||||
|
|
||||||
|
// A SamplingContext is passed to a TracesSampler to determine a sampling
|
||||||
|
// decision.
|
||||||
|
//
|
||||||
|
// TODO(tracing): possibly expand SamplingContext to include custom /
|
||||||
|
// user-provided data.
|
||||||
|
type SamplingContext struct {
|
||||||
|
Span *Span // The current span, always non-nil.
|
||||||
|
Parent *Span // The parent span, may be nil.
|
||||||
|
}
|
||||||
|
|
||||||
|
// The TracesSample type is an adapter to allow the use of ordinary
|
||||||
|
// functions as a TracesSampler.
|
||||||
|
type TracesSampler func(ctx SamplingContext) float64
|
||||||
|
|
||||||
|
func (f TracesSampler) Sample(ctx SamplingContext) float64 {
|
||||||
|
return f(ctx)
|
||||||
|
}
|
|
@ -0,0 +1,776 @@
|
||||||
|
package sentry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Span is the building block of a Sentry transaction. Spans build up a tree
|
||||||
|
// structure of timed operations. The span tree makes up a transaction event
|
||||||
|
// that is sent to Sentry when the root span is finished.
|
||||||
|
//
|
||||||
|
// Spans must be started with either StartSpan or Span.StartChild.
|
||||||
|
type Span struct { //nolint: maligned // prefer readability over optimal memory layout (see note below *)
|
||||||
|
TraceID TraceID `json:"trace_id"`
|
||||||
|
SpanID SpanID `json:"span_id"`
|
||||||
|
ParentSpanID SpanID `json:"parent_span_id"`
|
||||||
|
Op string `json:"op,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Status SpanStatus `json:"status,omitempty"`
|
||||||
|
Tags map[string]string `json:"tags,omitempty"`
|
||||||
|
StartTime time.Time `json:"start_timestamp"`
|
||||||
|
EndTime time.Time `json:"timestamp"`
|
||||||
|
Data map[string]interface{} `json:"data,omitempty"`
|
||||||
|
Sampled Sampled `json:"-"`
|
||||||
|
Source TransactionSource `json:"-"`
|
||||||
|
|
||||||
|
// sample rate the span was sampled with.
|
||||||
|
sampleRate float64
|
||||||
|
// ctx is the context where the span was started. Always non-nil.
|
||||||
|
ctx context.Context
|
||||||
|
// Dynamic Sampling context
|
||||||
|
dynamicSamplingContext DynamicSamplingContext
|
||||||
|
// parent refers to the immediate local parent span. A remote parent span is
|
||||||
|
// only referenced by setting ParentSpanID.
|
||||||
|
parent *Span
|
||||||
|
// isTransaction is true only for the root span of a local span tree. The
|
||||||
|
// root span is the first span started in a context. Note that a local root
|
||||||
|
// span may have a remote parent belonging to the same trace, therefore
|
||||||
|
// isTransaction depends on ctx and not on parent.
|
||||||
|
isTransaction bool
|
||||||
|
// recorder stores all spans in a transaction. Guaranteed to be non-nil.
|
||||||
|
recorder *spanRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// (*) Note on maligned:
|
||||||
|
//
|
||||||
|
// We prefer readability over optimal memory layout. If we ever decide to
|
||||||
|
// reorder fields, we can use a tool:
|
||||||
|
//
|
||||||
|
// go run honnef.co/go/tools/cmd/structlayout -json . Span | go run honnef.co/go/tools/cmd/structlayout-optimize
|
||||||
|
//
|
||||||
|
// Other structs would deserve reordering as well, for example Event.
|
||||||
|
|
||||||
|
// TODO: make Span.Tags and Span.Data opaque types (struct{unexported []slice}).
|
||||||
|
// An opaque type allows us to add methods and make it more convenient to use
|
||||||
|
// than maps, because maps require careful nil checks to use properly or rely on
|
||||||
|
// explicit initialization for every span, even when there might be no
|
||||||
|
// tags/data. For Span.Data, must gracefully handle values that cannot be
|
||||||
|
// marshaled into JSON (see transport.go:getRequestBodyFromEvent).
|
||||||
|
|
||||||
|
// StartSpan starts a new span to describe an operation. The new span will be a
|
||||||
|
// child of the last span stored in ctx, if any.
|
||||||
|
//
|
||||||
|
// One or more options can be used to modify the span properties. Typically one
|
||||||
|
// option as a function literal is enough. Combining multiple options can be
|
||||||
|
// useful to define and reuse specific properties with named functions.
|
||||||
|
//
|
||||||
|
// Caller should call the Finish method on the span to mark its end. Finishing a
|
||||||
|
// root span sends the span and all of its children, recursively, as a
|
||||||
|
// transaction to Sentry.
|
||||||
|
func StartSpan(ctx context.Context, operation string, options ...SpanOption) *Span {
|
||||||
|
parent, hasParent := ctx.Value(spanContextKey{}).(*Span)
|
||||||
|
var span Span
|
||||||
|
span = Span{
|
||||||
|
// defaults
|
||||||
|
Op: operation,
|
||||||
|
StartTime: time.Now(),
|
||||||
|
Sampled: SampledUndefined,
|
||||||
|
|
||||||
|
ctx: context.WithValue(ctx, spanContextKey{}, &span),
|
||||||
|
parent: parent,
|
||||||
|
isTransaction: !hasParent,
|
||||||
|
}
|
||||||
|
if hasParent {
|
||||||
|
span.TraceID = parent.TraceID
|
||||||
|
} else {
|
||||||
|
// Only set the Source if this is a transaction
|
||||||
|
span.Source = SourceCustom
|
||||||
|
|
||||||
|
// Implementation note:
|
||||||
|
//
|
||||||
|
// While math/rand is ~2x faster than crypto/rand (exact
|
||||||
|
// difference depends on hardware / OS), crypto/rand is probably
|
||||||
|
// fast enough and a safer choice.
|
||||||
|
//
|
||||||
|
// For reference, OpenTelemetry [1] uses crypto/rand to seed
|
||||||
|
// math/rand. AFAICT this approach does not preserve the
|
||||||
|
// properties from crypto/rand that make it suitable for
|
||||||
|
// cryptography. While it might be debatable whether those
|
||||||
|
// properties are important for us here, again, we're taking the
|
||||||
|
// safer path.
|
||||||
|
//
|
||||||
|
// See [2a] & [2b] for a discussion of some of the properties we
|
||||||
|
// obtain by using crypto/rand and [3a] & [3b] for why we avoid
|
||||||
|
// math/rand.
|
||||||
|
//
|
||||||
|
// Because the math/rand seed has only 64 bits (int64), if the
|
||||||
|
// first thing we do after seeding an RNG is to read in a random
|
||||||
|
// TraceID, there are only 2^64 possible values. Compared to
|
||||||
|
// UUID v4 that have 122 random bits, there is a much greater
|
||||||
|
// chance of collision [4a] & [4b].
|
||||||
|
//
|
||||||
|
// [1]: https://github.com/open-telemetry/opentelemetry-go/blob/958041ddf619a128/sdk/trace/trace.go#L25-L31
|
||||||
|
// [2a]: https://security.stackexchange.com/q/120352/246345
|
||||||
|
// [2b]: https://security.stackexchange.com/a/120365/246345
|
||||||
|
// [3a]: https://github.com/golang/go/issues/11871#issuecomment-126333686
|
||||||
|
// [3b]: https://github.com/golang/go/issues/11871#issuecomment-126357889
|
||||||
|
// [4a]: https://en.wikipedia.org/wiki/Universally_unique_identifier#Collisions
|
||||||
|
// [4b]: https://www.wolframalpha.com/input/?i=sqrt%282*2%5E64*ln%281%2F%281-0.5%29%29%29
|
||||||
|
_, err := rand.Read(span.TraceID[:])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err := rand.Read(span.SpanID[:])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if hasParent {
|
||||||
|
span.ParentSpanID = parent.SpanID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply options to override defaults.
|
||||||
|
for _, option := range options {
|
||||||
|
option(&span)
|
||||||
|
}
|
||||||
|
|
||||||
|
span.Sampled = span.sample()
|
||||||
|
|
||||||
|
if hasParent {
|
||||||
|
span.recorder = parent.spanRecorder()
|
||||||
|
} else {
|
||||||
|
span.recorder = &spanRecorder{}
|
||||||
|
}
|
||||||
|
span.recorder.record(&span)
|
||||||
|
|
||||||
|
// Update scope so that all events include a trace context, allowing
|
||||||
|
// Sentry to correlate errors to transactions/spans.
|
||||||
|
hubFromContext(ctx).Scope().SetContext("trace", span.traceContext().Map())
|
||||||
|
|
||||||
|
return &span
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finish sets the span's end time, unless already set. If the span is the root
|
||||||
|
// of a span tree, Finish sends the span tree to Sentry as a transaction.
|
||||||
|
func (s *Span) Finish() {
|
||||||
|
// TODO(tracing): maybe make Finish run at most once, such that
|
||||||
|
// (incorrectly) calling it twice never double sends to Sentry.
|
||||||
|
|
||||||
|
if s.EndTime.IsZero() {
|
||||||
|
s.EndTime = monotonicTimeSince(s.StartTime)
|
||||||
|
}
|
||||||
|
if !s.Sampled.Bool() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
event := s.toEvent()
|
||||||
|
if event == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(tracing): add breadcrumbs
|
||||||
|
// (see https://github.com/getsentry/sentry-python/blob/f6f3525f8812f609/sentry_sdk/tracing.py#L372)
|
||||||
|
|
||||||
|
hub := hubFromContext(s.ctx)
|
||||||
|
if hub.Scope().Transaction() == "" {
|
||||||
|
Logger.Printf("Missing transaction name for span with op = %q", s.Op)
|
||||||
|
}
|
||||||
|
hub.CaptureEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context returns the context containing the span.
|
||||||
|
func (s *Span) Context() context.Context { return s.ctx }
|
||||||
|
|
||||||
|
// StartChild starts a new child span.
|
||||||
|
//
|
||||||
|
// The call span.StartChild(operation, options...) is a shortcut for
|
||||||
|
// StartSpan(span.Context(), operation, options...).
|
||||||
|
func (s *Span) StartChild(operation string, options ...SpanOption) *Span {
|
||||||
|
return StartSpan(s.Context(), operation, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTag sets a tag on the span. It is recommended to use SetTag instead of
|
||||||
|
// accessing the tags map directly as SetTag takes care of initializing the map
|
||||||
|
// when necessary.
|
||||||
|
func (s *Span) SetTag(name, value string) {
|
||||||
|
if s.Tags == nil {
|
||||||
|
s.Tags = make(map[string]string)
|
||||||
|
}
|
||||||
|
s.Tags[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(tracing): maybe add shortcuts to get/set transaction name. Right now the
|
||||||
|
// transaction name is in the Scope, as it has existed there historically, prior
|
||||||
|
// to tracing.
|
||||||
|
//
|
||||||
|
// See Scope.Transaction() and Scope.SetTransaction().
|
||||||
|
//
|
||||||
|
// func (s *Span) TransactionName() string
|
||||||
|
// func (s *Span) SetTransactionName(name string)
|
||||||
|
|
||||||
|
// ToSentryTrace returns the trace propagation value used with the sentry-trace
|
||||||
|
// HTTP header.
|
||||||
|
func (s *Span) ToSentryTrace() string {
|
||||||
|
// TODO(tracing): add instrumentation for outgoing HTTP requests using
|
||||||
|
// ToSentryTrace.
|
||||||
|
var b strings.Builder
|
||||||
|
fmt.Fprintf(&b, "%s-%s", s.TraceID.Hex(), s.SpanID.Hex())
|
||||||
|
switch s.Sampled {
|
||||||
|
case SampledTrue:
|
||||||
|
b.WriteString("-1")
|
||||||
|
case SampledFalse:
|
||||||
|
b.WriteString("-0")
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Span) ToBaggage() string {
|
||||||
|
return s.dynamicSamplingContext.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// sentryTracePattern matches either
|
||||||
|
//
|
||||||
|
// TRACE_ID - SPAN_ID
|
||||||
|
// [[:xdigit:]]{32}-[[:xdigit:]]{16}
|
||||||
|
//
|
||||||
|
// or
|
||||||
|
//
|
||||||
|
// TRACE_ID - SPAN_ID - SAMPLED
|
||||||
|
// [[:xdigit:]]{32}-[[:xdigit:]]{16}-[01]
|
||||||
|
var sentryTracePattern = regexp.MustCompile(`^([[:xdigit:]]{32})-([[:xdigit:]]{16})(?:-([01]))?$`)
|
||||||
|
|
||||||
|
// updateFromSentryTrace parses a sentry-trace HTTP header (as returned by
|
||||||
|
// ToSentryTrace) and updates fields of the span. If the header cannot be
|
||||||
|
// recognized as valid, the span is left unchanged.
|
||||||
|
func (s *Span) updateFromSentryTrace(header []byte) {
|
||||||
|
m := sentryTracePattern.FindSubmatch(header)
|
||||||
|
if m == nil {
|
||||||
|
// no match
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = hex.Decode(s.TraceID[:], m[1])
|
||||||
|
_, _ = hex.Decode(s.ParentSpanID[:], m[2])
|
||||||
|
if len(m[3]) != 0 {
|
||||||
|
switch m[3][0] {
|
||||||
|
case '0':
|
||||||
|
s.Sampled = SampledFalse
|
||||||
|
case '1':
|
||||||
|
s.Sampled = SampledTrue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Span) updateFromBaggage(header []byte) {
|
||||||
|
if s.isTransaction {
|
||||||
|
dsc, err := DynamicSamplingContextFromHeader(header)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.dynamicSamplingContext = dsc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Span) MarshalJSON() ([]byte, error) {
|
||||||
|
// span aliases Span to allow calling json.Marshal without an infinite loop.
|
||||||
|
// It preserves all fields while none of the attached methods.
|
||||||
|
type span Span
|
||||||
|
var parentSpanID string
|
||||||
|
if s.ParentSpanID != zeroSpanID {
|
||||||
|
parentSpanID = s.ParentSpanID.String()
|
||||||
|
}
|
||||||
|
return json.Marshal(struct {
|
||||||
|
*span
|
||||||
|
ParentSpanID string `json:"parent_span_id,omitempty"`
|
||||||
|
}{
|
||||||
|
span: (*span)(s),
|
||||||
|
ParentSpanID: parentSpanID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Span) sample() Sampled {
|
||||||
|
hub := hubFromContext(s.ctx)
|
||||||
|
var clientOptions ClientOptions
|
||||||
|
client := hub.Client()
|
||||||
|
if client != nil {
|
||||||
|
clientOptions = hub.Client().Options()
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://develop.sentry.dev/sdk/performance/#sampling
|
||||||
|
// #1 tracing is not enabled.
|
||||||
|
if !clientOptions.EnableTracing {
|
||||||
|
Logger.Printf("Dropping transaction: EnableTracing is set to %t", clientOptions.EnableTracing)
|
||||||
|
s.sampleRate = 0.0
|
||||||
|
return SampledFalse
|
||||||
|
}
|
||||||
|
|
||||||
|
// #2 explicit sampling decision via StartSpan/StartTransaction options.
|
||||||
|
if s.Sampled != SampledUndefined {
|
||||||
|
Logger.Printf("Using explicit sampling decision from StartSpan/StartTransaction: %v", s.Sampled)
|
||||||
|
switch s.Sampled {
|
||||||
|
case SampledTrue:
|
||||||
|
s.sampleRate = 1.0
|
||||||
|
case SampledFalse:
|
||||||
|
s.sampleRate = 0.0
|
||||||
|
}
|
||||||
|
return s.Sampled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variant for non-transaction spans: they inherit the parent decision.
|
||||||
|
// Note: non-transaction should always have a parent, but we check both
|
||||||
|
// conditions anyway -- the first for semantic meaning, the second to
|
||||||
|
// avoid a nil pointer dereference.
|
||||||
|
if !s.isTransaction && s.parent != nil {
|
||||||
|
return s.parent.Sampled
|
||||||
|
}
|
||||||
|
|
||||||
|
// #3 use TracesSampler from ClientOptions.
|
||||||
|
sampler := clientOptions.TracesSampler
|
||||||
|
samplingContext := SamplingContext{Span: s, Parent: s.parent}
|
||||||
|
if sampler != nil {
|
||||||
|
tracesSamplerSampleRate := sampler.Sample(samplingContext)
|
||||||
|
s.sampleRate = tracesSamplerSampleRate
|
||||||
|
if tracesSamplerSampleRate < 0.0 || tracesSamplerSampleRate > 1.0 {
|
||||||
|
Logger.Printf("Dropping transaction: Returned TracesSampler rate is out of range [0.0, 1.0]: %f", tracesSamplerSampleRate)
|
||||||
|
return SampledFalse
|
||||||
|
}
|
||||||
|
if tracesSamplerSampleRate == 0 {
|
||||||
|
Logger.Printf("Dropping transaction: Returned TracesSampler rate is: %f", tracesSamplerSampleRate)
|
||||||
|
return SampledFalse
|
||||||
|
}
|
||||||
|
|
||||||
|
if rng.Float64() < tracesSamplerSampleRate {
|
||||||
|
return SampledTrue
|
||||||
|
}
|
||||||
|
Logger.Printf("Dropping transaction: TracesSampler returned rate: %f", tracesSamplerSampleRate)
|
||||||
|
return SampledFalse
|
||||||
|
}
|
||||||
|
// #4 inherit parent decision.
|
||||||
|
if s.parent != nil {
|
||||||
|
Logger.Printf("Using sampling decision from parent: %v", s.parent.Sampled)
|
||||||
|
switch s.parent.Sampled {
|
||||||
|
case SampledTrue:
|
||||||
|
s.sampleRate = 1.0
|
||||||
|
case SampledFalse:
|
||||||
|
s.sampleRate = 0.0
|
||||||
|
}
|
||||||
|
return s.parent.Sampled
|
||||||
|
}
|
||||||
|
|
||||||
|
// #5 use TracesSampleRate from ClientOptions.
|
||||||
|
sampleRate := clientOptions.TracesSampleRate
|
||||||
|
s.sampleRate = sampleRate
|
||||||
|
if sampleRate < 0.0 || sampleRate > 1.0 {
|
||||||
|
Logger.Printf("Dropping transaction: TracesSamplerRate out of range [0.0, 1.0]: %f", sampleRate)
|
||||||
|
return SampledFalse
|
||||||
|
}
|
||||||
|
if sampleRate == 0.0 {
|
||||||
|
Logger.Printf("Dropping transaction: TracesSampleRate rate is: %f", sampleRate)
|
||||||
|
return SampledFalse
|
||||||
|
}
|
||||||
|
|
||||||
|
if rng.Float64() < sampleRate {
|
||||||
|
return SampledTrue
|
||||||
|
}
|
||||||
|
|
||||||
|
return SampledFalse
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Span) toEvent() *Event {
|
||||||
|
if !s.isTransaction {
|
||||||
|
return nil // only transactions can be transformed into events
|
||||||
|
}
|
||||||
|
hub := hubFromContext(s.ctx)
|
||||||
|
|
||||||
|
children := s.recorder.children()
|
||||||
|
finished := make([]*Span, 0, len(children))
|
||||||
|
for _, child := range children {
|
||||||
|
if child.EndTime.IsZero() {
|
||||||
|
Logger.Printf("Dropped unfinished span: Op=%q TraceID=%s SpanID=%s", child.Op, child.TraceID, child.SpanID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
finished = append(finished, child)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and attach a DynamicSamplingContext to the transaction.
|
||||||
|
// If the DynamicSamplingContext is not frozen at this point, we can assume being head of trace.
|
||||||
|
if !s.dynamicSamplingContext.IsFrozen() {
|
||||||
|
s.dynamicSamplingContext = DynamicSamplingContextFromTransaction(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Event{
|
||||||
|
Type: transactionType,
|
||||||
|
Transaction: hub.Scope().Transaction(),
|
||||||
|
Contexts: map[string]Context{
|
||||||
|
"trace": s.traceContext().Map(),
|
||||||
|
},
|
||||||
|
Tags: s.Tags,
|
||||||
|
Extra: s.Data,
|
||||||
|
Timestamp: s.EndTime,
|
||||||
|
StartTime: s.StartTime,
|
||||||
|
Spans: finished,
|
||||||
|
TransactionInfo: &TransactionInfo{
|
||||||
|
Source: s.Source,
|
||||||
|
},
|
||||||
|
sdkMetaData: SDKMetaData{
|
||||||
|
dsc: s.dynamicSamplingContext,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Span) traceContext() *TraceContext {
|
||||||
|
return &TraceContext{
|
||||||
|
TraceID: s.TraceID,
|
||||||
|
SpanID: s.SpanID,
|
||||||
|
ParentSpanID: s.ParentSpanID,
|
||||||
|
Op: s.Op,
|
||||||
|
Description: s.Description,
|
||||||
|
Status: s.Status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// spanRecorder stores the span tree. Guaranteed to be non-nil.
|
||||||
|
func (s *Span) spanRecorder() *spanRecorder { return s.recorder }
|
||||||
|
|
||||||
|
// TraceID identifies a trace.
|
||||||
|
type TraceID [16]byte
|
||||||
|
|
||||||
|
func (id TraceID) Hex() []byte {
|
||||||
|
b := make([]byte, hex.EncodedLen(len(id)))
|
||||||
|
hex.Encode(b, id[:])
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id TraceID) String() string {
|
||||||
|
return string(id.Hex())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id TraceID) MarshalText() ([]byte, error) {
|
||||||
|
return id.Hex(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpanID identifies a span.
|
||||||
|
type SpanID [8]byte
|
||||||
|
|
||||||
|
func (id SpanID) Hex() []byte {
|
||||||
|
b := make([]byte, hex.EncodedLen(len(id)))
|
||||||
|
hex.Encode(b, id[:])
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id SpanID) String() string {
|
||||||
|
return string(id.Hex())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id SpanID) MarshalText() ([]byte, error) {
|
||||||
|
return id.Hex(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero values of TraceID and SpanID used for comparisons.
|
||||||
|
var (
|
||||||
|
zeroTraceID TraceID
|
||||||
|
zeroSpanID SpanID
|
||||||
|
)
|
||||||
|
|
||||||
|
// Contains information about how the name of the transaction was determined.
|
||||||
|
type TransactionSource string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SourceCustom TransactionSource = "custom"
|
||||||
|
SourceURL TransactionSource = "url"
|
||||||
|
SourceRoute TransactionSource = "route"
|
||||||
|
SourceView TransactionSource = "view"
|
||||||
|
SourceComponent TransactionSource = "component"
|
||||||
|
SourceTask TransactionSource = "task"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SpanStatus is the status of a span.
|
||||||
|
type SpanStatus uint8
|
||||||
|
|
||||||
|
// Implementation note:
|
||||||
|
//
|
||||||
|
// In Relay (ingestion), the SpanStatus type is an enum used as
|
||||||
|
// Annotated<SpanStatus> when embedded in structs, making it effectively
|
||||||
|
// Option<SpanStatus>. It means the status is either null or one of the known
|
||||||
|
// string values.
|
||||||
|
//
|
||||||
|
// In Snuba (search), the SpanStatus is stored as an uint8 and defaulted to 2
|
||||||
|
// ("unknown") when not set. It means that Discover searches for
|
||||||
|
// `transaction.status:unknown` return both transactions/spans with status
|
||||||
|
// `null` or `"unknown"`. Searches for `transaction.status:""` return nothing.
|
||||||
|
//
|
||||||
|
// With that in mind, the Go SDK default is SpanStatusUndefined, which is
|
||||||
|
// null/omitted when serializing to JSON, but integrations may update the status
|
||||||
|
// automatically based on contextual information.
|
||||||
|
|
||||||
|
const (
|
||||||
|
SpanStatusUndefined SpanStatus = iota
|
||||||
|
SpanStatusOK
|
||||||
|
SpanStatusCanceled
|
||||||
|
SpanStatusUnknown
|
||||||
|
SpanStatusInvalidArgument
|
||||||
|
SpanStatusDeadlineExceeded
|
||||||
|
SpanStatusNotFound
|
||||||
|
SpanStatusAlreadyExists
|
||||||
|
SpanStatusPermissionDenied
|
||||||
|
SpanStatusResourceExhausted
|
||||||
|
SpanStatusFailedPrecondition
|
||||||
|
SpanStatusAborted
|
||||||
|
SpanStatusOutOfRange
|
||||||
|
SpanStatusUnimplemented
|
||||||
|
SpanStatusInternalError
|
||||||
|
SpanStatusUnavailable
|
||||||
|
SpanStatusDataLoss
|
||||||
|
SpanStatusUnauthenticated
|
||||||
|
maxSpanStatus
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ss SpanStatus) String() string {
|
||||||
|
if ss >= maxSpanStatus {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
m := [maxSpanStatus]string{
|
||||||
|
"",
|
||||||
|
"ok",
|
||||||
|
"cancelled", // [sic]
|
||||||
|
"unknown",
|
||||||
|
"invalid_argument",
|
||||||
|
"deadline_exceeded",
|
||||||
|
"not_found",
|
||||||
|
"already_exists",
|
||||||
|
"permission_denied",
|
||||||
|
"resource_exhausted",
|
||||||
|
"failed_precondition",
|
||||||
|
"aborted",
|
||||||
|
"out_of_range",
|
||||||
|
"unimplemented",
|
||||||
|
"internal_error",
|
||||||
|
"unavailable",
|
||||||
|
"data_loss",
|
||||||
|
"unauthenticated",
|
||||||
|
}
|
||||||
|
return m[ss]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss SpanStatus) MarshalJSON() ([]byte, error) {
|
||||||
|
s := ss.String()
|
||||||
|
if s == "" {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
return json.Marshal(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A TraceContext carries information about an ongoing trace and is meant to be
|
||||||
|
// stored in Event.Contexts (as *TraceContext).
|
||||||
|
type TraceContext struct {
|
||||||
|
TraceID TraceID `json:"trace_id"`
|
||||||
|
SpanID SpanID `json:"span_id"`
|
||||||
|
ParentSpanID SpanID `json:"parent_span_id"`
|
||||||
|
Op string `json:"op,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Status SpanStatus `json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TraceContext) MarshalJSON() ([]byte, error) {
|
||||||
|
// traceContext aliases TraceContext to allow calling json.Marshal without
|
||||||
|
// an infinite loop. It preserves all fields while none of the attached
|
||||||
|
// methods.
|
||||||
|
type traceContext TraceContext
|
||||||
|
var parentSpanID string
|
||||||
|
if tc.ParentSpanID != zeroSpanID {
|
||||||
|
parentSpanID = tc.ParentSpanID.String()
|
||||||
|
}
|
||||||
|
return json.Marshal(struct {
|
||||||
|
*traceContext
|
||||||
|
ParentSpanID string `json:"parent_span_id,omitempty"`
|
||||||
|
}{
|
||||||
|
traceContext: (*traceContext)(tc),
|
||||||
|
ParentSpanID: parentSpanID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc TraceContext) Map() map[string]interface{} {
|
||||||
|
m := map[string]interface{}{
|
||||||
|
"trace_id": tc.TraceID,
|
||||||
|
"span_id": tc.SpanID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.ParentSpanID != [8]byte{} {
|
||||||
|
m["parent_span_id"] = tc.ParentSpanID
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.Op != "" {
|
||||||
|
m["op"] = tc.Op
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.Description != "" {
|
||||||
|
m["description"] = tc.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.Status > 0 && tc.Status < maxSpanStatus {
|
||||||
|
m["status"] = tc.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sampled signifies a sampling decision.
|
||||||
|
type Sampled int8
|
||||||
|
|
||||||
|
// The possible trace sampling decisions are: SampledFalse, SampledUndefined
|
||||||
|
// (default) and SampledTrue.
|
||||||
|
const (
|
||||||
|
SampledFalse Sampled = -1
|
||||||
|
SampledUndefined Sampled = 0
|
||||||
|
SampledTrue Sampled = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s Sampled) String() string {
|
||||||
|
switch s {
|
||||||
|
case SampledFalse:
|
||||||
|
return "SampledFalse"
|
||||||
|
case SampledUndefined:
|
||||||
|
return "SampledUndefined"
|
||||||
|
case SampledTrue:
|
||||||
|
return "SampledTrue"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("SampledInvalid(%d)", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bool returns true if the sample decision is SampledTrue, false otherwise.
|
||||||
|
func (s Sampled) Bool() bool {
|
||||||
|
return s == SampledTrue
|
||||||
|
}
|
||||||
|
|
||||||
|
// A SpanOption is a function that can modify the properties of a span.
|
||||||
|
type SpanOption func(s *Span)
|
||||||
|
|
||||||
|
// The TransactionName option sets the name of the current transaction.
|
||||||
|
//
|
||||||
|
// A span tree has a single transaction name, therefore using this option when
|
||||||
|
// starting a span affects the span tree as a whole, potentially overwriting a
|
||||||
|
// name set previously.
|
||||||
|
func TransactionName(name string) SpanOption {
|
||||||
|
return func(s *Span) {
|
||||||
|
hubFromContext(s.Context()).Scope().SetTransaction(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpName sets the operation name for a given span.
|
||||||
|
func OpName(name string) SpanOption {
|
||||||
|
return func(s *Span) {
|
||||||
|
s.Op = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransctionSource sets the source of the transaction name.
|
||||||
|
func TransctionSource(source TransactionSource) SpanOption {
|
||||||
|
return func(s *Span) {
|
||||||
|
s.Source = source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContinueFromRequest returns a span option that updates the span to continue
|
||||||
|
// an existing trace. If it cannot detect an existing trace in the request, the
|
||||||
|
// span will be left unchanged.
|
||||||
|
//
|
||||||
|
// ContinueFromRequest is an alias for:
|
||||||
|
//
|
||||||
|
// ContinueFromHeaders(r.Header.Get("sentry-trace"), r.Header.Get("baggage")).
|
||||||
|
func ContinueFromRequest(r *http.Request) SpanOption {
|
||||||
|
return ContinueFromHeaders(r.Header.Get("sentry-trace"), r.Header.Get("baggage"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContinueFromHeaders returns a span option that updates the span to continue
|
||||||
|
// an existing TraceID and propagates the Dynamic Sampling context.
|
||||||
|
func ContinueFromHeaders(trace, baggage string) SpanOption {
|
||||||
|
return func(s *Span) {
|
||||||
|
if trace != "" {
|
||||||
|
s.updateFromSentryTrace([]byte(trace))
|
||||||
|
}
|
||||||
|
if baggage != "" {
|
||||||
|
s.updateFromBaggage([]byte(baggage))
|
||||||
|
}
|
||||||
|
// In case a sentry-trace header is present but no baggage header,
|
||||||
|
// create an empty, frozen DynamicSamplingContext.
|
||||||
|
if trace != "" && baggage == "" {
|
||||||
|
s.dynamicSamplingContext = DynamicSamplingContext{
|
||||||
|
Frozen: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContinueFromTrace returns a span option that updates the span to continue
|
||||||
|
// an existing TraceID.
|
||||||
|
func ContinueFromTrace(trace string) SpanOption {
|
||||||
|
return func(s *Span) {
|
||||||
|
if trace == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.updateFromSentryTrace([]byte(trace))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// spanContextKey is used to store span values in contexts.
|
||||||
|
type spanContextKey struct{}
|
||||||
|
|
||||||
|
// TransactionFromContext returns the root span of the current transaction. It
|
||||||
|
// returns nil if no transaction is tracked in the context.
|
||||||
|
func TransactionFromContext(ctx context.Context) *Span {
|
||||||
|
if span, ok := ctx.Value(spanContextKey{}).(*Span); ok {
|
||||||
|
return span.recorder.root()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// spanFromContext returns the last span stored in the context or a dummy
|
||||||
|
// non-nil span.
|
||||||
|
//
|
||||||
|
// TODO(tracing): consider exporting this. Without this, users cannot retrieve a
|
||||||
|
// span from a context since spanContextKey is not exported.
|
||||||
|
//
|
||||||
|
// This can be added retroactively, and in the meantime think better whether it
|
||||||
|
// should return nil (like GetHubFromContext), always non-nil (like
|
||||||
|
// HubFromContext), or both: two exported functions.
|
||||||
|
//
|
||||||
|
// Note the equivalence:
|
||||||
|
//
|
||||||
|
// SpanFromContext(ctx).StartChild(...) === StartSpan(ctx, ...)
|
||||||
|
//
|
||||||
|
// So we don't aim spanFromContext at creating spans, but mutating existing
|
||||||
|
// spans that you'd have no access otherwise (because it was created in code you
|
||||||
|
// do not control, for example SDK auto-instrumentation).
|
||||||
|
//
|
||||||
|
// For now we provide TransactionFromContext, which solves the more common case
|
||||||
|
// of setting tags, etc, on the current transaction.
|
||||||
|
func spanFromContext(ctx context.Context) *Span {
|
||||||
|
if span, ok := ctx.Value(spanContextKey{}).(*Span); ok {
|
||||||
|
return span
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartTransaction will create a transaction (root span) if there's no existing
|
||||||
|
// transaction in the context otherwise, it will return the existing transaction.
|
||||||
|
func StartTransaction(ctx context.Context, name string, options ...SpanOption) *Span {
|
||||||
|
currentTransaction, exists := ctx.Value(spanContextKey{}).(*Span)
|
||||||
|
if exists {
|
||||||
|
return currentTransaction
|
||||||
|
}
|
||||||
|
|
||||||
|
options = append(options, TransactionName(name))
|
||||||
|
return StartSpan(
|
||||||
|
ctx,
|
||||||
|
"",
|
||||||
|
options...,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,592 @@
|
||||||
|
package sentry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/getsentry/sentry-go/internal/ratelimit"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultBufferSize = 30
|
||||||
|
const defaultTimeout = time.Second * 30
|
||||||
|
|
||||||
|
// maxDrainResponseBytes is the maximum number of bytes that transport
|
||||||
|
// implementations will read from response bodies when draining them.
|
||||||
|
//
|
||||||
|
// Sentry's ingestion API responses are typically short and the SDK doesn't need
|
||||||
|
// the contents of the response body. However, the net/http HTTP client requires
|
||||||
|
// response bodies to be fully drained (and closed) for TCP keep-alive to work.
|
||||||
|
//
|
||||||
|
// maxDrainResponseBytes strikes a balance between reading too much data (if the
|
||||||
|
// server is misbehaving) and reusing TCP connections.
|
||||||
|
const maxDrainResponseBytes = 16 << 10
|
||||||
|
|
||||||
|
// Transport is used by the Client to deliver events to remote server.
|
||||||
|
type Transport interface {
|
||||||
|
Flush(timeout time.Duration) bool
|
||||||
|
Configure(options ClientOptions)
|
||||||
|
SendEvent(event *Event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProxyConfig(options ClientOptions) func(*http.Request) (*url.URL, error) {
|
||||||
|
if options.HTTPSProxy != "" {
|
||||||
|
return func(*http.Request) (*url.URL, error) {
|
||||||
|
return url.Parse(options.HTTPSProxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.HTTPProxy != "" {
|
||||||
|
return func(*http.Request) (*url.URL, error) {
|
||||||
|
return url.Parse(options.HTTPProxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.ProxyFromEnvironment
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTLSConfig(options ClientOptions) *tls.Config {
|
||||||
|
if options.CaCerts != nil {
|
||||||
|
// #nosec G402 -- We should be using `MinVersion: tls.VersionTLS12`,
|
||||||
|
// but we don't want to break peoples code without the major bump.
|
||||||
|
return &tls.Config{
|
||||||
|
RootCAs: options.CaCerts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRequestBodyFromEvent(event *Event) []byte {
|
||||||
|
body, err := json.Marshal(event)
|
||||||
|
if err == nil {
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := fmt.Sprintf("Could not encode original event as JSON. "+
|
||||||
|
"Succeeded by removing Breadcrumbs, Contexts and Extra. "+
|
||||||
|
"Please verify the data you attach to the scope. "+
|
||||||
|
"Error: %s", err)
|
||||||
|
// Try to serialize the event, with all the contextual data that allows for interface{} stripped.
|
||||||
|
event.Breadcrumbs = nil
|
||||||
|
event.Contexts = nil
|
||||||
|
event.Extra = map[string]interface{}{
|
||||||
|
"info": msg,
|
||||||
|
}
|
||||||
|
body, err = json.Marshal(event)
|
||||||
|
if err == nil {
|
||||||
|
Logger.Println(msg)
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should _only_ happen when Event.Exception[0].Stacktrace.Frames[0].Vars is unserializable
|
||||||
|
// Which won't ever happen, as we don't use it now (although it's the part of public interface accepted by Sentry)
|
||||||
|
// Juuust in case something, somehow goes utterly wrong.
|
||||||
|
Logger.Println("Event couldn't be marshaled, even with stripped contextual data. Skipping delivery. " +
|
||||||
|
"Please notify the SDK owners with possibly broken payload.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func transactionEnvelopeFromBody(event *Event, dsn *Dsn, sentAt time.Time, body json.RawMessage) (*bytes.Buffer, error) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
enc := json.NewEncoder(&b)
|
||||||
|
|
||||||
|
// Construct the trace envelope header
|
||||||
|
var trace = map[string]string{}
|
||||||
|
if dsc := event.sdkMetaData.dsc; dsc.HasEntries() {
|
||||||
|
for k, v := range dsc.Entries {
|
||||||
|
trace[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Envelope header
|
||||||
|
err := enc.Encode(struct {
|
||||||
|
EventID EventID `json:"event_id"`
|
||||||
|
SentAt time.Time `json:"sent_at"`
|
||||||
|
Dsn string `json:"dsn"`
|
||||||
|
Sdk map[string]string `json:"sdk"`
|
||||||
|
Trace map[string]string `json:"trace,omitempty"`
|
||||||
|
}{
|
||||||
|
EventID: event.EventID,
|
||||||
|
SentAt: sentAt,
|
||||||
|
Trace: trace,
|
||||||
|
Dsn: dsn.String(),
|
||||||
|
Sdk: map[string]string{
|
||||||
|
"name": event.Sdk.Name,
|
||||||
|
"version": event.Sdk.Version,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item header
|
||||||
|
err = enc.Encode(struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Length int `json:"length"`
|
||||||
|
}{
|
||||||
|
Type: transactionType,
|
||||||
|
Length: len(body),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// payload
|
||||||
|
err = enc.Encode(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRequestFromEvent(event *Event, dsn *Dsn) (r *http.Request, err error) {
|
||||||
|
defer func() {
|
||||||
|
if r != nil {
|
||||||
|
r.Header.Set("User-Agent", userAgent)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
body := getRequestBodyFromEvent(event)
|
||||||
|
if body == nil {
|
||||||
|
return nil, errors.New("event could not be marshaled")
|
||||||
|
}
|
||||||
|
if event.Type == transactionType {
|
||||||
|
b, err := transactionEnvelopeFromBody(event, dsn, time.Now(), body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return http.NewRequest(
|
||||||
|
http.MethodPost,
|
||||||
|
dsn.EnvelopeAPIURL().String(),
|
||||||
|
b,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return http.NewRequest(
|
||||||
|
http.MethodPost,
|
||||||
|
dsn.StoreAPIURL().String(),
|
||||||
|
bytes.NewReader(body),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func categoryFor(eventType string) ratelimit.Category {
|
||||||
|
switch eventType {
|
||||||
|
case "":
|
||||||
|
return ratelimit.CategoryError
|
||||||
|
case transactionType:
|
||||||
|
return ratelimit.CategoryTransaction
|
||||||
|
default:
|
||||||
|
return ratelimit.Category(eventType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// HTTPTransport
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// A batch groups items that are processed sequentially.
|
||||||
|
type batch struct {
|
||||||
|
items chan batchItem
|
||||||
|
started chan struct{} // closed to signal items started to be worked on
|
||||||
|
done chan struct{} // closed to signal completion of all items
|
||||||
|
}
|
||||||
|
|
||||||
|
type batchItem struct {
|
||||||
|
request *http.Request
|
||||||
|
category ratelimit.Category
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPTransport is the default, non-blocking, implementation of Transport.
|
||||||
|
//
|
||||||
|
// Clients using this transport will enqueue requests in a buffer and return to
|
||||||
|
// the caller before any network communication has happened. Requests are sent
|
||||||
|
// to Sentry sequentially from a background goroutine.
|
||||||
|
type HTTPTransport struct {
|
||||||
|
dsn *Dsn
|
||||||
|
client *http.Client
|
||||||
|
transport http.RoundTripper
|
||||||
|
|
||||||
|
// buffer is a channel of batches. Calling Flush terminates work on the
|
||||||
|
// current in-flight items and starts a new batch for subsequent events.
|
||||||
|
buffer chan batch
|
||||||
|
|
||||||
|
start sync.Once
|
||||||
|
|
||||||
|
// Size of the transport buffer. Defaults to 30.
|
||||||
|
BufferSize int
|
||||||
|
// HTTP Client request timeout. Defaults to 30 seconds.
|
||||||
|
Timeout time.Duration
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
limits ratelimit.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHTTPTransport returns a new pre-configured instance of HTTPTransport.
|
||||||
|
func NewHTTPTransport() *HTTPTransport {
|
||||||
|
transport := HTTPTransport{
|
||||||
|
BufferSize: defaultBufferSize,
|
||||||
|
Timeout: defaultTimeout,
|
||||||
|
limits: make(ratelimit.Map),
|
||||||
|
}
|
||||||
|
return &transport
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure is called by the Client itself, providing it it's own ClientOptions.
|
||||||
|
func (t *HTTPTransport) Configure(options ClientOptions) {
|
||||||
|
dsn, err := NewDsn(options.Dsn)
|
||||||
|
if err != nil {
|
||||||
|
Logger.Printf("%v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.dsn = dsn
|
||||||
|
|
||||||
|
// A buffered channel with capacity 1 works like a mutex, ensuring only one
|
||||||
|
// goroutine can access the current batch at a given time. Access is
|
||||||
|
// synchronized by reading from and writing to the channel.
|
||||||
|
t.buffer = make(chan batch, 1)
|
||||||
|
t.buffer <- batch{
|
||||||
|
items: make(chan batchItem, t.BufferSize),
|
||||||
|
started: make(chan struct{}),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.HTTPTransport != nil {
|
||||||
|
t.transport = options.HTTPTransport
|
||||||
|
} else {
|
||||||
|
t.transport = &http.Transport{
|
||||||
|
Proxy: getProxyConfig(options),
|
||||||
|
TLSClientConfig: getTLSConfig(options),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.HTTPClient != nil {
|
||||||
|
t.client = options.HTTPClient
|
||||||
|
} else {
|
||||||
|
t.client = &http.Client{
|
||||||
|
Transport: t.transport,
|
||||||
|
Timeout: t.Timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.start.Do(func() {
|
||||||
|
go t.worker()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendEvent assembles a new packet out of Event and sends it to remote server.
|
||||||
|
func (t *HTTPTransport) SendEvent(event *Event) {
|
||||||
|
if t.dsn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
category := categoryFor(event.Type)
|
||||||
|
|
||||||
|
if t.disabled(category) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := getRequestFromEvent(event, t.dsn)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for headerKey, headerValue := range t.dsn.RequestHeaders() {
|
||||||
|
request.Header.Set(headerKey, headerValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// <-t.buffer is equivalent to acquiring a lock to access the current batch.
|
||||||
|
// A few lines below, t.buffer <- b releases the lock.
|
||||||
|
//
|
||||||
|
// The lock must be held during the select block below to guarantee that
|
||||||
|
// b.items is not closed while trying to send to it. Remember that sending
|
||||||
|
// on a closed channel panics.
|
||||||
|
//
|
||||||
|
// Note that the select block takes a bounded amount of CPU time because of
|
||||||
|
// the default case that is executed if sending on b.items would block. That
|
||||||
|
// is, the event is dropped if it cannot be sent immediately to the b.items
|
||||||
|
// channel (used as a queue).
|
||||||
|
b := <-t.buffer
|
||||||
|
|
||||||
|
select {
|
||||||
|
case b.items <- batchItem{
|
||||||
|
request: request,
|
||||||
|
category: category,
|
||||||
|
}:
|
||||||
|
var eventType string
|
||||||
|
if event.Type == transactionType {
|
||||||
|
eventType = "transaction"
|
||||||
|
} else {
|
||||||
|
eventType = fmt.Sprintf("%s event", event.Level)
|
||||||
|
}
|
||||||
|
Logger.Printf(
|
||||||
|
"Sending %s [%s] to %s project: %s",
|
||||||
|
eventType,
|
||||||
|
event.EventID,
|
||||||
|
t.dsn.host,
|
||||||
|
t.dsn.projectID,
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
Logger.Println("Event dropped due to transport buffer being full.")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.buffer <- b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush waits until any buffered events are sent to the Sentry server, blocking
|
||||||
|
// for at most the given timeout. It returns false if the timeout was reached.
|
||||||
|
// In that case, some events may not have been sent.
|
||||||
|
//
|
||||||
|
// Flush should be called before terminating the program to avoid
|
||||||
|
// unintentionally dropping events.
|
||||||
|
//
|
||||||
|
// Do not call Flush indiscriminately after every call to SendEvent. Instead, to
|
||||||
|
// have the SDK send events over the network synchronously, configure it to use
|
||||||
|
// the HTTPSyncTransport in the call to Init.
|
||||||
|
func (t *HTTPTransport) Flush(timeout time.Duration) bool {
|
||||||
|
toolate := time.After(timeout)
|
||||||
|
|
||||||
|
// Wait until processing the current batch has started or the timeout.
|
||||||
|
//
|
||||||
|
// We must wait until the worker has seen the current batch, because it is
|
||||||
|
// the only way b.done will be closed. If we do not wait, there is a
|
||||||
|
// possible execution flow in which b.done is never closed, and the only way
|
||||||
|
// out of Flush would be waiting for the timeout, which is undesired.
|
||||||
|
var b batch
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case b = <-t.buffer:
|
||||||
|
select {
|
||||||
|
case <-b.started:
|
||||||
|
goto started
|
||||||
|
default:
|
||||||
|
t.buffer <- b
|
||||||
|
}
|
||||||
|
case <-toolate:
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
started:
|
||||||
|
// Signal that there won't be any more items in this batch, so that the
|
||||||
|
// worker inner loop can end.
|
||||||
|
close(b.items)
|
||||||
|
// Start a new batch for subsequent events.
|
||||||
|
t.buffer <- batch{
|
||||||
|
items: make(chan batchItem, t.BufferSize),
|
||||||
|
started: make(chan struct{}),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait until the current batch is done or the timeout.
|
||||||
|
select {
|
||||||
|
case <-b.done:
|
||||||
|
Logger.Println("Buffer flushed successfully.")
|
||||||
|
return true
|
||||||
|
case <-toolate:
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
fail:
|
||||||
|
Logger.Println("Buffer flushing reached the timeout.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *HTTPTransport) worker() {
|
||||||
|
for b := range t.buffer {
|
||||||
|
// Signal that processing of the current batch has started.
|
||||||
|
close(b.started)
|
||||||
|
|
||||||
|
// Return the batch to the buffer so that other goroutines can use it.
|
||||||
|
// Equivalent to releasing a lock.
|
||||||
|
t.buffer <- b
|
||||||
|
|
||||||
|
// Process all batch items.
|
||||||
|
for item := range b.items {
|
||||||
|
if t.disabled(item.category) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := t.client.Do(item.request)
|
||||||
|
if err != nil {
|
||||||
|
Logger.Printf("There was an issue with sending an event: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.mu.Lock()
|
||||||
|
t.limits.Merge(ratelimit.FromResponse(response))
|
||||||
|
t.mu.Unlock()
|
||||||
|
// Drain body up to a limit and close it, allowing the
|
||||||
|
// transport to reuse TCP connections.
|
||||||
|
_, _ = io.CopyN(io.Discard, response.Body, maxDrainResponseBytes)
|
||||||
|
response.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal that processing of the batch is done.
|
||||||
|
close(b.done)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *HTTPTransport) disabled(c ratelimit.Category) bool {
|
||||||
|
t.mu.RLock()
|
||||||
|
defer t.mu.RUnlock()
|
||||||
|
disabled := t.limits.IsRateLimited(c)
|
||||||
|
if disabled {
|
||||||
|
Logger.Printf("Too many requests for %q, backing off till: %v", c, t.limits.Deadline(c))
|
||||||
|
}
|
||||||
|
return disabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// HTTPSyncTransport
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// HTTPSyncTransport is a blocking implementation of Transport.
|
||||||
|
//
|
||||||
|
// Clients using this transport will send requests to Sentry sequentially and
|
||||||
|
// block until a response is returned.
|
||||||
|
//
|
||||||
|
// The blocking behavior is useful in a limited set of use cases. For example,
|
||||||
|
// use it when deploying code to a Function as a Service ("Serverless")
|
||||||
|
// platform, where any work happening in a background goroutine is not
|
||||||
|
// guaranteed to execute.
|
||||||
|
//
|
||||||
|
// For most cases, prefer HTTPTransport.
|
||||||
|
type HTTPSyncTransport struct {
|
||||||
|
dsn *Dsn
|
||||||
|
client *http.Client
|
||||||
|
transport http.RoundTripper
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
limits ratelimit.Map
|
||||||
|
|
||||||
|
// HTTP Client request timeout. Defaults to 30 seconds.
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHTTPSyncTransport returns a new pre-configured instance of HTTPSyncTransport.
|
||||||
|
func NewHTTPSyncTransport() *HTTPSyncTransport {
|
||||||
|
transport := HTTPSyncTransport{
|
||||||
|
Timeout: defaultTimeout,
|
||||||
|
limits: make(ratelimit.Map),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &transport
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure is called by the Client itself, providing it it's own ClientOptions.
|
||||||
|
func (t *HTTPSyncTransport) Configure(options ClientOptions) {
|
||||||
|
dsn, err := NewDsn(options.Dsn)
|
||||||
|
if err != nil {
|
||||||
|
Logger.Printf("%v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.dsn = dsn
|
||||||
|
|
||||||
|
if options.HTTPTransport != nil {
|
||||||
|
t.transport = options.HTTPTransport
|
||||||
|
} else {
|
||||||
|
t.transport = &http.Transport{
|
||||||
|
Proxy: getProxyConfig(options),
|
||||||
|
TLSClientConfig: getTLSConfig(options),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.HTTPClient != nil {
|
||||||
|
t.client = options.HTTPClient
|
||||||
|
} else {
|
||||||
|
t.client = &http.Client{
|
||||||
|
Transport: t.transport,
|
||||||
|
Timeout: t.Timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendEvent assembles a new packet out of Event and sends it to remote server.
|
||||||
|
func (t *HTTPSyncTransport) SendEvent(event *Event) {
|
||||||
|
if t.dsn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.disabled(categoryFor(event.Type)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := getRequestFromEvent(event, t.dsn)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for headerKey, headerValue := range t.dsn.RequestHeaders() {
|
||||||
|
request.Header.Set(headerKey, headerValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventType string
|
||||||
|
if event.Type == transactionType {
|
||||||
|
eventType = "transaction"
|
||||||
|
} else {
|
||||||
|
eventType = fmt.Sprintf("%s event", event.Level)
|
||||||
|
}
|
||||||
|
Logger.Printf(
|
||||||
|
"Sending %s [%s] to %s project: %s",
|
||||||
|
eventType,
|
||||||
|
event.EventID,
|
||||||
|
t.dsn.host,
|
||||||
|
t.dsn.projectID,
|
||||||
|
)
|
||||||
|
|
||||||
|
response, err := t.client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
Logger.Printf("There was an issue with sending an event: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.mu.Lock()
|
||||||
|
t.limits.Merge(ratelimit.FromResponse(response))
|
||||||
|
t.mu.Unlock()
|
||||||
|
|
||||||
|
// Drain body up to a limit and close it, allowing the
|
||||||
|
// transport to reuse TCP connections.
|
||||||
|
_, _ = io.CopyN(io.Discard, response.Body, maxDrainResponseBytes)
|
||||||
|
response.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush is a no-op for HTTPSyncTransport. It always returns true immediately.
|
||||||
|
func (t *HTTPSyncTransport) Flush(_ time.Duration) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *HTTPSyncTransport) disabled(c ratelimit.Category) bool {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
disabled := t.limits.IsRateLimited(c)
|
||||||
|
if disabled {
|
||||||
|
Logger.Printf("Too many requests for %q, backing off till: %v", c, t.limits.Deadline(c))
|
||||||
|
}
|
||||||
|
return disabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// noopTransport
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// noopTransport is an implementation of Transport interface which drops all the events.
|
||||||
|
// Only used internally when an empty DSN is provided, which effectively disables the SDK.
|
||||||
|
type noopTransport struct{}
|
||||||
|
|
||||||
|
var _ Transport = noopTransport{}
|
||||||
|
|
||||||
|
func (noopTransport) Configure(ClientOptions) {
|
||||||
|
Logger.Println("Sentry client initialized with an empty DSN. Using noopTransport. No events will be delivered.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (noopTransport) SendEvent(*Event) {
|
||||||
|
Logger.Println("Event dropped due to noopTransport usage.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (noopTransport) Flush(time.Duration) bool {
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
package sentry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
exec "golang.org/x/sys/execabs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func uuid() string {
|
||||||
|
id := make([]byte, 16)
|
||||||
|
// Prefer rand.Read over rand.Reader, see https://go-review.googlesource.com/c/go/+/272326/.
|
||||||
|
_, _ = rand.Read(id)
|
||||||
|
id[6] &= 0x0F // clear version
|
||||||
|
id[6] |= 0x40 // set version to 4 (random uuid)
|
||||||
|
id[8] &= 0x3F // clear variant
|
||||||
|
id[8] |= 0x80 // set to IETF variant
|
||||||
|
return hex.EncodeToString(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileExists(fileName string) bool {
|
||||||
|
_, err := os.Stat(fileName)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// monotonicTimeSince replaces uses of time.Now() to take into account the
|
||||||
|
// monotonic clock reading stored in start, such that duration = end - start is
|
||||||
|
// unaffected by changes in the system wall clock.
|
||||||
|
func monotonicTimeSince(start time.Time) (end time.Time) {
|
||||||
|
return start.Add(time.Since(start))
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint: deadcode, unused
|
||||||
|
func prettyPrint(data interface{}) {
|
||||||
|
dbg, _ := json.MarshalIndent(data, "", " ")
|
||||||
|
fmt.Println(string(dbg))
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultRelease attempts to guess a default release for the currently running
|
||||||
|
// program.
|
||||||
|
func defaultRelease() (release string) {
|
||||||
|
// Return first non-empty environment variable known to hold release info, if any.
|
||||||
|
envs := []string{
|
||||||
|
"SENTRY_RELEASE",
|
||||||
|
"HEROKU_SLUG_COMMIT",
|
||||||
|
"SOURCE_VERSION",
|
||||||
|
"CODEBUILD_RESOLVED_SOURCE_VERSION",
|
||||||
|
"CIRCLE_SHA1",
|
||||||
|
"GAE_DEPLOYMENT_ID",
|
||||||
|
"GITHUB_SHA", // GitHub Actions - https://help.github.com/en/actions
|
||||||
|
"COMMIT_REF", // Netlify - https://docs.netlify.com/
|
||||||
|
"VERCEL_GIT_COMMIT_SHA", // Vercel - https://vercel.com/
|
||||||
|
"ZEIT_GITHUB_COMMIT_SHA", // Zeit (now known as Vercel)
|
||||||
|
"ZEIT_GITLAB_COMMIT_SHA",
|
||||||
|
"ZEIT_BITBUCKET_COMMIT_SHA",
|
||||||
|
}
|
||||||
|
for _, e := range envs {
|
||||||
|
if release = os.Getenv(e); release != "" {
|
||||||
|
Logger.Printf("Using release from environment variable %s: %s", e, release)
|
||||||
|
return release
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derive a version string from Git. Example outputs:
|
||||||
|
// v1.0.1-0-g9de4
|
||||||
|
// v2.0-8-g77df-dirty
|
||||||
|
// 4f72d7
|
||||||
|
cmd := exec.Command("git", "describe", "--long", "--always", "--dirty")
|
||||||
|
b, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
// Either Git is not available or the current directory is not a
|
||||||
|
// Git repository.
|
||||||
|
var s strings.Builder
|
||||||
|
fmt.Fprintf(&s, "Release detection failed: %v", err)
|
||||||
|
if err, ok := err.(*exec.ExitError); ok && len(err.Stderr) > 0 {
|
||||||
|
fmt.Fprintf(&s, ": %s", err.Stderr)
|
||||||
|
}
|
||||||
|
Logger.Print(s.String())
|
||||||
|
Logger.Print("Some Sentry features will not be available. See https://docs.sentry.io/product/releases/.")
|
||||||
|
Logger.Print("To stop seeing this message, pass a Release to sentry.Init or set the SENTRY_RELEASE environment variable.")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
release = strings.TrimSpace(string(b))
|
||||||
|
Logger.Printf("Using release from Git: %s", release)
|
||||||
|
return release
|
||||||
|
}
|
|
@ -1,15 +0,0 @@
|
||||||
language: go
|
|
||||||
sudo: false
|
|
||||||
go:
|
|
||||||
- 1.13.x
|
|
||||||
- tip
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- go get -t -v ./...
|
|
||||||
|
|
||||||
script:
|
|
||||||
- ./go.test.sh
|
|
||||||
|
|
||||||
after_success:
|
|
||||||
- bash <(curl -s https://codecov.io/bash)
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# go-colorable
|
# go-colorable
|
||||||
|
|
||||||
[![Build Status](https://travis-ci.org/mattn/go-colorable.svg?branch=master)](https://travis-ci.org/mattn/go-colorable)
|
[![Build Status](https://github.com/mattn/go-colorable/workflows/test/badge.svg)](https://github.com/mattn/go-colorable/actions?query=workflow%3Atest)
|
||||||
[![Codecov](https://codecov.io/gh/mattn/go-colorable/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-colorable)
|
[![Codecov](https://codecov.io/gh/mattn/go-colorable/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-colorable)
|
||||||
[![GoDoc](https://godoc.org/github.com/mattn/go-colorable?status.svg)](http://godoc.org/github.com/mattn/go-colorable)
|
[![GoDoc](https://godoc.org/github.com/mattn/go-colorable?status.svg)](http://godoc.org/github.com/mattn/go-colorable)
|
||||||
[![Go Report Card](https://goreportcard.com/badge/mattn/go-colorable)](https://goreportcard.com/report/mattn/go-colorable)
|
[![Go Report Card](https://goreportcard.com/badge/mattn/go-colorable)](https://goreportcard.com/report/mattn/go-colorable)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build appengine
|
||||||
// +build appengine
|
// +build appengine
|
||||||
|
|
||||||
package colorable
|
package colorable
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// +build !windows
|
//go:build !windows && !appengine
|
||||||
// +build !appengine
|
// +build !windows,!appengine
|
||||||
|
|
||||||
package colorable
|
package colorable
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// +build windows
|
//go:build windows && !appengine
|
||||||
// +build !appengine
|
// +build windows,!appengine
|
||||||
|
|
||||||
package colorable
|
package colorable
|
||||||
|
|
||||||
|
@ -452,18 +452,22 @@ func (w *Writer) Write(data []byte) (n int, err error) {
|
||||||
} else {
|
} else {
|
||||||
er = bytes.NewReader(data)
|
er = bytes.NewReader(data)
|
||||||
}
|
}
|
||||||
var bw [1]byte
|
var plaintext bytes.Buffer
|
||||||
loop:
|
loop:
|
||||||
for {
|
for {
|
||||||
c1, err := er.ReadByte()
|
c1, err := er.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
plaintext.WriteTo(w.out)
|
||||||
break loop
|
break loop
|
||||||
}
|
}
|
||||||
if c1 != 0x1b {
|
if c1 != 0x1b {
|
||||||
bw[0] = c1
|
plaintext.WriteByte(c1)
|
||||||
w.out.Write(bw[:])
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
_, err = plaintext.WriteTo(w.out)
|
||||||
|
if err != nil {
|
||||||
|
break loop
|
||||||
|
}
|
||||||
c2, err := er.ReadByte()
|
c2, err := er.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break loop
|
break loop
|
||||||
|
|
|
@ -18,18 +18,22 @@ func NewNonColorable(w io.Writer) io.Writer {
|
||||||
// Write writes data on console
|
// Write writes data on console
|
||||||
func (w *NonColorable) Write(data []byte) (n int, err error) {
|
func (w *NonColorable) Write(data []byte) (n int, err error) {
|
||||||
er := bytes.NewReader(data)
|
er := bytes.NewReader(data)
|
||||||
var bw [1]byte
|
var plaintext bytes.Buffer
|
||||||
loop:
|
loop:
|
||||||
for {
|
for {
|
||||||
c1, err := er.ReadByte()
|
c1, err := er.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
plaintext.WriteTo(w.out)
|
||||||
break loop
|
break loop
|
||||||
}
|
}
|
||||||
if c1 != 0x1b {
|
if c1 != 0x1b {
|
||||||
bw[0] = c1
|
plaintext.WriteByte(c1)
|
||||||
w.out.Write(bw[:])
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
_, err = plaintext.WriteTo(w.out)
|
||||||
|
if err != nil {
|
||||||
|
break loop
|
||||||
|
}
|
||||||
c2, err := er.ReadByte()
|
c2, err := er.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break loop
|
break loop
|
||||||
|
@ -38,7 +42,6 @@ loop:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
for {
|
for {
|
||||||
c, err := er.ReadByte()
|
c, err := er.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -47,7 +50,6 @@ loop:
|
||||||
if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' {
|
if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
buf.Write([]byte(string(c)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
language: go
|
|
||||||
sudo: false
|
|
||||||
go:
|
|
||||||
- 1.13.x
|
|
||||||
- tip
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- go get -t -v ./...
|
|
||||||
|
|
||||||
script:
|
|
||||||
- ./go.test.sh
|
|
||||||
|
|
||||||
after_success:
|
|
||||||
- bash <(curl -s https://codecov.io/bash)
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build (darwin || freebsd || openbsd || netbsd || dragonfly) && !appengine
|
||||||
// +build darwin freebsd openbsd netbsd dragonfly
|
// +build darwin freebsd openbsd netbsd dragonfly
|
||||||
// +build !appengine
|
// +build !appengine
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// +build appengine js nacl
|
//go:build appengine || js || nacl || wasm
|
||||||
|
// +build appengine js nacl wasm
|
||||||
|
|
||||||
package isatty
|
package isatty
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build plan9
|
||||||
// +build plan9
|
// +build plan9
|
||||||
|
|
||||||
package isatty
|
package isatty
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// +build solaris
|
//go:build solaris && !appengine
|
||||||
// +build !appengine
|
// +build solaris,!appengine
|
||||||
|
|
||||||
package isatty
|
package isatty
|
||||||
|
|
||||||
|
@ -8,10 +8,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsTerminal returns true if the given file descriptor is a terminal.
|
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||||
// see: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libbc/libc/gen/common/isatty.c
|
// see: https://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libc/port/gen/isatty.c
|
||||||
func IsTerminal(fd uintptr) bool {
|
func IsTerminal(fd uintptr) bool {
|
||||||
var termio unix.Termio
|
_, err := unix.IoctlGetTermio(int(fd), unix.TCGETA)
|
||||||
err := unix.IoctlSetTermio(int(fd), unix.TCGETA, &termio)
|
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// +build linux aix
|
//go:build (linux || aix || zos) && !appengine
|
||||||
|
// +build linux aix zos
|
||||||
// +build !appengine
|
// +build !appengine
|
||||||
|
|
||||||
package isatty
|
package isatty
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// +build windows
|
//go:build windows && !appengine
|
||||||
// +build !appengine
|
// +build windows,!appengine
|
||||||
|
|
||||||
package isatty
|
package isatty
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ func isCygwinPipeName(name string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFileNameByHandle use the undocomented ntdll NtQueryObject to get file full name from file handler
|
// getFileNameByHandle use the undocomented ntdll NtQueryObject to get file full name from file handler
|
||||||
// since GetFileInformationByHandleEx is not avilable under windows Vista and still some old fashion
|
// since GetFileInformationByHandleEx is not available under windows Vista and still some old fashion
|
||||||
// guys are using Windows XP, this is a workaround for those guys, it will also work on system from
|
// guys are using Windows XP, this is a workaround for those guys, it will also work on system from
|
||||||
// Windows vista to 10
|
// Windows vista to 10
|
||||||
// see https://stackoverflow.com/a/18792477 for details
|
// see https://stackoverflow.com/a/18792477 for details
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"extends": [
|
|
||||||
"config:base"
|
|
||||||
],
|
|
||||||
"postUpdateOptions": [
|
|
||||||
"gomodTidy"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -0,0 +1,162 @@
|
||||||
|
// Copyright 2014 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:generate go run gen.go gen_trieval.go
|
||||||
|
|
||||||
|
// Package cases provides general and language-specific case mappers.
|
||||||
|
package cases // import "golang.org/x/text/cases"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
"golang.org/x/text/transform"
|
||||||
|
)
|
||||||
|
|
||||||
|
// References:
|
||||||
|
// - Unicode Reference Manual Chapter 3.13, 4.2, and 5.18.
|
||||||
|
// - https://www.unicode.org/reports/tr29/
|
||||||
|
// - https://www.unicode.org/Public/6.3.0/ucd/CaseFolding.txt
|
||||||
|
// - https://www.unicode.org/Public/6.3.0/ucd/SpecialCasing.txt
|
||||||
|
// - https://www.unicode.org/Public/6.3.0/ucd/DerivedCoreProperties.txt
|
||||||
|
// - https://www.unicode.org/Public/6.3.0/ucd/auxiliary/WordBreakProperty.txt
|
||||||
|
// - https://www.unicode.org/Public/6.3.0/ucd/auxiliary/WordBreakTest.txt
|
||||||
|
// - http://userguide.icu-project.org/transforms/casemappings
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// - Case folding
|
||||||
|
// - Wide and Narrow?
|
||||||
|
// - Segmenter option for title casing.
|
||||||
|
// - ASCII fast paths
|
||||||
|
// - Encode Soft-Dotted property within trie somehow.
|
||||||
|
|
||||||
|
// A Caser transforms given input to a certain case. It implements
|
||||||
|
// transform.Transformer.
|
||||||
|
//
|
||||||
|
// A Caser may be stateful and should therefore not be shared between
|
||||||
|
// goroutines.
|
||||||
|
type Caser struct {
|
||||||
|
t transform.SpanningTransformer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns a new byte slice with the result of converting b to the case
|
||||||
|
// form implemented by c.
|
||||||
|
func (c Caser) Bytes(b []byte) []byte {
|
||||||
|
b, _, _ = transform.Bytes(c.t, b)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string with the result of transforming s to the case form
|
||||||
|
// implemented by c.
|
||||||
|
func (c Caser) String(s string) string {
|
||||||
|
s, _, _ = transform.String(c.t, s)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset resets the Caser to be reused for new input after a previous call to
|
||||||
|
// Transform.
|
||||||
|
func (c Caser) Reset() { c.t.Reset() }
|
||||||
|
|
||||||
|
// Transform implements the transform.Transformer interface and transforms the
|
||||||
|
// given input to the case form implemented by c.
|
||||||
|
func (c Caser) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||||
|
return c.t.Transform(dst, src, atEOF)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Span implements the transform.SpanningTransformer interface.
|
||||||
|
func (c Caser) Span(src []byte, atEOF bool) (n int, err error) {
|
||||||
|
return c.t.Span(src, atEOF)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upper returns a Caser for language-specific uppercasing.
|
||||||
|
func Upper(t language.Tag, opts ...Option) Caser {
|
||||||
|
return Caser{makeUpper(t, getOpts(opts...))}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lower returns a Caser for language-specific lowercasing.
|
||||||
|
func Lower(t language.Tag, opts ...Option) Caser {
|
||||||
|
return Caser{makeLower(t, getOpts(opts...))}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title returns a Caser for language-specific title casing. It uses an
|
||||||
|
// approximation of the default Unicode Word Break algorithm.
|
||||||
|
func Title(t language.Tag, opts ...Option) Caser {
|
||||||
|
return Caser{makeTitle(t, getOpts(opts...))}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fold returns a Caser that implements Unicode case folding. The returned Caser
|
||||||
|
// is stateless and safe to use concurrently by multiple goroutines.
|
||||||
|
//
|
||||||
|
// Case folding does not normalize the input and may not preserve a normal form.
|
||||||
|
// Use the collate or search package for more convenient and linguistically
|
||||||
|
// sound comparisons. Use golang.org/x/text/secure/precis for string comparisons
|
||||||
|
// where security aspects are a concern.
|
||||||
|
func Fold(opts ...Option) Caser {
|
||||||
|
return Caser{makeFold(getOpts(opts...))}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Option is used to modify the behavior of a Caser.
|
||||||
|
type Option func(o options) options
|
||||||
|
|
||||||
|
// TODO: consider these options to take a boolean as well, like FinalSigma.
|
||||||
|
// The advantage of using this approach is that other providers of a lower-case
|
||||||
|
// algorithm could set different defaults by prefixing a user-provided slice
|
||||||
|
// of options with their own. This is handy, for instance, for the precis
|
||||||
|
// package which would override the default to not handle the Greek final sigma.
|
||||||
|
|
||||||
|
var (
|
||||||
|
// NoLower disables the lowercasing of non-leading letters for a title
|
||||||
|
// caser.
|
||||||
|
NoLower Option = noLower
|
||||||
|
|
||||||
|
// Compact omits mappings in case folding for characters that would grow the
|
||||||
|
// input. (Unimplemented.)
|
||||||
|
Compact Option = compact
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: option to preserve a normal form, if applicable?
|
||||||
|
|
||||||
|
type options struct {
|
||||||
|
noLower bool
|
||||||
|
simple bool
|
||||||
|
|
||||||
|
// TODO: segmenter, max ignorable, alternative versions, etc.
|
||||||
|
|
||||||
|
ignoreFinalSigma bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOpts(o ...Option) (res options) {
|
||||||
|
for _, f := range o {
|
||||||
|
res = f(res)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func noLower(o options) options {
|
||||||
|
o.noLower = true
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
func compact(o options) options {
|
||||||
|
o.simple = true
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleFinalSigma specifies whether the special handling of Greek final sigma
|
||||||
|
// should be enabled. Unicode prescribes handling the Greek final sigma for all
|
||||||
|
// locales, but standards like IDNA and PRECIS override this default.
|
||||||
|
func HandleFinalSigma(enable bool) Option {
|
||||||
|
if enable {
|
||||||
|
return handleFinalSigma
|
||||||
|
}
|
||||||
|
return ignoreFinalSigma
|
||||||
|
}
|
||||||
|
|
||||||
|
func ignoreFinalSigma(o options) options {
|
||||||
|
o.ignoreFinalSigma = true
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleFinalSigma(o options) options {
|
||||||
|
o.ignoreFinalSigma = false
|
||||||
|
return o
|
||||||
|
}
|
|
@ -0,0 +1,376 @@
|
||||||
|
// Copyright 2014 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package cases
|
||||||
|
|
||||||
|
import "golang.org/x/text/transform"
|
||||||
|
|
||||||
|
// A context is used for iterating over source bytes, fetching case info and
|
||||||
|
// writing to a destination buffer.
|
||||||
|
//
|
||||||
|
// Casing operations may need more than one rune of context to decide how a rune
|
||||||
|
// should be cased. Casing implementations should call checkpoint on context
|
||||||
|
// whenever it is known to be safe to return the runes processed so far.
|
||||||
|
//
|
||||||
|
// It is recommended for implementations to not allow for more than 30 case
|
||||||
|
// ignorables as lookahead (analogous to the limit in norm) and to use state if
|
||||||
|
// unbounded lookahead is needed for cased runes.
|
||||||
|
type context struct {
|
||||||
|
dst, src []byte
|
||||||
|
atEOF bool
|
||||||
|
|
||||||
|
pDst int // pDst points past the last written rune in dst.
|
||||||
|
pSrc int // pSrc points to the start of the currently scanned rune.
|
||||||
|
|
||||||
|
// checkpoints safe to return in Transform, where nDst <= pDst and nSrc <= pSrc.
|
||||||
|
nDst, nSrc int
|
||||||
|
err error
|
||||||
|
|
||||||
|
sz int // size of current rune
|
||||||
|
info info // case information of currently scanned rune
|
||||||
|
|
||||||
|
// State preserved across calls to Transform.
|
||||||
|
isMidWord bool // false if next cased letter needs to be title-cased.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *context) Reset() {
|
||||||
|
c.isMidWord = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ret returns the return values for the Transform method. It checks whether
|
||||||
|
// there were insufficient bytes in src to complete and introduces an error
|
||||||
|
// accordingly, if necessary.
|
||||||
|
func (c *context) ret() (nDst, nSrc int, err error) {
|
||||||
|
if c.err != nil || c.nSrc == len(c.src) {
|
||||||
|
return c.nDst, c.nSrc, c.err
|
||||||
|
}
|
||||||
|
// This point is only reached by mappers if there was no short destination
|
||||||
|
// buffer. This means that the source buffer was exhausted and that c.sz was
|
||||||
|
// set to 0 by next.
|
||||||
|
if c.atEOF && c.pSrc == len(c.src) {
|
||||||
|
return c.pDst, c.pSrc, nil
|
||||||
|
}
|
||||||
|
return c.nDst, c.nSrc, transform.ErrShortSrc
|
||||||
|
}
|
||||||
|
|
||||||
|
// retSpan returns the return values for the Span method. It checks whether
|
||||||
|
// there were insufficient bytes in src to complete and introduces an error
|
||||||
|
// accordingly, if necessary.
|
||||||
|
func (c *context) retSpan() (n int, err error) {
|
||||||
|
_, nSrc, err := c.ret()
|
||||||
|
return nSrc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkpoint sets the return value buffer points for Transform to the current
|
||||||
|
// positions.
|
||||||
|
func (c *context) checkpoint() {
|
||||||
|
if c.err == nil {
|
||||||
|
c.nDst, c.nSrc = c.pDst, c.pSrc+c.sz
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unreadRune causes the last rune read by next to be reread on the next
|
||||||
|
// invocation of next. Only one unreadRune may be called after a call to next.
|
||||||
|
func (c *context) unreadRune() {
|
||||||
|
c.sz = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *context) next() bool {
|
||||||
|
c.pSrc += c.sz
|
||||||
|
if c.pSrc == len(c.src) || c.err != nil {
|
||||||
|
c.info, c.sz = 0, 0
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
v, sz := trie.lookup(c.src[c.pSrc:])
|
||||||
|
c.info, c.sz = info(v), sz
|
||||||
|
if c.sz == 0 {
|
||||||
|
if c.atEOF {
|
||||||
|
// A zero size means we have an incomplete rune. If we are atEOF,
|
||||||
|
// this means it is an illegal rune, which we will consume one
|
||||||
|
// byte at a time.
|
||||||
|
c.sz = 1
|
||||||
|
} else {
|
||||||
|
c.err = transform.ErrShortSrc
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeBytes adds bytes to dst.
|
||||||
|
func (c *context) writeBytes(b []byte) bool {
|
||||||
|
if len(c.dst)-c.pDst < len(b) {
|
||||||
|
c.err = transform.ErrShortDst
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// This loop is faster than using copy.
|
||||||
|
for _, ch := range b {
|
||||||
|
c.dst[c.pDst] = ch
|
||||||
|
c.pDst++
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeString writes the given string to dst.
|
||||||
|
func (c *context) writeString(s string) bool {
|
||||||
|
if len(c.dst)-c.pDst < len(s) {
|
||||||
|
c.err = transform.ErrShortDst
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// This loop is faster than using copy.
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
c.dst[c.pDst] = s[i]
|
||||||
|
c.pDst++
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy writes the current rune to dst.
|
||||||
|
func (c *context) copy() bool {
|
||||||
|
return c.writeBytes(c.src[c.pSrc : c.pSrc+c.sz])
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyXOR copies the current rune to dst and modifies it by applying the XOR
|
||||||
|
// pattern of the case info. It is the responsibility of the caller to ensure
|
||||||
|
// that this is a rune with a XOR pattern defined.
|
||||||
|
func (c *context) copyXOR() bool {
|
||||||
|
if !c.copy() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if c.info&xorIndexBit == 0 {
|
||||||
|
// Fast path for 6-bit XOR pattern, which covers most cases.
|
||||||
|
c.dst[c.pDst-1] ^= byte(c.info >> xorShift)
|
||||||
|
} else {
|
||||||
|
// Interpret XOR bits as an index.
|
||||||
|
// TODO: test performance for unrolling this loop. Verify that we have
|
||||||
|
// at least two bytes and at most three.
|
||||||
|
idx := c.info >> xorShift
|
||||||
|
for p := c.pDst - 1; ; p-- {
|
||||||
|
c.dst[p] ^= xorData[idx]
|
||||||
|
idx--
|
||||||
|
if xorData[idx] == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasPrefix returns true if src[pSrc:] starts with the given string.
|
||||||
|
func (c *context) hasPrefix(s string) bool {
|
||||||
|
b := c.src[c.pSrc:]
|
||||||
|
if len(b) < len(s) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, c := range b[:len(s)] {
|
||||||
|
if c != s[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// caseType returns an info with only the case bits, normalized to either
|
||||||
|
// cLower, cUpper, cTitle or cUncased.
|
||||||
|
func (c *context) caseType() info {
|
||||||
|
cm := c.info & 0x7
|
||||||
|
if cm < 4 {
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
if cm >= cXORCase {
|
||||||
|
// xor the last bit of the rune with the case type bits.
|
||||||
|
b := c.src[c.pSrc+c.sz-1]
|
||||||
|
return info(b&1) ^ cm&0x3
|
||||||
|
}
|
||||||
|
if cm == cIgnorableCased {
|
||||||
|
return cLower
|
||||||
|
}
|
||||||
|
return cUncased
|
||||||
|
}
|
||||||
|
|
||||||
|
// lower writes the lowercase version of the current rune to dst.
|
||||||
|
func lower(c *context) bool {
|
||||||
|
ct := c.caseType()
|
||||||
|
if c.info&hasMappingMask == 0 || ct == cLower {
|
||||||
|
return c.copy()
|
||||||
|
}
|
||||||
|
if c.info&exceptionBit == 0 {
|
||||||
|
return c.copyXOR()
|
||||||
|
}
|
||||||
|
e := exceptions[c.info>>exceptionShift:]
|
||||||
|
offset := 2 + e[0]&lengthMask // size of header + fold string
|
||||||
|
if nLower := (e[1] >> lengthBits) & lengthMask; nLower != noChange {
|
||||||
|
return c.writeString(e[offset : offset+nLower])
|
||||||
|
}
|
||||||
|
return c.copy()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLower(c *context) bool {
|
||||||
|
ct := c.caseType()
|
||||||
|
if c.info&hasMappingMask == 0 || ct == cLower {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if c.info&exceptionBit == 0 {
|
||||||
|
c.err = transform.ErrEndOfSpan
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
e := exceptions[c.info>>exceptionShift:]
|
||||||
|
if nLower := (e[1] >> lengthBits) & lengthMask; nLower != noChange {
|
||||||
|
c.err = transform.ErrEndOfSpan
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// upper writes the uppercase version of the current rune to dst.
|
||||||
|
func upper(c *context) bool {
|
||||||
|
ct := c.caseType()
|
||||||
|
if c.info&hasMappingMask == 0 || ct == cUpper {
|
||||||
|
return c.copy()
|
||||||
|
}
|
||||||
|
if c.info&exceptionBit == 0 {
|
||||||
|
return c.copyXOR()
|
||||||
|
}
|
||||||
|
e := exceptions[c.info>>exceptionShift:]
|
||||||
|
offset := 2 + e[0]&lengthMask // size of header + fold string
|
||||||
|
// Get length of first special case mapping.
|
||||||
|
n := (e[1] >> lengthBits) & lengthMask
|
||||||
|
if ct == cTitle {
|
||||||
|
// The first special case mapping is for lower. Set n to the second.
|
||||||
|
if n == noChange {
|
||||||
|
n = 0
|
||||||
|
}
|
||||||
|
n, e = e[1]&lengthMask, e[n:]
|
||||||
|
}
|
||||||
|
if n != noChange {
|
||||||
|
return c.writeString(e[offset : offset+n])
|
||||||
|
}
|
||||||
|
return c.copy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// isUpper writes the isUppercase version of the current rune to dst.
|
||||||
|
func isUpper(c *context) bool {
|
||||||
|
ct := c.caseType()
|
||||||
|
if c.info&hasMappingMask == 0 || ct == cUpper {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if c.info&exceptionBit == 0 {
|
||||||
|
c.err = transform.ErrEndOfSpan
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
e := exceptions[c.info>>exceptionShift:]
|
||||||
|
// Get length of first special case mapping.
|
||||||
|
n := (e[1] >> lengthBits) & lengthMask
|
||||||
|
if ct == cTitle {
|
||||||
|
n = e[1] & lengthMask
|
||||||
|
}
|
||||||
|
if n != noChange {
|
||||||
|
c.err = transform.ErrEndOfSpan
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// title writes the title case version of the current rune to dst.
|
||||||
|
func title(c *context) bool {
|
||||||
|
ct := c.caseType()
|
||||||
|
if c.info&hasMappingMask == 0 || ct == cTitle {
|
||||||
|
return c.copy()
|
||||||
|
}
|
||||||
|
if c.info&exceptionBit == 0 {
|
||||||
|
if ct == cLower {
|
||||||
|
return c.copyXOR()
|
||||||
|
}
|
||||||
|
return c.copy()
|
||||||
|
}
|
||||||
|
// Get the exception data.
|
||||||
|
e := exceptions[c.info>>exceptionShift:]
|
||||||
|
offset := 2 + e[0]&lengthMask // size of header + fold string
|
||||||
|
|
||||||
|
nFirst := (e[1] >> lengthBits) & lengthMask
|
||||||
|
if nTitle := e[1] & lengthMask; nTitle != noChange {
|
||||||
|
if nFirst != noChange {
|
||||||
|
e = e[nFirst:]
|
||||||
|
}
|
||||||
|
return c.writeString(e[offset : offset+nTitle])
|
||||||
|
}
|
||||||
|
if ct == cLower && nFirst != noChange {
|
||||||
|
// Use the uppercase version instead.
|
||||||
|
return c.writeString(e[offset : offset+nFirst])
|
||||||
|
}
|
||||||
|
// Already in correct case.
|
||||||
|
return c.copy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTitle reports whether the current rune is in title case.
|
||||||
|
func isTitle(c *context) bool {
|
||||||
|
ct := c.caseType()
|
||||||
|
if c.info&hasMappingMask == 0 || ct == cTitle {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if c.info&exceptionBit == 0 {
|
||||||
|
if ct == cLower {
|
||||||
|
c.err = transform.ErrEndOfSpan
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Get the exception data.
|
||||||
|
e := exceptions[c.info>>exceptionShift:]
|
||||||
|
if nTitle := e[1] & lengthMask; nTitle != noChange {
|
||||||
|
c.err = transform.ErrEndOfSpan
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
nFirst := (e[1] >> lengthBits) & lengthMask
|
||||||
|
if ct == cLower && nFirst != noChange {
|
||||||
|
c.err = transform.ErrEndOfSpan
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// foldFull writes the foldFull version of the current rune to dst.
|
||||||
|
func foldFull(c *context) bool {
|
||||||
|
if c.info&hasMappingMask == 0 {
|
||||||
|
return c.copy()
|
||||||
|
}
|
||||||
|
ct := c.caseType()
|
||||||
|
if c.info&exceptionBit == 0 {
|
||||||
|
if ct != cLower || c.info&inverseFoldBit != 0 {
|
||||||
|
return c.copyXOR()
|
||||||
|
}
|
||||||
|
return c.copy()
|
||||||
|
}
|
||||||
|
e := exceptions[c.info>>exceptionShift:]
|
||||||
|
n := e[0] & lengthMask
|
||||||
|
if n == 0 {
|
||||||
|
if ct == cLower {
|
||||||
|
return c.copy()
|
||||||
|
}
|
||||||
|
n = (e[1] >> lengthBits) & lengthMask
|
||||||
|
}
|
||||||
|
return c.writeString(e[2 : 2+n])
|
||||||
|
}
|
||||||
|
|
||||||
|
// isFoldFull reports whether the current run is mapped to foldFull
|
||||||
|
func isFoldFull(c *context) bool {
|
||||||
|
if c.info&hasMappingMask == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
ct := c.caseType()
|
||||||
|
if c.info&exceptionBit == 0 {
|
||||||
|
if ct != cLower || c.info&inverseFoldBit != 0 {
|
||||||
|
c.err = transform.ErrEndOfSpan
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
e := exceptions[c.info>>exceptionShift:]
|
||||||
|
n := e[0] & lengthMask
|
||||||
|
if n == 0 && ct == cLower {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
c.err = transform.ErrEndOfSpan
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
// Copyright 2016 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package cases
|
||||||
|
|
||||||
|
import "golang.org/x/text/transform"
|
||||||
|
|
||||||
|
type caseFolder struct{ transform.NopResetter }
|
||||||
|
|
||||||
|
// caseFolder implements the Transformer interface for doing case folding.
|
||||||
|
func (t *caseFolder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||||
|
c := context{dst: dst, src: src, atEOF: atEOF}
|
||||||
|
for c.next() {
|
||||||
|
foldFull(&c)
|
||||||
|
c.checkpoint()
|
||||||
|
}
|
||||||
|
return c.ret()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *caseFolder) Span(src []byte, atEOF bool) (n int, err error) {
|
||||||
|
c := context{src: src, atEOF: atEOF}
|
||||||
|
for c.next() && isFoldFull(&c) {
|
||||||
|
c.checkpoint()
|
||||||
|
}
|
||||||
|
return c.retSpan()
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeFold(o options) transform.SpanningTransformer {
|
||||||
|
// TODO: Special case folding, through option Language, Special/Turkic, or
|
||||||
|
// both.
|
||||||
|
// TODO: Implement Compact options.
|
||||||
|
return &caseFolder{}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
// Copyright 2016 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build icu
|
||||||
|
// +build icu
|
||||||
|
|
||||||
|
package cases
|
||||||
|
|
||||||
|
// Ideally these functions would be defined in a test file, but go test doesn't
|
||||||
|
// allow CGO in tests. The build tag should ensure either way that these
|
||||||
|
// functions will not end up in the package.
|
||||||
|
|
||||||
|
// TODO: Ensure that the correct ICU version is set.
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo LDFLAGS: -licui18n.57 -licuuc.57
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unicode/ustring.h>
|
||||||
|
#include <unicode/utypes.h>
|
||||||
|
#include <unicode/localpointer.h>
|
||||||
|
#include <unicode/ucasemap.h>
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import "unsafe"
|
||||||
|
|
||||||
|
func doICU(tag, caser, input string) string {
|
||||||
|
err := C.UErrorCode(0)
|
||||||
|
loc := C.CString(tag)
|
||||||
|
cm := C.ucasemap_open(loc, C.uint32_t(0), &err)
|
||||||
|
|
||||||
|
buf := make([]byte, len(input)*4)
|
||||||
|
dst := (*C.char)(unsafe.Pointer(&buf[0]))
|
||||||
|
src := C.CString(input)
|
||||||
|
|
||||||
|
cn := C.int32_t(0)
|
||||||
|
|
||||||
|
switch caser {
|
||||||
|
case "fold":
|
||||||
|
cn = C.ucasemap_utf8FoldCase(cm,
|
||||||
|
dst, C.int32_t(len(buf)),
|
||||||
|
src, C.int32_t(len(input)),
|
||||||
|
&err)
|
||||||
|
case "lower":
|
||||||
|
cn = C.ucasemap_utf8ToLower(cm,
|
||||||
|
dst, C.int32_t(len(buf)),
|
||||||
|
src, C.int32_t(len(input)),
|
||||||
|
&err)
|
||||||
|
case "upper":
|
||||||
|
cn = C.ucasemap_utf8ToUpper(cm,
|
||||||
|
dst, C.int32_t(len(buf)),
|
||||||
|
src, C.int32_t(len(input)),
|
||||||
|
&err)
|
||||||
|
case "title":
|
||||||
|
cn = C.ucasemap_utf8ToTitle(cm,
|
||||||
|
dst, C.int32_t(len(buf)),
|
||||||
|
src, C.int32_t(len(input)),
|
||||||
|
&err)
|
||||||
|
}
|
||||||
|
return string(buf[:cn])
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
// Copyright 2015 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package cases
|
||||||
|
|
||||||
|
func (c info) cccVal() info {
|
||||||
|
if c&exceptionBit != 0 {
|
||||||
|
return info(exceptions[c>>exceptionShift]) & cccMask
|
||||||
|
}
|
||||||
|
return c & cccMask
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c info) cccType() info {
|
||||||
|
ccc := c.cccVal()
|
||||||
|
if ccc <= cccZero {
|
||||||
|
return cccZero
|
||||||
|
}
|
||||||
|
return ccc
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement full Unicode breaking algorithm:
|
||||||
|
// 1) Implement breaking in separate package.
|
||||||
|
// 2) Use the breaker here.
|
||||||
|
// 3) Compare table size and performance of using the more generic breaker.
|
||||||
|
//
|
||||||
|
// Note that we can extend the current algorithm to be much more accurate. This
|
||||||
|
// only makes sense, though, if the performance and/or space penalty of using
|
||||||
|
// the generic breaker is big. Extra data will only be needed for non-cased
|
||||||
|
// runes, which means there are sufficient bits left in the caseType.
|
||||||
|
// ICU prohibits breaking in such cases as well.
|
||||||
|
|
||||||
|
// For the purpose of title casing we use an approximation of the Unicode Word
|
||||||
|
// Breaking algorithm defined in Annex #29:
|
||||||
|
// https://www.unicode.org/reports/tr29/#Default_Grapheme_Cluster_Table.
|
||||||
|
//
|
||||||
|
// For our approximation, we group the Word Break types into the following
|
||||||
|
// categories, with associated rules:
|
||||||
|
//
|
||||||
|
// 1) Letter:
|
||||||
|
// ALetter, Hebrew_Letter, Numeric, ExtendNumLet, Extend, Format_FE, ZWJ.
|
||||||
|
// Rule: Never break between consecutive runes of this category.
|
||||||
|
//
|
||||||
|
// 2) Mid:
|
||||||
|
// MidLetter, MidNumLet, Single_Quote.
|
||||||
|
// (Cf. case-ignorable: MidLetter, MidNumLet, Single_Quote or cat is Mn,
|
||||||
|
// Me, Cf, Lm or Sk).
|
||||||
|
// Rule: Don't break between Letter and Mid, but break between two Mids.
|
||||||
|
//
|
||||||
|
// 3) Break:
|
||||||
|
// Any other category: NewLine, MidNum, CR, LF, Double_Quote, Katakana, and
|
||||||
|
// Other.
|
||||||
|
// These categories should always result in a break between two cased letters.
|
||||||
|
// Rule: Always break.
|
||||||
|
//
|
||||||
|
// Note 1: the Katakana and MidNum categories can, in esoteric cases, result in
|
||||||
|
// preventing a break between two cased letters. For now we will ignore this
|
||||||
|
// (e.g. [ALetter] [ExtendNumLet] [Katakana] [ExtendNumLet] [ALetter] and
|
||||||
|
// [ALetter] [Numeric] [MidNum] [Numeric] [ALetter].)
|
||||||
|
//
|
||||||
|
// Note 2: the rule for Mid is very approximate, but works in most cases. To
|
||||||
|
// improve, we could store the categories in the trie value and use a FA to
|
||||||
|
// manage breaks. See TODO comment above.
|
||||||
|
//
|
||||||
|
// Note 3: according to the spec, it is possible for the Extend category to
|
||||||
|
// introduce breaks between other categories grouped in Letter. However, this
|
||||||
|
// is undesirable for our purposes. ICU prevents breaks in such cases as well.
|
||||||
|
|
||||||
|
// isBreak returns whether this rune should introduce a break.
|
||||||
|
func (c info) isBreak() bool {
|
||||||
|
return c.cccVal() == cccBreak
|
||||||
|
}
|
||||||
|
|
||||||
|
// isLetter returns whether the rune is of break type ALetter, Hebrew_Letter,
|
||||||
|
// Numeric, ExtendNumLet, or Extend.
|
||||||
|
func (c info) isLetter() bool {
|
||||||
|
ccc := c.cccVal()
|
||||||
|
if ccc == cccZero {
|
||||||
|
return !c.isCaseIgnorable()
|
||||||
|
}
|
||||||
|
return ccc != cccBreak
|
||||||
|
}
|
|
@ -0,0 +1,816 @@
|
||||||
|
// Copyright 2014 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package cases
|
||||||
|
|
||||||
|
// This file contains the definitions of case mappings for all supported
|
||||||
|
// languages. The rules for the language-specific tailorings were taken and
|
||||||
|
// modified from the CLDR transform definitions in common/transforms.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"golang.org/x/text/internal"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
"golang.org/x/text/transform"
|
||||||
|
"golang.org/x/text/unicode/norm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A mapFunc takes a context set to the current rune and writes the mapped
|
||||||
|
// version to the same context. It may advance the context to the next rune. It
|
||||||
|
// returns whether a checkpoint is possible: whether the pDst bytes written to
|
||||||
|
// dst so far won't need changing as we see more source bytes.
|
||||||
|
type mapFunc func(*context) bool
|
||||||
|
|
||||||
|
// A spanFunc takes a context set to the current rune and returns whether this
|
||||||
|
// rune would be altered when written to the output. It may advance the context
|
||||||
|
// to the next rune. It returns whether a checkpoint is possible.
|
||||||
|
type spanFunc func(*context) bool
|
||||||
|
|
||||||
|
// maxIgnorable defines the maximum number of ignorables to consider for
|
||||||
|
// lookahead operations.
|
||||||
|
const maxIgnorable = 30
|
||||||
|
|
||||||
|
// supported lists the language tags for which we have tailorings.
|
||||||
|
const supported = "und af az el lt nl tr"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
tags := []language.Tag{}
|
||||||
|
for _, s := range strings.Split(supported, " ") {
|
||||||
|
tags = append(tags, language.MustParse(s))
|
||||||
|
}
|
||||||
|
matcher = internal.NewInheritanceMatcher(tags)
|
||||||
|
Supported = language.NewCoverage(tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
matcher *internal.InheritanceMatcher
|
||||||
|
|
||||||
|
Supported language.Coverage
|
||||||
|
|
||||||
|
// We keep the following lists separate, instead of having a single per-
|
||||||
|
// language struct, to give the compiler a chance to remove unused code.
|
||||||
|
|
||||||
|
// Some uppercase mappers are stateless, so we can precompute the
|
||||||
|
// Transformers and save a bit on runtime allocations.
|
||||||
|
upperFunc = []struct {
|
||||||
|
upper mapFunc
|
||||||
|
span spanFunc
|
||||||
|
}{
|
||||||
|
{nil, nil}, // und
|
||||||
|
{nil, nil}, // af
|
||||||
|
{aztrUpper(upper), isUpper}, // az
|
||||||
|
{elUpper, noSpan}, // el
|
||||||
|
{ltUpper(upper), noSpan}, // lt
|
||||||
|
{nil, nil}, // nl
|
||||||
|
{aztrUpper(upper), isUpper}, // tr
|
||||||
|
}
|
||||||
|
|
||||||
|
undUpper transform.SpanningTransformer = &undUpperCaser{}
|
||||||
|
undLower transform.SpanningTransformer = &undLowerCaser{}
|
||||||
|
undLowerIgnoreSigma transform.SpanningTransformer = &undLowerIgnoreSigmaCaser{}
|
||||||
|
|
||||||
|
lowerFunc = []mapFunc{
|
||||||
|
nil, // und
|
||||||
|
nil, // af
|
||||||
|
aztrLower, // az
|
||||||
|
nil, // el
|
||||||
|
ltLower, // lt
|
||||||
|
nil, // nl
|
||||||
|
aztrLower, // tr
|
||||||
|
}
|
||||||
|
|
||||||
|
titleInfos = []struct {
|
||||||
|
title mapFunc
|
||||||
|
lower mapFunc
|
||||||
|
titleSpan spanFunc
|
||||||
|
rewrite func(*context)
|
||||||
|
}{
|
||||||
|
{title, lower, isTitle, nil}, // und
|
||||||
|
{title, lower, isTitle, afnlRewrite}, // af
|
||||||
|
{aztrUpper(title), aztrLower, isTitle, nil}, // az
|
||||||
|
{title, lower, isTitle, nil}, // el
|
||||||
|
{ltUpper(title), ltLower, noSpan, nil}, // lt
|
||||||
|
{nlTitle, lower, nlTitleSpan, afnlRewrite}, // nl
|
||||||
|
{aztrUpper(title), aztrLower, isTitle, nil}, // tr
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeUpper(t language.Tag, o options) transform.SpanningTransformer {
|
||||||
|
_, i, _ := matcher.Match(t)
|
||||||
|
f := upperFunc[i].upper
|
||||||
|
if f == nil {
|
||||||
|
return undUpper
|
||||||
|
}
|
||||||
|
return &simpleCaser{f: f, span: upperFunc[i].span}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeLower(t language.Tag, o options) transform.SpanningTransformer {
|
||||||
|
_, i, _ := matcher.Match(t)
|
||||||
|
f := lowerFunc[i]
|
||||||
|
if f == nil {
|
||||||
|
if o.ignoreFinalSigma {
|
||||||
|
return undLowerIgnoreSigma
|
||||||
|
}
|
||||||
|
return undLower
|
||||||
|
}
|
||||||
|
if o.ignoreFinalSigma {
|
||||||
|
return &simpleCaser{f: f, span: isLower}
|
||||||
|
}
|
||||||
|
return &lowerCaser{
|
||||||
|
first: f,
|
||||||
|
midWord: finalSigma(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeTitle(t language.Tag, o options) transform.SpanningTransformer {
|
||||||
|
_, i, _ := matcher.Match(t)
|
||||||
|
x := &titleInfos[i]
|
||||||
|
lower := x.lower
|
||||||
|
if o.noLower {
|
||||||
|
lower = (*context).copy
|
||||||
|
} else if !o.ignoreFinalSigma {
|
||||||
|
lower = finalSigma(lower)
|
||||||
|
}
|
||||||
|
return &titleCaser{
|
||||||
|
title: x.title,
|
||||||
|
lower: lower,
|
||||||
|
titleSpan: x.titleSpan,
|
||||||
|
rewrite: x.rewrite,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func noSpan(c *context) bool {
|
||||||
|
c.err = transform.ErrEndOfSpan
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: consider a similar special case for the fast majority lower case. This
|
||||||
|
// is a bit more involved so will require some more precise benchmarking to
|
||||||
|
// justify it.
|
||||||
|
|
||||||
|
type undUpperCaser struct{ transform.NopResetter }
|
||||||
|
|
||||||
|
// undUpperCaser implements the Transformer interface for doing an upper case
|
||||||
|
// mapping for the root locale (und). It eliminates the need for an allocation
|
||||||
|
// as it prevents escaping by not using function pointers.
|
||||||
|
func (t undUpperCaser) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||||
|
c := context{dst: dst, src: src, atEOF: atEOF}
|
||||||
|
for c.next() {
|
||||||
|
upper(&c)
|
||||||
|
c.checkpoint()
|
||||||
|
}
|
||||||
|
return c.ret()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t undUpperCaser) Span(src []byte, atEOF bool) (n int, err error) {
|
||||||
|
c := context{src: src, atEOF: atEOF}
|
||||||
|
for c.next() && isUpper(&c) {
|
||||||
|
c.checkpoint()
|
||||||
|
}
|
||||||
|
return c.retSpan()
|
||||||
|
}
|
||||||
|
|
||||||
|
// undLowerIgnoreSigmaCaser implements the Transformer interface for doing
|
||||||
|
// a lower case mapping for the root locale (und) ignoring final sigma
|
||||||
|
// handling. This casing algorithm is used in some performance-critical packages
|
||||||
|
// like secure/precis and x/net/http/idna, which warrants its special-casing.
|
||||||
|
type undLowerIgnoreSigmaCaser struct{ transform.NopResetter }
|
||||||
|
|
||||||
|
func (t undLowerIgnoreSigmaCaser) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||||
|
c := context{dst: dst, src: src, atEOF: atEOF}
|
||||||
|
for c.next() && lower(&c) {
|
||||||
|
c.checkpoint()
|
||||||
|
}
|
||||||
|
return c.ret()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Span implements a generic lower-casing. This is possible as isLower works
|
||||||
|
// for all lowercasing variants. All lowercase variants only vary in how they
|
||||||
|
// transform a non-lowercase letter. They will never change an already lowercase
|
||||||
|
// letter. In addition, there is no state.
|
||||||
|
func (t undLowerIgnoreSigmaCaser) Span(src []byte, atEOF bool) (n int, err error) {
|
||||||
|
c := context{src: src, atEOF: atEOF}
|
||||||
|
for c.next() && isLower(&c) {
|
||||||
|
c.checkpoint()
|
||||||
|
}
|
||||||
|
return c.retSpan()
|
||||||
|
}
|
||||||
|
|
||||||
|
type simpleCaser struct {
|
||||||
|
context
|
||||||
|
f mapFunc
|
||||||
|
span spanFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// simpleCaser implements the Transformer interface for doing a case operation
|
||||||
|
// on a rune-by-rune basis.
|
||||||
|
func (t *simpleCaser) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||||
|
c := context{dst: dst, src: src, atEOF: atEOF}
|
||||||
|
for c.next() && t.f(&c) {
|
||||||
|
c.checkpoint()
|
||||||
|
}
|
||||||
|
return c.ret()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *simpleCaser) Span(src []byte, atEOF bool) (n int, err error) {
|
||||||
|
c := context{src: src, atEOF: atEOF}
|
||||||
|
for c.next() && t.span(&c) {
|
||||||
|
c.checkpoint()
|
||||||
|
}
|
||||||
|
return c.retSpan()
|
||||||
|
}
|
||||||
|
|
||||||
|
// undLowerCaser implements the Transformer interface for doing a lower case
|
||||||
|
// mapping for the root locale (und) ignoring final sigma handling. This casing
|
||||||
|
// algorithm is used in some performance-critical packages like secure/precis
|
||||||
|
// and x/net/http/idna, which warrants its special-casing.
|
||||||
|
type undLowerCaser struct{ transform.NopResetter }
|
||||||
|
|
||||||
|
func (t undLowerCaser) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||||
|
c := context{dst: dst, src: src, atEOF: atEOF}
|
||||||
|
|
||||||
|
for isInterWord := true; c.next(); {
|
||||||
|
if isInterWord {
|
||||||
|
if c.info.isCased() {
|
||||||
|
if !lower(&c) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
isInterWord = false
|
||||||
|
} else if !c.copy() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if c.info.isNotCasedAndNotCaseIgnorable() {
|
||||||
|
if !c.copy() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
isInterWord = true
|
||||||
|
} else if !c.hasPrefix("Σ") {
|
||||||
|
if !lower(&c) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if !finalSigmaBody(&c) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.checkpoint()
|
||||||
|
}
|
||||||
|
return c.ret()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t undLowerCaser) Span(src []byte, atEOF bool) (n int, err error) {
|
||||||
|
c := context{src: src, atEOF: atEOF}
|
||||||
|
for c.next() && isLower(&c) {
|
||||||
|
c.checkpoint()
|
||||||
|
}
|
||||||
|
return c.retSpan()
|
||||||
|
}
|
||||||
|
|
||||||
|
// lowerCaser implements the Transformer interface. The default Unicode lower
|
||||||
|
// casing requires different treatment for the first and subsequent characters
|
||||||
|
// of a word, most notably to handle the Greek final Sigma.
|
||||||
|
type lowerCaser struct {
|
||||||
|
undLowerIgnoreSigmaCaser
|
||||||
|
|
||||||
|
context
|
||||||
|
|
||||||
|
first, midWord mapFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *lowerCaser) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||||
|
t.context = context{dst: dst, src: src, atEOF: atEOF}
|
||||||
|
c := &t.context
|
||||||
|
|
||||||
|
for isInterWord := true; c.next(); {
|
||||||
|
if isInterWord {
|
||||||
|
if c.info.isCased() {
|
||||||
|
if !t.first(c) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
isInterWord = false
|
||||||
|
} else if !c.copy() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if c.info.isNotCasedAndNotCaseIgnorable() {
|
||||||
|
if !c.copy() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
isInterWord = true
|
||||||
|
} else if !t.midWord(c) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.checkpoint()
|
||||||
|
}
|
||||||
|
return c.ret()
|
||||||
|
}
|
||||||
|
|
||||||
|
// titleCaser implements the Transformer interface. Title casing algorithms
|
||||||
|
// distinguish between the first letter of a word and subsequent letters of the
|
||||||
|
// same word. It uses state to avoid requiring a potentially infinite lookahead.
|
||||||
|
type titleCaser struct {
|
||||||
|
context
|
||||||
|
|
||||||
|
// rune mappings used by the actual casing algorithms.
|
||||||
|
title mapFunc
|
||||||
|
lower mapFunc
|
||||||
|
titleSpan spanFunc
|
||||||
|
|
||||||
|
rewrite func(*context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform implements the standard Unicode title case algorithm as defined in
|
||||||
|
// Chapter 3 of The Unicode Standard:
|
||||||
|
// toTitlecase(X): Find the word boundaries in X according to Unicode Standard
|
||||||
|
// Annex #29, "Unicode Text Segmentation." For each word boundary, find the
|
||||||
|
// first cased character F following the word boundary. If F exists, map F to
|
||||||
|
// Titlecase_Mapping(F); then map all characters C between F and the following
|
||||||
|
// word boundary to Lowercase_Mapping(C).
|
||||||
|
func (t *titleCaser) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||||
|
t.context = context{dst: dst, src: src, atEOF: atEOF, isMidWord: t.isMidWord}
|
||||||
|
c := &t.context
|
||||||
|
|
||||||
|
if !c.next() {
|
||||||
|
return c.ret()
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
p := c.info
|
||||||
|
if t.rewrite != nil {
|
||||||
|
t.rewrite(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
wasMid := p.isMid()
|
||||||
|
// Break out of this loop on failure to ensure we do not modify the
|
||||||
|
// state incorrectly.
|
||||||
|
if p.isCased() {
|
||||||
|
if !c.isMidWord {
|
||||||
|
if !t.title(c) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
c.isMidWord = true
|
||||||
|
} else if !t.lower(c) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if !c.copy() {
|
||||||
|
break
|
||||||
|
} else if p.isBreak() {
|
||||||
|
c.isMidWord = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// As we save the state of the transformer, it is safe to call
|
||||||
|
// checkpoint after any successful write.
|
||||||
|
if !(c.isMidWord && wasMid) {
|
||||||
|
c.checkpoint()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.next() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if wasMid && c.info.isMid() {
|
||||||
|
c.isMidWord = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.ret()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *titleCaser) Span(src []byte, atEOF bool) (n int, err error) {
|
||||||
|
t.context = context{src: src, atEOF: atEOF, isMidWord: t.isMidWord}
|
||||||
|
c := &t.context
|
||||||
|
|
||||||
|
if !c.next() {
|
||||||
|
return c.retSpan()
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
p := c.info
|
||||||
|
if t.rewrite != nil {
|
||||||
|
t.rewrite(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
wasMid := p.isMid()
|
||||||
|
// Break out of this loop on failure to ensure we do not modify the
|
||||||
|
// state incorrectly.
|
||||||
|
if p.isCased() {
|
||||||
|
if !c.isMidWord {
|
||||||
|
if !t.titleSpan(c) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
c.isMidWord = true
|
||||||
|
} else if !isLower(c) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if p.isBreak() {
|
||||||
|
c.isMidWord = false
|
||||||
|
}
|
||||||
|
// As we save the state of the transformer, it is safe to call
|
||||||
|
// checkpoint after any successful write.
|
||||||
|
if !(c.isMidWord && wasMid) {
|
||||||
|
c.checkpoint()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.next() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if wasMid && c.info.isMid() {
|
||||||
|
c.isMidWord = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.retSpan()
|
||||||
|
}
|
||||||
|
|
||||||
|
// finalSigma adds Greek final Sigma handing to another casing function. It
|
||||||
|
// determines whether a lowercased sigma should be σ or ς, by looking ahead for
|
||||||
|
// case-ignorables and a cased letters.
|
||||||
|
func finalSigma(f mapFunc) mapFunc {
|
||||||
|
return func(c *context) bool {
|
||||||
|
if !c.hasPrefix("Σ") {
|
||||||
|
return f(c)
|
||||||
|
}
|
||||||
|
return finalSigmaBody(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func finalSigmaBody(c *context) bool {
|
||||||
|
// Current rune must be ∑.
|
||||||
|
|
||||||
|
// ::NFD();
|
||||||
|
// # 03A3; 03C2; 03A3; 03A3; Final_Sigma; # GREEK CAPITAL LETTER SIGMA
|
||||||
|
// Σ } [:case-ignorable:]* [:cased:] → σ;
|
||||||
|
// [:cased:] [:case-ignorable:]* { Σ → ς;
|
||||||
|
// ::Any-Lower;
|
||||||
|
// ::NFC();
|
||||||
|
|
||||||
|
p := c.pDst
|
||||||
|
c.writeString("ς")
|
||||||
|
|
||||||
|
// TODO: we should do this here, but right now this will never have an
|
||||||
|
// effect as this is called when the prefix is Sigma, whereas Dutch and
|
||||||
|
// Afrikaans only test for an apostrophe.
|
||||||
|
//
|
||||||
|
// if t.rewrite != nil {
|
||||||
|
// t.rewrite(c)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// We need to do one more iteration after maxIgnorable, as a cased
|
||||||
|
// letter is not an ignorable and may modify the result.
|
||||||
|
wasMid := false
|
||||||
|
for i := 0; i < maxIgnorable+1; i++ {
|
||||||
|
if !c.next() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !c.info.isCaseIgnorable() {
|
||||||
|
// All Midword runes are also case ignorable, so we are
|
||||||
|
// guaranteed to have a letter or word break here. As we are
|
||||||
|
// unreading the run, there is no need to unset c.isMidWord;
|
||||||
|
// the title caser will handle this.
|
||||||
|
if c.info.isCased() {
|
||||||
|
// p+1 is guaranteed to be in bounds: if writing ς was
|
||||||
|
// successful, p+1 will contain the second byte of ς. If not,
|
||||||
|
// this function will have returned after c.next returned false.
|
||||||
|
c.dst[p+1]++ // ς → σ
|
||||||
|
}
|
||||||
|
c.unreadRune()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// A case ignorable may also introduce a word break, so we may need
|
||||||
|
// to continue searching even after detecting a break.
|
||||||
|
isMid := c.info.isMid()
|
||||||
|
if (wasMid && isMid) || c.info.isBreak() {
|
||||||
|
c.isMidWord = false
|
||||||
|
}
|
||||||
|
wasMid = isMid
|
||||||
|
c.copy()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// finalSigmaSpan would be the same as isLower.
|
||||||
|
|
||||||
|
// elUpper implements Greek upper casing, which entails removing a predefined
|
||||||
|
// set of non-blocked modifiers. Note that these accents should not be removed
|
||||||
|
// for title casing!
|
||||||
|
// Example: "Οδός" -> "ΟΔΟΣ".
|
||||||
|
func elUpper(c *context) bool {
|
||||||
|
// From CLDR:
|
||||||
|
// [:Greek:] [^[:ccc=Not_Reordered:][:ccc=Above:]]*? { [\u0313\u0314\u0301\u0300\u0306\u0342\u0308\u0304] → ;
|
||||||
|
// [:Greek:] [^[:ccc=Not_Reordered:][:ccc=Iota_Subscript:]]*? { \u0345 → ;
|
||||||
|
|
||||||
|
r, _ := utf8.DecodeRune(c.src[c.pSrc:])
|
||||||
|
oldPDst := c.pDst
|
||||||
|
if !upper(c) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !unicode.Is(unicode.Greek, r) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
i := 0
|
||||||
|
// Take the properties of the uppercased rune that is already written to the
|
||||||
|
// destination. This saves us the trouble of having to uppercase the
|
||||||
|
// decomposed rune again.
|
||||||
|
if b := norm.NFD.Properties(c.dst[oldPDst:]).Decomposition(); b != nil {
|
||||||
|
// Restore the destination position and process the decomposed rune.
|
||||||
|
r, sz := utf8.DecodeRune(b)
|
||||||
|
if r <= 0xFF { // See A.6.1
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
c.pDst = oldPDst
|
||||||
|
// Insert the first rune and ignore the modifiers. See A.6.2.
|
||||||
|
c.writeBytes(b[:sz])
|
||||||
|
i = len(b[sz:]) / 2 // Greek modifiers are always of length 2.
|
||||||
|
}
|
||||||
|
|
||||||
|
for ; i < maxIgnorable && c.next(); i++ {
|
||||||
|
switch r, _ := utf8.DecodeRune(c.src[c.pSrc:]); r {
|
||||||
|
// Above and Iota Subscript
|
||||||
|
case 0x0300, // U+0300 COMBINING GRAVE ACCENT
|
||||||
|
0x0301, // U+0301 COMBINING ACUTE ACCENT
|
||||||
|
0x0304, // U+0304 COMBINING MACRON
|
||||||
|
0x0306, // U+0306 COMBINING BREVE
|
||||||
|
0x0308, // U+0308 COMBINING DIAERESIS
|
||||||
|
0x0313, // U+0313 COMBINING COMMA ABOVE
|
||||||
|
0x0314, // U+0314 COMBINING REVERSED COMMA ABOVE
|
||||||
|
0x0342, // U+0342 COMBINING GREEK PERISPOMENI
|
||||||
|
0x0345: // U+0345 COMBINING GREEK YPOGEGRAMMENI
|
||||||
|
// No-op. Gobble the modifier.
|
||||||
|
|
||||||
|
default:
|
||||||
|
switch v, _ := trie.lookup(c.src[c.pSrc:]); info(v).cccType() {
|
||||||
|
case cccZero:
|
||||||
|
c.unreadRune()
|
||||||
|
return true
|
||||||
|
|
||||||
|
// We don't need to test for IotaSubscript as the only rune that
|
||||||
|
// qualifies (U+0345) was already excluded in the switch statement
|
||||||
|
// above. See A.4.
|
||||||
|
|
||||||
|
case cccAbove:
|
||||||
|
return c.copy()
|
||||||
|
default:
|
||||||
|
// Some other modifier. We're still allowed to gobble Greek
|
||||||
|
// modifiers after this.
|
||||||
|
c.copy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i == maxIgnorable
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement elUpperSpan (low-priority: complex and infrequent).
|
||||||
|
|
||||||
|
func ltLower(c *context) bool {
|
||||||
|
// From CLDR:
|
||||||
|
// # Introduce an explicit dot above when lowercasing capital I's and J's
|
||||||
|
// # whenever there are more accents above.
|
||||||
|
// # (of the accents used in Lithuanian: grave, acute, tilde above, and ogonek)
|
||||||
|
// # 0049; 0069 0307; 0049; 0049; lt More_Above; # LATIN CAPITAL LETTER I
|
||||||
|
// # 004A; 006A 0307; 004A; 004A; lt More_Above; # LATIN CAPITAL LETTER J
|
||||||
|
// # 012E; 012F 0307; 012E; 012E; lt More_Above; # LATIN CAPITAL LETTER I WITH OGONEK
|
||||||
|
// # 00CC; 0069 0307 0300; 00CC; 00CC; lt; # LATIN CAPITAL LETTER I WITH GRAVE
|
||||||
|
// # 00CD; 0069 0307 0301; 00CD; 00CD; lt; # LATIN CAPITAL LETTER I WITH ACUTE
|
||||||
|
// # 0128; 0069 0307 0303; 0128; 0128; lt; # LATIN CAPITAL LETTER I WITH TILDE
|
||||||
|
// ::NFD();
|
||||||
|
// I } [^[:ccc=Not_Reordered:][:ccc=Above:]]* [:ccc=Above:] → i \u0307;
|
||||||
|
// J } [^[:ccc=Not_Reordered:][:ccc=Above:]]* [:ccc=Above:] → j \u0307;
|
||||||
|
// I \u0328 (Į) } [^[:ccc=Not_Reordered:][:ccc=Above:]]* [:ccc=Above:] → i \u0328 \u0307;
|
||||||
|
// I \u0300 (Ì) → i \u0307 \u0300;
|
||||||
|
// I \u0301 (Í) → i \u0307 \u0301;
|
||||||
|
// I \u0303 (Ĩ) → i \u0307 \u0303;
|
||||||
|
// ::Any-Lower();
|
||||||
|
// ::NFC();
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
if r := c.src[c.pSrc]; r < utf8.RuneSelf {
|
||||||
|
lower(c)
|
||||||
|
if r != 'I' && r != 'J' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p := norm.NFD.Properties(c.src[c.pSrc:])
|
||||||
|
if d := p.Decomposition(); len(d) >= 3 && (d[0] == 'I' || d[0] == 'J') {
|
||||||
|
// UTF-8 optimization: the decomposition will only have an above
|
||||||
|
// modifier if the last rune of the decomposition is in [U+300-U+311].
|
||||||
|
// In all other cases, a decomposition starting with I is always
|
||||||
|
// an I followed by modifiers that are not cased themselves. See A.2.
|
||||||
|
if d[1] == 0xCC && d[2] <= 0x91 { // A.2.4.
|
||||||
|
if !c.writeBytes(d[:1]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
c.dst[c.pDst-1] += 'a' - 'A' // lower
|
||||||
|
|
||||||
|
// Assumption: modifier never changes on lowercase. See A.1.
|
||||||
|
// Assumption: all modifiers added have CCC = Above. See A.2.3.
|
||||||
|
return c.writeString("\u0307") && c.writeBytes(d[1:])
|
||||||
|
}
|
||||||
|
// In all other cases the additional modifiers will have a CCC
|
||||||
|
// that is less than 230 (Above). We will insert the U+0307, if
|
||||||
|
// needed, after these modifiers so that a string in FCD form
|
||||||
|
// will remain so. See A.2.2.
|
||||||
|
lower(c)
|
||||||
|
i = 1
|
||||||
|
} else {
|
||||||
|
return lower(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ; i < maxIgnorable && c.next(); i++ {
|
||||||
|
switch c.info.cccType() {
|
||||||
|
case cccZero:
|
||||||
|
c.unreadRune()
|
||||||
|
return true
|
||||||
|
case cccAbove:
|
||||||
|
return c.writeString("\u0307") && c.copy() // See A.1.
|
||||||
|
default:
|
||||||
|
c.copy() // See A.1.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i == maxIgnorable
|
||||||
|
}
|
||||||
|
|
||||||
|
// ltLowerSpan would be the same as isLower.
|
||||||
|
|
||||||
|
func ltUpper(f mapFunc) mapFunc {
|
||||||
|
return func(c *context) bool {
|
||||||
|
// Unicode:
|
||||||
|
// 0307; 0307; ; ; lt After_Soft_Dotted; # COMBINING DOT ABOVE
|
||||||
|
//
|
||||||
|
// From CLDR:
|
||||||
|
// # Remove \u0307 following soft-dotteds (i, j, and the like), with possible
|
||||||
|
// # intervening non-230 marks.
|
||||||
|
// ::NFD();
|
||||||
|
// [:Soft_Dotted:] [^[:ccc=Not_Reordered:][:ccc=Above:]]* { \u0307 → ;
|
||||||
|
// ::Any-Upper();
|
||||||
|
// ::NFC();
|
||||||
|
|
||||||
|
// TODO: See A.5. A soft-dotted rune never has an exception. This would
|
||||||
|
// allow us to overload the exception bit and encode this property in
|
||||||
|
// info. Need to measure performance impact of this.
|
||||||
|
r, _ := utf8.DecodeRune(c.src[c.pSrc:])
|
||||||
|
oldPDst := c.pDst
|
||||||
|
if !f(c) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !unicode.Is(unicode.Soft_Dotted, r) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't need to do an NFD normalization, as a soft-dotted rune never
|
||||||
|
// contains U+0307. See A.3.
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for ; i < maxIgnorable && c.next(); i++ {
|
||||||
|
switch c.info.cccType() {
|
||||||
|
case cccZero:
|
||||||
|
c.unreadRune()
|
||||||
|
return true
|
||||||
|
case cccAbove:
|
||||||
|
if c.hasPrefix("\u0307") {
|
||||||
|
// We don't do a full NFC, but rather combine runes for
|
||||||
|
// some of the common cases. (Returning NFC or
|
||||||
|
// preserving normal form is neither a requirement nor
|
||||||
|
// a possibility anyway).
|
||||||
|
if !c.next() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if c.dst[oldPDst] == 'I' && c.pDst == oldPDst+1 && c.src[c.pSrc] == 0xcc {
|
||||||
|
s := ""
|
||||||
|
switch c.src[c.pSrc+1] {
|
||||||
|
case 0x80: // U+0300 COMBINING GRAVE ACCENT
|
||||||
|
s = "\u00cc" // U+00CC LATIN CAPITAL LETTER I WITH GRAVE
|
||||||
|
case 0x81: // U+0301 COMBINING ACUTE ACCENT
|
||||||
|
s = "\u00cd" // U+00CD LATIN CAPITAL LETTER I WITH ACUTE
|
||||||
|
case 0x83: // U+0303 COMBINING TILDE
|
||||||
|
s = "\u0128" // U+0128 LATIN CAPITAL LETTER I WITH TILDE
|
||||||
|
case 0x88: // U+0308 COMBINING DIAERESIS
|
||||||
|
s = "\u00cf" // U+00CF LATIN CAPITAL LETTER I WITH DIAERESIS
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if s != "" {
|
||||||
|
c.pDst = oldPDst
|
||||||
|
return c.writeString(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.copy()
|
||||||
|
default:
|
||||||
|
c.copy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i == maxIgnorable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement ltUpperSpan (low priority: complex and infrequent).
|
||||||
|
|
||||||
|
func aztrUpper(f mapFunc) mapFunc {
|
||||||
|
return func(c *context) bool {
|
||||||
|
// i→İ;
|
||||||
|
if c.src[c.pSrc] == 'i' {
|
||||||
|
return c.writeString("İ")
|
||||||
|
}
|
||||||
|
return f(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func aztrLower(c *context) (done bool) {
|
||||||
|
// From CLDR:
|
||||||
|
// # I and i-dotless; I-dot and i are case pairs in Turkish and Azeri
|
||||||
|
// # 0130; 0069; 0130; 0130; tr; # LATIN CAPITAL LETTER I WITH DOT ABOVE
|
||||||
|
// İ→i;
|
||||||
|
// # When lowercasing, remove dot_above in the sequence I + dot_above, which will turn into i.
|
||||||
|
// # This matches the behavior of the canonically equivalent I-dot_above
|
||||||
|
// # 0307; ; 0307; 0307; tr After_I; # COMBINING DOT ABOVE
|
||||||
|
// # When lowercasing, unless an I is before a dot_above, it turns into a dotless i.
|
||||||
|
// # 0049; 0131; 0049; 0049; tr Not_Before_Dot; # LATIN CAPITAL LETTER I
|
||||||
|
// I([^[:ccc=Not_Reordered:][:ccc=Above:]]*)\u0307 → i$1 ;
|
||||||
|
// I→ı ;
|
||||||
|
// ::Any-Lower();
|
||||||
|
if c.hasPrefix("\u0130") { // İ
|
||||||
|
return c.writeString("i")
|
||||||
|
}
|
||||||
|
if c.src[c.pSrc] != 'I' {
|
||||||
|
return lower(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We ignore the lower-case I for now, but insert it later when we know
|
||||||
|
// which form we need.
|
||||||
|
start := c.pSrc + c.sz
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
Loop:
|
||||||
|
// We check for up to n ignorables before \u0307. As \u0307 is an
|
||||||
|
// ignorable as well, n is maxIgnorable-1.
|
||||||
|
for ; i < maxIgnorable && c.next(); i++ {
|
||||||
|
switch c.info.cccType() {
|
||||||
|
case cccAbove:
|
||||||
|
if c.hasPrefix("\u0307") {
|
||||||
|
return c.writeString("i") && c.writeBytes(c.src[start:c.pSrc]) // ignore U+0307
|
||||||
|
}
|
||||||
|
done = true
|
||||||
|
break Loop
|
||||||
|
case cccZero:
|
||||||
|
c.unreadRune()
|
||||||
|
done = true
|
||||||
|
break Loop
|
||||||
|
default:
|
||||||
|
// We'll write this rune after we know which starter to use.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i == maxIgnorable {
|
||||||
|
done = true
|
||||||
|
}
|
||||||
|
return c.writeString("ı") && c.writeBytes(c.src[start:c.pSrc+c.sz]) && done
|
||||||
|
}
|
||||||
|
|
||||||
|
// aztrLowerSpan would be the same as isLower.
|
||||||
|
|
||||||
|
func nlTitle(c *context) bool {
|
||||||
|
// From CLDR:
|
||||||
|
// # Special titlecasing for Dutch initial "ij".
|
||||||
|
// ::Any-Title();
|
||||||
|
// # Fix up Ij at the beginning of a "word" (per Any-Title, notUAX #29)
|
||||||
|
// [:^WB=ALetter:] [:WB=Extend:]* [[:WB=MidLetter:][:WB=MidNumLet:]]? { Ij } → IJ ;
|
||||||
|
if c.src[c.pSrc] != 'I' && c.src[c.pSrc] != 'i' {
|
||||||
|
return title(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.writeString("I") || !c.next() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if c.src[c.pSrc] == 'j' || c.src[c.pSrc] == 'J' {
|
||||||
|
return c.writeString("J")
|
||||||
|
}
|
||||||
|
c.unreadRune()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func nlTitleSpan(c *context) bool {
|
||||||
|
// From CLDR:
|
||||||
|
// # Special titlecasing for Dutch initial "ij".
|
||||||
|
// ::Any-Title();
|
||||||
|
// # Fix up Ij at the beginning of a "word" (per Any-Title, notUAX #29)
|
||||||
|
// [:^WB=ALetter:] [:WB=Extend:]* [[:WB=MidLetter:][:WB=MidNumLet:]]? { Ij } → IJ ;
|
||||||
|
if c.src[c.pSrc] != 'I' {
|
||||||
|
return isTitle(c)
|
||||||
|
}
|
||||||
|
if !c.next() || c.src[c.pSrc] == 'j' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if c.src[c.pSrc] != 'J' {
|
||||||
|
c.unreadRune()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not part of CLDR, but see https://unicode.org/cldr/trac/ticket/7078.
|
||||||
|
func afnlRewrite(c *context) {
|
||||||
|
if c.hasPrefix("'") || c.hasPrefix("’") {
|
||||||
|
c.isMidWord = true
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,217 @@
|
||||||
|
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
|
||||||
|
|
||||||
|
package cases
|
||||||
|
|
||||||
|
// This file contains definitions for interpreting the trie value of the case
|
||||||
|
// trie generated by "go run gen*.go". It is shared by both the generator
|
||||||
|
// program and the resultant package. Sharing is achieved by the generator
|
||||||
|
// copying gen_trieval.go to trieval.go and changing what's above this comment.
|
||||||
|
|
||||||
|
// info holds case information for a single rune. It is the value returned
|
||||||
|
// by a trie lookup. Most mapping information can be stored in a single 16-bit
|
||||||
|
// value. If not, for example when a rune is mapped to multiple runes, the value
|
||||||
|
// stores some basic case data and an index into an array with additional data.
|
||||||
|
//
|
||||||
|
// The per-rune values have the following format:
|
||||||
|
//
|
||||||
|
// if (exception) {
|
||||||
|
// 15..4 unsigned exception index
|
||||||
|
// } else {
|
||||||
|
// 15..8 XOR pattern or index to XOR pattern for case mapping
|
||||||
|
// Only 13..8 are used for XOR patterns.
|
||||||
|
// 7 inverseFold (fold to upper, not to lower)
|
||||||
|
// 6 index: interpret the XOR pattern as an index
|
||||||
|
// or isMid if case mode is cIgnorableUncased.
|
||||||
|
// 5..4 CCC: zero (normal or break), above or other
|
||||||
|
// }
|
||||||
|
// 3 exception: interpret this value as an exception index
|
||||||
|
// (TODO: is this bit necessary? Probably implied from case mode.)
|
||||||
|
// 2..0 case mode
|
||||||
|
//
|
||||||
|
// For the non-exceptional cases, a rune must be either uncased, lowercase or
|
||||||
|
// uppercase. If the rune is cased, the XOR pattern maps either a lowercase
|
||||||
|
// rune to uppercase or an uppercase rune to lowercase (applied to the 10
|
||||||
|
// least-significant bits of the rune).
|
||||||
|
//
|
||||||
|
// See the definitions below for a more detailed description of the various
|
||||||
|
// bits.
|
||||||
|
type info uint16
|
||||||
|
|
||||||
|
const (
|
||||||
|
casedMask = 0x0003
|
||||||
|
fullCasedMask = 0x0007
|
||||||
|
ignorableMask = 0x0006
|
||||||
|
ignorableValue = 0x0004
|
||||||
|
|
||||||
|
inverseFoldBit = 1 << 7
|
||||||
|
isMidBit = 1 << 6
|
||||||
|
|
||||||
|
exceptionBit = 1 << 3
|
||||||
|
exceptionShift = 4
|
||||||
|
numExceptionBits = 12
|
||||||
|
|
||||||
|
xorIndexBit = 1 << 6
|
||||||
|
xorShift = 8
|
||||||
|
|
||||||
|
// There is no mapping if all xor bits and the exception bit are zero.
|
||||||
|
hasMappingMask = 0xff80 | exceptionBit
|
||||||
|
)
|
||||||
|
|
||||||
|
// The case mode bits encodes the case type of a rune. This includes uncased,
|
||||||
|
// title, upper and lower case and case ignorable. (For a definition of these
|
||||||
|
// terms see Chapter 3 of The Unicode Standard Core Specification.) In some rare
|
||||||
|
// cases, a rune can be both cased and case-ignorable. This is encoded by
|
||||||
|
// cIgnorableCased. A rune of this type is always lower case. Some runes are
|
||||||
|
// cased while not having a mapping.
|
||||||
|
//
|
||||||
|
// A common pattern for scripts in the Unicode standard is for upper and lower
|
||||||
|
// case runes to alternate for increasing rune values (e.g. the accented Latin
|
||||||
|
// ranges starting from U+0100 and U+1E00 among others and some Cyrillic
|
||||||
|
// characters). We use this property by defining a cXORCase mode, where the case
|
||||||
|
// mode (always upper or lower case) is derived from the rune value. As the XOR
|
||||||
|
// pattern for case mappings is often identical for successive runes, using
|
||||||
|
// cXORCase can result in large series of identical trie values. This, in turn,
|
||||||
|
// allows us to better compress the trie blocks.
|
||||||
|
const (
|
||||||
|
cUncased info = iota // 000
|
||||||
|
cTitle // 001
|
||||||
|
cLower // 010
|
||||||
|
cUpper // 011
|
||||||
|
cIgnorableUncased // 100
|
||||||
|
cIgnorableCased // 101 // lower case if mappings exist
|
||||||
|
cXORCase // 11x // case is cLower | ((rune&1) ^ x)
|
||||||
|
|
||||||
|
maxCaseMode = cUpper
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c info) isCased() bool {
|
||||||
|
return c&casedMask != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c info) isCaseIgnorable() bool {
|
||||||
|
return c&ignorableMask == ignorableValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c info) isNotCasedAndNotCaseIgnorable() bool {
|
||||||
|
return c&fullCasedMask == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c info) isCaseIgnorableAndNotCased() bool {
|
||||||
|
return c&fullCasedMask == cIgnorableUncased
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c info) isMid() bool {
|
||||||
|
return c&(fullCasedMask|isMidBit) == isMidBit|cIgnorableUncased
|
||||||
|
}
|
||||||
|
|
||||||
|
// The case mapping implementation will need to know about various Canonical
|
||||||
|
// Combining Class (CCC) values. We encode two of these in the trie value:
|
||||||
|
// cccZero (0) and cccAbove (230). If the value is cccOther, it means that
|
||||||
|
// CCC(r) > 0, but not 230. A value of cccBreak means that CCC(r) == 0 and that
|
||||||
|
// the rune also has the break category Break (see below).
|
||||||
|
const (
|
||||||
|
cccBreak info = iota << 4
|
||||||
|
cccZero
|
||||||
|
cccAbove
|
||||||
|
cccOther
|
||||||
|
|
||||||
|
cccMask = cccBreak | cccZero | cccAbove | cccOther
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
starter = 0
|
||||||
|
above = 230
|
||||||
|
iotaSubscript = 240
|
||||||
|
)
|
||||||
|
|
||||||
|
// The exceptions slice holds data that does not fit in a normal info entry.
|
||||||
|
// The entry is pointed to by the exception index in an entry. It has the
|
||||||
|
// following format:
|
||||||
|
//
|
||||||
|
// Header:
|
||||||
|
//
|
||||||
|
// byte 0:
|
||||||
|
// 7..6 unused
|
||||||
|
// 5..4 CCC type (same bits as entry)
|
||||||
|
// 3 unused
|
||||||
|
// 2..0 length of fold
|
||||||
|
//
|
||||||
|
// byte 1:
|
||||||
|
// 7..6 unused
|
||||||
|
// 5..3 length of 1st mapping of case type
|
||||||
|
// 2..0 length of 2nd mapping of case type
|
||||||
|
//
|
||||||
|
// case 1st 2nd
|
||||||
|
// lower -> upper, title
|
||||||
|
// upper -> lower, title
|
||||||
|
// title -> lower, upper
|
||||||
|
//
|
||||||
|
// Lengths with the value 0x7 indicate no value and implies no change.
|
||||||
|
// A length of 0 indicates a mapping to zero-length string.
|
||||||
|
//
|
||||||
|
// Body bytes:
|
||||||
|
//
|
||||||
|
// case folding bytes
|
||||||
|
// lowercase mapping bytes
|
||||||
|
// uppercase mapping bytes
|
||||||
|
// titlecase mapping bytes
|
||||||
|
// closure mapping bytes (for NFKC_Casefold). (TODO)
|
||||||
|
//
|
||||||
|
// Fallbacks:
|
||||||
|
//
|
||||||
|
// missing fold -> lower
|
||||||
|
// missing title -> upper
|
||||||
|
// all missing -> original rune
|
||||||
|
//
|
||||||
|
// exceptions starts with a dummy byte to enforce that there is no zero index
|
||||||
|
// value.
|
||||||
|
const (
|
||||||
|
lengthMask = 0x07
|
||||||
|
lengthBits = 3
|
||||||
|
noChange = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
// References to generated trie.
|
||||||
|
|
||||||
|
var trie = newCaseTrie(0)
|
||||||
|
|
||||||
|
var sparse = sparseBlocks{
|
||||||
|
values: sparseValues[:],
|
||||||
|
offsets: sparseOffsets[:],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sparse block lookup code.
|
||||||
|
|
||||||
|
// valueRange is an entry in a sparse block.
|
||||||
|
type valueRange struct {
|
||||||
|
value uint16
|
||||||
|
lo, hi byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type sparseBlocks struct {
|
||||||
|
values []valueRange
|
||||||
|
offsets []uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookup returns the value from values block n for byte b using binary search.
|
||||||
|
func (s *sparseBlocks) lookup(n uint32, b byte) uint16 {
|
||||||
|
lo := s.offsets[n]
|
||||||
|
hi := s.offsets[n+1]
|
||||||
|
for lo < hi {
|
||||||
|
m := lo + (hi-lo)/2
|
||||||
|
r := s.values[m]
|
||||||
|
if r.lo <= b && b <= r.hi {
|
||||||
|
return r.value
|
||||||
|
}
|
||||||
|
if b < r.lo {
|
||||||
|
hi = m
|
||||||
|
} else {
|
||||||
|
lo = m + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// lastRuneForTesting is the last rune used for testing. Everything after this
|
||||||
|
// is boring.
|
||||||
|
const lastRuneForTesting = rune(0x1FFFF)
|
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright 2015 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package internal contains non-exported functionality that are used by
|
||||||
|
// packages in the text repository.
|
||||||
|
package internal // import "golang.org/x/text/internal"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SortTags sorts tags in place.
|
||||||
|
func SortTags(tags []language.Tag) {
|
||||||
|
sort.Sort(sorter(tags))
|
||||||
|
}
|
||||||
|
|
||||||
|
type sorter []language.Tag
|
||||||
|
|
||||||
|
func (s sorter) Len() int {
|
||||||
|
return len(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sorter) Swap(i, j int) {
|
||||||
|
s[i], s[j] = s[j], s[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sorter) Less(i, j int) bool {
|
||||||
|
return s[i].String() < s[j].String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UniqueTags sorts and filters duplicate tags in place and returns a slice with
|
||||||
|
// only unique tags.
|
||||||
|
func UniqueTags(tags []language.Tag) []language.Tag {
|
||||||
|
if len(tags) <= 1 {
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
SortTags(tags)
|
||||||
|
k := 0
|
||||||
|
for i := 1; i < len(tags); i++ {
|
||||||
|
if tags[k].String() < tags[i].String() {
|
||||||
|
k++
|
||||||
|
tags[k] = tags[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tags[:k+1]
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
|
||||||
|
|
||||||
|
package language
|
||||||
|
|
||||||
|
// This file contains code common to the maketables.go and the package code.
|
||||||
|
|
||||||
|
// AliasType is the type of an alias in AliasMap.
|
||||||
|
type AliasType int8
|
||||||
|
|
||||||
|
const (
|
||||||
|
Deprecated AliasType = iota
|
||||||
|
Macro
|
||||||
|
Legacy
|
||||||
|
|
||||||
|
AliasTypeUnknown AliasType = -1
|
||||||
|
)
|
|
@ -0,0 +1,29 @@
|
||||||
|
// Copyright 2018 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package language
|
||||||
|
|
||||||
|
// CompactCoreInfo is a compact integer with the three core tags encoded.
|
||||||
|
type CompactCoreInfo uint32
|
||||||
|
|
||||||
|
// GetCompactCore generates a uint32 value that is guaranteed to be unique for
|
||||||
|
// different language, region, and script values.
|
||||||
|
func GetCompactCore(t Tag) (cci CompactCoreInfo, ok bool) {
|
||||||
|
if t.LangID > langNoIndexOffset {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
cci |= CompactCoreInfo(t.LangID) << (8 + 12)
|
||||||
|
cci |= CompactCoreInfo(t.ScriptID) << 12
|
||||||
|
cci |= CompactCoreInfo(t.RegionID)
|
||||||
|
return cci, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag generates a tag from c.
|
||||||
|
func (c CompactCoreInfo) Tag() Tag {
|
||||||
|
return Tag{
|
||||||
|
LangID: Language(c >> 20),
|
||||||
|
RegionID: Region(c & 0x3ff),
|
||||||
|
ScriptID: Script(c>>12) & 0xff,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
// Copyright 2018 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package compact defines a compact representation of language tags.
|
||||||
|
//
|
||||||
|
// Common language tags (at least all for which locale information is defined
|
||||||
|
// in CLDR) are assigned a unique index. Each Tag is associated with such an
|
||||||
|
// ID for selecting language-related resources (such as translations) as well
|
||||||
|
// as one for selecting regional defaults (currency, number formatting, etc.)
|
||||||
|
//
|
||||||
|
// It may want to export this functionality at some point, but at this point
|
||||||
|
// this is only available for use within x/text.
|
||||||
|
package compact // import "golang.org/x/text/internal/language/compact"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/text/internal/language"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ID is an integer identifying a single tag.
|
||||||
|
type ID uint16
|
||||||
|
|
||||||
|
func getCoreIndex(t language.Tag) (id ID, ok bool) {
|
||||||
|
cci, ok := language.GetCompactCore(t)
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
i := sort.Search(len(coreTags), func(i int) bool {
|
||||||
|
return cci <= coreTags[i]
|
||||||
|
})
|
||||||
|
if i == len(coreTags) || coreTags[i] != cci {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return ID(i), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent returns the ID of the parent or the root ID if id is already the root.
|
||||||
|
func (id ID) Parent() ID {
|
||||||
|
return parents[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag converts id to an internal language Tag.
|
||||||
|
func (id ID) Tag() language.Tag {
|
||||||
|
if int(id) >= len(coreTags) {
|
||||||
|
return specialTags[int(id)-len(coreTags)]
|
||||||
|
}
|
||||||
|
return coreTags[id].Tag()
|
||||||
|
}
|
||||||
|
|
||||||
|
var specialTags []language.Tag
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
tags := strings.Split(specialTagsStr, " ")
|
||||||
|
specialTags = make([]language.Tag, len(tags))
|
||||||
|
for i, t := range tags {
|
||||||
|
specialTags[i] = language.MustParse(t)
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue