diff --git a/CHANGELOG.md b/CHANGELOG.md index 64a9318a0db..5df2648d82f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,7 +51,7 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio ### Improvements -- TODO ([#XXX](https://github.com/kedacore/keda/issue/XXX)) +- **Azure Data Exporer Scaler**: Use azidentity SDK ([#4489](https://github.com/kedacore/keda/issues/4489)) - **GCP PubSub Scaler**: Make it more flexible for metrics ([#4243](https://github.com/kedacore/keda/issues/4243)) ### Fixes @@ -66,7 +66,12 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio ### Deprecations -- TODO ([#XXX](https://github.com/kedacore/keda/issue/XXX)) +You can find all deprecations in [this overview](https://github.com/kedacore/keda/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Abreaking-change) and [join the discussion here](https://github.com/kedacore/keda/discussions/categories/deprecations). + +New deprecation(s): + +- **Azure Data Explorer**: Deprecate `metadata.clientSecret` ([#4514](https://github.com/kedacore/keda/issues/4514)) + ### Breaking Changes diff --git a/go.mod b/go.mod index cc8ef91b9ec..dade20efd9a 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( cloud.google.com/go/storage v1.30.1 github.com/Azure/azure-amqp-common-go/v4 v4.1.0 github.com/Azure/azure-event-hubs-go/v3 v3.5.0 - github.com/Azure/azure-kusto-go v0.9.2 + github.com/Azure/azure-kusto-go v0.13.0 github.com/Azure/azure-sdk-for-go v68.0.0+incompatible github.com/Azure/azure-sdk-for-go/sdk/azcore v1.5.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0-beta.5 @@ -245,6 +245,8 @@ require ( github.com/rivo/uniseg v0.4.4 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/samber/lo v1.37.0 // indirect + github.com/shopspring/decimal v1.3.1 // indirect github.com/sirupsen/logrus v1.9.0 // indirect github.com/spf13/afero v1.6.0 // indirect github.com/spf13/cobra v1.6.1 // indirect @@ -281,6 +283,7 @@ require ( go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/crypto v0.7.0 // indirect + golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect golang.org/x/mod v0.9.0 // indirect golang.org/x/net v0.9.0 // indirect golang.org/x/sys v0.7.0 // indirect diff --git a/go.sum b/go.sum index 5d8304deede..8763b83f503 100644 --- a/go.sum +++ b/go.sum @@ -51,28 +51,24 @@ github.com/Azure/azure-amqp-common-go/v4 v4.1.0 h1:gcS6P4q/Qv1nmdq1IWoU3mLYlHnvN github.com/Azure/azure-amqp-common-go/v4 v4.1.0/go.mod h1:HDiTPilyFCWPOT8dBeSjGztqgrW27LctWs/4p6nR9FY= github.com/Azure/azure-event-hubs-go/v3 v3.5.0 h1:H3nhguNPKFH+Z6GP0GZTJVsiuHjqrz5rsRsD816zNkQ= github.com/Azure/azure-event-hubs-go/v3 v3.5.0/go.mod h1:7zahh3eGktdZ6Xtfv0F0mrnllBR0iE9TJMbGDsqxwkE= -github.com/Azure/azure-kusto-go v0.9.2 h1:AP0bNhGyvc51XHi6nBMABDjCqgJb/KGrCrcm7KvYFyk= -github.com/Azure/azure-kusto-go v0.9.2/go.mod h1:i7WCtgt4XeHge3+Oi5sq84HYhneNi7VY7hr35wsUdrg= +github.com/Azure/azure-kusto-go v0.13.0 h1:aJRp0DpZVGJchQqp0Z3XxO6VqhtCSKwyq8B6sOoia7U= +github.com/Azure/azure-kusto-go v0.13.0/go.mod h1:AyWTO0r50y7rAkxkTveLtn80njNyXpJP5WADvoSZ/P4= github.com/Azure/azure-pipeline-go v0.1.8/go.mod h1:XA1kFWRVhSK+KNFiOhfv83Fv8L9achrP7OxIzeTn1Yg= github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U= github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= -github.com/Azure/azure-sdk-for-go v61.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= -github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.5.0 h1:xGLAFFd9D3iLGxYiUGPdITSzsFmU1K8VtfuUHWAoN7M= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.5.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0-beta.5 h1:F8ii3ek6K2tnf9gmv/YFktyOci9DuJboh/rKXMS2FaQ= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0-beta.5/go.mod h1:ZJteiLBLt8CmYc6yJFe5YErRHQ4FpTEwgXomR1ikcy8= github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= -github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.2.1 h1:ryVRjO3SrGrSM8PNlLuMbMYFz9vexPzvenNUEBfsgCo= github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.2.1/go.mod h1:R6+0udeRV8iYSTVuT5RT7If4sc46K5Bz3ZKrmvZQF7U= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0/go.mod h1:tPaiy8S5bQ+S5sOiDlINkp7+Ef339+Nz5L5XO+cnOHo= github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7OgcSXPpwp3tx6qk= github.com/Azure/azure-storage-blob-go v0.15.0/go.mod h1:vbjsVbX0dlxnRc4FFMPsS9BsJWPcne7GB7onqlPvz58= github.com/Azure/azure-storage-queue-go v0.0.0-20191125232315-636801874cdd h1:b3wyxBl3vvr15tUAziPBPK354y+LSdfPCpex5oBttHo= @@ -88,7 +84,6 @@ github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4Uw github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= github.com/Azure/go-autorest/autorest/adal v0.9.22 h1:/GblQdIudfEM3AWWZ0mrYJQSd7JS4S/Mbzh6F0ov0Xc= github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.11/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 h1:wkAZRgT/pn8HhFyzfe9UnqOjJYqlembgCTi72Bm/xKk= github.com/Azure/go-autorest/autorest/azure/auth v0.5.12/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg= @@ -164,7 +159,6 @@ github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx2 github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= -github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -205,7 +199,6 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= -github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= @@ -666,7 +659,11 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw= +github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -708,6 +705,7 @@ github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhV github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA= github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y= github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE= @@ -829,6 +827,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/pkg/scalers/azure/azure_data_explorer.go b/pkg/scalers/azure/azure_data_explorer.go index 9aa195c0243..915e5ece3d2 100644 --- a/pkg/scalers/azure/azure_data_explorer.go +++ b/pkg/scalers/azure/azure_data_explorer.go @@ -20,12 +20,14 @@ import ( "context" "fmt" "io" + "net/http" "strconv" "github.com/Azure/azure-kusto-go/kusto" "github.com/Azure/azure-kusto-go/kusto/data/table" - "github.com/Azure/azure-kusto-go/kusto/unsafe" - "github.com/Azure/go-autorest/autorest/azure/auth" + "github.com/Azure/azure-kusto-go/kusto/kql" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" logf "sigs.k8s.io/controller-runtime/pkg/log" kedav1alpha1 "github.com/kedacore/keda/v2/apis/keda/v1alpha1" @@ -47,18 +49,13 @@ type DataExplorerMetadata struct { var azureDataExplorerLogger = logf.Log.WithName("azure_data_explorer_scaler") -func CreateAzureDataExplorerClient(ctx context.Context, metadata *DataExplorerMetadata) (*kusto.Client, error) { - authConfig, err := getDataExplorerAuthConfig(ctx, metadata) +func CreateAzureDataExplorerClient(metadata *DataExplorerMetadata, httpClient *http.Client) (*kusto.Client, error) { + kcsb, err := getDataExplorerAuthConfig(metadata) if err != nil { return nil, fmt.Errorf("failed to get data explorer auth config: %w", err) } - authorizer, err := authConfig.Authorizer() - if err != nil { - return nil, fmt.Errorf("failed to get authorizer: %w", err) - } - - client, err := kusto.New(metadata.Endpoint, kusto.Authorization{Authorizer: authorizer}) + client, err := kusto.New(kcsb, kusto.WithHttpClient(httpClient)) if err != nil { return nil, fmt.Errorf("failed to create kusto client: %w", err) } @@ -66,41 +63,53 @@ func CreateAzureDataExplorerClient(ctx context.Context, metadata *DataExplorerMe return client, nil } -func getDataExplorerAuthConfig(ctx context.Context, metadata *DataExplorerMetadata) (auth.AuthorizerConfig, error) { - var authConfig auth.AuthorizerConfig +func getDataExplorerAuthConfig(metadata *DataExplorerMetadata) (*kusto.ConnectionStringBuilder, error) { + kcsb := kusto.NewConnectionStringBuilder(metadata.Endpoint) switch metadata.PodIdentity.Provider { case "", kedav1alpha1.PodIdentityProviderNone: - if metadata.ClientID != "" && metadata.ClientSecret != "" && metadata.TenantID != "" { - config := auth.NewClientCredentialsConfig(metadata.ClientID, metadata.ClientSecret, metadata.TenantID) - config.Resource = metadata.Endpoint - config.AADEndpoint = metadata.ActiveDirectoryEndpoint - azureDataExplorerLogger.V(1).Info("Creating Azure Data Explorer Client using clientID, clientSecret and tenantID") - - authConfig = config - return authConfig, nil + azureDataExplorerLogger.V(1).Info("Creating Azure Data Explorer Client using clientID, clientSecret and tenantID") + if metadata.ClientID == "" { + return nil, fmt.Errorf("missing credentials. please ensure that ClientID is provided") + } + if metadata.ClientSecret == "" { + return nil, fmt.Errorf("missing credentials. please ensure that ClientSecret is provided") + } + if metadata.TenantID == "" { + return nil, fmt.Errorf("missing credentials. please ensure that TenantID is provided") } - case kedav1alpha1.PodIdentityProviderAzure: - config := auth.NewMSIConfig() - config.Resource = metadata.Endpoint - config.ClientID = metadata.PodIdentity.IdentityID - azureDataExplorerLogger.V(1).Info("Creating Azure Data Explorer Client using Pod Identity") - - authConfig = config - return authConfig, nil - case kedav1alpha1.PodIdentityProviderAzureWorkload: - azureDataExplorerLogger.V(1).Info("Creating Azure Data Explorer Client using Workload Identity") - authConfig = NewAzureADWorkloadIdentityConfig(ctx, metadata.PodIdentity.IdentityID, metadata.Endpoint) - return authConfig, nil + kcsb.WithAadAppKey(metadata.ClientID, metadata.ClientSecret, metadata.TenantID) + // This should be here because internaly the SDK resets the configuration + // after calling `WithAadAppKey` + clientOptions := &policy.ClientOptions{ + Cloud: cloud.Configuration{ + ActiveDirectoryAuthorityHost: metadata.ActiveDirectoryEndpoint, + Services: map[cloud.ServiceName]cloud.ServiceConfiguration{}, + }, + } + kcsb.AttachPolicyClientOptions(clientOptions) + + case kedav1alpha1.PodIdentityProviderAzure, kedav1alpha1.PodIdentityProviderAzureWorkload: + azureDataExplorerLogger.V(1).Info(fmt.Sprintf("Creating Azure Data Explorer Client using podIdentity %s", metadata.PodIdentity.Provider)) + creds, chainedErr := NewChainedCredential(metadata.PodIdentity.IdentityID, metadata.PodIdentity.Provider) + if chainedErr != nil { + return nil, chainedErr + } + kcsb.WithTokenCredential(creds) + // We don't need to call to kcsb.AttachPolicyClientOptions because WI/AAD-Pod-Identity manages + // it based on their own configurations + + default: + return nil, fmt.Errorf("missing credentials. please reconfigure your scaled object metadata") } - return nil, fmt.Errorf("missing credentials. please reconfigure your scaled object metadata") + return kcsb, nil } func GetAzureDataExplorerMetricValue(ctx context.Context, client *kusto.Client, db string, query string) (float64, error) { azureDataExplorerLogger.V(1).Info("Querying Azure Data Explorer", "db", db, "query", query) - iter, err := client.Query(ctx, db, kusto.NewStmt("", kusto.UnsafeStmt(unsafe.Stmt{Add: true, SuppressWarning: false})).UnsafeAdd(query)) + iter, err := client.Query(ctx, db, kql.New("").AddUnsafe(query)) if err != nil { return -1, fmt.Errorf("failed to get azure data explorer metric result from query %s: %w", query, err) } diff --git a/pkg/scalers/azure/azure_data_explorer_test.go b/pkg/scalers/azure/azure_data_explorer_test.go index 1d5fa4d3359..9b7e7d618ea 100644 --- a/pkg/scalers/azure/azure_data_explorer_test.go +++ b/pkg/scalers/azure/azure_data_explorer_test.go @@ -17,7 +17,6 @@ limitations under the License. package azure import ( - "context" "testing" "github.com/Azure/azure-kusto-go/kusto/data/errors" @@ -64,19 +63,19 @@ var testExtractDataExplorerMetricValues = []testExtractDataExplorerMetricValue{ var testGetDataExplorerAuthConfigs = []testGetDataExplorerAuthConfig{ // Auth with aad app - pass - {testMetadata: &DataExplorerMetadata{ClientID: clientID, ClientSecret: secret, TenantID: tenantID}, isError: false}, + {testMetadata: &DataExplorerMetadata{ClientID: clientID, ClientSecret: secret, TenantID: tenantID, Endpoint: "https://test.kusto.windows.net", ActiveDirectoryEndpoint: "https://test.kusto.windows.net"}, isError: false}, // Auth with podIdentity - pass - {testMetadata: &DataExplorerMetadata{PodIdentity: kedav1alpha1.AuthPodIdentity{Provider: kedav1alpha1.PodIdentityProviderAzure}}, isError: false}, + {testMetadata: &DataExplorerMetadata{PodIdentity: kedav1alpha1.AuthPodIdentity{Provider: kedav1alpha1.PodIdentityProviderAzure}, Endpoint: "https://test.kusto.windows.net", ActiveDirectoryEndpoint: "https://test.kusto.windows.net"}, isError: false}, // Auth with workload identity - pass - {testMetadata: &DataExplorerMetadata{PodIdentity: kedav1alpha1.AuthPodIdentity{Provider: kedav1alpha1.PodIdentityProviderAzureWorkload}}, isError: false}, + {testMetadata: &DataExplorerMetadata{PodIdentity: kedav1alpha1.AuthPodIdentity{Provider: kedav1alpha1.PodIdentityProviderAzureWorkload}, Endpoint: "https://test.kusto.windows.net", ActiveDirectoryEndpoint: "https://test.kusto.windows.net"}, isError: false}, // Empty metadata - fail - {testMetadata: &DataExplorerMetadata{}, isError: true}, + {testMetadata: &DataExplorerMetadata{Endpoint: "https://test.kusto.windows.net", ActiveDirectoryEndpoint: "https://test.kusto.windows.net"}, isError: true}, // Empty tenantID - fail - {testMetadata: &DataExplorerMetadata{ClientID: clientID, ClientSecret: secret}, isError: true}, + {testMetadata: &DataExplorerMetadata{ClientID: clientID, ClientSecret: secret, Endpoint: "https://test.kusto.windows.net", ActiveDirectoryEndpoint: "https://test.kusto.windows.net"}, isError: true}, // Empty clientID - fail - {testMetadata: &DataExplorerMetadata{ClientSecret: secret, TenantID: tenantID}, isError: true}, + {testMetadata: &DataExplorerMetadata{ClientSecret: secret, TenantID: tenantID, Endpoint: "https://test.kusto.windows.net", ActiveDirectoryEndpoint: "https://test.kusto.windows.net"}, isError: true}, // Empty clientSecret - fail - {testMetadata: &DataExplorerMetadata{ClientID: clientID, TenantID: tenantID}, isError: true}, + {testMetadata: &DataExplorerMetadata{ClientID: clientID, TenantID: tenantID, Endpoint: "https://test.kusto.windows.net", ActiveDirectoryEndpoint: "https://test.kusto.windows.net"}, isError: true}, } func TestExtractDataExplorerMetricValue(t *testing.T) { @@ -93,7 +92,7 @@ func TestExtractDataExplorerMetricValue(t *testing.T) { func TestGetDataExplorerAuthConfig(t *testing.T) { for _, testData := range testGetDataExplorerAuthConfigs { - _, err := getDataExplorerAuthConfig(context.TODO(), testData.testMetadata) + _, err := getDataExplorerAuthConfig(testData.testMetadata) if err != nil && !testData.isError { t.Error("Expected success but got error", err) } diff --git a/pkg/scalers/azure_data_explorer_scaler.go b/pkg/scalers/azure_data_explorer_scaler.go index eae954eb31f..f3d2ee05a0b 100644 --- a/pkg/scalers/azure_data_explorer_scaler.go +++ b/pkg/scalers/azure_data_explorer_scaler.go @@ -42,7 +42,7 @@ type azureDataExplorerScaler struct { const adxName = "azure-data-explorer" -func NewAzureDataExplorerScaler(ctx context.Context, config *ScalerConfig) (Scaler, error) { +func NewAzureDataExplorerScaler(config *ScalerConfig) (Scaler, error) { metricType, err := GetMetricTargetType(config) if err != nil { return nil, fmt.Errorf("error getting scaler metric type: %w", err) @@ -55,7 +55,8 @@ func NewAzureDataExplorerScaler(ctx context.Context, config *ScalerConfig) (Scal return nil, fmt.Errorf("failed to parse azure data explorer metadata: %w", err) } - client, err := azure.CreateAzureDataExplorerClient(ctx, metadata) + httpClient := kedautil.CreateHTTPClient(config.GlobalHTTPTimeout, false) + client, err := azure.CreateAzureDataExplorerClient(metadata, httpClient) if err != nil { return nil, fmt.Errorf("failed to create azure data explorer client: %w", err) } @@ -158,10 +159,17 @@ func parseAzureDataExplorerAuthParams(config *ScalerConfig, logger logr.Logger) } metadata.ClientID = clientID + // FIXME: DEPRECATED to be removed in v2.13 + // We should get the secret only from AuthConfig or env clientSecret, err := getParameterFromConfig(config, "clientSecret", true) if err != nil { return nil, err } + if val, ok := config.TriggerMetadata["clientSecret"]; ok && val != "" { + logger.Info("getting 'clientSecret' from metadata is deprecated, use 'clientSecretFromEnv' or TriggerAuthentication instead") + } + // FIXME: DEPRECATED to be removed in v2.13 + metadata.ClientSecret = clientSecret default: return nil, fmt.Errorf("error parsing auth params") diff --git a/pkg/scaling/scalers_builder.go b/pkg/scaling/scalers_builder.go index 30a0c2e9ef0..7a4a53b24e5 100644 --- a/pkg/scaling/scalers_builder.go +++ b/pkg/scaling/scalers_builder.go @@ -123,7 +123,7 @@ func buildScaler(ctx context.Context, client client.Client, triggerType string, case "azure-blob": return scalers.NewAzureBlobScaler(config) case "azure-data-explorer": - return scalers.NewAzureDataExplorerScaler(ctx, config) + return scalers.NewAzureDataExplorerScaler(config) case "azure-eventhub": return scalers.NewAzureEventHubScaler(ctx, config) case "azure-log-analytics": diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/client_details.go b/vendor/github.com/Azure/azure-kusto-go/kusto/client_details.go new file mode 100644 index 00000000000..b25b3eeb896 --- /dev/null +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/client_details.go @@ -0,0 +1,131 @@ +package kusto + +import ( + "fmt" + "github.com/Azure/azure-kusto-go/kusto/internal/version" + "github.com/Azure/azure-kusto-go/kusto/utils" + "os" + "os/user" + "path/filepath" + "regexp" + "runtime" + "strings" + + "github.com/samber/lo" +) + +type ClientDetails struct { + // applicationForTracing is the name of the application that is using the client. + applicationForTracing string + // userNameForTracing is the name of the user that is using the client. + userNameForTracing string + // clientVersionForTracing is the version of the client. + clientVersionForTracing string +} + +func NewClientDetails(applicationForTracing string, userNameForTracing string) *ClientDetails { + return &ClientDetails{applicationForTracing: applicationForTracing, userNameForTracing: userNameForTracing} +} + +type StringPair struct { + Key string + Value string +} + +const NONE = "[none]" + +var defaultTracingValuesOnce = utils.NewOnceWithInit[ClientDetails](func() (ClientDetails, error) { + return ClientDetails{ + applicationForTracing: filepath.Base(os.Args[0]), + userNameForTracing: getOsUser(), + clientVersionForTracing: buildHeaderFormat(StringPair{Key: "Kusto.Go.Client", Value: version.Kusto}, StringPair{Key: "Runtime.Go", Value: runtime.Version()}), + }, nil +}) + +func getOsUser() string { + var final string + current, err := user.Current() + if err == nil && current.Username != "" { + final = current.Username + } else { + // get from env and try domain too + final = os.Getenv("USERNAME") + domain := os.Getenv("USERDOMAIN") + if !isEmpty(domain) && !isEmpty(final) { + final = domain + "\\" + final + } + } + + if isEmpty(final) { + final = NONE + } + + return final +} + +var escapeRegex = regexp.MustCompile("[\\r\\n\\s{}|]+") + +func escape(s string) string { + return "{" + escapeRegex.ReplaceAllString(s, "_") + "}" +} + +func defaultTracingValues() ClientDetails { + r, _ := defaultTracingValuesOnce.DoWithInit() + return r +} + +func (c *ClientDetails) ApplicationForTracing() string { + if c.applicationForTracing == "" { + return defaultTracingValues().applicationForTracing + } + return c.applicationForTracing +} + +func (c *ClientDetails) UserNameForTracing() string { + if c.userNameForTracing == "" { + return defaultTracingValues().userNameForTracing + } + return c.userNameForTracing +} + +func (c *ClientDetails) ClientVersionForTracing() string { + return defaultTracingValues().clientVersionForTracing +} + +func buildHeaderFormat(args ...StringPair) string { + return strings.Join(lo.Map(args, func(arg StringPair, _ int) string { + return fmt.Sprintf("%s:%s", arg.Key, escape(arg.Value)) + }), "|") +} + +func setConnectorDetails(name, version, appName, appVersion string, sendUser bool, overrideUser string, additionalFields ...StringPair) (string, string) { + var additionalFieldsList []StringPair + + additionalFieldsList = append(additionalFieldsList, StringPair{Key: "Kusto." + name, Value: version}) + + if appName == "" { + appName = defaultTracingValues().applicationForTracing + } + if appVersion == "" { + appVersion = NONE + } + + additionalFieldsList = append(additionalFieldsList, StringPair{Key: "App.{" + appName + "}", Value: appVersion}) + if additionalFields != nil { + additionalFieldsList = append(additionalFieldsList, additionalFields...) + } + + app := buildHeaderFormat(additionalFieldsList...) + + user := NONE + + if sendUser { + if overrideUser != "" { + user = overrideUser + } else { + user = defaultTracingValues().userNameForTracing + } + } + + return app, user +} diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/cloudinfo.go b/vendor/github.com/Azure/azure-kusto-go/kusto/cloudinfo.go new file mode 100644 index 00000000000..6e1a9ecf113 --- /dev/null +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/cloudinfo.go @@ -0,0 +1,118 @@ +package kusto + +import ( + "encoding/json" + "fmt" + kustoErrors "github.com/Azure/azure-kusto-go/kusto/data/errors" + "github.com/Azure/azure-kusto-go/kusto/utils" + "io" + "net/http" + "net/url" + "os" + "strings" + "sync" +) + +// abstraction to query metadata and use this information for providing all +// information needed for connection string builder to provide all the requisite information + +const ( + metadataPath = "/v1/rest/auth/metadata" + defaultAuthEnvVarName = "AadAuthorityUri" + defaultKustoClientAppId = "db662dc1-0cfe-4e1c-a843-19a68e65be58" + defaultPublicLoginUrl = "https://login.microsoftonline.com" + defaultRedirectUri = "https://microsoft/kustoclient" + defaultKustoServiceResourceId = "https://kusto.kusto.windows.net" + defaultFirstPartyAuthorityUrl = "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a" +) + +// retrieved metadata +type metaResp struct { + AzureAD CloudInfo +} + +type CloudInfo struct { + LoginEndpoint string `json:"LoginEndpoint"` + LoginMfaRequired bool `json:"LoginMfaRequired"` + KustoClientAppID string `json:"KustoClientAppId"` + KustoClientRedirectURI string `json:"KustoClientRedirectUri"` + KustoServiceResourceID string `json:"KustoServiceResourceId"` + FirstPartyAuthorityURL string `json:"FirstPartyAuthorityUrl"` +} + +var defaultCloudInfo = CloudInfo{ + LoginEndpoint: getEnvOrDefault(defaultAuthEnvVarName, defaultPublicLoginUrl), + LoginMfaRequired: false, + KustoClientAppID: defaultKustoClientAppId, + KustoClientRedirectURI: defaultRedirectUri, + KustoServiceResourceID: defaultKustoServiceResourceId, + FirstPartyAuthorityURL: defaultFirstPartyAuthorityUrl, +} + +// cache to query it once per instance +var cloudInfoCache sync.Map + +func GetMetadata(kustoUri string, httpClient *http.Client) (CloudInfo, error) { + // retrieve &return if exists + once, ok := cloudInfoCache.Load(kustoUri) + if !ok { + once = utils.NewOnce[CloudInfo]() + cloudInfoCache.Store(kustoUri, once) + } + + return once.(utils.Once[CloudInfo]).Do(func() (CloudInfo, error) { + u, err := url.Parse(kustoUri) + if err != nil { + return CloudInfo{}, err + } + if !strings.HasPrefix(u.Path, "/") { + u.Path = "/" + u.Path + } + u = u.JoinPath(metadataPath) + // TODO should we make this timeout configurable. + req, err := http.NewRequest("GET", u.String(), nil) + + if err != nil { + return CloudInfo{}, kustoErrors.E(kustoErrors.OpCloudInfo, kustoErrors.KHTTPError, err) + } + resp, err := httpClient.Do(req) + + if err != nil { + return CloudInfo{}, err + } + + // Handle internal server error as a special case and return as an error (to be consistent with other SDK's) + if resp.StatusCode >= 300 && resp.StatusCode != 404 { + return CloudInfo{}, kustoErrors.E(kustoErrors.OpCloudInfo, kustoErrors.KHTTPError, fmt.Errorf("error %s when querying endpoint %s", + resp.Status, u.String()), + ) + } + + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + return CloudInfo{}, kustoErrors.E(kustoErrors.OpCloudInfo, kustoErrors.KHTTPError, err) + } + + // Covers scenarios of 200/OK with no body or a 404 where there is no body + if len(b) == 0 { + return defaultCloudInfo, nil + } + + md := metaResp{} + + if err := json.Unmarshal(b, &md); err != nil { + return CloudInfo{}, err + } + // this should be set in the map by now + return md.AzureAD, nil + }) +} + +func getEnvOrDefault(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/conn.go b/vendor/github.com/Azure/azure-kusto-go/kusto/conn.go index e13377b13ed..b986e789b27 100644 --- a/vendor/github.com/Azure/azure-kusto-go/kusto/conn.go +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/conn.go @@ -1,6 +1,6 @@ package kusto -// conn.go holds the connection to the Kusto server and provides methods to do queries +// Conn.go holds the connection to the Kusto server and provides methods to do queries // and receive Kusto frames back. import ( @@ -14,15 +14,15 @@ import ( "regexp" "strings" "sync" + "sync/atomic" + "unicode" "github.com/Azure/azure-kusto-go/kusto/data/errors" "github.com/Azure/azure-kusto-go/kusto/internal/frames" v1 "github.com/Azure/azure-kusto-go/kusto/internal/frames/v1" v2 "github.com/Azure/azure-kusto-go/kusto/internal/frames/v2" "github.com/Azure/azure-kusto-go/kusto/internal/response" - "github.com/Azure/azure-kusto-go/kusto/internal/version" - - "github.com/Azure/go-autorest/autorest" + truestedEndpoints "github.com/Azure/azure-kusto-go/kusto/trusted_endpoints" "github.com/google/uuid" ) @@ -34,15 +34,18 @@ var bufferPool = sync.Pool{ }, } -// conn provides connectivity to a Kusto instance. -type conn struct { - auth autorest.Authorizer - endMgmt, endQuery, streamQuery *url.URL - client *http.Client +// Conn provides connectivity to a Kusto instance. +type Conn struct { + endpoint string + auth Authorization + endMgmt, endQuery, endStreamIngest *url.URL + client *http.Client + endpointValidated atomic.Bool + clientDetails *ClientDetails } -// newConn returns a new conn object with an injected http.Client -func newConn(endpoint string, auth Authorization, client *http.Client) (*conn, error) { +// NewConn returns a new Conn object with an injected http.Client +func NewConn(endpoint string, auth Authorization, client *http.Client, clientDetails *ClientDetails) (*Conn, error) { if !validURL.MatchString(endpoint) { return nil, errors.ES(errors.OpServConn, errors.KClientArgs, "endpoint is not valid(%s), should be https://.*", endpoint).SetNoRetry() } @@ -51,13 +54,17 @@ func newConn(endpoint string, auth Authorization, client *http.Client) (*conn, e if err != nil { return nil, errors.ES(errors.OpServConn, errors.KClientArgs, "could not parse the endpoint(%s): %s", endpoint, err).SetNoRetry() } + if !strings.HasPrefix(u.Path, "/") { + u.Path = "/" + u.Path + } - c := &conn{ - auth: auth.Authorizer, - endMgmt: &url.URL{Scheme: "https", Host: u.Host, Path: "/v1/rest/mgmt"}, - endQuery: &url.URL{Scheme: "https", Host: u.Host, Path: "/v2/rest/query"}, - streamQuery: &url.URL{Scheme: "https", Host: u.Host, Path: "/v1/rest/ingest/"}, - client: client, + c := &Conn{ + auth: auth, + endMgmt: u.JoinPath("/v1/rest/mgmt"), + endQuery: u.JoinPath("/v2/rest/query"), + endStreamIngest: u.JoinPath("/v1/rest/ingest"), + client: client, + clientDetails: clientDetails, } return c, nil @@ -71,12 +78,11 @@ type queryMsg struct { type connOptions struct { queryOptions *queryOptions - mgmtOptions *mgmtOptions } // query makes a query for the purpose of extracting data from Kusto. Context can be used to set // a timeout or cancel the query. Queries cannot take longer than 5 minutes. -func (c *conn) query(ctx context.Context, db string, query Stmt, options *queryOptions) (execResp, error) { +func (c *Conn) query(ctx context.Context, db string, query Statement, options *queryOptions) (execResp, error) { if strings.HasPrefix(strings.TrimSpace(query.String()), ".") { return execResp{}, errors.ES(errors.OpQuery, errors.KClientArgs, "a Stmt to Query() cannot begin with a period(.), only Mgmt() calls can do that").SetNoRetry() } @@ -85,16 +91,17 @@ func (c *conn) query(ctx context.Context, db string, query Stmt, options *queryO } // mgmt is used to do management queries to Kusto. -func (c *conn) mgmt(ctx context.Context, db string, query Stmt, options *mgmtOptions) (execResp, error) { +func (c *Conn) mgmt(ctx context.Context, db string, query Statement, options *queryOptions) (execResp, error) { return c.execute(ctx, execMgmt, db, query, *options.requestProperties) } -func (c *conn) queryToJson(ctx context.Context, db string, query Stmt, options *queryOptions) (string, error) { +func (c *Conn) queryToJson(ctx context.Context, db string, query Statement, options *queryOptions) (string, error) { _, _, _, body, e := c.doRequest(ctx, execQuery, db, query, *options.requestProperties) if e != nil { return "", e } + defer body.Close() all, e := io.ReadAll(body) return string(all), e } @@ -110,8 +117,8 @@ type execResp struct { frameCh chan frames.Frame } -func (c *conn) execute(ctx context.Context, execType int, db string, query Stmt, properties requestProperties) (execResp, error) { - op, header, resp, body, e := c.doRequest(ctx, execType, db, query, properties) +func (c *Conn) execute(ctx context.Context, execType int, db string, query Statement, properties requestProperties) (execResp, error) { + op, reqHeader, respHeader, body, e := c.doRequest(ctx, execType, db, query, properties) if e != nil { return execResp{}, e } @@ -128,10 +135,12 @@ func (c *conn) execute(ctx context.Context, execType int, db string, query Stmt, frameCh := dec.Decode(ctx, body, op) - return execResp{reqHeader: header, respHeader: resp.Header, frameCh: frameCh}, nil + return execResp{reqHeader: reqHeader, respHeader: respHeader, frameCh: frameCh}, nil } -func (c *conn) doRequest(ctx context.Context, execType int, db string, query Stmt, properties requestProperties) (errors.Op, http.Header, *http.Response, io.ReadCloser, error) { +func (c *Conn) doRequest(ctx context.Context, execType int, db string, query Statement, properties requestProperties) (errors.Op, http.Header, http.Header, + io.ReadCloser, error) { + err := c.validateEndpoint() var op errors.Op if execType == execQuery { op = errors.OpQuery @@ -139,22 +148,8 @@ func (c *conn) doRequest(ctx context.Context, execType int, db string, query Stm op = errors.OpMgmt } - header := http.Header{} - header.Add("Accept", "application/json") - header.Add("Accept-Encoding", "gzip") - header.Add("x-ms-client-version", "Kusto.Go.Client: "+version.Kusto) - header.Add("Content-Type", "application/json; charset=utf-8") - header.Add("x-ms-client-request-id", "KGC.execute;"+uuid.New().String()) - - if properties.Application != "" { - header.Add("x-ms-app", properties.Application) - } - - if properties.User != "" { - header.Add("x-ms-user", properties.User) - } - var endpoint *url.URL + buff := bufferPool.Get().(*bytes.Buffer) buff.Reset() defer bufferPool.Put(buff) @@ -162,10 +157,16 @@ func (c *conn) doRequest(ctx context.Context, execType int, db string, query Stm switch execType { case execQuery, execMgmt: var err error + var csl string + if query.SupportsInlineParameters() || properties.QueryParameters.Count() == 0 { + csl = query.String() + } else { + csl = fmt.Sprintf("%s\n%s", properties.QueryParameters.ToDeclarationString(), query.String()) + } err = json.NewEncoder(buff).Encode( queryMsg{ DB: db, - CSL: query.String(), + CSL: csl, Properties: properties, }, ) @@ -181,45 +182,119 @@ func (c *conn) doRequest(ctx context.Context, execType int, db string, query Stm return 0, nil, nil, nil, errors.ES(op, errors.KInternal, "internal error: did not understand the type of execType: %d", execType) } + headers := c.getHeaders(properties) + responseHeaders, closer, err := c.doRequestImpl(ctx, op, endpoint, io.NopCloser(buff), headers, fmt.Sprintf("With query: %s", query.String())) + return op, headers, responseHeaders, closer, err +} + +func (c *Conn) doRequestImpl( + ctx context.Context, + op errors.Op, + endpoint *url.URL, + buff io.ReadCloser, + headers http.Header, + errorContext string) (http.Header, io.ReadCloser, error) { + + // Replace non-ascii chars in headers with '?' + for _, values := range headers { + var builder strings.Builder + for i := range values { + for _, char := range values[i] { + if char > unicode.MaxASCII { + builder.WriteRune('?') + } else { + builder.WriteRune(char) + } + } + values[i] = builder.String() + } + } + + if c.auth.TokenProvider != nil && c.auth.TokenProvider.AuthorizationRequired() { + c.auth.TokenProvider.SetHttp(c.client) + token, tokenType, tkerr := c.auth.TokenProvider.AcquireToken(ctx) + if tkerr != nil { + return nil, nil, errors.ES(op, errors.KInternal, "Error while getting token : %s", tkerr) + } + headers.Add("Authorization", fmt.Sprintf("%s %s", tokenType, token)) + } + req := &http.Request{ Method: http.MethodPost, URL: endpoint, - Header: header, - Body: io.NopCloser(buff), - } - - var err error - prep := c.auth.WithAuthorization() - req, err = prep(autorest.CreatePreparer()).Prepare(req) - if err != nil { - return 0, nil, nil, nil, errors.E(op, errors.KInternal, err) + Header: headers, + Body: buff, } resp, err := c.client.Do(req.WithContext(ctx)) if err != nil { // TODO(jdoak): We need a http error unwrap function that pulls out an *errors.Error. - return 0, nil, nil, nil, errors.E(op, errors.KHTTPError, fmt.Errorf("with query %q: %w", query.String(), err)) + return nil, nil, errors.E(op, errors.KHTTPError, fmt.Errorf("%v, %w", errorContext, err)) } body, err := response.TranslateBody(resp, op) if err != nil { - return 0, nil, nil, nil, err + return nil, nil, err } if resp.StatusCode != http.StatusOK { - return 0, nil, nil, nil, errors.HTTP(op, resp.Status, resp.StatusCode, body, fmt.Sprintf("error from Kusto endpoint for query %q: ", query.String())) + return nil, nil, errors.HTTP(op, resp.Status, resp.StatusCode, body, fmt.Sprintf("error from Kusto endpoint, %v", errorContext)) } - return op, header, resp, body, nil + return resp.Header, body, nil +} + +func (c *Conn) validateEndpoint() error { + if !c.endpointValidated.Load() { + var err error + if cloud, err := GetMetadata(c.endpoint, c.client); err == nil { + err = truestedEndpoints.Instance.ValidateTrustedEndpoint(c.endpoint, cloud.LoginEndpoint) + if err == nil { + c.endpointValidated.Store(true) + } + } + + return err + } + + return nil } -func (c *conn) Close() error { - if c.auth == nil { - return nil +const ClientRequestIdHeader = "x-ms-client-request-id" +const ApplicationHeader = "x-ms-app" +const UserHeader = "x-ms-user" +const ClientVersionHeader = "x-ms-client-version" + +func (c *Conn) getHeaders(properties requestProperties) http.Header { + header := http.Header{} + header.Add("Accept", "application/json") + header.Add("Accept-Encoding", "gzip, deflate") + header.Add("Content-Type", "application/json; charset=utf-8") + header.Add("Connection", "Keep-Alive") + header.Add("x-ms-version", "2019-02-13") + + if properties.ClientRequestID != "" { + header.Add(ClientRequestIdHeader, properties.ClientRequestID) + } else { + header.Add(ClientRequestIdHeader, "KGC.execute;"+uuid.New().String()) } - if closer, ok := c.auth.(io.Closer); ok { - return closer.Close() + if properties.Application != "" { + header.Add(ApplicationHeader, properties.Application) + } else { + header.Add(ApplicationHeader, c.clientDetails.ApplicationForTracing()) } + if properties.User != "" { + header.Add(UserHeader, properties.User) + } else { + header.Add(UserHeader, c.clientDetails.UserNameForTracing()) + } + + header.Add(ClientVersionHeader, c.clientDetails.ClientVersionForTracing()) + return header +} + +func (c *Conn) Close() error { + c.client.CloseIdleConnections() return nil } diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/conn_streaming_ingest.go b/vendor/github.com/Azure/azure-kusto-go/kusto/conn_streaming_ingest.go new file mode 100644 index 00000000000..24cb1f42790 --- /dev/null +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/conn_streaming_ingest.go @@ -0,0 +1,70 @@ +package kusto + +import ( + "context" + "fmt" + "github.com/Azure/azure-kusto-go/kusto/data/errors" + "github.com/google/uuid" + "io" + "net/url" + "time" +) + +type DataFormatForStreaming interface { + CamelCase() string + KnownOrDefault() DataFormatForStreaming +} + +var ( + streamingIngestDefaultTimeout = 10 * time.Minute +) + +func (c *Conn) StreamIngest(ctx context.Context, db, table string, payload io.Reader, format DataFormatForStreaming, mappingName string, clientRequestId string) error { + streamUrl, err := url.Parse(c.endStreamIngest.String()) + if err != nil { + return errors.ES(errors.OpIngestStream, errors.KClientArgs, "could not parse the stream endpoint(%s): %s", c.endStreamIngest.String(), err).SetNoRetry() + } + path, err := url.JoinPath(streamUrl.Path, db, table) + if err != nil { + return errors.ES(errors.OpIngestStream, errors.KClientArgs, "could not join the stream endpoint(%s) with the db(%s) and table(%s): %s", c.endStreamIngest.String(), db, table, err).SetNoRetry() + } + streamUrl.Path = path + + qv := url.Values{} + if mappingName != "" { + qv.Add("mappingName", mappingName) + } + qv.Add("streamFormat", format.KnownOrDefault().CamelCase()) + streamUrl.RawQuery = qv.Encode() + + var closeablePayload io.ReadCloser + var ok bool + if closeablePayload, ok = payload.(io.ReadCloser); !ok { + closeablePayload = io.NopCloser(payload) + } + + if clientRequestId == "" { + clientRequestId = "KGC.executeStreaming;" + uuid.New().String() + } + + properties := requestProperties{} + properties.ClientRequestID = clientRequestId + headers := c.getHeaders(properties) + headers.Del("Content-Type") + headers.Add("Content-Encoding", "gzip") + + if _, ok := ctx.Deadline(); !ok { + ctx, _ = context.WithTimeout(ctx, streamingIngestDefaultTimeout) + } + + _, body, err := c.doRequestImpl(ctx, errors.OpIngestStream, streamUrl, closeablePayload, headers, fmt.Sprintf("With db: %s, table: %s, mappingName: %s, clientRequestId: %s", db, table, mappingName, clientRequestId)) + if body != nil { + body.Close() + } + + if err != nil { + return errors.ES(errors.OpIngestStream, errors.KHTTPError, "streaming ingestion failed: endpoint(%s): %s", streamUrl.String(), err) + } + + return nil +} diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/data/errors/errors.go b/vendor/github.com/Azure/azure-kusto-go/kusto/data/errors/errors.go index fd16be551d5..6b25f78d750 100644 --- a/vendor/github.com/Azure/azure-kusto-go/kusto/data/errors/errors.go +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/data/errors/errors.go @@ -32,12 +32,14 @@ type Op uint16 //go:generate stringer -type Op const ( - OpUnknown Op = 0 // OpUnknown indicates that the operation that caused the problem is unknown. - OpQuery Op = 1 // OpQuery indicates that a Query() call is being made. - OpMgmt Op = 2 // OpMgmt indicates that a Mgmt() call is being made. - OpServConn Op = 3 // OpServConn indicates that the client is attempting to connect to the service. - OpIngestStream Op = 4 // OpIngestStream indicates the client is making a streaming ingestion call. - OpFileIngest Op = 5 // OpFileIngest indicates the client is making a file ingestion call. + OpUnknown Op = 0 // OpUnknown indicates that the operation that caused the problem is unknown. + OpQuery Op = 1 // OpQuery indicates that a Query() call is being made. + OpMgmt Op = 2 // OpMgmt indicates that a Mgmt() call is being made. + OpServConn Op = 3 // OpServConn indicates that the client is attempting to connect to the service. + OpIngestStream Op = 4 // OpIngestStream indicates the client is making a streaming ingestion call. + OpFileIngest Op = 5 // OpFileIngest indicates the client is making a file ingestion call. + OpCloudInfo Op = 6 // OpCloudInfo indicates an error fetching data from the cloud metadata. + OpTokenProvider Op = 7 // OpTokenProvider indicates an error creating a token provider. ) // Kind field classifies the error as one of a set of standard conditions. diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/data/types/types.go b/vendor/github.com/Azure/azure-kusto-go/kusto/data/types/types.go index 30a812619bf..389185c3406 100644 --- a/vendor/github.com/Azure/azure-kusto-go/kusto/data/types/types.go +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/data/types/types.go @@ -5,8 +5,7 @@ on the column's type setting. The type information here is used to define what types of paramaters will substituted for in Stmt objects and to do discovery on table columns, if trying to do some type of dynamic discovery. - -Column +# Column Column represents a Column type. A user should never try to implemenet these. Instead they should use the constants defined in this package, such as types.Bool, types.DateTime, ... Any other value that these diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/data/value/value.go b/vendor/github.com/Azure/azure-kusto-go/kusto/data/value/value.go index 42103e06688..35c59e46dca 100644 --- a/vendor/github.com/Azure/azure-kusto-go/kusto/data/value/value.go +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/data/value/value.go @@ -2,7 +2,7 @@ Package value holds Kusto data value representations. All types provide a Kusto that stores the native value and Valid which indicates if the value was set or was null. -Kusto Value +# Kusto Value A value.Kusto can hold types that represent Kusto Scalar types that define column data. We represent that with an interface: diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/doc.go b/vendor/github.com/Azure/azure-kusto-go/kusto/doc.go index c78c55e5a6f..3ddd46642a7 100644 --- a/vendor/github.com/Azure/azure-kusto-go/kusto/doc.go +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/doc.go @@ -11,56 +11,206 @@ For details on the Azure Kusto service, see: https://azure.microsoft.com/en-us/s For general documentation on APIs and the Kusto query language, see: https://docs.microsoft.com/en-us/azure/data-explorer/ +## Examples -Creating an Authorizer and a Client +Examples for various scenarios can be found on [pkg.go.dev](https://pkg.go.dev/github.com/Azure/azure-kusto-go#readme-examples) or in the example*_test.go files in our GitHub repo for [azure-kusto-go](https://github.com/Azure/azure-kusto-go/tree/master/kusto). -To begin using this package, create an Authorizer and a client targeting your Kusto endpoint: - // auth package is: "github.com/Azure/go-autorest/autorest/azure/auth" +### Create the connection string - authorizer := kusto.Authorization{ - Config: auth.NewClientCredentialsConfig(clientID, clientSecret, tenantID), - } +Azure Data Explorer (Kusto) connection strings are created using a connection string builder for an existing Azure Data Explorer (Kusto) cluster endpoint of the form `https://..kusto.windows.net`. + +```go +kustoConnectionStringBuilder := kusto.NewConnectionStringBuilder(endpoint) +``` + +### Create and authenticate the client + +Azure Data Explorer (Kusto) clients are created from a connection string and authenticated using a credential from the [Azure Identity package][azure_identity_pkg], like [DefaultAzureCredential][default_azure_credential]. +You can also authenticate a client using a system- or user-assigned managed identity with Azure Active Directory (AAD) credentials. + +#### Using the `DefaultAzureCredential` + +```go +// kusto package is: github.com/Azure/azure-kusto-go/kusto + +// Initialize a new kusto client using the default Azure credential +kustoConnectionString := kustoConnectionStringBuilder.WithDefaultAzureCredential() +client, err = kusto.New(kustoConnectionString) - client, err := kusto.New(endpoint, authorizer) if err != nil { panic("add error handling") } -For more examples on ways to create an Authorization object, see the Authorization object documentation. +// Be sure to close the client when you're done. (Error handling omitted for brevity.) +defer client.Close() +``` + +#### Using the `az cli` + +```go +kustoConnectionString := kustoConnectionStringBuilder.WithAzCli() +client, err = kusto.New(kustoConnectionString) +``` + +#### Using a system-assigned managed identity + +```go +kustoConnectionString := kustoConnectionStringBuilder.WithSystemManagedIdentity() +client, err = kusto.New(kustoConnectionString) +``` + +#### Using a user-assigned managed identity + +```go +kustoConnectionString := kustoConnectionStringBuilder.WithUserManagedIdentity(clientID) +client, err = kusto.New(kustoConnectionString) +``` + +#### Using a bearer token + +```go +kustoConnectionString := kustoConnectionStringBuilder.WithApplicationToken(appId, token) +client, err = kusto.New(kustoConnectionString) +``` + +#### Using an app id and secret + +```go +kustoConnectionString := kustoConnectionStringBuilder.WithAadAppKey(clientID, clientSecret, tenantID) +client, err = kusto.New(kustoConnectionString) +``` + +#### Using an application certificate + +```go +kustoConnectionString := kustoConnectionStringBuilder.WithAppCertificate(appId, certificate, thumbprint, sendCertChain, authorityID) +client, err = kusto.New(kustoConnectionString) +``` + +### Querying + +#### Simple queries + +* Works for queries and management commands. +* Limited to queries that can be built using a string literal known at compile time. + +The simplest queries can be built using `kql.New`: + +```go +query := kql.New("systemNodes | project CollectionTime, NodeId") +``` + +Queries can only be built using a string literals known at compile time, and special methods for specific parts of the query. +The reason for this is to discourage the use of string concatenation to build queries, which can lead to security vulnerabilities. + +#### Queries with parameters + +* Can re-use the same query with different parameters. +* Only work for queries, management commands are not supported. + +It is recommended to use parameters for queries that contain user input. +Management commands can not use parameters, and therefore should be built using the builder (see next section). + +Parameters can be implicitly referenced in a query: + +```go +query := kql.New("systemNodes | project CollectionTime, NodeId | where CollectionTime > startTime and NodeId == nodeIdValue") +``` + +Here, `startTime` and `nodeIdValue` are parameters that can be passed to the query. + +To Pass the parameters values to the query, create `kql.Parameters`: + +``` +params := kql.NewParameters().AddDateTime("startTime", dt).AddInt("nodeIdValue", 1) +``` + +And then pass it to the `Query` method, as an option: +```go +results, err := client.Query(ctx, database, query, QueryParameters(params)) + + if err != nil { + panic("add error handling") + } + +// You can see the generated parameters using the ToDeclarationString() method: +fmt.Println(params.ToDeclarationString()) // declare query_parameters(startTime:datetime, nodeIdValue:int); +``` + +#### Queries with inline parameters +* Works for queries and management commands. +* More involved building of queries, but allows for more flexibility. -Querying for Rows +Queries with runtime data can be built using `kql.New`. +The builder will only accept the correct types for each part of the query, and will escape any special characters in the data. -Kusto provides a single method for querying, Query(). Query uses a Stmt object to provides SQL-like injection protection -and accepts only string constants for arguments. - // table package is: data/table +For example, here is a query that dynamically accepts values for the table name, and the comparison parameters for the columns: + +```go +dt, _ := time.Parse(time.RFC3339Nano, "2020-03-04T14:05:01.3109965Z") +tableName := "system nodes" +value := 1 + +query := kql.New("") + + .AddTable(tableName) + .AddLiteral(" | where CollectionTime == ").AddDateTime(dt) + .AddLiteral(" and ") + .AddLiteral("NodeId == ").AddInt(value) + +// To view the query string, use the String() method: +fmt.Println(query.String()) +// Output: ['system nodes'] | where CollectionTime == datetime(2020-03-04T14:05:01.3109965Z) and NodeId == int(1) +``` + +Building queries like this is useful for queries that are built from user input, or for queries that are built from a template, and are valid for management commands too. + +#### Query For Rows + +The kusto `table` package queries data into a ***table.Row** which can be printed or have the column data extracted. + +```go +// table package is: github.com/Azure/azure-kusto-go/kusto/data/table + +// Query our database table "systemNodes" for the CollectionTimes and the NodeIds. +iter, err := client.Query(ctx, "database", query) - // Query our database table "systemNodes" for the CollectionTimes and the NodeIds. - iter, err := client.Query(ctx, "database", kusto.NewStmt("systemNodes | project CollectionTime, NodeId")) if err != nil { panic("add error handling") } - defer iter.Stop() - // .Do() will call the function for every row in the table. - err = iter.Do( - func(row *table.Row) error { - if row.Replace { - fmt.Println("---") // Replace flag indicates that the query result should be cleared and replaced with this row - } - fmt.Println(row) // As a convenience, printing a *table.Row will output csv - return nil +defer iter.Stop() + +// .Do() will call the function for every row in the table. +err = iter.DoOnRowOrError( + + func(row *table.Row, e *kustoErrors.Error) error { + if e != nil { + return e + } + if row.Replace { + fmt.Println("---") // Replace flag indicates that the query result should be cleared and replaced with this row + } + fmt.Println(row) // As a convenience, printing a *table.Row will output csv + return nil }, - ) + +) + if err != nil { panic("add error handling") } +``` + +#### Query Into Structs -Querying Rows Into Structs +Users will often want to turn the returned data into Go structs that are easier to work with. The ***table.Row** object +that is returned supports this via the `.ToStruct()` method. -Keeping our query the same, instead of printing the Rows we will simply put them into a slice of structs +```go +// NodeRec represents our Kusto data that will be returned. - // NodeRec represents our Kusto data that will be returned. type NodeRec struct { // ID is the table's NodeId. We use the field tag here to to instruct our client to convert NodeId to ID. ID int64 `kusto:"NodeId"` @@ -68,107 +218,137 @@ Keeping our query the same, instead of printing the Rows we will simply put them CollectionTime time.Time } - iter, err := client.Query(ctx, "database", kusto.NewStmt("systemNodes | project CollectionTime, NodeId")) +iter, err := client.Query(ctx, "database", query) + if err != nil { panic("add error handling") } - defer iter.Stop() - recs := []NodeRec{} - err = iter.Do( - func(row *table.Row) error { +defer iter.Stop() + +recs := []NodeRec{} +err = iter.DoOnRowOrError( + + func(row *table.Row, e *kustoErrors.Error) error { + if e != nil { + return e + } rec := NodeRec{} if err := row.ToStruct(&rec); err != nil { return err } if row.Replace { - recs = recs[:0] // Replace flag indicates that the query result should be cleared and replaced with this row + recs = recs[:0] // Replace flag indicates that the query result should be cleared and replaced with this row } recs = append(recs, rec) return nil }, - ) + +) + + if err != nil { + panic("add error handling") + } + +``` + +### Ingestion + +The `ingest` package provides access to Kusto's ingestion service for importing data into Kusto. This requires +some prerequisite knowledge of acceptable data formats, mapping references, etc. + +That documentation can be found [here](https://docs.microsoft.com/en-us/azure/kusto/management/data-ingestion/) + +If ingesting data from memory, it is suggested that you stream the data in via `FromReader()` passing in the reader +from an `io.Pipe()`. The data will not begin ingestion until the writer closes. + +#### Creating a queued ingestion client + +Setup is quite simple, simply pass a `*kusto.Client`, the name of the database and table you wish to ingest into. + +```go +in, err := ingest.New(kustoClient, "database", "table") + if err != nil { panic("add error handling") } -A struct object can use fields to store the Kusto values as normal Go values, pointers to Go values and as value.Kusto types. -The value.Kusto types are useful when you need to distiguish between the zero value of a variable and the value not being -set in Kusto. - -All value.Kusto types have a .Value and .Valid field. .Value is the native Go value, .Valid is a bool which -indicates if the value was set. More information can be found in the sub-package data/value. - -The following is a conversion table from the Kusto column types to native Go values within a struct that are allowed: - - From Kusto Type To Go Kusto Type - ============================================================================== - bool value.Bool, bool, *bool - ------------------------------------------------------------------------------ - datetime value.DateTime, time.Time, *time.Time - ------------------------------------------------------------------------------ - dynamic value.Dynamic, string, *string - ------------------------------------------------------------------------------ - guid value.GUID, uuid.UUID, *uuid.UUID - ------------------------------------------------------------------------------ - int value.Int, int32, *int32 - ------------------------------------------------------------------------------ - long value.Long, int64, *int64 - ------------------------------------------------------------------------------ - real value.Real, float64, *float64 - ------------------------------------------------------------------------------ - string value.String, string, *string - ------------------------------------------------------------------------------ - timestamp value.Timestamp, time.Duration, *time.Duration - ------------------------------------------------------------------------------ - decimal value.Decimal, string, *string - ============================================================================== - -For more information on Kusto scalar types, see: https://docs.microsoft.com/en-us/azure/kusto/query/scalar-data-types/ - - -Stmt - -Every query is done using a Stmt. A Stmt is built with Go string constants and can do variable substitution -using Kusto's Query Paramaters. - - // rootStmt builds a query that will gather all nodes in the DB. - rootStmt := kusto.NewStmt("systemNodes | project CollectionTime, NodeId") - - // singleNodeStmt creates a new Stmt based on rootStmt and adds a "where" clause to find a single node. - // We pass a definition that sets the word ParamNodeId to a variable that will be substituted for a - // Kusto Long type (which is a a Go int64). - singleNodeStmt := rootStmt.Add(" | where NodeId == ParamNodeId").MustDefinitions( - kusto.NewDefinitions().Must( - kusto.ParamTypes{ - "ParamNodeId": kusto.ParamType{Type: types.Long}, - }, - ), - ) - - // Query using our singleNodeStmt, variable substituting for ParamNodeId - iter, err := client.Query( - ctx, - "database", - singleNode.MustParameters( - kusto.NewParameters().Must( - kusto.QueryValues{"ParamNodeId": int64(100)}, - ), - ), - ) - - -Ingest - -Support for Kusto ingestion from local files, Azure Blob Storage and streaming is supported in the sub-package ingest. -See documentation in that package for more details - -Mocking +// Be sure to close the ingestor when you're done. (Error handling omitted for brevity.) +defer in.Close() +``` + +#### Other Ingestion Clients + +There are other ingestion clients that can be used for different ingestion scenarios. The `ingest` package provides +the following clients: + - Queued Ingest - `ingest.New()` - the default client, uses queues and batching to ingest data. Most reliable. + - Streaming Ingest - `ingest.NewStreaming()` - Directly streams data into the engine. Fast, but is limited with size and can fail. + - Managed Streaming Ingest - `ingest.NewManaged()` - Combines a streaming ingest client with a queued ingest client to provide a reliable ingestion method that is fast and can ingest large amounts of data. + Managed Streaming will try to stream the data, and if it fails multiple times, it will fall back to a queued ingestion. + +#### Ingestion From a File + +Ingesting a local file requires simply passing the path to the file to be ingested: + +```go + + if _, err := in.FromFile(ctx, "/path/to/a/local/file"); err != nil { + panic("add error handling") + } + +``` + +`FromFile()` will accept Unix path names on Unix platforms and Windows path names on Windows platforms. +The file will not be deleted after upload (there is an option that will allow that though). + +#### From a Blob Storage File + +This package will also accept ingestion from an Azure Blob Storage file: + +```go + + if _, err := in.FromFile(ctx, "https://myaccount.blob.core.windows.net/$root/myblob"); err != nil { + panic("add error handling") + } + +``` + +This will ingest a file from Azure Blob Storage. We only support `https://` paths and your domain name may differ than what is here. + +#### Ingestion from an io.Reader + +Sometimes you want to ingest a stream of data that you have in memory without writing to disk. You can do this simply by chunking the +data via an `io.Reader`. + +```go +r, w := io.Pipe() + +enc := json.NewEncoder(w) + + go func() { + defer w.Close() + for _, data := range dataSet { + if err := enc.Encode(data); err != nil { + panic("add error handling") + } + } + }() + + if _, err := in.FromReader(ctx, r); err != nil { + panic("add error handling") + } + +``` + +It is important to remember that `FromReader()` will terminate when it receives an `io.EOF` from the `io.Reader`. Use `io.Readers` that won't +return `io.EOF` until the `io.Writer` is closed (such as `io.Pipe`). + +# Mocking To support mocking for this client in your code for hermetic testing purposes, this client supports mocking the data returned by our RowIterator object. Please see the MockRows documentation for code examples. -Package Examples +# Package Examples Below you will find a simple and complex example of doing Query() the represent compiled code: */ diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/internal/frames/unmarshal/json/decode.go b/vendor/github.com/Azure/azure-kusto-go/kusto/internal/frames/unmarshal/json/decode.go index 468321787af..4d38dae4f16 100644 --- a/vendor/github.com/Azure/azure-kusto-go/kusto/internal/frames/unmarshal/json/decode.go +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/internal/frames/unmarshal/json/decode.go @@ -86,14 +86,13 @@ import ( // // The JSON null value unmarshals into an interface, map, pointer, or slice // by setting that Go value to nil. Because null is often used in JSON to mean -// ``not present,'' unmarshaling a JSON null into any other Go type has no effect +// “not present,” unmarshaling a JSON null into any other Go type has no effect // on the value and produces no error. // // When unmarshaling quoted strings, invalid UTF-8 or // invalid UTF-16 surrogate pairs are not treated as an error. // Instead, they are replaced by the Unicode replacement // character U+FFFD. -// func Unmarshal(data []byte, v interface{}) error { // Check for well-formedness. // Avoids filling out half a data structure diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/internal/frames/unmarshal/json/stream.go b/vendor/github.com/Azure/azure-kusto-go/kusto/internal/frames/unmarshal/json/stream.go index 36f9c9d62b7..196d33be376 100644 --- a/vendor/github.com/Azure/azure-kusto-go/kusto/internal/frames/unmarshal/json/stream.go +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/internal/frames/unmarshal/json/stream.go @@ -287,7 +287,6 @@ var _ Unmarshaler = (*RawMessage)(nil) // Number, for JSON numbers // string, for JSON string literals // nil, for JSON null -// type Token interface{} const ( diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/internal/frames/v2/v2.go b/vendor/github.com/Azure/azure-kusto-go/kusto/internal/frames/v2/v2.go index e41ebb93636..9432b67f7be 100644 --- a/vendor/github.com/Azure/azure-kusto-go/kusto/internal/frames/v2/v2.go +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/internal/frames/v2/v2.go @@ -86,9 +86,10 @@ type DataSetCompletion struct { // Cancelled indicates that the request was cancelled. Cancelled bool // OneAPIErrors is a list of errors encountered. - OneAPIErrors []string `json:"OneApiErrors"` + OneAPIErrors []interface{} `json:"OneApiErrors"` - Op errors.Op `json:"-"` + Error errors.Error + Op errors.Op `json:"-"` } // IsFrame implements frame.Frame. @@ -98,8 +99,13 @@ func (DataSetCompletion) IsFrame() {} func (d *DataSetCompletion) UnmarshalRaw(raw json.RawMessage) error { err := json.Unmarshal(raw, &d) if err != nil { - err = errors.GetCombinedError(fmt.Errorf("json parsing failed: %v", raw), err) + err = errors.GetCombinedError(fmt.Errorf("json parsing failed: %v", string(raw)), err) + } else { + if d.HasErrors { + d.Error = *errors.OneToErr(map[string]interface{}{"OneApiErrors": d.OneAPIErrors}, d.Op) + } } + return err } diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/internal/response/response.go b/vendor/github.com/Azure/azure-kusto-go/kusto/internal/response/response.go index 3c2dedaa28c..1c0ecc66d6d 100644 --- a/vendor/github.com/Azure/azure-kusto-go/kusto/internal/response/response.go +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/internal/response/response.go @@ -11,21 +11,41 @@ import ( "github.com/Azure/azure-kusto-go/kusto/data/errors" ) +type originalCloser struct { + original io.ReadCloser + wrapper io.ReadCloser +} + +func (o *originalCloser) Read(p []byte) (n int, err error) { + return o.wrapper.Read(p) +} + +func (o *originalCloser) Close() error { + if err := o.wrapper.Close(); err != nil { + return err + } + return o.original.Close() +} + func TranslateBody(resp *http.Response, op errors.Op) (io.ReadCloser, error) { body := resp.Body + var wrapper io.ReadCloser switch enc := strings.ToLower(resp.Header.Get("Content-Encoding")); enc { case "": return body, nil case "gzip": var err error - body, err = gzip.NewReader(resp.Body) + wrapper, err = gzip.NewReader(resp.Body) if err != nil { return nil, errors.E(op, errors.KInternal, fmt.Errorf("gzip reader error: %w", err)) } case "deflate": - body = flate.NewReader(resp.Body) + wrapper = flate.NewReader(resp.Body) default: return nil, errors.ES(op, errors.KInternal, "Content-Encoding was unrecognized: %s", enc) } - return body, nil + return &originalCloser{ + original: body, + wrapper: wrapper, + }, nil } diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/internal/version/version.go b/vendor/github.com/Azure/azure-kusto-go/kusto/internal/version/version.go index 324b4c00370..ebc2b37d1cc 100644 --- a/vendor/github.com/Azure/azure-kusto-go/kusto/internal/version/version.go +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/internal/version/version.go @@ -2,4 +2,4 @@ package version // Kusto is the version of this client package that is communicated to the server. -const Kusto = "0.9.2" +const Kusto = "0.13.0" diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/kcsb.go b/vendor/github.com/Azure/azure-kusto-go/kusto/kcsb.go new file mode 100644 index 00000000000..064d4ec2832 --- /dev/null +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/kcsb.go @@ -0,0 +1,451 @@ +package kusto + +import ( + "fmt" + kustoErrors "github.com/Azure/azure-kusto-go/kusto/data/errors" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "strconv" + "strings" +) + +type ConnectionStringBuilder struct { + DataSource string + AadUserID string + Password string + UserToken string + ApplicationClientId string + ApplicationKey string + AuthorityId string + ApplicationCertificate string + ApplicationCertificateThumbprint string + SendCertificateChain bool + ApplicationToken string + AzCli bool + MsiAuthentication bool + ManagedServiceIdentity string + InteractiveLogin bool + RedirectURL string + DefaultAuth bool + ClientOptions *azcore.ClientOptions + ApplicationForTracing string + UserForTracing string + TokenCredential azcore.TokenCredential +} + +const ( + dataSource string = "DataSource" + aadUserId string = "AADUserID" + password string = "Password" + applicationClientId string = "ApplicationClientId" + applicationKey string = "ApplicationKey" + applicationCertificate string = "ApplicationCertificate" + authorityId string = "AuthorityId" + applicationToken string = "ApplicationToken" + userToken string = "UserToken" + applicationCertificateThumbprint string = "ApplicationCertificateThumbprint" + sendCertificateChain string = "SendCertificateChain" + interactiveLogin string = "InteractiveLogin" + domainHint string = "RedirectURL" +) + +const ( + BEARER_TYPE = "Bearer" +) + +var csMapping = map[string]string{"datasource": dataSource, "data source": dataSource, "addr": dataSource, "address": dataSource, "network address": dataSource, "server": dataSource, + "aad user id": aadUserId, "aaduserid": aadUserId, + "password": password, "pwd": password, + "application client id": applicationClientId, "applicationclientid": applicationClientId, "appclientid": applicationClientId, + "application key": applicationKey, "applicationkey": applicationKey, "appkey": applicationKey, + "application certificate": applicationCertificate, "applicationcertificate": applicationCertificate, + "application certificate thumbprint": applicationCertificateThumbprint, "applicationcertificatethumbprint": applicationCertificateThumbprint, + "sendcertificatechain": sendCertificateChain, "send certificate chain": sendCertificateChain, + "authority id": authorityId, "authorityid": authorityId, "authority": authorityId, "tenantid": authorityId, "tenant": authorityId, "tid": authorityId, + "application token": applicationToken, "applicationtoken": applicationToken, "apptoken": applicationToken, + "user token": userToken, "usertoken": userToken, "usrtoken": userToken, + "interactive login": interactiveLogin, "interactivelogin": interactiveLogin, + "domain hint": domainHint, "domainhint": domainHint, +} + +func requireNonEmpty(key string, value string) { + if isEmpty(value) { + panic(fmt.Sprintf("Error: %s cannot be null", key)) + } +} + +func assignValue(kcsb *ConnectionStringBuilder, rawKey string, value string) error { + rawKey = strings.ToLower(strings.Trim(rawKey, " ")) + parsedKey, ok := csMapping[rawKey] + if !ok { + return fmt.Errorf("Error: unsupported key %q in connection string ", rawKey) + } + switch parsedKey { + case dataSource: + kcsb.DataSource = value + case aadUserId: + kcsb.AadUserID = value + case password: + kcsb.Password = value + case applicationClientId: + kcsb.ApplicationClientId = value + case applicationKey: + kcsb.ApplicationKey = value + case applicationCertificate: + kcsb.ApplicationCertificate = value + case applicationCertificateThumbprint: + kcsb.ApplicationCertificateThumbprint = value + case sendCertificateChain: + bval, _ := strconv.ParseBool(value) + kcsb.SendCertificateChain = bval + case authorityId: + kcsb.AuthorityId = value + case applicationToken: + kcsb.ApplicationToken = value + case userToken: + kcsb.UserToken = value + case interactiveLogin: + bval, _ := strconv.ParseBool(value) + kcsb.InteractiveLogin = bval + case domainHint: + kcsb.RedirectURL = value + } + return nil +} + +// NewConnectionStringBuilder Creates new Kusto ConnectionStringBuilder. +// Params takes kusto connection string connStr: string. Kusto connection string should be of the format: +// https://..kusto.windows.net;AAD User ID="user@microsoft.com";Password=P@ssWord +// For more information please look at: +// https://docs.microsoft.com/azure/data-explorer/kusto/api/connection-strings/kusto +func NewConnectionStringBuilder(connStr string) *ConnectionStringBuilder { + kcsb := ConnectionStringBuilder{} + if isEmpty(connStr) { + panic("error: Connection string cannot be empty") + } + connStrArr := strings.Split(connStr, ";") + if !strings.Contains(connStrArr[0], "=") { + connStrArr[0] = "Data Source=" + connStrArr[0] + } + + for _, kvp := range connStrArr { + if isEmpty(strings.Trim(kvp, " ")) { + continue + } + kvparr := strings.Split(kvp, "=") + val := strings.Trim(kvparr[1], " ") + if isEmpty(val) { + continue + } + if err := assignValue(&kcsb, kvparr[0], val); err != nil { + panic(err) + } + } + + return &kcsb +} + +func (kcsb *ConnectionStringBuilder) resetConnectionString() { + kcsb.AadUserID = "" + kcsb.Password = "" + kcsb.UserToken = "" + kcsb.ApplicationClientId = "" + kcsb.ApplicationKey = "" + kcsb.AuthorityId = "" + kcsb.ApplicationCertificate = "" + kcsb.ApplicationCertificateThumbprint = "" + kcsb.SendCertificateChain = false + kcsb.ApplicationToken = "" + kcsb.AzCli = false + kcsb.MsiAuthentication = false + kcsb.ManagedServiceIdentity = "" + kcsb.InteractiveLogin = false + kcsb.RedirectURL = "" + kcsb.ClientOptions = nil + kcsb.DefaultAuth = false + kcsb.TokenCredential = nil +} + +// WithAadUserPassAuth Creates a Kusto Connection string builder that will authenticate with AAD user name and password. +func (kcsb *ConnectionStringBuilder) WithAadUserPassAuth(uname string, pswrd string, authorityID string) *ConnectionStringBuilder { + requireNonEmpty(dataSource, kcsb.DataSource) + requireNonEmpty(aadUserId, uname) + requireNonEmpty(password, pswrd) + kcsb.resetConnectionString() + kcsb.AadUserID = uname + kcsb.Password = pswrd + kcsb.AuthorityId = authorityID + return kcsb +} + +// WitAadUserToken Creates a Kusto Connection string builder that will authenticate with AAD user token +func (kcsb *ConnectionStringBuilder) WitAadUserToken(usertoken string) *ConnectionStringBuilder { + requireNonEmpty(dataSource, kcsb.DataSource) + requireNonEmpty(userToken, usertoken) + kcsb.resetConnectionString() + kcsb.UserToken = usertoken + return kcsb +} + +// WithAadAppKey Creates a Kusto Connection string builder that will authenticate with AAD application and key. +func (kcsb *ConnectionStringBuilder) WithAadAppKey(appId string, appKey string, authorityID string) *ConnectionStringBuilder { + requireNonEmpty(dataSource, kcsb.DataSource) + requireNonEmpty(applicationClientId, appId) + requireNonEmpty(applicationKey, appKey) + requireNonEmpty(authorityId, authorityID) + kcsb.resetConnectionString() + kcsb.ApplicationClientId = appId + kcsb.ApplicationKey = appKey + kcsb.AuthorityId = authorityID + return kcsb +} + +// WithAppCertificate Creates a Kusto Connection string builder that will authenticate with AAD application using a certificate. +func (kcsb *ConnectionStringBuilder) WithAppCertificate(appId string, certificate string, thumprint string, sendCertChain bool, authorityID string) *ConnectionStringBuilder { + requireNonEmpty(dataSource, kcsb.DataSource) + requireNonEmpty(applicationCertificate, certificate) + requireNonEmpty(authorityId, authorityID) + kcsb.resetConnectionString() + kcsb.ApplicationClientId = appId + kcsb.AuthorityId = authorityID + + kcsb.ApplicationCertificate = certificate + kcsb.ApplicationCertificateThumbprint = thumprint + kcsb.SendCertificateChain = sendCertChain + return kcsb +} + +// WithApplicationToken Creates a Kusto Connection string builder that will authenticate with AAD application and an application token. +func (kcsb *ConnectionStringBuilder) WithApplicationToken(appId string, appToken string) *ConnectionStringBuilder { + requireNonEmpty(dataSource, kcsb.DataSource) + requireNonEmpty(applicationToken, appToken) + kcsb.resetConnectionString() + kcsb.ApplicationToken = appToken + return kcsb +} + +// WithAzCli Creates a Kusto Connection string builder that will use existing authenticated az cli profile password. +func (kcsb *ConnectionStringBuilder) WithAzCli() *ConnectionStringBuilder { + requireNonEmpty(dataSource, kcsb.DataSource) + kcsb.resetConnectionString() + kcsb.AzCli = true + return kcsb +} + +// WithUserManagedIdentity Creates a Kusto Connection string builder that will authenticate with AAD application, using +// an application token obtained from a Microsoft Service Identity endpoint using user assigned id. +func (kcsb *ConnectionStringBuilder) WithUserManagedIdentity(clientID string) *ConnectionStringBuilder { + requireNonEmpty(dataSource, kcsb.DataSource) + kcsb.resetConnectionString() + kcsb.MsiAuthentication = true + kcsb.ManagedServiceIdentity = clientID + return kcsb +} + +// WithSystemManagedIdentity Creates a Kusto Connection string builder that will authenticate with AAD application, using +// an application token obtained from a Microsoft Service Identity endpoint using system assigned id. +func (kcsb *ConnectionStringBuilder) WithSystemManagedIdentity() *ConnectionStringBuilder { + requireNonEmpty(dataSource, kcsb.DataSource) + kcsb.resetConnectionString() + kcsb.MsiAuthentication = true + return kcsb +} + +// WithInteractiveLogin Creates a Kusto Connection string builder that will authenticate by launching the system default browser +// to interactively authenticate a user, and obtain an access token +func (kcsb *ConnectionStringBuilder) WithInteractiveLogin(authorityID string) *ConnectionStringBuilder { + requireNonEmpty(dataSource, kcsb.DataSource) + kcsb.resetConnectionString() + if !isEmpty(authorityID) { + kcsb.AuthorityId = authorityID + } + kcsb.InteractiveLogin = true + return kcsb +} + +// AttachPolicyClientOptions Assigns ClientOptions to string builder that contains configuration settings like Logging and Retry configs for a client's pipeline. +// Read more at https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azcore@v1.2.0/policy#ClientOptions +func (kcsb *ConnectionStringBuilder) AttachPolicyClientOptions(options *azcore.ClientOptions) *ConnectionStringBuilder { + requireNonEmpty(dataSource, kcsb.DataSource) + if options != nil { + kcsb.ClientOptions = options + } + return kcsb +} + +// WithDefaultAzureCredential Create Kusto Conntection String that will be used for default auth mode. The order of auth will be via environment variables, managed identity and Azure CLI . +// Read more at https://learn.microsoft.com/azure/developer/go/azure-sdk-authentication?tabs=bash#2-authenticate-with-azure +func (kcsb *ConnectionStringBuilder) WithDefaultAzureCredential() *ConnectionStringBuilder { + kcsb.resetConnectionString() + kcsb.DefaultAuth = true + return kcsb +} + +func (kcsb *ConnectionStringBuilder) WithTokenCredential(tokenCredential azcore.TokenCredential) *ConnectionStringBuilder { + kcsb.resetConnectionString() + kcsb.TokenCredential = tokenCredential + return kcsb +} + +// Method to be used for generating TokenCredential +func (kcsb *ConnectionStringBuilder) newTokenProvider() (*TokenProvider, error) { + tkp := &TokenProvider{} + tkp.tokenScheme = BEARER_TYPE + + var init func(*CloudInfo, *azcore.ClientOptions, string) (azcore.TokenCredential, error) + + switch { + case kcsb.InteractiveLogin: + init = func(ci *CloudInfo, cliOpts *azcore.ClientOptions, appClientId string) (azcore.TokenCredential, error) { + inOpts := &azidentity.InteractiveBrowserCredentialOptions{} + inOpts.ClientID = ci.KustoClientAppID + inOpts.TenantID = kcsb.AuthorityId + inOpts.RedirectURL = ci.KustoClientRedirectURI + inOpts.ClientOptions = *cliOpts + + cred, err := azidentity.NewInteractiveBrowserCredential(inOpts) + if err != nil { + return nil, kustoErrors.E(kustoErrors.OpTokenProvider, kustoErrors.KOther, + fmt.Errorf("error: Couldn't retrieve client credentials using Interactive Login. "+ + "Error: %s", err)) + } + + return cred, nil + } + case !isEmpty(kcsb.AadUserID) && !isEmpty(kcsb.Password): + init = func(ci *CloudInfo, cliOpts *azcore.ClientOptions, appClientId string) (azcore.TokenCredential, error) { + opts := &azidentity.UsernamePasswordCredentialOptions{ClientOptions: *cliOpts} + + cred, err := azidentity.NewUsernamePasswordCredential(kcsb.AuthorityId, appClientId, kcsb.AadUserID, kcsb.Password, opts) + + if err != nil { + return nil, kustoErrors.E(kustoErrors.OpTokenProvider, kustoErrors.KOther, + fmt.Errorf("error: Couldn't retrieve client credentials using Username Password. Error: %s", err)) + } + + return cred, nil + } + case !isEmpty(kcsb.ApplicationClientId) && !isEmpty(kcsb.ApplicationKey): + init = func(ci *CloudInfo, cliOpts *azcore.ClientOptions, appClientId string) (azcore.TokenCredential, error) { + authorityId := kcsb.AuthorityId + + if isEmpty(authorityId) { + authorityId = ci.FirstPartyAuthorityURL + } + + opts := &azidentity.ClientSecretCredentialOptions{ClientOptions: *cliOpts} + + cred, err := azidentity.NewClientSecretCredential(authorityId, appClientId, kcsb.ApplicationKey, opts) + + if err != nil { + return nil, kustoErrors.E(kustoErrors.OpTokenProvider, kustoErrors.KOther, + fmt.Errorf("error: Couldn't retrieve client credentials using Client Secret. Error: %s", err)) + } + + return cred, nil + } + case !isEmpty(kcsb.ApplicationCertificate): + init = func(ci *CloudInfo, cliOpts *azcore.ClientOptions, appClientId string) (azcore.TokenCredential, error) { + opts := &azidentity.ClientCertificateCredentialOptions{ClientOptions: *cliOpts} + opts.SendCertificateChain = kcsb.SendCertificateChain + + cert, thumprintKey, err := azidentity.ParseCertificates([]byte(kcsb.ApplicationCertificate), []byte(kcsb.ApplicationCertificateThumbprint)) + if err != nil { + return nil, kustoErrors.E(kustoErrors.OpTokenProvider, kustoErrors.KOther, err) + } + cred, err := azidentity.NewClientCertificateCredential(kcsb.AuthorityId, appClientId, cert, thumprintKey, opts) + + if err != nil { + return nil, kustoErrors.E(kustoErrors.OpTokenProvider, kustoErrors.KOther, + fmt.Errorf("error: Couldn't retrieve client credentials using Application Certificate: %s", err)) + } + + return cred, nil + } + case kcsb.MsiAuthentication: + init = func(ci *CloudInfo, cliOpts *azcore.ClientOptions, appClientId string) (azcore.TokenCredential, error) { + opts := &azidentity.ManagedIdentityCredentialOptions{ClientOptions: *cliOpts} + if !isEmpty(kcsb.ManagedServiceIdentity) { + opts.ID = azidentity.ClientID(kcsb.ManagedServiceIdentity) + } + + cred, err := azidentity.NewManagedIdentityCredential(opts) + + if err != nil { + return nil, kustoErrors.E(kustoErrors.OpTokenProvider, kustoErrors.KOther, + fmt.Errorf("error: Couldn't retrieve client credentials using Managed Identity: %s", err)) + } + + return cred, nil + } + case !isEmpty(kcsb.UserToken): + { + tkp.customToken = kcsb.UserToken + } + case !isEmpty(kcsb.ApplicationToken): + { + tkp.customToken = kcsb.ApplicationToken + } + case kcsb.AzCli: + init = func(ci *CloudInfo, cliOpts *azcore.ClientOptions, appClientId string) (azcore.TokenCredential, error) { + authorityId := kcsb.AuthorityId + + if isEmpty(authorityId) { + authorityId = ci.FirstPartyAuthorityURL + } + + opts := &azidentity.AzureCLICredentialOptions{} + opts.TenantID = kcsb.AuthorityId + cred, err := azidentity.NewAzureCLICredential(opts) + + if err != nil { + return nil, kustoErrors.E(kustoErrors.OpTokenProvider, kustoErrors.KOther, + fmt.Errorf("error: Couldn't retrieve client credentials using Azure CLI: %s", err)) + } + + return cred, nil + } + case kcsb.DefaultAuth: + init = func(ci *CloudInfo, cliOpts *azcore.ClientOptions, appClientId string) (azcore.TokenCredential, error) { + //Default Azure authentication + opts := &azidentity.DefaultAzureCredentialOptions{} + opts.ClientOptions = *cliOpts + if kcsb.ClientOptions != nil { + opts.ClientOptions = *kcsb.ClientOptions + } + if !isEmpty(kcsb.AuthorityId) { + opts.TenantID = kcsb.AuthorityId + } + + cred, err := azidentity.NewDefaultAzureCredential(opts) + + if err != nil { + return nil, kustoErrors.E(kustoErrors.OpTokenProvider, kustoErrors.KOther, + fmt.Errorf("error: Couldn't retrieve client credentials for DefaultAzureCredential: %s", err)) + } + + return cred, nil + } + case kcsb.TokenCredential != nil: + init = func(ci *CloudInfo, cliOpts *azcore.ClientOptions, appClientId string) (azcore.TokenCredential, error) { + return kcsb.TokenCredential, nil + } + + } + + if init != nil { + tkp.setInit(kcsb, init) + } + + return tkp, nil +} + +func isEmpty(str string) bool { + return strings.TrimSpace(str) == "" +} + +func (kcsb *ConnectionStringBuilder) SetConnectorDetails(name, version, appName, appVersion string, sendUser bool, overrideUser string, additionalFields ...StringPair) { + app, user := setConnectorDetails(name, version, appName, appVersion, sendUser, overrideUser, additionalFields...) + kcsb.ApplicationForTracing = app + kcsb.UserForTracing = user +} diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/kql/builder.go b/vendor/github.com/Azure/azure-kusto-go/kusto/kql/builder.go new file mode 100644 index 00000000000..cc90e7f67d0 --- /dev/null +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/kql/builder.go @@ -0,0 +1,108 @@ +package kql + +import ( + "errors" + "fmt" + "github.com/Azure/azure-kusto-go/kusto/data/types" + "github.com/google/uuid" + "github.com/shopspring/decimal" + "strings" + "time" +) + +// stringConstant is an internal type that cannot be created outside the package. The only two ways to build +// a stringConstant is to pass a string constant or use a local function to build the stringConstant. +// This allows us to enforce the use of constants or strings built with injection protection. +type stringConstant string + +// String implements fmt.Stringer. +func (s stringConstant) String() string { + return string(s) +} + +type Builder struct { + builder strings.Builder +} + +func New(value stringConstant) *Builder { + return (&Builder{ + builder: strings.Builder{}, + }).AddLiteral(value) +} + +func FromBuilder(builder *Builder) *Builder { + return New(stringConstant(builder.String())) +} + +// String implements fmt.Stringer. +func (b *Builder) String() string { + return b.builder.String() +} +func (b *Builder) addBase(value fmt.Stringer) *Builder { + b.builder.WriteString(value.String()) + return b +} + +// AddUnsafe enables unsafe actions on a Builder - adds a string as is, no validation checking or escaping. +// This turns off safety features that could allow a service client to compromise your data store. +// USE AT YOUR OWN RISK! +func (b *Builder) AddUnsafe(value string) *Builder { + b.builder.WriteString(value) + return b +} + +func (b *Builder) AddLiteral(value stringConstant) *Builder { + return b.addBase(value) +} + +func (b *Builder) AddBool(value bool) *Builder { + return b.addBase(newValue(value, types.Bool)) +} + +func (b *Builder) AddDateTime(value time.Time) *Builder { + return b.addBase(newValue(value, types.DateTime)) +} + +func (b *Builder) AddDynamic(value interface{}) *Builder { + return b.addBase(newValue(value, types.Dynamic)) +} + +func (b *Builder) AddGUID(value uuid.UUID) *Builder { + return b.addBase(newValue(value, types.GUID)) +} + +func (b *Builder) AddInt(value int32) *Builder { + return b.addBase(newValue(value, types.Int)) +} + +func (b *Builder) AddLong(value int64) *Builder { + return b.addBase(newValue(value, types.Long)) +} + +func (b *Builder) AddReal(value float64) *Builder { + return b.addBase(newValue(value, types.Real)) +} + +func (b *Builder) AddString(value string) *Builder { + return b.addBase(newValue(value, types.String)) +} + +func (b *Builder) AddTimespan(value time.Duration) *Builder { + return b.addBase(newValue(value, types.Timespan)) +} + +func (b *Builder) AddDecimal(value decimal.Decimal) *Builder { + return b.addBase(newValue(value, types.Decimal)) +} + +func (b *Builder) GetParameters() (map[string]string, error) { + return nil, errors.New("this option does not support Parameters") +} +func (b *Builder) SupportsInlineParameters() bool { + return false +} + +// Reset resets the stringBuilder +func (b *Builder) Reset() { + b.builder.Reset() +} diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/kql/identifier.go b/vendor/github.com/Azure/azure-kusto-go/kusto/kql/identifier.go new file mode 100644 index 00000000000..dd3318aa3f2 --- /dev/null +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/kql/identifier.go @@ -0,0 +1,39 @@ +package kql + +import "fmt" + +func (b *Builder) AddDatabase(database string) *Builder { + return b.addBase(stringConstant(fmt.Sprintf("%s(%s)", "database", QuoteString(database, false)))) +} + +func (b *Builder) AddTable(table string) *Builder { + return b.addBase(stringConstant(NormalizeName(table))) +} + +func (b *Builder) AddKeyword(keyword string) *Builder { + if RequiresQuoting(keyword) { + panic("Invalid keyword. Cannot add a keyword that requires escaping.") + } + return b.addBase(stringConstant(keyword)) +} + +func (b *Builder) AddColumn(column string) *Builder { + return b.addBase(stringConstant(NormalizeName(column))) +} + +func (b *Builder) AddFunction(function string) *Builder { + return b.addBase(stringConstant(NormalizeName(function))) +} + +// NormalizeName normalizes a string in order to be used safely in the engine - given "query" will produce [\"query\"]. +func NormalizeName(name string) string { + if name == "" { + return name + } + + if !RequiresQuoting(name) { + return name + } + + return "[" + QuoteString(name, false) + "]" +} diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/kql/query_parameters.go b/vendor/github.com/Azure/azure-kusto-go/kusto/kql/query_parameters.go new file mode 100644 index 00000000000..0ee604aeb5c --- /dev/null +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/kql/query_parameters.go @@ -0,0 +1,106 @@ +package kql + +import ( + "github.com/Azure/azure-kusto-go/kusto/data/types" + "github.com/google/uuid" + "github.com/shopspring/decimal" + "strings" + "time" +) + +type Parameters struct { + parameters map[string]Value +} + +func NewParameters() *Parameters { + return &Parameters{parameters: make(map[string]Value)} +} + +func (q *Parameters) Count() int { + return len(q.parameters) +} +func (q *Parameters) addBase(key string, value Value) *Parameters { + if RequiresQuoting(key) { + panic("Invalid parameter values. make sure to adhere to KQL entity name conventions and escaping rules.") + } + q.parameters[key] = value + return q +} + +func (q *Parameters) AddBool(key string, value bool) *Parameters { + return q.addBase(key, newValue(value, types.Bool)) +} + +func (q *Parameters) AddDateTime(key string, value time.Time) *Parameters { + return q.addBase(key, newValue(value, types.DateTime)) +} + +func (q *Parameters) AddDynamic(key string, value interface{}) *Parameters { + return q.addBase(key, newValue(value, types.Dynamic)) +} + +func (q *Parameters) AddGUID(key string, value uuid.UUID) *Parameters { + return q.addBase(key, newValue(value, types.GUID)) +} + +func (q *Parameters) AddInt(key string, value int32) *Parameters { + return q.addBase(key, newValue(value, types.Int)) +} + +func (q *Parameters) AddLong(key string, value int64) *Parameters { + return q.addBase(key, newValue(value, types.Long)) +} + +func (q *Parameters) AddReal(key string, value float64) *Parameters { + return q.addBase(key, newValue(value, types.Real)) +} + +func (q *Parameters) AddString(key string, value string) *Parameters { + return q.addBase(key, newValue(value, types.String)) +} + +func (q *Parameters) AddTimespan(key string, value time.Duration) *Parameters { + return q.addBase(key, newValue(value, types.Timespan)) +} + +func (q *Parameters) AddDecimal(key string, value decimal.Decimal) *Parameters { + return q.addBase(key, newValue(value, types.Decimal)) +} + +func (q *Parameters) ToDeclarationString() string { + const ( + declare = "declare query_parameters(" + closeStmt = ");" + ) + var build = strings.Builder{} + + if len(q.parameters) == 0 { + return "" + } + + build.WriteString(declare) + comma := len(q.parameters) + for key, paramVals := range q.parameters { + build.WriteString(key) + build.WriteString(":") + build.WriteString(string(paramVals.Type())) + if comma > 1 { + build.WriteString(", ") + } + comma-- + } + build.WriteString(closeStmt) + return build.String() +} +func (q *Parameters) ToParameterCollection() map[string]string { + var parameters = make(map[string]string) + for key, paramVals := range q.parameters { + parameters[key] = paramVals.String() + } + return parameters +} + +// Reset resets the parameters map +func (q *Parameters) Reset() { + q.parameters = make(map[string]Value) +} diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/kql/string_utils.go b/vendor/github.com/Azure/azure-kusto-go/kusto/kql/string_utils.go new file mode 100644 index 00000000000..e15544b617e --- /dev/null +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/kql/string_utils.go @@ -0,0 +1,119 @@ +package kql + +import ( + "fmt" + "strings" + "time" + "unicode" +) + +// RequiresQuoting checks whether a given string is an identifier +func RequiresQuoting(value string) bool { + if value == "" { + return false + } + + for _, c := range value { + if c == '_' { + continue + } + + if c > unicode.MaxLatin1 || !(unicode.IsLetter(c) || unicode.IsDigit(c)) { + return true + } + } + return false +} + +func QuoteString(value string, hidden bool) string { + if value == "" { + return value + } + + var literal strings.Builder + + if hidden { + literal.WriteString("h") + } + literal.WriteString("\"") + for _, c := range value { + switch c { + case '\'': + literal.WriteString("\\'") + + case '"': + literal.WriteString("\\\"") + + case '\\': + literal.WriteString("\\\\") + + case '\x00': + literal.WriteString("\\0") + + case '\a': + literal.WriteString("\\a") + + case '\b': + literal.WriteString("\\b") + + case '\f': + literal.WriteString("\\f") + + case '\n': + literal.WriteString("\\n") + + case '\r': + literal.WriteString("\\r") + + case '\t': + literal.WriteString("\\t") + + case '\v': + literal.WriteString("\\v") + + default: + if !ShouldBeEscaped(c) { + literal.WriteString(string(c)) + } else { + literal.WriteString(fmt.Sprintf("\\u%04x", c)) + } + + } + } + literal.WriteString("\"") + + return literal.String() +} + +// ShouldBeEscaped Checks whether a rune should be escaped or not based on it's type. +func ShouldBeEscaped(c int32) bool { + if c <= unicode.MaxLatin1 { + return unicode.IsControl(c) + } + return true +} + +func FormatTimespan(duration time.Duration) string { + // Calculate the number of days in the duration + days := duration / (24 * time.Hour) + + // Calculate the remaining time after subtracting the days + remaining := duration - days*24*time.Hour + + daysStr := "" + if days > 0 { + daysStr = fmt.Sprintf("%d.", days) + } + + // Use the `fmt.Sprintf()` function to format the duration as a string + return fmt.Sprintf("%s%02d:%02d:%02d.%07d", + daysStr, + int(remaining.Hours()), + int(remaining.Minutes())%60, + int(remaining.Seconds())%60, + int((remaining.Nanoseconds())%1000000000/100)) +} + +func FormatDatetime(datetime time.Time) string { + return datetime.Format("2006-01-02T15:04:05.9999999Z07:00") +} diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/kql/value.go b/vendor/github.com/Azure/azure-kusto-go/kusto/kql/value.go new file mode 100644 index 00000000000..d8c4fc69e0a --- /dev/null +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/kql/value.go @@ -0,0 +1,52 @@ +package kql + +import ( + "fmt" + "github.com/Azure/azure-kusto-go/kusto/data/types" + "github.com/Azure/azure-kusto-go/kusto/data/value" + "time" +) + +type Value interface { + fmt.Stringer + Value() interface{} + Type() types.Column +} + +type kqlValue struct { + value interface{} + kustoType types.Column +} + +func (v *kqlValue) Value() interface{} { + return v.value +} + +func (v *kqlValue) Type() types.Column { + return v.kustoType +} + +func (v *kqlValue) String() string { + val := v.value + switch v.kustoType { + case types.String: + return QuoteString(val.(string), false) + case types.DateTime: + val = FormatDatetime(val.(time.Time)) + case types.Timespan: + val = FormatTimespan(val.(time.Duration)) + case types.Dynamic: + got := value.Dynamic{} + _ = got.Unmarshal(val) + val = got + } + + return fmt.Sprintf("%v(%v)", v.kustoType, val) +} + +func newValue(value interface{}, kustoType types.Column) Value { + return &kqlValue{ + value: value, + kustoType: kustoType, + } +} diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/kusto.go b/vendor/github.com/Azure/azure-kusto-go/kusto/kusto.go index 5e6ec7731b2..bc202825ad2 100644 --- a/vendor/github.com/Azure/azure-kusto-go/kusto/kusto.go +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/kusto.go @@ -5,7 +5,6 @@ import ( "io" "net/http" "net/url" - "reflect" "strings" "sync" "time" @@ -13,99 +12,27 @@ import ( "github.com/Azure/azure-kusto-go/kusto/data/errors" "github.com/Azure/azure-kusto-go/kusto/internal/frames" v2 "github.com/Azure/azure-kusto-go/kusto/internal/frames/v2" - - "github.com/Azure/go-autorest/autorest" - "github.com/Azure/go-autorest/autorest/azure/auth" ) // queryer provides for getting a stream of Kusto frames. Exists to allow fake Kusto streams in tests. type queryer interface { io.Closer - query(ctx context.Context, db string, query Stmt, options *queryOptions) (execResp, error) - mgmt(ctx context.Context, db string, query Stmt, options *mgmtOptions) (execResp, error) - queryToJson(ctx context.Context, db string, query Stmt, options *queryOptions) (string, error) + query(ctx context.Context, db string, query Statement, options *queryOptions) (execResp, error) + mgmt(ctx context.Context, db string, query Statement, options *queryOptions) (execResp, error) + queryToJson(ctx context.Context, db string, query Statement, options *queryOptions) (string, error) } -// Authorization provides the ADAL authorizer needed to access the resource. You can set Authorizer or -// Config, but not both. +// Authorization provides the TokenProvider needed to acquire the auth token. type Authorization struct { - // Authorizer provides an authorizer to use when talking to Kusto. If this is set, the - // Authorizer must have its Resource (also called Resource ID) set to the endpoint passed - // to the New() constructor. This will be something like "https://somename.westus.kusto.windows.net". - // This package will try to set that automatically for you. - Authorizer autorest.Authorizer - // Config provides the authorizer's config that can create the authorizer. We recommending setting - // this instead of Authorizer, as we will automatically set the Resource ID with the endpoint passed. - Config auth.AuthorizerConfig + // Token provider that can be used to get the access token. + TokenProvider *TokenProvider } -// Validate validates the Authorization object against the endpoint an preps it for use. -// For internal use only. -func (a *Authorization) Validate(endpoint string) error { - const rescField = "Resource" - - if strings.Contains(strings.ToLower(endpoint), ".azuresynapse") { - endpoint = "https://kusto.kusto.windows.net" - } - - if a.Authorizer != nil && a.Config != nil { - return errors.ES(errors.OpServConn, errors.KClientArgs, "cannot set Authoriztion.Authorizer and Authorizer.Config") - } - if a.Authorizer == nil && a.Config == nil { - return errors.ES(errors.OpServConn, errors.KClientArgs, "cannot leave all Authoriztion fields as zero values") - } - if a.Authorizer != nil { - return nil - } - - // This is sort of hacky, in that we are using what we know about the current auth library's internals - // structure to try and make this fix. But the auth library is confusing and this will stem off a bunch of - // support calls, so it is worth attempting. - v := reflect.ValueOf(a.Config) - switch v.Kind() { - // This piece of code is what I call hopeful thinking. The New*() calls in auth.go should return pointers - // (they did an interface which is bad). So this is hoping someone passed a pointer in the Authorizer interface. - case reflect.Ptr: - if reflect.PtrTo(v.Type()).Kind() == reflect.Struct { - v = v.Elem() - if f := v.FieldByName(rescField); !f.IsZero() { - if f.Kind() == reflect.String { - f.SetString(endpoint) - } - } else { - return errors.ES(errors.OpServConn, errors.KClientArgs, "the Authorization.Config passed to the Kusto client did not have an underlying .Resource field") - } - } else { - return errors.ES(errors.OpServConn, errors.KClientArgs, "the Authorization.Config passed to the Kusto client was a pointer to a %T, which is not a struct", a.Config) - } - // This is how we are likely to get the Authorizer. So since we can't change the fields, now we have to type assert - // to the underlying type and put back a new copy. Note: it seems to me that we should be get a copy of a.Config - // and then set the field (without using unsafe), then do the re-assignment. But I haven't been able to parse this out atm. - case reflect.Struct: - switch t := a.Config.(type) { - case auth.ClientCredentialsConfig: - t.Resource = endpoint - a.Config = t - case auth.DeviceFlowConfig: - t.Resource = endpoint - a.Config = t - case auth.MSIConfig: - t.Resource = endpoint - a.Config = t - default: - return errors.ES(errors.OpServConn, errors.KClientArgs, "the Authiorization.Config passed to the Kusto client is not a type we know how to deal with: %T", t) - } - default: - return errors.ES(errors.OpServConn, errors.KClientArgs, "the Authorization.Config passed to the Kusto client was not a Pointer to a struct or a struct, is a: %T", a.Config) - - } - var err error - a.Authorizer, err = a.Config.Authorizer() - if err != nil { - return errors.E(errors.OpServConn, errors.KClientArgs, err) - } - return nil -} +const ( + defaultMgmtTimeout = time.Hour + defaultQueryTimeout = 4 * time.Minute + clientServerDelta = 30 * time.Second +) // Client is a client to a Kusto instance. type Client struct { @@ -114,13 +41,22 @@ type Client struct { auth Authorization mgmtConnMu sync.Mutex http *http.Client + clientDetails *ClientDetails } // Option is an optional argument type for New(). type Option func(c *Client) -// New returns a new Client. endpoint is the Kusto endpoint to use, example: https://somename.westus.kusto.windows.net . -func New(endpoint string, auth Authorization, options ...Option) (*Client, error) { +// New returns a new Client. +func New(kcsb *ConnectionStringBuilder, options ...Option) (*Client, error) { + tkp, err := kcsb.newTokenProvider() + if err != nil { + return nil, err + } + auth := &Authorization{ + TokenProvider: tkp, + } + endpoint := kcsb.DataSource u, err := url.Parse(endpoint) if err != nil { return nil, errors.ES(errors.OpServConn, errors.KClientArgs, "could not parse the endpoint(%s): %s", endpoint, err).SetNoRetry() @@ -134,20 +70,20 @@ func New(endpoint string, auth Authorization, options ...Option) (*Client, error ) } - client := &Client{auth: auth, endpoint: endpoint} + client := &Client{auth: *auth, endpoint: endpoint, clientDetails: NewClientDetails(kcsb.ApplicationForTracing, kcsb.UserForTracing)} for _, o := range options { o(client) } - if err := auth.Validate(endpoint); err != nil { - return nil, err - } - if client.http == nil { - client.http = &http.Client{} + client.http = &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } } - conn, err := newConn(endpoint, auth, client.http) + conn, err := NewConn(endpoint, *auth, client.http, client.clientDetails) if err != nil { return nil, err } @@ -167,8 +103,8 @@ type QueryOption func(q *queryOptions) error // Note: QueryOption are defined in queryopts.go file -// MgmtOption is an option type for a call to Mgmt(). -type MgmtOption func(m *mgmtOptions) error +// Deprecated: MgmtOption will be removed in a future release. Use QueryOption instead. +type MgmtOption = QueryOption // Note: MgmtOption are defined in queryopts.go file @@ -194,13 +130,13 @@ const ( // query is a injection safe Stmt object. Queries cannot take longer than 5 minutes by default and have row/size limitations. // Note that the server has a timeout of 4 minutes for a query by default unless the context deadline is set. Queries can // take a maximum of 1 hour. -func (c *Client) Query(ctx context.Context, db string, query Stmt, options ...QueryOption) (*RowIterator, error) { +func (c *Client) Query(ctx context.Context, db string, query Statement, options ...QueryOption) (*RowIterator, error) { ctx, cancel, err := contextSetup(ctx, false) // Note: cancel is called when *RowIterator has Stop() called. if err != nil { return nil, err } - opts, err := setQueryOptions(ctx, errors.OpQuery, query, options...) + opts, err := setQueryOptions(ctx, errors.OpQuery, query, queryCall, options...) if err != nil { return nil, err } @@ -254,13 +190,13 @@ func (c *Client) Query(ctx context.Context, db string, query Stmt, options ...Qu return iter, nil } -func (c *Client) QueryToJson(ctx context.Context, db string, query Stmt, options ...QueryOption) (string, error) { +func (c *Client) QueryToJson(ctx context.Context, db string, query Statement, options ...QueryOption) (string, error) { ctx, cancel, err := contextSetup(ctx, false) // Note: cancel is called when *RowIterator has Stop() called. if err != nil { return "", err } - opts, err := setQueryOptions(ctx, errors.OpQuery, query, options...) + opts, err := setQueryOptions(ctx, errors.OpQuery, query, queryCall, options...) if err != nil { return "", err } @@ -284,9 +220,11 @@ func (c *Client) QueryToJson(ctx context.Context, db string, query Stmt, options // Mgmt accepts a Stmt, but that Stmt cannot have any query parameters attached at this time. // Note that the server has a timeout of 10 minutes for a management call by default unless the context deadline is set. // There is a maximum of 1 hour. -func (c *Client) Mgmt(ctx context.Context, db string, query Stmt, options ...MgmtOption) (*RowIterator, error) { - if !query.params.IsZero() || !query.defs.IsZero() { - return nil, errors.ES(errors.OpMgmt, errors.KClientArgs, "a Mgmt() call cannot accept a Stmt object that has Definitions or Parameters attached") +func (c *Client) Mgmt(ctx context.Context, db string, query Statement, options ...QueryOption) (*RowIterator, error) { + if stmt, ok := query.(Stmt); ok { + if !stmt.params.IsZero() || !stmt.defs.IsZero() { + return nil, errors.ES(errors.OpMgmt, errors.KClientArgs, "a Mgmt() call cannot accept a Stmt object that has Definitions or Parameters attached") + } } ctx, cancel, err := contextSetup(ctx, true) // Note: cancel is called when *RowIterator has Stop() called. @@ -294,12 +232,12 @@ func (c *Client) Mgmt(ctx context.Context, db string, query Stmt, options ...Mgm return nil, err } - opts, err := setMgmtOptions(ctx, errors.OpMgmt, query, options...) + opts, err := setQueryOptions(ctx, errors.OpQuery, query, mgmtCall, options...) if err != nil { return nil, err } - conn, err := c.getConn(mgmtCall, connOptions{mgmtOptions: opts}) + conn, err := c.getConn(mgmtCall, connOptions{queryOptions: opts}) if err != nil { return nil, err } @@ -326,75 +264,64 @@ func (c *Client) Mgmt(ctx context.Context, db string, query Stmt, options ...Mgm return iter, nil } -func setQueryOptions(ctx context.Context, op errors.Op, query Stmt, options ...QueryOption) (*queryOptions, error) { - params, err := query.params.toParameters(query.defs) - if err != nil { - return nil, errors.ES(op, errors.KClientArgs, "QueryValues in the the Stmt were incorrect: %s", err).SetNoRetry() - } - - // Match our server deadline to our context.Deadline. This should be set from withing kusto.Query() to always have a value. - deadline, ok := ctx.Deadline() - if ok { - options = append( - options, - queryServerTimeout(deadline.Sub(nower())), - ) - } - +func setQueryOptions(ctx context.Context, op errors.Op, query Statement, queryType int, options ...QueryOption) (*queryOptions, error) { opt := &queryOptions{ requestProperties: &requestProperties{ - Options: map[string]interface{}{}, - Parameters: params, + Options: map[string]interface{}{}, }, } - /*if op == errors.OpQuery { + + if op == errors.OpQuery { // We want progressive frames by default for Query(), but not Mgmt() because it uses v1 framing and ingestion endpoints // do not support it. - opt.requestProperties.Options["results_progressive_enabled"] = true - }*/ - opt.requestProperties.Options["results_progressive_enabled"] = true + opt.requestProperties.Options[RequestProgressiveEnabledValue] = true + } for _, o := range options { if err := o(opt); err != nil { return nil, errors.ES(op, errors.KClientArgs, "QueryValues in the the Stmt were incorrect: %s", err).SetNoRetry() } } + + CalculateTimeout(ctx, opt, queryType) + + if query.SupportsInlineParameters() { + if opt.requestProperties.QueryParameters.Count() != 0 { + return nil, errors.ES(op, errors.KClientArgs, "kusto.Stmt does not support the QueryParameters option. Construct your query using `kql.New`").SetNoRetry() + } + params, err := query.GetParameters() + if err != nil { + return nil, errors.ES(op, errors.KClientArgs, "Parameter validation error: %s", err).SetNoRetry() + } + + opt.requestProperties.Parameters = params + } return opt, nil } -func setMgmtOptions(ctx context.Context, op errors.Op, query Stmt, options ...MgmtOption) (*mgmtOptions, error) { - params, err := query.params.toParameters(query.defs) - if err != nil { - return nil, errors.ES(op, errors.KClientArgs, "QueryValues in the the Stmt were incorrect: %s", err).SetNoRetry() +func CalculateTimeout(ctx context.Context, opt *queryOptions, queryType int) { + // If the user has specified a timeout, use that. + if val, ok := opt.requestProperties.Options[NoRequestTimeoutValue]; ok && val.(bool) { + return } - - // Match our server deadline to our context.Deadline. This should be set from withing kusto.Query() to always have a value. - deadline, ok := ctx.Deadline() - if ok { - options = append( - options, - mgmtServerTimeout(deadline.Sub(nower())), - ) + if _, ok := opt.requestProperties.Options[ServerTimeoutValue]; ok { + return } - opt := &mgmtOptions{ - requestProperties: &requestProperties{ - Options: map[string]interface{}{}, - Parameters: params, - }, - } - if op == errors.OpQuery { - // We want progressive frames by default for Query(), but not Mgmt() because it uses v1 framing and ingestion endpoints - // do not support it. - opt.requestProperties.Options["results_progressive_enabled"] = true + // Otherwise use the context deadline, if it exists. If it doesn't, use the default timeout. + if deadline, ok := ctx.Deadline(); ok { + opt.requestProperties.Options[ServerTimeoutValue] = deadline.Sub(time.Now()) + return } - for _, o := range options { - if err := o(opt); err != nil { - return nil, errors.ES(op, errors.KClientArgs, "QueryValues in the the Stmt were incorrect: %s", err).SetNoRetry() - } + var timeout time.Duration + switch queryType { + case queryCall: + timeout = defaultQueryTimeout + case mgmtCall: + timeout = defaultMgmtTimeout } - return opt, nil + opt.requestProperties.Options[ServerTimeoutValue] = timeout + clientServerDelta } func (c *Client) getConn(callType callType, options connOptions) (queryer, error) { @@ -402,8 +329,8 @@ func (c *Client) getConn(callType callType, options connOptions) (queryer, error case queryCall: return c.conn, nil case mgmtCall: - delete(options.mgmtOptions.requestProperties.Options, "results_progressive_enabled") - if options.mgmtOptions.queryIngestion { + delete(options.queryOptions.requestProperties.Options, "results_progressive_enabled") + if options.queryOptions.queryIngestion { c.mgmtConnMu.Lock() defer c.mgmtConnMu.Unlock() @@ -414,10 +341,12 @@ func (c *Client) getConn(callType callType, options connOptions) (queryer, error u, _ := url.Parse(c.endpoint) // Don't care about the error u.Host = "ingest-" + u.Host auth := c.auth - if err := auth.Validate(u.String()); err != nil { - return nil, err + var details *ClientDetails + if innerConn, ok := c.conn.(*Conn); ok { + details = innerConn.clientDetails } - iconn, err := newConn(u.String(), auth, c.http) + + iconn, err := NewConn(u.String(), auth, c.http, details) if err != nil { return nil, err } @@ -431,12 +360,10 @@ func (c *Client) getConn(callType callType, options connOptions) (queryer, error } } -var nower = time.Now - func contextSetup(ctx context.Context, mgmtCall bool) (context.Context, context.CancelFunc, error) { t, ok := ctx.Deadline() if ok { - d := t.Sub(nower()) + d := t.Sub(time.Now()) if d > 1*time.Hour { if mgmtCall { return ctx, nil, errors.ES(errors.OpMgmt, errors.KClientArgs, "cannot set a deadline greater than 1 hour(%s)", d) @@ -458,6 +385,10 @@ func (c *Client) HttpClient() *http.Client { return c.http } +func (c *Client) ClientDetails() *ClientDetails { + return c.clientDetails +} + func (c *Client) Close() error { var err error if c.conn != nil { diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/mgmtopts.go b/vendor/github.com/Azure/azure-kusto-go/kusto/mgmtopts.go deleted file mode 100644 index c3862a8c008..00000000000 --- a/vendor/github.com/Azure/azure-kusto-go/kusto/mgmtopts.go +++ /dev/null @@ -1,42 +0,0 @@ -package kusto - -import ( - "time" - - "github.com/Azure/azure-kusto-go/kusto/data/errors" - "github.com/Azure/azure-kusto-go/kusto/data/value" -) - -type mgmtOptions struct { - requestProperties *requestProperties - queryIngestion bool -} - -// Deprecated: Writing mode is now the default. Use the `RequestReadonly` option to make a read-only request. -func AllowWrite() MgmtOption { - return func(m *mgmtOptions) error { - return nil - } -} - -// IngestionEndpoint will instruct the Mgmt call to connect to the ingest-[endpoint] instead of [endpoint]. -// This is not often used by end users and can only be used with a Mgmt() call. -func IngestionEndpoint() MgmtOption { - return func(m *mgmtOptions) error { - m.queryIngestion = true - return nil - } -} - -// mgmtServerTimeout is the amount of time the server will allow a call to take. -// NOTE: I have made the serverTimeout private. For the moment, I'm going to use the context.Context timer -// to set timeouts via this private method. -func mgmtServerTimeout(d time.Duration) MgmtOption { - return func(m *mgmtOptions) error { - if d > 1*time.Hour { - return errors.ES(errors.OpQuery, errors.KClientArgs, "ServerTimeout option was set to %v, but can't be more than 1 hour", d) - } - m.requestProperties.Options["servertimeout"] = value.Timespan{Valid: true, Value: d}.Marshal() - return nil - } -} diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/mock.go b/vendor/github.com/Azure/azure-kusto-go/kusto/mock.go index 6e78b7cb8cd..6b19a8ec330 100644 --- a/vendor/github.com/Azure/azure-kusto-go/kusto/mock.go +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/mock.go @@ -13,7 +13,6 @@ import ( "github.com/Azure/azure-kusto-go/kusto/data/value" "github.com/Azure/azure-kusto-go/kusto/internal/frames" v1 "github.com/Azure/azure-kusto-go/kusto/internal/frames/v1" - "github.com/Azure/go-autorest/autorest" ) type columnData struct { @@ -122,19 +121,19 @@ func (m *MockRows) Error(err error) error { type mockConn struct { } -func (m mockConn) queryToJson(ctx context.Context, db string, query Stmt, options *queryOptions) (string, error) { - return "[]]", nil +func (m mockConn) queryToJson(ctx context.Context, db string, query Statement, options *queryOptions) (string, error) { + return "[]", nil } func (m mockConn) Close() error { return nil } -func (m mockConn) query(_ context.Context, _ string, _ Stmt, _ *queryOptions) (execResp, error) { +func (m mockConn) query(_ context.Context, _ string, _ Statement, _ *queryOptions) (execResp, error) { return execResp{}, nil } -func (m mockConn) mgmt(_ context.Context, _ string, _ Stmt, _ *mgmtOptions) (execResp, error) { +func (m mockConn) mgmt(_ context.Context, _ string, _ Statement, _ *queryOptions) (execResp, error) { framesCh := make(chan frames.Frame, 100) framesCh <- v1.DataTable{} close(framesCh) @@ -146,12 +145,17 @@ func (m mockConn) mgmt(_ context.Context, _ string, _ Stmt, _ *mgmtOptions) (exe } func NewMockClient() *Client { + + kcsb := NewConnectionStringBuilder("https://sdkse2etest.eastus.kusto.windows.net") + tkp, _ := kcsb.newTokenProvider() + return &Client{ - conn: mockConn{}, - ingestConn: mockConn{}, - endpoint: "https://sdkse2etest.eastus.kusto.windows.net", - auth: Authorization{Authorizer: autorest.NewBasicAuthorizer("", "")}, - mgmtConnMu: sync.Mutex{}, - http: &http.Client{}, + conn: mockConn{}, + ingestConn: mockConn{}, + endpoint: "https://sdkse2etest.eastus.kusto.windows.net", + auth: Authorization{TokenProvider: tkp}, + mgmtConnMu: sync.Mutex{}, + http: &http.Client{}, + clientDetails: NewClientDetails("test", "test"), } } diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/query_builder.go b/vendor/github.com/Azure/azure-kusto-go/kusto/query_builder.go index 1eecb6fc506..48bd164e7e2 100644 --- a/vendor/github.com/Azure/azure-kusto-go/kusto/query_builder.go +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/query_builder.go @@ -473,6 +473,13 @@ func (q Parameters) validate(p Definitions) (Parameters, error) { return q, nil } +// Statement is an interface designated to generalize query/management objects - both Stmt, and kql.StatementBuilder +type Statement interface { + fmt.Stringer + GetParameters() (map[string]string, error) + SupportsInlineParameters() bool +} + // Stmt is a Kusto Query statement. A Stmt is thread-safe, but methods on the Stmt are not. // All methods on a Stmt do not alter the statement, they return a new Stmt object with the changes. // This includes a copy of the Definitions and Parameters objects, if provided. This allows a @@ -487,6 +494,13 @@ type Stmt struct { // StmtOption is an optional argument to NewStmt(). type StmtOption func(s *Stmt) +func (s Stmt) GetParameters() (map[string]string, error) { + return s.params.toParameters(s.defs) +} +func (s Stmt) SupportsInlineParameters() bool { + return true +} + // UnsafeStmt enables unsafe actions on a Stmt and all Stmts derived from that Stmt. // This turns off safety features that could allow a service client to compromise your data store. // USE AT YOUR OWN RISK! @@ -497,6 +511,7 @@ func UnsafeStmt(options unsafe.Stmt) StmtOption { } } +// Deprecated: Use kql.New and kql.NewParameters instead. // NewStmt creates a Stmt from a string constant. func NewStmt(query stringConstant, options ...StmtOption) Stmt { s := Stmt{queryStr: query.String()} diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/queryopts.go b/vendor/github.com/Azure/azure-kusto-go/kusto/queryopts.go index ab94e4969e0..1f6a3cd8b0a 100644 --- a/vendor/github.com/Azure/azure-kusto-go/kusto/queryopts.go +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/queryopts.go @@ -4,9 +4,9 @@ package kusto // it clogs up the main kusto.go file. import ( + "github.com/Azure/azure-kusto-go/kusto/kql" "time" - "github.com/Azure/azure-kusto-go/kusto/data/errors" "github.com/Azure/azure-kusto-go/kusto/data/value" ) @@ -14,16 +14,20 @@ import ( // For more information please look at: https://docs.microsoft.com/en-us/azure/kusto/api/netfx/request-properties // Not all of the documented options are implemented. type requestProperties struct { - Options map[string]interface{} - Parameters map[string]string - Application string - User string + Options map[string]interface{} + Parameters map[string]string + Application string `json:"-"` + User string `json:"-"` + QueryParameters kql.Parameters `json:"-"` + ClientRequestID string `json:"-"` } type queryOptions struct { requestProperties *requestProperties + queryIngestion bool } +const RequestProgressiveEnabledValue = "results_progressive_enabled" const NoRequestTimeoutValue = "norequesttimeout" const NoTruncationValue = "notruncation" const ServerTimeoutValue = "servertimeout" @@ -75,6 +79,14 @@ const TruncationMaxRecordsValue = "truncation_max_records" const TruncationMaxSizeValue = "truncation_max_size" const ValidatePermissionsValue = "validate_permissions" +// ClientRequestID sets the x-ms-client-request-id header, and can be used to identify the request in the `.show queries` output. +func ClientRequestID(clientRequestID string) QueryOption { + return func(q *queryOptions) error { + q.requestProperties.ClientRequestID = clientRequestID + return nil + } +} + // Application sets the x-ms-app header, and can be used to identify the application making the request in the `.show queries` output. func Application(appName string) QueryOption { return func(q *queryOptions) error { @@ -83,6 +95,15 @@ func Application(appName string) QueryOption { } } +// QueryParameters sets the parameters to be used in the query. +func QueryParameters(queryParameters *kql.Parameters) QueryOption { + return func(q *queryOptions) error { + q.requestProperties.QueryParameters = *queryParameters + q.requestProperties.Parameters = queryParameters.ToParameterCollection() + return nil + } +} + // User sets the x-ms-user header, and can be used to identify the user making the request in the `.show queries` output. func User(userName string) QueryOption { return func(q *queryOptions) error { @@ -110,19 +131,14 @@ func NoTruncation() QueryOption { // ResultsProgressiveDisable disables the progressive query stream. func ResultsProgressiveDisable() QueryOption { return func(q *queryOptions) error { - delete(q.requestProperties.Options, "results_progressive_enabled") + delete(q.requestProperties.Options, RequestProgressiveEnabledValue) return nil } } -// queryServerTimeout is the amount of time the server will allow a query to take. -// NOTE: I have made the serverTimeout private. For the moment, I'm going to use the context.Context timer -// to set timeouts via this private method. -func queryServerTimeout(d time.Duration) QueryOption { +// ServerTimeout overrides the default request timeout. +func ServerTimeout(d time.Duration) QueryOption { return func(q *queryOptions) error { - if d > 1*time.Hour { - return errors.ES(errors.OpQuery, errors.KClientArgs, "ServerTimeout option was set to %v, but can't be more than 1 hour", d) - } q.requestProperties.Options[ServerTimeoutValue] = value.Timespan{Valid: true, Value: d}.Marshal() return nil } @@ -550,3 +566,12 @@ func ValidatePermissions() QueryOption { return nil } } + +// IngestionEndpoint will instruct the Mgmt call to connect to the ingest-[endpoint] instead of [endpoint]. +// This is not often used by end users and can only be used with a Mgmt() call. +func IngestionEndpoint() QueryOption { + return func(m *queryOptions) error { + m.queryIngestion = true + return nil + } +} diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/tokenprovider.go b/vendor/github.com/Azure/azure-kusto-go/kusto/tokenprovider.go new file mode 100644 index 00000000000..6f2f5e09eb3 --- /dev/null +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/tokenprovider.go @@ -0,0 +1,125 @@ +package kusto + +import ( + "context" + "fmt" + "net/http" + "strings" + "sync/atomic" + + "github.com/Azure/azure-kusto-go/kusto/utils" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" +) + +type TokenProvider struct { + tokenCred azcore.TokenCredential //Holds the received token credential as per the authorization + tokenScheme string //Contains token scheme for tokenprovider + customToken string //Holds the custom auth token to be used for authorization + initOnce utils.OnceWithInit[*tokenWrapperResult] //To ensure tokenprovider will be initialized only once while aquiring token + scopes []string //Contains scopes of the auth token + http atomic.Value //Contains the http client to be used for token provider +} + +// tokenProvider need to be received as reference, to reflect updations to the structs +func (tkp *TokenProvider) AcquireToken(ctx context.Context) (string, string, error) { + if !isEmpty(tkp.customToken) { + return tkp.customToken, tkp.tokenScheme, nil + } + + if tkp.initOnce != nil { + _, err := tkp.initOnce.DoWithInit() + if err != nil { + return "", "", err + } + } + + if tkp.tokenCred != nil { + token, err := tkp.tokenCred.GetToken(ctx, policy.TokenRequestOptions{Scopes: tkp.scopes}) + if err != nil { + return "", "", err + } + return token.Token, tkp.tokenScheme, nil + } + + return "", "", fmt.Errorf("Error: No token info present in token provider") +} + +func (tkp *TokenProvider) AuthorizationRequired() bool { + return !(tkp.initOnce == nil && tkp.tokenCred == nil && isEmpty(tkp.customToken)) +} + +type tokenWrapperResult struct { + credential azcore.TokenCredential + scopes []string +} + +func (tkp *TokenProvider) setInit(kcsb *ConnectionStringBuilder, f func(*CloudInfo, *azcore.ClientOptions, string) (azcore.TokenCredential, error)) { + tkp.initOnce = utils.NewOnceWithInit(func() (*tokenWrapperResult, error) { + wrapper, err := tokenWrapper(kcsb, func() *http.Client { return tkp.http.Load().(*http.Client) }, f) + if err != nil { + return nil, err + } + + tkp.tokenCred = wrapper.credential + tkp.scopes = wrapper.scopes + + return wrapper, err + }) +} + +func (tkp *TokenProvider) SetHttp(http *http.Client) { + tkp.http.Store(http) +} + +func tokenWrapper(kcsb *ConnectionStringBuilder, http func() *http.Client, f func(*CloudInfo, *azcore.ClientOptions, string) (azcore.TokenCredential, error)) (*tokenWrapperResult, + error) { + ci, cliOpts, appClientId, err := getCommonCloudInfo(kcsb, http) + if err != nil { + return nil, err + } + + credential, err := f(ci, cliOpts, appClientId) + if err != nil { + return nil, err + } + + resourceURI := ci.KustoServiceResourceID + if ci.LoginMfaRequired { + resourceURI = strings.Replace(resourceURI, ".kusto.", ".kustomfa.", 1) + } + scopes := []string{fmt.Sprintf("%s/.default", resourceURI)} + + return &tokenWrapperResult{ + credential: credential, + scopes: scopes, + }, nil +} + +func getCommonCloudInfo(kcsb *ConnectionStringBuilder, http func() *http.Client) (*CloudInfo, *azcore.ClientOptions, string, error) { + client := http() + if http == nil { + return nil, nil, "", fmt.Errorf("error: No http client provided") + } + + cloud, err := GetMetadata(kcsb.DataSource, client) + if err != nil { + return nil, nil, "", err + } + cliOpts := kcsb.ClientOptions + appClientId := kcsb.ApplicationClientId + if cliOpts == nil { + cliOpts = &azcore.ClientOptions{ + Transport: client, + } + } + if isEmpty(cliOpts.Cloud.ActiveDirectoryAuthorityHost) { + cliOpts.Cloud.ActiveDirectoryAuthorityHost = cloud.LoginEndpoint + } + if isEmpty(appClientId) { + appClientId = cloud.KustoClientAppID + } + cliOpts.Transport = utils.Transporter{Http: client} + return &cloud, cliOpts, appClientId, nil +} diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/trusted_endpoints/trusted_endpoints.go b/vendor/github.com/Azure/azure-kusto-go/kusto/trusted_endpoints/trusted_endpoints.go new file mode 100644 index 00000000000..aa466774c96 --- /dev/null +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/trusted_endpoints/trusted_endpoints.go @@ -0,0 +1,241 @@ +package truestedEndpoints + +import ( + _ "embed" + "encoding/json" + "fmt" + "math" + "net/url" + "strings" + + "github.com/Azure/azure-kusto-go/kusto/data/errors" + "github.com/samber/lo" +) + +var ( + Instance = createInstance() + //go:embed well_known_kusto_endpoints.json + jsonFile []byte +) + +type AllowedEndpoints struct { + AllowedKustoSuffixes []string + AllowedKustoHostnames []string +} + +type WellKnownKustoEndpointsDataStruct struct { + AllowedEndpointsByLogin map[string]AllowedEndpoints +} + +func createInstance() *TrustedEndpoints { + matchers := map[string]*FastSuffixMatcher{} + wellKnownData := WellKnownKustoEndpointsDataStruct{} + + err := json.Unmarshal(jsonFile, &wellKnownData) + if err != nil { + panic(err.Error()) + } + + for key, value := range wellKnownData.AllowedEndpointsByLogin { + rules := []MatchRule{} + for _, suf := range value.AllowedKustoSuffixes { + rules = append(rules, MatchRule{suffix: suf, exact: false}) + } + for _, host := range value.AllowedKustoHostnames { + rules = append(rules, MatchRule{suffix: host, exact: true}) + } + + f, err := newFastSuffixMatcher(rules) + if err != nil { + panic(err.Error()) + } + matchers[key] = f + } + + return &TrustedEndpoints{matchers: matchers} +} + +// SetOverridePolicy Set a policy to override all other trusted rules +func (trusted *TrustedEndpoints) SetOverridePolicy(matcher func(string) bool) { + trusted.overrideMatcher = matcher +} + +type TrustedEndpoints struct { + matchers map[string]*FastSuffixMatcher + additionalMatcher *FastSuffixMatcher + overrideMatcher func(string) bool +} + +type MatchRule struct { + suffix string + exact bool +} + +type FastSuffixMatcher struct { + suffixLength int + rules map[string][]MatchRule +} + +func tailLowerCase(str string, length int) string { + if length <= 0 { + return "" + } + + if length >= len(str) { + return strings.ToLower(str) + } + + return strings.ToLower(str[len(str)-length:]) +} + +func (matcher *FastSuffixMatcher) isMatch(candidate string) bool { + if len(candidate) < matcher.suffixLength { + return false + } + if lst, ok := matcher.rules[tailLowerCase(candidate, matcher.suffixLength)]; ok { + for _, rule := range lst { + if strings.HasSuffix(strings.ToLower(candidate), rule.suffix) { + if len(candidate) == len(rule.suffix) || !rule.exact { + return true + } + } + } + } + + return false +} + +func newFastSuffixMatcher(rules []MatchRule) (*FastSuffixMatcher, error) { + minSufLen := len(lo.MinBy(rules, func(a MatchRule, cur MatchRule) bool { + return len(a.suffix) < len(cur.suffix) + }).suffix) + + if minSufLen == 0 || minSufLen == math.MaxInt32 { + return nil, errors.ES( + errors.OpUnknown, + errors.KClientArgs, + "FastSuffixMatcher should have at list one rule with at least one character", + ).SetNoRetry() + + } + + processedRules := map[string][]MatchRule{} + for _, rule := range rules { + suffix := tailLowerCase(rule.suffix, minSufLen) + if lst, ok := processedRules[suffix]; !ok { + processedRules[suffix] = []MatchRule{rule} + } else { + processedRules[suffix] = append(lst, rule) + } + } + + return &FastSuffixMatcher{ + suffixLength: minSufLen, + rules: processedRules, + }, nil +} + +func values[T comparable, R any](m map[T]R) []R { + l := make([]R, 0, len(m)) + for _, val := range m { + l = append(l, val) + } + + return l +} + +func createFastSuffixMatcherFromExisting(rules []MatchRule, existing *FastSuffixMatcher) (*FastSuffixMatcher, error) { + if existing == nil || len(existing.rules) == 0 { + return newFastSuffixMatcher(rules) + } + + if rules == nil || len(rules) == 0 { + return existing, nil + } + + for _, elem := range existing.rules { + rules = append(rules, elem...) + } + + return newFastSuffixMatcher(rules) +} + +// AddTrustedHosts Add or set a list of trusted endpoints rules +func (trusted *TrustedEndpoints) AddTrustedHosts(rules []MatchRule, replace bool) error { + if rules == nil || len(rules) == 0 { + if replace { + trusted.additionalMatcher = nil + } + return nil + } + + if replace { + trusted.additionalMatcher = nil + } + + matcher, err := createFastSuffixMatcherFromExisting(rules, trusted.additionalMatcher) + trusted.additionalMatcher = matcher + return err +} + +// ValidateTrustedEndpoint Validates the endpoint uri trusted +func (trusted *TrustedEndpoints) ValidateTrustedEndpoint(endpoint string, loginEndpoint string) error { + u, err := url.Parse(endpoint) + if err != nil { + return err + } + + host := u.Host + if host == "" { + host = endpoint + } + + // Check that target hostname is trusted and can accept security token + return trusted.validateHostnameIsTrusted(host, loginEndpoint) +} + +func isLocalAddress(host string) bool { + if host == "localhost" || host == "127.0.0.1" || host == "::1" || host == "[::1]" { + return true + } + + if strings.HasPrefix(host, "127.") && len(host) <= 15 && len(host) >= 9 { + for _, c := range host { + if c != '.' && (c < '0' || c > '9') { + return false + } + } + return true + } + + return false +} + +func (trusted *TrustedEndpoints) validateHostnameIsTrusted(host string, loginEndpoint string) error { + // The loopback is unconditionally allowed (since we trust ourselves) + if isLocalAddress(host) { + return nil + } + + // Either check the override matcher OR the matcher: + override := trusted.overrideMatcher + if override != nil && override(host) { + return nil + } else { + matcher, ok := trusted.matchers[strings.ToLower(loginEndpoint)] + if ok && (*matcher).isMatch(host) { + return nil + } + } + + matcher := trusted.additionalMatcher + if matcher != nil && matcher.isMatch(host) { + return nil + } + + return errors.ES( + errors.OpUnknown, + errors.KClientArgs, + fmt.Sprintf("Can't communicate with '%s' as this hostname is currently not trusted; please see https://aka.ms/kustotrustedendpoints.", host), + ).SetNoRetry() +} diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/trusted_endpoints/well_known_kusto_endpoints.json b/vendor/github.com/Azure/azure-kusto-go/kusto/trusted_endpoints/well_known_kusto_endpoints.json new file mode 100644 index 00000000000..9a64e68df5d --- /dev/null +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/trusted_endpoints/well_known_kusto_endpoints.json @@ -0,0 +1,87 @@ +{ + "_Comments": [ + "KWE .linkedin.com suffix is excluded from list pending a more specific suffix", + "LogAnalytics, AppInsigts & AzureMonitor are taken from https://msazure.visualstudio.com/One/_wiki/wikis/One.wiki/138095/Calling-the-ADX-Proxy following talk with Raz Ronen", + "The two DXP suffixes come from KWE code", + "The PlayFab suffixes are a superset of KWE and PowerBI Kusto Connector code", + "Aria hostname is fixed" + ], + "AllowedEndpointsByLogin": { + "https://login.microsoftonline.com": { + "AllowedKustoSuffixes": [ + ".dxp.aad.azure.com", + ".dxp-dev.aad.azure.com", + ".kusto.azuresynapse.net", + ".kusto.windows.net", + ".kustodev.azuresynapse-dogfood.net", + ".kustodev.windows.net", + ".kustomfa.windows.net", + ".playfabapi.com", + ".playfab.com", + ".kusto.data.microsoft.com", + ".kusto.fabric.microsoft.com" + ], + "AllowedKustoHostnames": [ + "ade.applicationinsights.io", + "ade.loganalytics.io", + "adx.aimon.applicationinsights.azure.com", + "adx.applicationinsights.azure.com", + "adx.int.applicationinsights.azure.com", + "adx.int.loganalytics.azure.com", + "adx.int.monitor.azure.com", + "adx.loganalytics.azure.com", + "adx.monitor.azure.com", + "kusto.aria.microsoft.com", + "eu.kusto.aria.microsoft.com" + ] + }, + "https://login.microsoftonline.us": { + "AllowedKustoSuffixes": [ + ".kusto.usgovcloudapi.net", + ".kustomfa.usgovcloudapi.net" + + ], + "AllowedKustoHostnames": [ + "adx.applicationinsights.azure.us", + "adx.loganalytics.azure.us", + "adx.monitor.azure.us" + ] + }, + "https://login.partner.microsoftonline.cn": { + "AllowedKustoSuffixes": [ + ".kusto.azuresynapse.azure.cn", + ".kusto.chinacloudapi.cn", + ".kustomfa.chinacloudapi.cn", + ".playfab.cn" + ], + "AllowedKustoHostnames": [ + "adx.applicationinsights.azure.cn", + "adx.loganalytics.azure.cn", + "adx.monitor.azure.cn" + ] + }, + "https://login.microsoftonline.eaglex.ic.gov": { + "AllowedKustoSuffixes": [ + ".kusto.core.eaglex.ic.gov", + ".kustomfa.core.eaglex.ic.gov" + + ], + "AllowedKustoHostnames": [ + "adx.applicationinsights.azure.eaglex.ic.gov", + "adx.loganalytics.azure.eaglex.ic.gov", + "adx.monitor.azure.eaglex.ic.gov" + ] + }, + "https://login.microsoftonline.microsoft.scloud": { + "AllowedKustoSuffixes": [ + ".kusto.core.microsoft.scloud", + ".kustomfa.core.microsoft.scloud" + ], + "AllowedKustoHostnames": [ + "adx.applicationinsights.azure.microsoft.scloud", + "adx.loganalytics.azure.microsoft.scloud", + "adx.monitor.azure.microsoft.scloud" + ] + } + } +} diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/utils/once.go b/vendor/github.com/Azure/azure-kusto-go/kusto/utils/once.go new file mode 100644 index 00000000000..e7249363555 --- /dev/null +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/utils/once.go @@ -0,0 +1,74 @@ +package utils + +import "sync" + +type Once[Out any] interface { + Do(f func() (Out, error)) (Out, error) + Done() bool + Result() (bool, Out, error) +} + +type OnceWithInit[Out any] interface { + Once[Out] + DoWithInit() (Out, error) +} + +type once[Out any] struct { + inner sync.Once + result Out + err error + done bool +} + +type onceWithInit[Out any] struct { + inner Once[Out] + f func() (Out, error) +} + +func NewOnce[Out any]() Once[Out] { + var empty Out + return &once[Out]{ + inner: sync.Once{}, + result: empty, + err: nil, + done: false, + } +} + +func NewOnceWithInit[Out any](f func() (Out, error)) OnceWithInit[Out] { + return &onceWithInit[Out]{ + inner: NewOnce[Out](), + f: f, + } +} +func (o *onceWithInit[Out]) DoWithInit() (Out, error) { + return o.inner.Do(o.f) +} + +func (o *onceWithInit[Out]) Do(f func() (Out, error)) (Out, error) { + return o.inner.Do(f) +} + +func (o *onceWithInit[Out]) Done() bool { + return o.inner.Done() +} + +func (o *onceWithInit[Out]) Result() (bool, Out, error) { + return o.inner.Result() +} + +func (o *once[Out]) Do(f func() (Out, error)) (Out, error) { + o.inner.Do(func() { + o.result, o.err = f() + o.done = true + }) + return o.result, o.err +} + +func (o *once[Out]) Done() bool { + return o.done +} + +func (o *once[Out]) Result() (bool, Out, error) { + return o.done, o.result, o.err +} diff --git a/vendor/github.com/Azure/azure-kusto-go/kusto/utils/transporter.go b/vendor/github.com/Azure/azure-kusto-go/kusto/utils/transporter.go new file mode 100644 index 00000000000..fb87fd3c9a5 --- /dev/null +++ b/vendor/github.com/Azure/azure-kusto-go/kusto/utils/transporter.go @@ -0,0 +1,11 @@ +package utils + +import "net/http" + +type Transporter struct { + Http *http.Client +} + +func (t Transporter) Do(req *http.Request) (*http.Response, error) { + return t.Http.Do(req) +} diff --git a/vendor/github.com/samber/lo/.gitignore b/vendor/github.com/samber/lo/.gitignore new file mode 100644 index 00000000000..e5ecc5c40a4 --- /dev/null +++ b/vendor/github.com/samber/lo/.gitignore @@ -0,0 +1,38 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/go +# Edit at https://www.toptal.com/developers/gitignore?templates=go + +### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +### Go Patch ### +/vendor/ +/Godeps/ + +# End of https://www.toptal.com/developers/gitignore/api/go + +cover.out +cover.html +.vscode + +.idea/ diff --git a/vendor/github.com/samber/lo/.travis.yml b/vendor/github.com/samber/lo/.travis.yml new file mode 100644 index 00000000000..f0de7f51c46 --- /dev/null +++ b/vendor/github.com/samber/lo/.travis.yml @@ -0,0 +1,7 @@ +language: go +before_install: + - go mod download + - make tools +go: + - "1.18" +script: make test diff --git a/vendor/github.com/samber/lo/CHANGELOG.md b/vendor/github.com/samber/lo/CHANGELOG.md new file mode 100644 index 00000000000..ea3ef7a536e --- /dev/null +++ b/vendor/github.com/samber/lo/CHANGELOG.md @@ -0,0 +1,411 @@ +# Changelog + +@samber: I sometimes forget to update this file. Ping me on [Twitter](https://twitter.com/samuelberthe) or open an issue in case of error. We need to keep a clear changelog for easier lib upgrade. + +## 1.37.0 (2022-12-15) + +Adding: +- lo.PartialX +- lo.Transaction + +Improvement: +- lo.Associate / lo.SliceToMap: faster memory allocation + +Chore: +- Remove *_test.go files from releases, in order to cleanup dev dependencies + +## 1.36.0 (2022-11-28) + +Adding: +- lo.AttemptWhile +- lo.AttemptWhileWithDelay + +## 1.35.0 (2022-11-15) + +Adding: +- lo.RandomString +- lo.BufferWithTimeout (alias to lo.BatchWithTimeout) +- lo.Buffer (alias to lo.Batch) + +Change: +- lo.Slice: avoid panic caused by out-of-bounds + +Deprecation: +- lo.BatchWithTimeout +- lo.Batch + +## 1.34.0 (2022-11-12) + +Improving: +- lo.Union: faster and can receive more than 2 lists + +Adding: +- lo.FanIn (alias to lo.ChannelMerge) +- lo.FanOut + +Deprecation: +- lo.ChannelMerge + +## 1.33.0 (2022-10-14) + +Adding: +- lo.ChannelMerge + +Improving: +- helpers with callbacks/predicates/iteratee now have named arguments, for easier autocompletion + +## 1.32.0 (2022-10-10) + +Adding: + +- lo.ChannelToSlice +- lo.CountValues +- lo.CountValuesBy +- lo.MapEntries +- lo.Sum +- lo.Interleave +- TupleX.Unpack() + +## 1.31.0 (2022-10-06) + +Adding: + +- lo.SliceToChannel +- lo.Generator +- lo.Batch +- lo.BatchWithTimeout + +## 1.30.1 (2022-10-06) + +Fix: + +- lo.Try1: remove generic type +- lo.Validate: format error properly + +## 1.30.0 (2022-10-04) + +Adding: + +- lo.TernaryF +- lo.Validate + +## 1.29.0 (2022-10-02) + +Adding: + +- lo.ErrorAs +- lo.TryOr +- lo.TryOrX + +## 1.28.0 (2022-09-05) + +Adding: + +- lo.ChannelDispatcher with 6 dispatching strategies: + - lo.DispatchingStrategyRoundRobin + - lo.DispatchingStrategyRandom + - lo.DispatchingStrategyWeightedRandom + - lo.DispatchingStrategyFirst + - lo.DispatchingStrategyLeast + - lo.DispatchingStrategyMost + +## 1.27.1 (2022-08-15) + +Bugfix: + +- Removed comparable constraint for lo.FindKeyBy + +## 1.27.0 (2022-07-29) + +Breaking: + +- Change of MapToSlice prototype: `MapToSlice[K comparable, V any, R any](in map[K]V, iteratee func(V, K) R) []R` -> `MapToSlice[K comparable, V any, R any](in map[K]V, iteratee func(K, V) R) []R` + +Added: + +- lo.ChunkString +- lo.SliceToMap (alias to lo.Associate) + +## 1.26.0 (2022-07-24) + +Adding: + +- lo.Associate +- lo.ReduceRight +- lo.FromPtrOr +- lo.MapToSlice +- lo.IsSorted +- lo.IsSortedByKey + +## 1.25.0 (2022-07-04) + +Adding: + +- lo.FindUniques +- lo.FindUniquesBy +- lo.FindDuplicates +- lo.FindDuplicatesBy +- lo.IsNotEmpty + +## 1.24.0 (2022-07-04) + +Adding: + +- lo.Without +- lo.WithoutEmpty + +## 1.23.0 (2022-07-04) + +Adding: + +- lo.FindKey +- lo.FindKeyBy + +## 1.22.0 (2022-07-04) + +Adding: + +- lo.Slice +- lo.FromPtr +- lo.IsEmpty +- lo.Compact +- lo.ToPairs: alias to lo.Entries +- lo.FromPairs: alias to lo.FromEntries +- lo.Partial + +Change: + +- lo.Must + lo.MustX: add context to panic message + +Fix: + +- lo.Nth: out of bound exception (#137) + +## 1.21.0 (2022-05-10) + +Adding: + +- lo.ToAnySlice +- lo.FromAnySlice + +## 1.20.0 (2022-05-02) + +Adding: + +- lo.Synchronize +- lo.SumBy + +Change: +- Removed generic type definition for lo.Try0: `lo.Try0[T]()` -> `lo.Try0()` + +## 1.19.0 (2022-04-30) + +Adding: + +- lo.RepeatBy +- lo.Subset +- lo.Replace +- lo.ReplaceAll +- lo.Substring +- lo.RuneLength + +## 1.18.0 (2022-04-28) + +Adding: + +- lo.SomeBy +- lo.EveryBy +- lo.None +- lo.NoneBy + +## 1.17.0 (2022-04-27) + +Adding: + +- lo.Unpack2 -> lo.Unpack3 +- lo.Async0 -> lo.Async6 + +## 1.16.0 (2022-04-26) + +Adding: + +- lo.AttemptWithDelay + +## 1.15.0 (2022-04-22) + +Improvement: + +- lo.Must: error or boolean value + +## 1.14.0 (2022-04-21) + +Adding: + +- lo.Coalesce + +## 1.13.0 (2022-04-14) + +Adding: + +- PickBy +- PickByKeys +- PickByValues +- OmitBy +- OmitByKeys +- OmitByValues +- Clamp +- MapKeys +- Invert +- IfF + ElseIfF + ElseF +- T0() + T1() + T2() + T3() + ... + +## 1.12.0 (2022-04-12) + +Adding: + +- Must +- Must{0-6} +- FindOrElse +- Async +- MinBy +- MaxBy +- Count +- CountBy +- FindIndexOf +- FindLastIndexOf +- FilterMap + +## 1.11.0 (2022-03-11) + +Adding: + +- Try +- Try{0-6} +- TryWitchValue +- TryCatch +- TryCatchWitchValue +- Debounce +- Reject + +## 1.10.0 (2022-03-11) + +Adding: + +- Range +- RangeFrom +- RangeWithSteps + +## 1.9.0 (2022-03-10) + +Added + +- Drop +- DropRight +- DropWhile +- DropRightWhile + +## 1.8.0 (2022-03-10) + +Adding Union. + +## 1.7.0 (2022-03-09) + +Adding ContainBy + +Adding MapValues + +Adding FlatMap + +## 1.6.0 (2022-03-07) + +Fixed PartitionBy. + +Adding Sample + +Adding Samples + +## 1.5.0 (2022-03-07) + +Adding Times + +Adding Attempt + +Adding Repeat + +## 1.4.0 (2022-03-07) + +- adding tuple types (2->9) +- adding Zip + Unzip +- adding lo.PartitionBy + lop.PartitionBy +- adding lop.GroupBy +- fixing Nth + +## 1.3.0 (2022-03-03) + +Last and Nth return errors + +## 1.2.0 (2022-03-03) + +Adding `lop.Map` and `lop.ForEach`. + +## 1.1.0 (2022-03-03) + +Adding `i int` param to `lo.Map()`, `lo.Filter()`, `lo.ForEach()` and `lo.Reduce()` predicates. + +## 1.0.0 (2022-03-02) + +*Initial release* + +Supported helpers for slices: + +- Filter +- Map +- Reduce +- ForEach +- Uniq +- UniqBy +- GroupBy +- Chunk +- Flatten +- Shuffle +- Reverse +- Fill +- ToMap + +Supported helpers for maps: + +- Keys +- Values +- Entries +- FromEntries +- Assign (maps merge) + +Supported intersection helpers: + +- Contains +- Every +- Some +- Intersect +- Difference + +Supported search helpers: + +- IndexOf +- LastIndexOf +- Find +- Min +- Max +- Last +- Nth + +Other functional programming helpers: + +- Ternary (1 line if/else statement) +- If / ElseIf / Else +- Switch / Case / Default +- ToPtr +- ToSlicePtr + +Constraints: + +- Clonable diff --git a/vendor/github.com/samber/lo/Dockerfile b/vendor/github.com/samber/lo/Dockerfile new file mode 100644 index 00000000000..bd01bbbb45b --- /dev/null +++ b/vendor/github.com/samber/lo/Dockerfile @@ -0,0 +1,8 @@ + +FROM golang:1.18 + +WORKDIR /go/src/github.com/samber/lo + +COPY Makefile go.* ./ + +RUN make tools diff --git a/vendor/github.com/samber/lo/LICENSE b/vendor/github.com/samber/lo/LICENSE new file mode 100644 index 00000000000..c3dc72d9ab8 --- /dev/null +++ b/vendor/github.com/samber/lo/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Samuel Berthe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/samber/lo/Makefile b/vendor/github.com/samber/lo/Makefile new file mode 100644 index 00000000000..57bb49159f5 --- /dev/null +++ b/vendor/github.com/samber/lo/Makefile @@ -0,0 +1,44 @@ + +BIN=go + +build: + ${BIN} build -v ./... + +test: + go test -race -v ./... +watch-test: + reflex -t 50ms -s -- sh -c 'gotest -race -v ./...' + +bench: + go test -benchmem -count 3 -bench ./... +watch-bench: + reflex -t 50ms -s -- sh -c 'go test -benchmem -count 3 -bench ./...' + +coverage: + ${BIN} test -v -coverprofile=cover.out -covermode=atomic . + ${BIN} tool cover -html=cover.out -o cover.html + +# tools +tools: + ${BIN} install github.com/cespare/reflex@latest + ${BIN} install github.com/rakyll/gotest@latest + ${BIN} install github.com/psampaz/go-mod-outdated@latest + ${BIN} install github.com/jondot/goweight@latest + ${BIN} install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + ${BIN} get -t -u golang.org/x/tools/cmd/cover + ${BIN} install github.com/sonatype-nexus-community/nancy@latest + go mod tidy + +lint: + golangci-lint run --timeout 60s --max-same-issues 50 ./... +lint-fix: + golangci-lint run --timeout 60s --max-same-issues 50 --fix ./... + +audit: tools + ${BIN} list -json -m all | nancy sleuth + +outdated: tools + ${BIN} list -u -m -json all | go-mod-outdated -update -direct + +weight: tools + goweight diff --git a/vendor/github.com/samber/lo/README.md b/vendor/github.com/samber/lo/README.md new file mode 100644 index 00000000000..ffb7c43895f --- /dev/null +++ b/vendor/github.com/samber/lo/README.md @@ -0,0 +1,2851 @@ +# lo + +[![tag](https://img.shields.io/github/tag/samber/lo.svg)](https://github.com/samber/lo/releases) +![Go Version](https://img.shields.io/badge/Go-%3E%3D%201.18-%23007d9c) +[![GoDoc](https://godoc.org/github.com/samber/lo?status.svg)](https://pkg.go.dev/github.com/samber/lo) +![Build Status](https://github.com/samber/lo/actions/workflows/go.yml/badge.svg) +[![Go report](https://goreportcard.com/badge/github.com/samber/lo)](https://goreportcard.com/report/github.com/samber/lo) +[![Coverage](https://img.shields.io/codecov/c/github/samber/lo)](https://codecov.io/gh/samber/lo) +[![Contributors](https://img.shields.io/github/contributors/samber/lo)](https://github.com/samber/lo/graphs/contributors) +[![License](https://img.shields.io/github/license/samber/lo)](./LICENSE) + +✨ **`samber/lo` is a Lodash-style Go library based on Go 1.18+ Generics.** + +This project started as an experiment with the new generics implementation. It may look like [Lodash](https://github.com/lodash/lodash) in some aspects. I used to code with the fantastic ["go-funk"](https://github.com/thoas/go-funk) package, but "go-funk" uses reflection and therefore is not typesafe. + +As expected, benchmarks demonstrate that generics are much faster than implementations based on the "reflect" package. Benchmarks also show similar performance gains compared to pure `for` loops. [See below](#-benchmark). + +In the future, 5 to 10 helpers will overlap with those coming into the Go standard library (under package names `slices` and `maps`). I feel this library is legitimate and offers many more valuable abstractions. + +**See also:** + +- [samber/do](https://github.com/samber/do): A dependency injection toolkit based on Go 1.18+ Generics +- [samber/mo](https://github.com/samber/mo): Monads based on Go 1.18+ Generics (Option, Result, Either...) + +**Why this name?** + +I wanted a **short name**, similar to "Lodash" and no Go package currently uses this name. + +![](img/logo-full.png) + +## 🚀 Install + +```sh +go get github.com/samber/lo@v1 +``` + +This library is v1 and follows SemVer strictly. + +No breaking changes will be made to exported APIs before v2.0.0. + +## 💡 Usage + +You can import `lo` using: + +```go +import ( + "github.com/samber/lo" + lop "github.com/samber/lo/parallel" +) +``` + +Then use one of the helpers below: + +```go +names := lo.Uniq[string]([]string{"Samuel", "John", "Samuel"}) +// []string{"Samuel", "John"} +``` + +Most of the time, the compiler will be able to infer the type so that you can call: `lo.Uniq([]string{...})`. + +### Tips for lazy developers + +I cannot recommend it, but in case you are too lazy for repeating `lo.` everywhere, you can import the entire library into the namespace. + +```go +import ( + . "github.com/samber/lo" +) +``` + +I take no responsibility on this junk. 😁 💩 + +## 🤠 Spec + +GoDoc: [https://godoc.org/github.com/samber/lo](https://godoc.org/github.com/samber/lo) + +Supported helpers for slices: + +- [Filter](#filter) +- [Map](#map) +- [FilterMap](#filtermap) +- [FlatMap](#flatmap) +- [Reduce](#reduce) +- [ReduceRight](#reduceright) +- [ForEach](#foreach) +- [Times](#times) +- [Uniq](#uniq) +- [UniqBy](#uniqby) +- [GroupBy](#groupby) +- [Chunk](#chunk) +- [PartitionBy](#partitionby) +- [Flatten](#flatten) +- [Interleave](#interleave) +- [Shuffle](#shuffle) +- [Reverse](#reverse) +- [Fill](#fill) +- [Repeat](#repeat) +- [RepeatBy](#repeatby) +- [KeyBy](#keyby) +- [Associate / SliceToMap](#associate-alias-slicetomap) +- [Drop](#drop) +- [DropRight](#dropright) +- [DropWhile](#dropwhile) +- [DropRightWhile](#droprightwhile) +- [Reject](#reject) +- [Count](#count) +- [CountBy](#countby) +- [CountValues](#countvalues) +- [CountValuesBy](#countvaluesby) +- [Subset](#subset) +- [Slice](#slice) +- [Replace](#replace) +- [ReplaceAll](#replaceall) +- [Compact](#compact) +- [IsSorted](#issorted) +- [IsSortedByKey](#issortedbykey) + +Supported helpers for maps: + +- [Keys](#keys) +- [Values](#values) +- [PickBy](#pickby) +- [PickByKeys](#pickbykeys) +- [PickByValues](#pickbyvalues) +- [OmitBy](#omitby) +- [OmitByKeys](#omitbykeys) +- [OmitByValues](#omitbyvalues) +- [Entries / ToPairs](#entries-alias-topairs) +- [FromEntries / FromPairs](#fromentries-alias-frompairs) +- [Invert](#invert) +- [Assign (merge of maps)](#assign) +- [MapKeys](#mapkeys) +- [MapValues](#mapvalues) +- [MapEntries](#mapentries) +- [MapToSlice](#maptoslice) + +Supported math helpers: + +- [Range / RangeFrom / RangeWithSteps](#range--rangefrom--rangewithsteps) +- [Clamp](#clamp) +- [Sum](#sum) +- [SumBy](#sumby) + +Supported helpers for strings: + +- [RandomString](#randomstring) +- [Substring](#substring) +- [ChunkString](#chunkstring) +- [RuneLength](#runelength) + +Supported helpers for tuples: + +- [T2 -> T9](#t2---t9) +- [Unpack2 -> Unpack9](#unpack2---unpack9) +- [Zip2 -> Zip9](#zip2---zip9) +- [Unzip2 -> Unzip9](#unzip2---unzip9) + +Supported helpers for channels: + +- [ChannelDispatcher](#channeldispatcher) +- [SliceToChannel](#slicetochannel) +- [Generator](#generator) +- [Buffer](#buffer) +- [BufferWithTimeout](#bufferwithtimeout) +- [FanIn](#fanin) +- [FanOut](#fanout) + +Supported intersection helpers: + +- [Contains](#contains) +- [ContainsBy](#containsby) +- [Every](#every) +- [EveryBy](#everyby) +- [Some](#some) +- [SomeBy](#someby) +- [None](#none) +- [NoneBy](#noneby) +- [Intersect](#intersect) +- [Difference](#difference) +- [Union](#union) +- [Without](#without) +- [WithoutEmpty](#withoutempty) + +Supported search helpers: + +- [IndexOf](#indexof) +- [LastIndexOf](#lastindexof) +- [Find](#find) +- [FindIndexOf](#findindexof) +- [FindLastIndexOf](#findlastindexof) +- [FindKey](#findkey) +- [FindKeyBy](#findkeyby) +- [FindUniques](#finduniques) +- [FindUniquesBy](#finduniquesby) +- [FindDuplicates](#findduplicates) +- [FindDuplicatesBy](#findduplicatesby) +- [Min](#min) +- [MinBy](#minby) +- [Max](#max) +- [MaxBy](#maxby) +- [Last](#last) +- [Nth](#nth) +- [Sample](#sample) +- [Samples](#samples) + +Conditional helpers: + +- [Ternary](#ternary) +- [TernaryF](#ternaryf) +- [If / ElseIf / Else](#if--elseif--else) +- [Switch / Case / Default](#switch--case--default) + +Type manipulation helpers: + +- [ToPtr](#toptr) +- [FromPtr](#fromptr) +- [FromPtrOr](#fromptror) +- [ToSlicePtr](#tosliceptr) +- [ToAnySlice](#toanyslice) +- [FromAnySlice](#fromanyslice) +- [Empty](#empty) +- [IsEmpty](#isempty) +- [IsNotEmpty](#isnotempty) +- [Coalesce](#coalesce) + +Function helpers: + +- [Partial](#partial) +- [Partial2 -> Partial5](#partial2---partial5) + +Concurrency helpers: + +- [Attempt](#attempt) +- [AttemptWhile](#attemptwhile) +- [AttemptWithDelay](#attemptwithdelay) +- [AttemptWhileWithDelay](#attemptwhilewithdelay) +- [Debounce](#debounce) +- [Synchronize](#synchronize) +- [Async](#async) +- [Transaction](#transaction) + +Error handling: + +- [Validate](#validate) +- [Must](#must) +- [Try](#try) +- [Try1 -> Try6](#try0-6) +- [TryOr](#tryor) +- [TryOr1 -> TryOr6](#tryor0-6) +- [TryCatch](#trycatch) +- [TryWithErrorValue](#trywitherrorvalue) +- [TryCatchWithErrorValue](#trycatchwitherrorvalue) +- [ErrorsAs](#errorsas) + +Constraints: + +- Clonable + +### Filter + +Iterates over a collection and returns an array of all the elements the predicate function returns `true` for. + +```go +even := lo.Filter[int]([]int{1, 2, 3, 4}, func(x int, index int) bool { + return x%2 == 0 +}) +// []int{2, 4} +``` + +[[play](https://go.dev/play/p/Apjg3WeSi7K)] + +### Map + +Manipulates a slice of one type and transforms it into a slice of another type: + +```go +import "github.com/samber/lo" + +lo.Map[int64, string]([]int64{1, 2, 3, 4}, func(x int64, index int) string { + return strconv.FormatInt(x, 10) +}) +// []string{"1", "2", "3", "4"} +``` + +[[play](https://go.dev/play/p/OkPcYAhBo0D)] + +Parallel processing: like `lo.Map()`, but the mapper function is called in a goroutine. Results are returned in the same order. + +```go +import lop "github.com/samber/lo/parallel" + +lop.Map[int64, string]([]int64{1, 2, 3, 4}, func(x int64, _ int) string { + return strconv.FormatInt(x, 10) +}) +// []string{"1", "2", "3", "4"} +``` + +### FilterMap + +Returns a slice which obtained after both filtering and mapping using the given callback function. + +The callback function should return two values: the result of the mapping operation and whether the result element should be included or not. + +```go +matching := lo.FilterMap[string, string]([]string{"cpu", "gpu", "mouse", "keyboard"}, func(x string, _ int) (string, bool) { + if strings.HasSuffix(x, "pu") { + return "xpu", true + } + return "", false +}) +// []string{"xpu", "xpu"} +``` + +[[play](https://go.dev/play/p/-AuYXfy7opz)] + +### FlatMap + +Manipulates a slice and transforms and flattens it to a slice of another type. + +```go +lo.FlatMap[int, string]([]int{0, 1, 2}, func(x int, _ int) []string { + return []string{ + strconv.FormatInt(x, 10), + strconv.FormatInt(x, 10), + } +}) +// []string{"0", "0", "1", "1", "2", "2"} +``` + +[[play](https://go.dev/play/p/YSoYmQTA8-U)] + +### Reduce + +Reduces a collection to a single value. The value is calculated by accumulating the result of running each element in the collection through an accumulator function. Each successive invocation is supplied with the return value returned by the previous call. + +```go +sum := lo.Reduce[int, int]([]int{1, 2, 3, 4}, func(agg int, item int, _ int) int { + return agg + item +}, 0) +// 10 +``` + +[[play](https://go.dev/play/p/R4UHXZNaaUG)] + +### ReduceRight + +Like `lo.Reduce` except that it iterates over elements of collection from right to left. + +```go +result := lo.ReduceRight[[]int, []int]([][]int{{0, 1}, {2, 3}, {4, 5}}, func(agg []int, item []int, _ int) []int { + return append(agg, item...) +}, []int{}) +// []int{4, 5, 2, 3, 0, 1} +``` + +[[play](https://go.dev/play/p/Fq3W70l7wXF)] + +### ForEach + +Iterates over elements of a collection and invokes the function over each element. + +```go +import "github.com/samber/lo" + +lo.ForEach[string]([]string{"hello", "world"}, func(x string, _ int) { + println(x) +}) +// prints "hello\nworld\n" +``` + +[[play](https://go.dev/play/p/oofyiUPRf8t)] + +Parallel processing: like `lo.ForEach()`, but the callback is called as a goroutine. + +```go +import lop "github.com/samber/lo/parallel" + +lop.ForEach[string]([]string{"hello", "world"}, func(x string, _ int) { + println(x) +}) +// prints "hello\nworld\n" or "world\nhello\n" +``` + +### Times + +Times invokes the iteratee n times, returning an array of the results of each invocation. The iteratee is invoked with index as argument. + +```go +import "github.com/samber/lo" + +lo.Times[string](3, func(i int) string { + return strconv.FormatInt(int64(i), 10) +}) +// []string{"0", "1", "2"} +``` + +[[play](https://go.dev/play/p/vgQj3Glr6lT)] + +Parallel processing: like `lo.Times()`, but callback is called in goroutine. + +```go +import lop "github.com/samber/lo/parallel" + +lop.Times[string](3, func(i int) string { + return strconv.FormatInt(int64(i), 10) +}) +// []string{"0", "1", "2"} +``` + +### Uniq + +Returns a duplicate-free version of an array, in which only the first occurrence of each element is kept. The order of result values is determined by the order they occur in the array. + +```go +uniqValues := lo.Uniq[int]([]int{1, 2, 2, 1}) +// []int{1, 2} +``` + +[[play](https://go.dev/play/p/DTzbeXZ6iEN)] + +### UniqBy + +Returns a duplicate-free version of an array, in which only the first occurrence of each element is kept. The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is invoked for each element in array to generate the criterion by which uniqueness is computed. + +```go +uniqValues := lo.UniqBy[int, int]([]int{0, 1, 2, 3, 4, 5}, func(i int) int { + return i%3 +}) +// []int{0, 1, 2} +``` + +[[play](https://go.dev/play/p/g42Z3QSb53u)] + +### GroupBy + +Returns an object composed of keys generated from the results of running each element of collection through iteratee. + +```go +import lo "github.com/samber/lo" + +groups := lo.GroupBy[int, int]([]int{0, 1, 2, 3, 4, 5}, func(i int) int { + return i%3 +}) +// map[int][]int{0: []int{0, 3}, 1: []int{1, 4}, 2: []int{2, 5}} +``` + +[[play](https://go.dev/play/p/XnQBd_v6brd)] + +Parallel processing: like `lo.GroupBy()`, but callback is called in goroutine. + +```go +import lop "github.com/samber/lo/parallel" + +lop.GroupBy[int, int]([]int{0, 1, 2, 3, 4, 5}, func(i int) int { + return i%3 +}) +// map[int][]int{0: []int{0, 3}, 1: []int{1, 4}, 2: []int{2, 5}} +``` + +### Chunk + +Returns an array of elements split into groups the length of size. If array can't be split evenly, the final chunk will be the remaining elements. + +```go +lo.Chunk[int]([]int{0, 1, 2, 3, 4, 5}, 2) +// [][]int{{0, 1}, {2, 3}, {4, 5}} + +lo.Chunk[int]([]int{0, 1, 2, 3, 4, 5, 6}, 2) +// [][]int{{0, 1}, {2, 3}, {4, 5}, {6}} + +lo.Chunk[int]([]int{}, 2) +// [][]int{} + +lo.Chunk[int]([]int{0}, 2) +// [][]int{{0}} +``` + +[[play](https://go.dev/play/p/EeKl0AuTehH)] + +### PartitionBy + +Returns an array of elements split into groups. The order of grouped values is determined by the order they occur in collection. The grouping is generated from the results of running each element of collection through iteratee. + +```go +import lo "github.com/samber/lo" + +partitions := lo.PartitionBy[int, string]([]int{-2, -1, 0, 1, 2, 3, 4, 5}, func(x int) string { + if x < 0 { + return "negative" + } else if x%2 == 0 { + return "even" + } + return "odd" +}) +// [][]int{{-2, -1}, {0, 2, 4}, {1, 3, 5}} +``` + +[[play](https://go.dev/play/p/NfQ_nGjkgXW)] + +Parallel processing: like `lo.PartitionBy()`, but callback is called in goroutine. Results are returned in the same order. + +```go +import lop "github.com/samber/lo/parallel" + +partitions := lop.PartitionBy[int, string]([]int{-2, -1, 0, 1, 2, 3, 4, 5}, func(x int) string { + if x < 0 { + return "negative" + } else if x%2 == 0 { + return "even" + } + return "odd" +}) +// [][]int{{-2, -1}, {0, 2, 4}, {1, 3, 5}} +``` + +### Flatten + +Returns an array a single level deep. + +```go +flat := lo.Flatten[int]([][]int{{0, 1}, {2, 3, 4, 5}}) +// []int{0, 1, 2, 3, 4, 5} +``` + +[[play](https://go.dev/play/p/rbp9ORaMpjw)] + +### Interleave + +Round-robin alternating input slices and sequentially appending value at index into result. + +```go +interleaved := lo.Interleave[int]([]int{1, 4, 7}, []int{2, 5, 8}, []int{3, 6, 9}) +// []int{1, 2, 3, 4, 5, 6, 7, 8, 9} + +interleaved := lo.Interleave[int]([]int{1}, []int{2, 5, 8}, []int{3, 6}, []int{4, 7, 9, 10}) +// []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} +``` + +[[play](https://go.dev/play/p/DDhlwrShbwe)] + +### Shuffle + +Returns an array of shuffled values. Uses the Fisher-Yates shuffle algorithm. + +```go +randomOrder := lo.Shuffle[int]([]int{0, 1, 2, 3, 4, 5}) +// []int{1, 4, 0, 3, 5, 2} +``` + +[[play](https://go.dev/play/p/Qp73bnTDnc7)] + +### Reverse + +Reverses array so that the first element becomes the last, the second element becomes the second to last, and so on. + +```go +reverseOrder := lo.Reverse[int]([]int{0, 1, 2, 3, 4, 5}) +// []int{5, 4, 3, 2, 1, 0} +``` + +[[play](https://go.dev/play/p/fhUMLvZ7vS6)] + +### Fill + +Fills elements of array with `initial` value. + +```go +type foo struct { + bar string +} + +func (f foo) Clone() foo { + return foo{f.bar} +} + +initializedSlice := lo.Fill[foo]([]foo{foo{"a"}, foo{"a"}}, foo{"b"}) +// []foo{foo{"b"}, foo{"b"}} +``` + +[[play](https://go.dev/play/p/VwR34GzqEub)] + +### Repeat + +Builds a slice with N copies of initial value. + +```go +type foo struct { + bar string +} + +func (f foo) Clone() foo { + return foo{f.bar} +} + +slice := lo.Repeat[foo](2, foo{"a"}) +// []foo{foo{"a"}, foo{"a"}} +``` + +[[play](https://go.dev/play/p/g3uHXbmc3b6)] + +### RepeatBy + +Builds a slice with values returned by N calls of callback. + +```go +slice := lo.RepeatBy[string](0, func (i int) string { + return strconv.FormatInt(int64(math.Pow(float64(i), 2)), 10) +}) +// []string{} + +slice := lo.RepeatBy[string](5, func(i int) string { + return strconv.FormatInt(int64(math.Pow(float64(i), 2)), 10) +}) +// []string{"0", "1", "4", "9", "16"} +``` + +[[play](https://go.dev/play/p/ozZLCtX_hNU)] + +### KeyBy + +Transforms a slice or an array of structs to a map based on a pivot callback. + +```go +m := lo.KeyBy[int, string]([]string{"a", "aa", "aaa"}, func(str string) int { + return len(str) +}) +// map[int]string{1: "a", 2: "aa", 3: "aaa"} + +type Character struct { + dir string + code int +} +characters := []Character{ + {dir: "left", code: 97}, + {dir: "right", code: 100}, +} +result := lo.KeyBy[string, Character](characters, func(char Character) string { + return string(rune(char.code)) +}) +//map[a:{dir:left code:97} d:{dir:right code:100}] +``` + +[[play](https://go.dev/play/p/mdaClUAT-zZ)] + +### Associate (alias: SliceToMap) + +Returns a map containing key-value pairs provided by transform function applied to elements of the given slice. +If any of two pairs would have the same key the last one gets added to the map. + +The order of keys in returned map is not specified and is not guaranteed to be the same from the original array. + +```go +in := []*foo{{baz: "apple", bar: 1}, {baz: "banana", bar: 2}} + +aMap := lo.Associate[*foo, string, int](in, func (f *foo) (string, int) { + return f.baz, f.bar +}) +// map[string][int]{ "apple":1, "banana":2 } +``` + +[[play](https://go.dev/play/p/WHa2CfMO3Lr)] + +### Drop + +Drops n elements from the beginning of a slice or array. + +```go +l := lo.Drop[int]([]int{0, 1, 2, 3, 4, 5}, 2) +// []int{2, 3, 4, 5} +``` + +[[play](https://go.dev/play/p/JswS7vXRJP2)] + +### DropRight + +Drops n elements from the end of a slice or array. + +```go +l := lo.DropRight[int]([]int{0, 1, 2, 3, 4, 5}, 2) +// []int{0, 1, 2, 3} +``` + +[[play](https://go.dev/play/p/GG0nXkSJJa3)] + +### DropWhile + +Drop elements from the beginning of a slice or array while the predicate returns true. + +```go +l := lo.DropWhile[string]([]string{"a", "aa", "aaa", "aa", "aa"}, func(val string) bool { + return len(val) <= 2 +}) +// []string{"aaa", "aa", "aa"} +``` + +[[play](https://go.dev/play/p/7gBPYw2IK16)] + +### DropRightWhile + +Drop elements from the end of a slice or array while the predicate returns true. + +```go +l := lo.DropRightWhile[string]([]string{"a", "aa", "aaa", "aa", "aa"}, func(val string) bool { + return len(val) <= 2 +}) +// []string{"a", "aa", "aaa"} +``` + +[[play](https://go.dev/play/p/3-n71oEC0Hz)] + +### Reject + +The opposite of Filter, this method returns the elements of collection that predicate does not return truthy for. + +```go +odd := lo.Reject[int]([]int{1, 2, 3, 4}, func(x int, _ int) bool { + return x%2 == 0 +}) +// []int{1, 3} +``` + +[[play](https://go.dev/play/p/YkLMODy1WEL)] + +### Count + +Counts the number of elements in the collection that compare equal to value. + +```go +count := lo.Count[int]([]int{1, 5, 1}, 1) +// 2 +``` + +[[play](https://go.dev/play/p/Y3FlK54yveC)] + +### CountBy + +Counts the number of elements in the collection for which predicate is true. + +```go +count := lo.CountBy[int]([]int{1, 5, 1}, func(i int) bool { + return i < 4 +}) +// 2 +``` + +[[play](https://go.dev/play/p/ByQbNYQQi4X)] + +### CountValues + +Counts the number of each element in the collection. + +```go +lo.CountValues([]int{}) +// map[int]int{} + +lo.CountValues([]int{1, 2}) +// map[int]int{1: 1, 2: 1} + +lo.CountValues([]int{1, 2, 2}) +// map[int]int{1: 1, 2: 2} + +lo.CountValues([]string{"foo", "bar", ""}) +// map[string]int{"": 1, "foo": 1, "bar": 1} + +lo.CountValues([]string{"foo", "bar", "bar"}) +// map[string]int{"foo": 1, "bar": 2} +``` + +[[play](https://go.dev/play/p/-p-PyLT4dfy)] + +### CountValuesBy + +Counts the number of each element in the collection. It ss equivalent to chaining lo.Map and lo.CountValues. + +```go +isEven := func(v int) bool { + return v%2==0 +} + +lo.CountValuesBy([]int{}, isEven) +// map[bool]int{} + +lo.CountValuesBy([]int{1, 2}, isEven) +// map[bool]int{false: 1, true: 1} + +lo.CountValuesBy([]int{1, 2, 2}, isEven) +// map[bool]int{false: 1, true: 2} + +length := func(v string) int { + return len(v) +} + +lo.CountValuesBy([]string{"foo", "bar", ""}, length) +// map[int]int{0: 1, 3: 2} + +lo.CountValuesBy([]string{"foo", "bar", "bar"}, length) +// map[int]int{3: 3} +``` + +[[play](https://go.dev/play/p/2U0dG1SnOmS)] + +### Subset + +Returns a copy of a slice from `offset` up to `length` elements. Like `slice[start:start+length]`, but does not panic on overflow. + +```go +in := []int{0, 1, 2, 3, 4} + +sub := lo.Subset(in, 2, 3) +// []int{2, 3, 4} + +sub := lo.Subset(in, -4, 3) +// []int{1, 2, 3} + +sub := lo.Subset(in, -2, math.MaxUint) +// []int{3, 4} +``` + +[[play](https://go.dev/play/p/tOQu1GhFcog)] + +### Slice + +Returns a copy of a slice from `start` up to, but not including `end`. Like `slice[start:end]`, but does not panic on overflow. + +```go +in := []int{0, 1, 2, 3, 4} + +slice := lo.Slice(in, 0, 5) +// []int{0, 1, 2, 3, 4} + +slice := lo.Slice(in, 2, 3) +// []int{2} + +slice := lo.Slice(in, 2, 6) +// []int{2, 3, 4} + +slice := lo.Slice(in, 4, 3) +// []int{} +``` + +[[play](https://go.dev/play/p/8XWYhfMMA1h)] + +### Replace + +Returns a copy of the slice with the first n non-overlapping instances of old replaced by new. + +```go +in := []int{0, 1, 0, 1, 2, 3, 0} + +slice := lo.Replace(in, 0, 42, 1) +// []int{42, 1, 0, 1, 2, 3, 0} + +slice := lo.Replace(in, -1, 42, 1) +// []int{0, 1, 0, 1, 2, 3, 0} + +slice := lo.Replace(in, 0, 42, 2) +// []int{42, 1, 42, 1, 2, 3, 0} + +slice := lo.Replace(in, 0, 42, -1) +// []int{42, 1, 42, 1, 2, 3, 42} +``` + +[[play](https://go.dev/play/p/XfPzmf9gql6)] + +### ReplaceAll + +Returns a copy of the slice with all non-overlapping instances of old replaced by new. + +```go +in := []int{0, 1, 0, 1, 2, 3, 0} + +slice := lo.ReplaceAll(in, 0, 42) +// []int{42, 1, 42, 1, 2, 3, 42} + +slice := lo.ReplaceAll(in, -1, 42) +// []int{0, 1, 0, 1, 2, 3, 0} +``` + +[[play](https://go.dev/play/p/a9xZFUHfYcV)] + +### Compact + +Returns a slice of all non-zero elements. + +```go +in := []string{"", "foo", "", "bar", ""} + +slice := lo.Compact[string](in) +// []string{"foo", "bar"} +``` + +[[play](https://go.dev/play/p/tXiy-iK6PAc)] + +### IsSorted + +Checks if a slice is sorted. + +```go +slice := lo.IsSorted([]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) +// true +``` + +[[play](https://go.dev/play/p/mc3qR-t4mcx)] + +### IsSortedByKey + +Checks if a slice is sorted by iteratee. + +```go +slice := lo.IsSortedByKey([]string{"a", "bb", "ccc"}, func(s string) int { + return len(s) +}) +// true +``` + +[[play](https://go.dev/play/p/wiG6XyBBu49)] + +### Keys + +Creates an array of the map keys. + +```go +keys := lo.Keys[string, int](map[string]int{"foo": 1, "bar": 2}) +// []string{"foo", "bar"} +``` + +[[play](https://go.dev/play/p/Uu11fHASqrU)] + +### Values + +Creates an array of the map values. + +```go +values := lo.Values[string, int](map[string]int{"foo": 1, "bar": 2}) +// []int{1, 2} +``` + +[[play](https://go.dev/play/p/nnRTQkzQfF6)] + +### PickBy + +Returns same map type filtered by given predicate. + +```go +m := lo.PickBy[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, func(key string, value int) bool { + return value%2 == 1 +}) +// map[string]int{"foo": 1, "baz": 3} +``` + +[[play](https://go.dev/play/p/kdg8GR_QMmf)] + +### PickByKeys + +Returns same map type filtered by given keys. + +```go +m := lo.PickByKeys[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, []string{"foo", "baz"}) +// map[string]int{"foo": 1, "baz": 3} +``` + +[[play](https://go.dev/play/p/R1imbuci9qU)] + +### PickByValues + +Returns same map type filtered by given values. + +```go +m := lo.PickByValues[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, []int{1, 3}) +// map[string]int{"foo": 1, "baz": 3} +``` + +[[play](https://go.dev/play/p/1zdzSvbfsJc)] + +### OmitBy + +Returns same map type filtered by given predicate. + +```go +m := lo.OmitBy[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, func(key string, value int) bool { + return value%2 == 1 +}) +// map[string]int{"bar": 2} +``` + +[[play](https://go.dev/play/p/EtBsR43bdsd)] + +### OmitByKeys + +Returns same map type filtered by given keys. + +```go +m := lo.OmitByKeys[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, []string{"foo", "baz"}) +// map[string]int{"bar": 2} +``` + +[[play](https://go.dev/play/p/t1QjCrs-ysk)] + +### OmitByValues + +Returns same map type filtered by given values. + +```go +m := lo.OmitByValues[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, []int{1, 3}) +// map[string]int{"bar": 2} +``` + +[[play](https://go.dev/play/p/9UYZi-hrs8j)] + +### Entries (alias: ToPairs) + +Transforms a map into array of key/value pairs. + +```go +entries := lo.Entries[string, int](map[string]int{"foo": 1, "bar": 2}) +// []lo.Entry[string, int]{ +// { +// Key: "foo", +// Value: 1, +// }, +// { +// Key: "bar", +// Value: 2, +// }, +// } +``` + +[[play](https://go.dev/play/p/3Dhgx46gawJ)] + +### FromEntries (alias: FromPairs) + +Transforms an array of key/value pairs into a map. + +```go +m := lo.FromEntries[string, int]([]lo.Entry[string, int]{ + { + Key: "foo", + Value: 1, + }, + { + Key: "bar", + Value: 2, + }, +}) +// map[string]int{"foo": 1, "bar": 2} +``` + +[[play](https://go.dev/play/p/oIr5KHFGCEN)] + +### Invert + +Creates a map composed of the inverted keys and values. If map contains duplicate values, subsequent values overwrite property assignments of previous values. + +```go +m1 := lo.Invert[string, int](map[string]int{"a": 1, "b": 2}) +// map[int]string{1: "a", 2: "b"} + +m2 := lo.Invert[string, int](map[string]int{"a": 1, "b": 2, "c": 1}) +// map[int]string{1: "c", 2: "b"} +``` + +[[play](https://go.dev/play/p/rFQ4rak6iA1)] + +### Assign + +Merges multiple maps from left to right. + +```go +mergedMaps := lo.Assign[string, int]( + map[string]int{"a": 1, "b": 2}, + map[string]int{"b": 3, "c": 4}, +) +// map[string]int{"a": 1, "b": 3, "c": 4} +``` + +[[play](https://go.dev/play/p/VhwfJOyxf5o)] + +### MapKeys + +Manipulates a map keys and transforms it to a map of another type. + +```go +m2 := lo.MapKeys[int, int, string](map[int]int{1: 1, 2: 2, 3: 3, 4: 4}, func(_ int, v int) string { + return strconv.FormatInt(int64(v), 10) +}) +// map[string]int{"1": 1, "2": 2, "3": 3, "4": 4} +``` + +[[play](https://go.dev/play/p/9_4WPIqOetJ)] + +### MapValues + +Manipulates a map values and transforms it to a map of another type. + +```go +m1 := map[int]int64{1: 1, 2: 2, 3: 3} + +m2 := lo.MapValues[int, int64, string](m1, func(x int64, _ int) string { + return strconv.FormatInt(x, 10) +}) +// map[int]string{1: "1", 2: "2", 3: "3"} +``` + +[[play](https://go.dev/play/p/T_8xAfvcf0W)] + +### MapEntries + +Manipulates a map entries and transforms it to a map of another type. + +```go +in := map[string]int{"foo": 1, "bar": 2} + +out := lo.MapEntries(in, func(k string, v int) (int, string) { + return v,k +}) +// map[int]string{1: "foo", 2: "bar"} +``` + +[[play](https://go.dev/play/p/VuvNQzxKimT)] + +### MapToSlice + +Transforms a map into a slice based on specific iteratee. + +```go +m := map[int]int64{1: 4, 2: 5, 3: 6} + +s := lo.MapToSlice(m, func(k int, v int64) string { + return fmt.Sprintf("%d_%d", k, v) +}) +// []string{"1_4", "2_5", "3_6"} +``` + +[[play](https://go.dev/play/p/ZuiCZpDt6LD)] + +### Range / RangeFrom / RangeWithSteps + +Creates an array of numbers (positive and/or negative) progressing from start up to, but not including end. + +```go +result := lo.Range(4) +// [0, 1, 2, 3] + +result := lo.Range(-4) +// [0, -1, -2, -3] + +result := lo.RangeFrom(1, 5) +// [1, 2, 3, 4, 5] + +result := lo.RangeFrom[float64](1.0, 5) +// [1.0, 2.0, 3.0, 4.0, 5.0] + +result := lo.RangeWithSteps(0, 20, 5) +// [0, 5, 10, 15] + +result := lo.RangeWithSteps[float32](-1.0, -4.0, -1.0) +// [-1.0, -2.0, -3.0] + +result := lo.RangeWithSteps(1, 4, -1) +// [] + +result := lo.Range(0) +// [] +``` + +[[play](https://go.dev/play/p/0r6VimXAi9H)] + +### Clamp + +Clamps number within the inclusive lower and upper bounds. + +```go +r1 := lo.Clamp(0, -10, 10) +// 0 + +r2 := lo.Clamp(-42, -10, 10) +// -10 + +r3 := lo.Clamp(42, -10, 10) +// 10 +``` + +[[play](https://go.dev/play/p/RU4lJNC2hlI)] + +### Sum + +Sums the values in a collection. + +If collection is empty 0 is returned. + +```go +list := []int{1, 2, 3, 4, 5} +sum := lo.Sum(list) +// 15 +``` + +[[play](https://go.dev/play/p/upfeJVqs4Bt)] + +### SumBy + +Summarizes the values in a collection using the given return value from the iteration function. + +If collection is empty 0 is returned. + +```go +strings := []string{"foo", "bar"} +sum := lo.SumBy(strings, func(item string) int { + return len(item) +}) +// 6 +``` + +[[play](https://go.dev/play/p/Dz_a_7jN_ca)] + +### RandomString + +Returns a random string of the specified length and made of the specified charset. + +```go +str := lo.RandomString(5, lo.LettersCharset) +// example: "eIGbt" +``` + +[[play](https://go.dev/play/p/rRseOQVVum4)] + +### Substring + +Return part of a string. + +```go +sub := lo.Substring("hello", 2, 3) +// "llo" + +sub := lo.Substring("hello", -4, 3) +// "ell" + +sub := lo.Substring("hello", -2, math.MaxUint) +// "lo" +``` + +[[play](https://go.dev/play/p/TQlxQi82Lu1)] + +### ChunkString + +Returns an array of strings split into groups the length of size. If array can't be split evenly, the final chunk will be the remaining elements. + +```go +lo.ChunkString("123456", 2) +// []string{"12", "34", "56"} + +lo.ChunkString("1234567", 2) +// []string{"12", "34", "56", "7"} + +lo.ChunkString("", 2) +// []string{""} + +lo.ChunkString("1", 2) +// []string{"1"} +``` + +[[play](https://go.dev/play/p/__FLTuJVz54)] + +### RuneLength + +An alias to utf8.RuneCountInString which returns the number of runes in string. + +```go +sub := lo.RuneLength("hellô") +// 5 + +sub := len("hellô") +// 6 +``` + +[[play](https://go.dev/play/p/tuhgW_lWY8l)] + +### T2 -> T9 + +Creates a tuple from a list of values. + +```go +tuple1 := lo.T2("x", 1) +// Tuple2[string, int]{A: "x", B: 1} + +func example() (string, int) { return "y", 2 } +tuple2 := lo.T2(example()) +// Tuple2[string, int]{A: "y", B: 2} +``` + +[[play](https://go.dev/play/p/IllL3ZO4BQm)] + +### Unpack2 -> Unpack9 + +Returns values contained in tuple. + +```go +r1, r2 := lo.Unpack2[string, int](lo.Tuple2[string, int]{"a", 1}) +// "a", 1 +``` + +Unpack is also available as a method of TupleX. + +```go +tuple2 := lo.T2("a", 1) +a, b := tuple2.Unpack() +// "a" 1 +``` + +[[play](https://go.dev/play/p/xVP_k0kJ96W)] + +### Zip2 -> Zip9 + +Zip creates a slice of grouped elements, the first of which contains the first elements of the given arrays, the second of which contains the second elements of the given arrays, and so on. + +When collections have different size, the Tuple attributes are filled with zero value. + +```go +tuples := lo.Zip2[string, int]([]string{"a", "b"}, []int{1, 2}) +// []Tuple2[string, int]{{A: "a", B: 1}, {A: "b", B: 2}} +``` + +[[play](https://go.dev/play/p/jujaA6GaJTp)] + +### Unzip2 -> Unzip9 + +Unzip accepts an array of grouped elements and creates an array regrouping the elements to their pre-zip configuration. + +```go +a, b := lo.Unzip2[string, int]([]Tuple2[string, int]{{A: "a", B: 1}, {A: "b", B: 2}}) +// []string{"a", "b"} +// []int{1, 2} +``` + +[[play](https://go.dev/play/p/ciHugugvaAW)] + +### ChannelDispatcher + +Distributes messages from input channels into N child channels. Close events are propagated to children. + +Underlying channels can have a fixed buffer capacity or be unbuffered when cap is 0. + +```go +ch := make(chan int, 42) +for i := 0; i <= 10; i++ { + ch <- i +} + +children := lo.ChannelDispatcher(ch, 5, 10, DispatchingStrategyRoundRobin[int]) +// []<-chan int{...} + +consumer := func(c <-chan int) { + for { + msg, ok := <-c + if !ok { + println("closed") + break + } + + println(msg) + } +} + +for i := range children { + go consumer(children[i]) +} +``` + +Many distributions strategies are available: + +- [lo.DispatchingStrategyRoundRobin](./channel.go): Distributes messages in a rotating sequential manner. +- [lo.DispatchingStrategyRandom](./channel.go): Distributes messages in a random manner. +- [lo.DispatchingStrategyWeightedRandom](./channel.go): Distributes messages in a weighted manner. +- [lo.DispatchingStrategyFirst](./channel.go): Distributes messages in the first non-full channel. +- [lo.DispatchingStrategyLeast](./channel.go): Distributes messages in the emptiest channel. +- [lo.DispatchingStrategyMost](./channel.go): Distributes to the fullest channel. + +Some strategies bring fallback, in order to favor non-blocking behaviors. See implementations. + +For custom strategies, just implement the `lo.DispatchingStrategy` prototype: + +```go +type DispatchingStrategy[T any] func(message T, messageIndex uint64, channels []<-chan T) int +``` + +Eg: + +```go +type Message struct { + TenantID uuid.UUID +} + +func hash(id uuid.UUID) int { + h := fnv.New32a() + h.Write([]byte(id.String())) + return int(h.Sum32()) +} + +// Routes messages per TenantID. +customStrategy := func(message pubsub.AMQPSubMessage, messageIndex uint64, channels []<-chan pubsub.AMQPSubMessage) int { + destination := hash(message.TenantID) % len(channels) + + // check if channel is full + if len(channels[destination]) < cap(channels[destination]) { + return destination + } + + // fallback when child channel is full + return utils.DispatchingStrategyRoundRobin(message, uint64(destination), channels) +} + +children := lo.ChannelDispatcher(ch, 5, 10, customStrategy) +... +``` + +### SliceToChannel + +Returns a read-only channels of collection elements. Channel is closed after last element. Channel capacity can be customized. + +```go +list := []int{1, 2, 3, 4, 5} + +for v := range lo.SliceToChannel(2, list) { + println(v) +} +// prints 1, then 2, then 3, then 4, then 5 +``` + +### ChannelToSlice + +Returns a slice built from channels items. Blocks until channel closes. + +```go +list := []int{1, 2, 3, 4, 5} +ch := lo.SliceToChannel(2, list) + +items := ChannelToSlice(ch) +// []int{1, 2, 3, 4, 5} +``` + +### Generator + +Implements the generator design pattern. Channel is closed after last element. Channel capacity can be customized. + +```go +generator := func(yield func(int)) { + yield(1) + yield(2) + yield(3) +} + +for v := range lo.Generator(2, generator) { + println(v) +} +// prints 1, then 2, then 3 +``` + +### Buffer + +Creates a slice of n elements from a channel. Returns the slice, the slice length, the read time and the channel status (opened/closed). + +```go +ch := lo.SliceToChannel(2, []int{1, 2, 3, 4, 5}) + +items1, length1, duration1, ok1 := lo.Buffer(ch, 3) +// []int{1, 2, 3}, 3, 0s, true +items2, length2, duration2, ok2 := lo.Buffer(ch, 3) +// []int{4, 5}, 2, 0s, false +``` + +Example: RabbitMQ consumer 👇 + +```go +ch := readFromQueue() + +for { + // read 1k items + items, length, _, ok := lo.Buffer(ch, 1000) + + // do batching stuff + + if !ok { + break + } +} +``` + +### BufferWithTimeout + +Creates a slice of n elements from a channel, with timeout. Returns the slice, the slice length, the read time and the channel status (opened/closed). + +```go +generator := func(yield func(int)) { + for i := 0; i < 5; i++ { + yield(i) + time.Sleep(35*time.Millisecond) + } +} + +ch := lo.Generator(0, generator) + +items1, length1, duration1, ok1 := lo.BufferWithTimeout(ch, 3, 100*time.Millisecond) +// []int{1, 2}, 2, 100ms, true +items2, length2, duration2, ok2 := lo.BufferWithTimeout(ch, 3, 100*time.Millisecond) +// []int{3, 4, 5}, 3, 75ms, true +items3, length3, duration2, ok3 := lo.BufferWithTimeout(ch, 3, 100*time.Millisecond) +// []int{}, 0, 10ms, false +``` + +Example: RabbitMQ consumer 👇 + +```go +ch := readFromQueue() + +for { + // read 1k items + // wait up to 1 second + items, length, _, ok := lo.BufferWithTimeout(ch, 1000, 1*time.Second) + + // do batching stuff + + if !ok { + break + } +} +``` + +Example: Multithreaded RabbitMQ consumer 👇 + +```go +ch := readFromQueue() + +// 5 workers +// prefetch 1k messages per worker +children := lo.ChannelDispatcher(ch, 5, 1000, DispatchingStrategyFirst[int]) + +consumer := func(c <-chan int) { + for { + // read 1k items + // wait up to 1 second + items, length, _, ok := lo.BufferWithTimeout(ch, 1000, 1*time.Second) + + // do batching stuff + + if !ok { + break + } + } +} + +for i := range children { + go consumer(children[i]) +} +``` + +### FanIn + +Merge messages from multiple input channels into a single buffered channel. Output messages has no priority. When all upstream channels reach EOF, downstream channel closes. + +```go +stream1 := make(chan int, 42) +stream2 := make(chan int, 42) +stream3 := make(chan int, 42) + +all := lo.FanIn(100, stream1, stream2, stream3) +// <-chan int +``` + +### FanOut + +Broadcasts all the upstream messages to multiple downstream channels. When upstream channel reach EOF, downstream channels close. If any downstream channels is full, broadcasting is paused. + +```go +stream := make(chan int, 42) + +all := lo.FanOut(5, 100, stream) +// [5]<-chan int +``` + +### Contains + +Returns true if an element is present in a collection. + +```go +present := lo.Contains[int]([]int{0, 1, 2, 3, 4, 5}, 5) +// true +``` + +### ContainsBy + +Returns true if the predicate function returns `true`. + +```go +present := lo.ContainsBy[int]([]int{0, 1, 2, 3, 4, 5}, func(x int) bool { + return x == 3 +}) +// true +``` + +### Every + +Returns true if all elements of a subset are contained into a collection or if the subset is empty. + +```go +ok := lo.Every[int]([]int{0, 1, 2, 3, 4, 5}, []int{0, 2}) +// true + +ok := lo.Every[int]([]int{0, 1, 2, 3, 4, 5}, []int{0, 6}) +// false +``` + +### EveryBy + +Returns true if the predicate returns true for all of the elements in the collection or if the collection is empty. + +```go +b := EveryBy[int]([]int{1, 2, 3, 4}, func(x int) bool { + return x < 5 +}) +// true +``` + +### Some + +Returns true if at least 1 element of a subset is contained into a collection. +If the subset is empty Some returns false. + +```go +ok := lo.Some[int]([]int{0, 1, 2, 3, 4, 5}, []int{0, 2}) +// true + +ok := lo.Some[int]([]int{0, 1, 2, 3, 4, 5}, []int{-1, 6}) +// false +``` + +### SomeBy + +Returns true if the predicate returns true for any of the elements in the collection. +If the collection is empty SomeBy returns false. + +```go +b := SomeBy[int]([]int{1, 2, 3, 4}, func(x int) bool { + return x < 3 +}) +// true +``` + +### None + +Returns true if no element of a subset are contained into a collection or if the subset is empty. + +```go +b := None[int]([]int{0, 1, 2, 3, 4, 5}, []int{0, 2}) +// false +b := None[int]([]int{0, 1, 2, 3, 4, 5}, []int{-1, 6}) +// true +``` + +### NoneBy + +Returns true if the predicate returns true for none of the elements in the collection or if the collection is empty. + +```go +b := NoneBy[int]([]int{1, 2, 3, 4}, func(x int) bool { + return x < 0 +}) +// true +``` + +### Intersect + +Returns the intersection between two collections. + +```go +result1 := lo.Intersect[int]([]int{0, 1, 2, 3, 4, 5}, []int{0, 2}) +// []int{0, 2} + +result2 := lo.Intersect[int]([]int{0, 1, 2, 3, 4, 5}, []int{0, 6} +// []int{0} + +result3 := lo.Intersect[int]([]int{0, 1, 2, 3, 4, 5}, []int{-1, 6}) +// []int{} +``` + +### Difference + +Returns the difference between two collections. + +- The first value is the collection of element absent of list2. +- The second value is the collection of element absent of list1. + +```go +left, right := lo.Difference[int]([]int{0, 1, 2, 3, 4, 5}, []int{0, 2, 6}) +// []int{1, 3, 4, 5}, []int{6} + +left, right := lo.Difference[int]([]int{0, 1, 2, 3, 4, 5}, []int{0, 1, 2, 3, 4, 5}) +// []int{}, []int{} +``` + +### Union + +Returns all distinct elements from given collections. Result will not change the order of elements relatively. + +```go +union := lo.Union[int]([]int{0, 1, 2, 3, 4, 5}, []int{0, 2}, []int{0, 10}) +// []int{0, 1, 2, 3, 4, 5, 10} +``` + +### Without + +Returns slice excluding all given values. + +```go +subset := lo.Without[int]([]int{0, 2, 10}, 2) +// []int{0, 10} + +subset := lo.Without[int]([]int{0, 2, 10}, 0, 1, 2, 3, 4, 5) +// []int{10} +``` + +### WithoutEmpty + +Returns slice excluding empty values. + +```go +subset := lo.WithoutEmpty[int]([]int{0, 2, 10}) +// []int{2, 10} +``` + +### IndexOf + +Returns the index at which the first occurrence of a value is found in an array or return -1 if the value cannot be found. + +```go +found := lo.IndexOf[int]([]int{0, 1, 2, 1, 2, 3}, 2) +// 2 + +notFound := lo.IndexOf[int]([]int{0, 1, 2, 1, 2, 3}, 6) +// -1 +``` + +### LastIndexOf + +Returns the index at which the last occurrence of a value is found in an array or return -1 if the value cannot be found. + +```go +found := lo.LastIndexOf[int]([]int{0, 1, 2, 1, 2, 3}, 2) +// 4 + +notFound := lo.LastIndexOf[int]([]int{0, 1, 2, 1, 2, 3}, 6) +// -1 +``` + +### Find + +Search an element in a slice based on a predicate. It returns element and true if element was found. + +```go +str, ok := lo.Find[string]([]string{"a", "b", "c", "d"}, func(i string) bool { + return i == "b" +}) +// "b", true + +str, ok := lo.Find[string]([]string{"foobar"}, func(i string) bool { + return i == "b" +}) +// "", false +``` + +### FindIndexOf + +FindIndexOf searches an element in a slice based on a predicate and returns the index and true. It returns -1 and false if the element is not found. + +```go +str, index, ok := lo.FindIndexOf[string]([]string{"a", "b", "a", "b"}, func(i string) bool { + return i == "b" +}) +// "b", 1, true + +str, index, ok := lo.FindIndexOf[string]([]string{"foobar"}, func(i string) bool { + return i == "b" +}) +// "", -1, false +``` + +### FindLastIndexOf + +FindLastIndexOf searches an element in a slice based on a predicate and returns the index and true. It returns -1 and false if the element is not found. + +```go +str, index, ok := lo.FindLastIndexOf[string]([]string{"a", "b", "a", "b"}, func(i string) bool { + return i == "b" +}) +// "b", 4, true + +str, index, ok := lo.FindLastIndexOf[string]([]string{"foobar"}, func(i string) bool { + return i == "b" +}) +// "", -1, false +``` + +### FindKey + +Returns the key of the first value matching. + +```go +result1, ok1 := lo.FindKey(map[string]int{"foo": 1, "bar": 2, "baz": 3}, 2) +// "bar", true + +result2, ok2 := lo.FindKey(map[string]int{"foo": 1, "bar": 2, "baz": 3}, 42) +// "", false + +type test struct { + foobar string +} +result3, ok3 := lo.FindKey(map[string]test{"foo": test{"foo"}, "bar": test{"bar"}, "baz": test{"baz"}}, test{"foo"}) +// "foo", true +``` + +### FindKeyBy + +Returns the key of the first element predicate returns truthy for. + +```go +result1, ok1 := lo.FindKeyBy(map[string]int{"foo": 1, "bar": 2, "baz": 3}, func(k string, v int) bool { + return k == "foo" +}) +// "foo", true + +result2, ok2 := lo.FindKeyBy(map[string]int{"foo": 1, "bar": 2, "baz": 3}, func(k string, v int) bool { + return false +}) +// "", false +``` + +### FindUniques + +Returns a slice with all the unique elements of the collection. The order of result values is determined by the order they occur in the array. + +```go +uniqueValues := lo.FindUniques[int]([]int{1, 2, 2, 1, 2, 3}) +// []int{3} +``` + +### FindUniquesBy + +Returns a slice with all the unique elements of the collection. The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is invoked for each element in array to generate the criterion by which uniqueness is computed. + +```go +uniqueValues := lo.FindUniquesBy[int, int]([]int{3, 4, 5, 6, 7}, func(i int) int { + return i%3 +}) +// []int{5} +``` + +### FindDuplicates + +Returns a slice with the first occurrence of each duplicated elements of the collection. The order of result values is determined by the order they occur in the array. + +```go +duplicatedValues := lo.FindDuplicates[int]([]int{1, 2, 2, 1, 2, 3}) +// []int{1, 2} +``` + +### FindDuplicatesBy + +Returns a slice with the first occurrence of each duplicated elements of the collection. The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is invoked for each element in array to generate the criterion by which uniqueness is computed. + +```go +duplicatedValues := lo.FindDuplicatesBy[int, int]([]int{3, 4, 5, 6, 7}, func(i int) int { + return i%3 +}) +// []int{3, 4} +``` + +### Min + +Search the minimum value of a collection. + +Returns zero value when collection is empty. + +```go +min := lo.Min([]int{1, 2, 3}) +// 1 + +min := lo.Min([]int{}) +// 0 +``` + +### MinBy + +Search the minimum value of a collection using the given comparison function. + +If several values of the collection are equal to the smallest value, returns the first such value. + +Returns zero value when collection is empty. + +```go +min := lo.MinBy([]string{"s1", "string2", "s3"}, func(item string, min string) bool { + return len(item) < len(min) +}) +// "s1" + +min := lo.MinBy([]string{}, func(item string, min string) bool { + return len(item) < len(min) +}) +// "" +``` + +### Max + +Search the maximum value of a collection. + +Returns zero value when collection is empty. + +```go +max := lo.Max([]int{1, 2, 3}) +// 3 + +max := lo.Max([]int{}) +// 0 +``` + +### MaxBy + +Search the maximum value of a collection using the given comparison function. + +If several values of the collection are equal to the greatest value, returns the first such value. + +Returns zero value when collection is empty. + +```go +max := lo.MaxBy([]string{"string1", "s2", "string3"}, func(item string, max string) bool { + return len(item) > len(max) +}) +// "string1" + +max := lo.MaxBy([]string{}, func(item string, max string) bool { + return len(item) > len(max) +}) +// "" +``` + +### Last + +Returns the last element of a collection or error if empty. + +```go +last, err := lo.Last[int]([]int{1, 2, 3}) +// 3 +``` + +### Nth + +Returns the element at index `nth` of collection. If `nth` is negative, the nth element from the end is returned. An error is returned when nth is out of slice bounds. + +```go +nth, err := lo.Nth[int]([]int{0, 1, 2, 3}, 2) +// 2 + +nth, err := lo.Nth[int]([]int{0, 1, 2, 3}, -2) +// 2 +``` + +### Sample + +Returns a random item from collection. + +```go +lo.Sample[string]([]string{"a", "b", "c"}) +// a random string from []string{"a", "b", "c"} + +lo.Sample[string]([]string{}) +// "" +``` + +### Samples + +Returns N random unique items from collection. + +```go +lo.Samples[string]([]string{"a", "b", "c"}, 3) +// []string{"a", "b", "c"} in random order +``` + +### Ternary + +A 1 line if/else statement. + +```go +result := lo.Ternary[string](true, "a", "b") +// "a" + +result := lo.Ternary[string](false, "a", "b") +// "b" +``` + +[[play](https://go.dev/play/p/t-D7WBL44h2)] + +### TernaryF + +A 1 line if/else statement whose options are functions. + +```go +result := lo.TernaryF[string](true, func() string { return "a" }, func() string { return "b" }) +// "a" + +result := lo.TernaryF[string](false, func() string { return "a" }, func() string { return "b" }) +// "b" +``` + +Useful to avoid nil-pointer dereferencing in intializations, or avoid running unnecessary code + +```go +var s *string + +someStr := TernaryF[string](s == nil, func() string { return uuid.New().String() }, func() string { return *s }) +// ef782193-c30c-4e2e-a7ae-f8ab5e125e02 +``` + +[[play](https://go.dev/play/p/AO4VW20JoqM)] + +### If / ElseIf / Else + +```go +result := lo.If[int](true, 1). + ElseIf(false, 2). + Else(3) +// 1 + +result := lo.If[int](false, 1). + ElseIf(true, 2). + Else(3) +// 2 + +result := lo.If[int](false, 1). + ElseIf(false, 2). + Else(3) +// 3 +``` + +Using callbacks: + +```go +result := lo.IfF[int](true, func () int { + return 1 + }). + ElseIfF(false, func () int { + return 2 + }). + ElseF(func () int { + return 3 + }) +// 1 +``` + +Mixed: + +```go +result := lo.IfF[int](true, func () int { + return 1 + }). + Else(42) +// 1 +``` + +[[play](https://go.dev/play/p/WSw3ApMxhyW)] + +### Switch / Case / Default + +```go +result := lo.Switch[int, string](1). + Case(1, "1"). + Case(2, "2"). + Default("3") +// "1" + +result := lo.Switch[int, string](2). + Case(1, "1"). + Case(2, "2"). + Default("3") +// "2" + +result := lo.Switch[int, string](42). + Case(1, "1"). + Case(2, "2"). + Default("3") +// "3" +``` + +Using callbacks: + +```go +result := lo.Switch[int, string](1). + CaseF(1, func() string { + return "1" + }). + CaseF(2, func() string { + return "2" + }). + DefaultF(func() string { + return "3" + }) +// "1" +``` + +Mixed: + +```go +result := lo.Switch[int, string](1). + CaseF(1, func() string { + return "1" + }). + Default("42") +// "1" +``` + +[[play](https://go.dev/play/p/TGbKUMAeRUd)] + +### ToPtr + +Returns a pointer copy of value. + +```go +ptr := lo.ToPtr[string]("hello world") +// *string{"hello world"} +``` + +### FromPtr + +Returns the pointer value or empty. + +```go +str := "hello world" +value := lo.FromPtr[string](&str) +// "hello world" + +value := lo.FromPtr[string](nil) +// "" +``` + +### FromPtrOr + +Returns the pointer value or the fallback value. + +```go +str := "hello world" +value := lo.FromPtrOr[string](&str, "empty") +// "hello world" + +value := lo.FromPtrOr[string](nil, "empty") +// "empty" +``` + +### ToSlicePtr + +Returns a slice of pointer copy of value. + +```go +ptr := lo.ToSlicePtr[string]([]string{"hello", "world"}) +// []*string{"hello", "world"} +``` + +### ToAnySlice + +Returns a slice with all elements mapped to `any` type. + +```go +elements := lo.ToAnySlice[int]([]int{1, 5, 1}) +// []any{1, 5, 1} +``` + +### FromAnySlice + +Returns an `any` slice with all elements mapped to a type. Returns false in case of type conversion failure. + +```go +elements, ok := lo.FromAnySlice[string]([]any{"foobar", 42}) +// []string{}, false + +elements, ok := lo.FromAnySlice[string]([]any{"foobar", "42"}) +// []string{"foobar", "42"}, true +``` + +### Empty + +Returns an empty value. + +```go +lo.Empty[int]() +// 0 +lo.Empty[string]() +// "" +lo.Empty[bool]() +// false +``` + +### IsEmpty + +Returns true if argument is a zero value. + +```go +lo.IsEmpty[int](0) +// true +lo.IsEmpty[int](42) +// false + +lo.IsEmpty[string]("") +// true +lo.IsEmpty[bool]("foobar") +// false + +type test struct { + foobar string +} + +lo.IsEmpty[test](test{foobar: ""}) +// true +lo.IsEmpty[test](test{foobar: "foobar"}) +// false +``` + +### IsNotEmpty + +Returns true if argument is a zero value. + +```go +lo.IsNotEmpty[int](0) +// false +lo.IsNotEmpty[int](42) +// true + +lo.IsNotEmpty[string]("") +// false +lo.IsNotEmpty[bool]("foobar") +// true + +type test struct { + foobar string +} + +lo.IsNotEmpty[test](test{foobar: ""}) +// false +lo.IsNotEmpty[test](test{foobar: "foobar"}) +// true +``` + +### Coalesce + +Returns the first non-empty arguments. Arguments must be comparable. + +```go +result, ok := lo.Coalesce(0, 1, 2, 3) +// 1 true + +result, ok := lo.Coalesce("") +// "" false + +var nilStr *string +str := "foobar" +result, ok := lo.Coalesce[*string](nil, nilStr, &str) +// &"foobar" true +``` + +### Partial + +Returns new function that, when called, has its first argument set to the provided value. + +```go +add := func(x, y int) int { return x + y } +f := lo.Partial(add, 5) + +f(10) +// 15 + +f(42) +// 47 +``` + +### Partial2 -> Partial5 + +Returns new function that, when called, has its first argument set to the provided value. + +```go +add := func(x, y, z int) int { return x + y + z } +f := lo.Partial2(add, 42) + +f(10, 5) +// 57 + +f(42, -4) +// 80 +``` + +### Attempt + +Invokes a function N times until it returns valid output. Returning either the caught error or nil. When first argument is less than `1`, the function runs until a successful response is returned. + +```go +iter, err := lo.Attempt(42, func(i int) error { + if i == 5 { + return nil + } + + return fmt.Errorf("failed") +}) +// 6 +// nil + +iter, err := lo.Attempt(2, func(i int) error { + if i == 5 { + return nil + } + + return fmt.Errorf("failed") +}) +// 2 +// error "failed" + +iter, err := lo.Attempt(0, func(i int) error { + if i < 42 { + return fmt.Errorf("failed") + } + + return nil +}) +// 43 +// nil +``` + +For more advanced retry strategies (delay, exponential backoff...), please take a look on [cenkalti/backoff](https://github.com/cenkalti/backoff). + +[[play](https://go.dev/play/p/3ggJZ2ZKcMj)] + +### AttemptWithDelay + +Invokes a function N times until it returns valid output, with a pause between each call. Returning either the caught error or nil. + +When first argument is less than `1`, the function runs until a successful response is returned. + +```go +iter, duration, err := lo.AttemptWithDelay(5, 2*time.Second, func(i int, duration time.Duration) error { + if i == 2 { + return nil + } + + return fmt.Errorf("failed") +}) +// 3 +// ~ 4 seconds +// nil +``` + +For more advanced retry strategies (delay, exponential backoff...), please take a look on [cenkalti/backoff](https://github.com/cenkalti/backoff). + +[[play](https://go.dev/play/p/tVs6CygC7m1)] + +### AttemptWhile + +Invokes a function N times until it returns valid output. Returning either the caught error or nil, and along with a bool value to identifying whether it needs invoke function continuously. It will terminate the invoke immediately if second bool value is returned with falsy value. + +When first argument is less than `1`, the function runs until a successful response is returned. + +```go +count1, err1 := lo.AttemptWhile(5, func(i int) (error, bool) { + err := doMockedHTTPRequest(i) + if err != nil { + if errors.Is(err, ErrBadRequest) { // lets assume ErrBadRequest is a critical error that needs to terminate the invoke + return err, false // flag the second return value as false to terminate the invoke + } + + return err, true + } + + return nil, false +}) +``` + +For more advanced retry strategies (delay, exponential backoff...), please take a look on [cenkalti/backoff](https://github.com/cenkalti/backoff). + +[[play](https://go.dev/play/p/M2wVq24PaZM)] + +### AttemptWhileWithDelay + +Invokes a function N times until it returns valid output, with a pause between each call. Returning either the caught error or nil, and along with a bool value to identifying whether it needs to invoke function continuously. It will terminate the invoke immediately if second bool value is returned with falsy value. + +When first argument is less than `1`, the function runs until a successful response is returned. + +```go +count1, time1, err1 := lo.AttemptWhileWithDelay(5, time.Millisecond, func(i int, d time.Duration) (error, bool) { + err := doMockedHTTPRequest(i) + if err != nil { + if errors.Is(err, ErrBadRequest) { // lets assume ErrBadRequest is a critical error that needs to terminate the invoke + return err, false // flag the second return value as false to terminate the invoke + } + + return err, true + } + + return nil, false +}) +``` + +For more advanced retry strategies (delay, exponential backoff...), please take a look on [cenkalti/backoff](https://github.com/cenkalti/backoff). + +[[play](https://go.dev/play/p/cfcmhvLO-nv)] + +### Debounce + +`NewDebounce` creates a debounced instance that delays invoking functions given until after wait milliseconds have elapsed, until `cancel` is called. + +```go +f := func() { + println("Called once after 100ms when debounce stopped invoking!") +} + +debounce, cancel := lo.NewDebounce(100 * time.Millisecond, f) +for j := 0; j < 10; j++ { + debounce() +} + +time.Sleep(1 * time.Second) +cancel() +``` + +[[play](https://go.dev/play/p/mz32VMK2nqe)] + +### Synchronize + +Wraps the underlying callback in a mutex. It receives an optional mutex. + +```go +s := lo.Synchronize() + +for i := 0; i < 10; i++ { + go s.Do(func () { + println("will be called sequentially") + }) +} +``` + +It is equivalent to: + +```go +mu := sync.Mutex{} + +func foobar() { + mu.Lock() + defer mu.Unlock() + + // ... +} +``` + +### Async + +Executes a function in a goroutine and returns the result in a channel. + +```go +ch := lo.Async(func() error { time.Sleep(10 * time.Second); return nil }) +// chan error (nil) +``` + +### Async{0->6} + +Executes a function in a goroutine and returns the result in a channel. +For function with multiple return values, the results will be returned as a tuple inside the channel. +For function without return, struct{} will be returned in the channel. + +```go +ch := lo.Async0(func() { time.Sleep(10 * time.Second) }) +// chan struct{} + +ch := lo.Async1(func() int { + time.Sleep(10 * time.Second); + return 42 +}) +// chan int (42) + +ch := lo.Async2(func() (int, string) { + time.Sleep(10 * time.Second); + return 42, "Hello" +}) +// chan lo.Tuple2[int, string] ({42, "Hello"}) +``` + +### Transaction + +Implements a Saga pattern. + +```go +transaction := NewTransaction[int](). + Then( + func(state int) (int, error) { + fmt.Println("step 1") + return state + 10, nil + }, + func(state int) int { + fmt.Println("rollback 1") + return state - 10 + }, + ). + Then( + func(state int) (int, error) { + fmt.Println("step 2") + return state + 15, nil + }, + func(state int) int { + fmt.Println("rollback 2") + return state - 15 + }, + ). + Then( + func(state int) (int, error) { + fmt.Println("step 3") + + if true { + return state, fmt.Errorf("error") + } + + return state + 42, nil + }, + func(state int) int { + fmt.Println("rollback 3") + return state - 42 + }, + ) + +_, _ = transaction.Process(-5) + +// Output: +// step 1 +// step 2 +// step 3 +// rollback 2 +// rollback 1 +``` + +### Validate + +Helper function that creates an error when a condition is not met. + +```go +slice := []string{"a"} +val := lo.Validate(len(slice) == 0, "Slice should be empty but contains %v", slice) +// error("Slice should be empty but contains [a]") + +slice := []string{} +val := lo.Validate(len(slice) == 0, "Slice should be empty but contains %v", slice) +// nil +``` + +[[play](https://go.dev/play/p/vPyh51XpCBt)] + +### Must + +Wraps a function call to panics if second argument is `error` or `false`, returns the value otherwise. + +```go +val := lo.Must(time.Parse("2006-01-02", "2022-01-15")) +// 2022-01-15 + +val := lo.Must(time.Parse("2006-01-02", "bad-value")) +// panics +``` + +[[play](https://go.dev/play/p/TMoWrRp3DyC)] + +### Must{0->6} + +Must\* has the same behavior as Must, but returns multiple values. + +```go +func example0() (error) +func example1() (int, error) +func example2() (int, string, error) +func example3() (int, string, time.Date, error) +func example4() (int, string, time.Date, bool, error) +func example5() (int, string, time.Date, bool, float64, error) +func example6() (int, string, time.Date, bool, float64, byte, error) + +lo.Must0(example0()) +val1 := lo.Must1(example1()) // alias to Must +val1, val2 := lo.Must2(example2()) +val1, val2, val3 := lo.Must3(example3()) +val1, val2, val3, val4 := lo.Must4(example4()) +val1, val2, val3, val4, val5 := lo.Must5(example5()) +val1, val2, val3, val4, val5, val6 := lo.Must6(example6()) +``` + +You can wrap functions like `func (...) (..., ok bool)`. + +```go +// math.Signbit(float64) bool +lo.Must0(math.Signbit(v)) + +// bytes.Cut([]byte,[]byte) ([]byte, []byte, bool) +before, after := lo.Must2(bytes.Cut(s, sep)) +``` + +You can give context to the panic message by adding some printf-like arguments. + +```go +val, ok := lo.Find(myString, func(i string) bool { + return i == requiredChar +}) +lo.Must0(ok, "'%s' must always contain '%s'", myString, requiredChar) + +list := []int{0, 1, 2} +item := 5 +lo.Must0(lo.Contains[int](list, item), "'%s' must always contain '%s'", list, item) +... +``` + +[[play](https://go.dev/play/p/TMoWrRp3DyC)] + +### Try + +Calls the function and return false in case of error and on panic. + +```go +ok := lo.Try(func() error { + panic("error") + return nil +}) +// false + +ok := lo.Try(func() error { + return nil +}) +// true + +ok := lo.Try(func() error { + return fmt.Errorf("error") +}) +// false +``` + +[[play](https://go.dev/play/p/mTyyWUvn9u4)] + +### Try{0->6} + +The same behavior than `Try`, but callback returns 2 variables. + +```go +ok := lo.Try2(func() (string, error) { + panic("error") + return "", nil +}) +// false +``` + +[[play](https://go.dev/play/p/mTyyWUvn9u4)] + +### TryOr + +Calls the function and return a default value in case of error and on panic. + +```go +str, ok := lo.TryOr(func() (string, error) { + panic("error") + return "hello", nil +}, "world") +// world +// false + +ok := lo.TryOr(func() error { + return "hello", nil +}, "world") +// hello +// true + +ok := lo.TryOr(func() error { + return "hello", fmt.Errorf("error") +}, "world") +// world +// false +``` + +[[play](https://go.dev/play/p/B4F7Wg2Zh9X)] + +### TryOr{0->6} + +The same behavior than `TryOr`, but callback returns 2 variables. + +```go +str, nbr, ok := lo.TryOr2(func() (string, int, error) { + panic("error") + return "hello", 42, nil +}, "world", 21) +// world +// 21 +// false +``` + +[[play](https://go.dev/play/p/B4F7Wg2Zh9X)] + +### TryWithErrorValue + +The same behavior than `Try`, but also returns value passed to panic. + +```go +err, ok := lo.TryWithErrorValue(func() error { + panic("error") + return nil +}) +// "error", false +``` + +[[play](https://go.dev/play/p/Kc7afQIT2Fs)] + +### TryCatch + +The same behavior than `Try`, but calls the catch function in case of error. + +```go +caught := false + +ok := lo.TryCatch(func() error { + panic("error") + return nil +}, func() { + caught = true +}) +// false +// caught == true +``` + +[[play](https://go.dev/play/p/PnOON-EqBiU)] + +### TryCatchWithErrorValue + +The same behavior than `TryWithErrorValue`, but calls the catch function in case of error. + +```go +caught := false + +ok := lo.TryCatchWithErrorValue(func() error { + panic("error") + return nil +}, func(val any) { + caught = val == "error" +}) +// false +// caught == true +``` + +[[play](https://go.dev/play/p/8Pc9gwX_GZO)] + +### ErrorsAs + +A shortcut for: + +```go +err := doSomething() + +var rateLimitErr *RateLimitError +if ok := errors.As(err, &rateLimitErr); ok { + // retry later +} +``` + +1 line `lo` helper: + +```go +err := doSomething() + +if rateLimitErr, ok := lo.ErrorsAs[*RateLimitError](err); ok { + // retry later +} +``` + +[[play](https://go.dev/play/p/8wk5rH8UfrE)] + +## 🛩 Benchmark + +We executed a simple benchmark with the a dead-simple `lo.Map` loop: + +See the full implementation [here](./benchmark_test.go). + +```go +_ = lo.Map[int64](arr, func(x int64, i int) string { + return strconv.FormatInt(x, 10) +}) +``` + +**Result:** + +Here is a comparison between `lo.Map`, `lop.Map`, `go-funk` library and a simple Go `for` loop. + +``` +$ go test -benchmem -bench ./... +goos: linux +goarch: amd64 +pkg: github.com/samber/lo +cpu: Intel(R) Core(TM) i5-7267U CPU @ 3.10GHz +cpu: Intel(R) Core(TM) i7 CPU 920 @ 2.67GHz +BenchmarkMap/lo.Map-8 8 132728237 ns/op 39998945 B/op 1000002 allocs/op +BenchmarkMap/lop.Map-8 2 503947830 ns/op 119999956 B/op 3000007 allocs/op +BenchmarkMap/reflect-8 2 826400560 ns/op 170326512 B/op 4000042 allocs/op +BenchmarkMap/for-8 9 126252954 ns/op 39998674 B/op 1000001 allocs/op +PASS +ok github.com/samber/lo 6.657s +``` + +- `lo.Map` is way faster (x7) than `go-funk`, a reflection-based Map implementation. +- `lo.Map` have the same allocation profile than `for`. +- `lo.Map` is 4% slower than `for`. +- `lop.Map` is slower than `lo.Map` because it implies more memory allocation and locks. `lop.Map` will be useful for long-running callbacks, such as i/o bound processing. +- `for` beats other implementations for memory and CPU. + +## 🤝 Contributing + +- Ping me on twitter [@samuelberthe](https://twitter.com/samuelberthe) (DMs, mentions, whatever :)) +- Fork the [project](https://github.com/samber/lo) +- Fix [open issues](https://github.com/samber/lo/issues) or request new features + +Don't hesitate ;) + +### With Docker + +```bash +docker-compose run --rm dev +``` + +### Without Docker + +```bash +# Install some dev dependencies +make tools + +# Run tests +make test +# or +make watch-test +``` + +## 👤 Contributors + +![Contributors](https://contrib.rocks/image?repo=samber/lo) + +## 💫 Show your support + +Give a ⭐️ if this project helped you! + +[![support us](https://c5.patreon.com/external/logo/become_a_patron_button.png)](https://www.patreon.com/samber) + +## 📝 License + +Copyright © 2022 [Samuel Berthe](https://github.com/samber). + +This project is [MIT](./LICENSE) licensed. diff --git a/vendor/github.com/samber/lo/channel.go b/vendor/github.com/samber/lo/channel.go new file mode 100644 index 00000000000..e5af2504322 --- /dev/null +++ b/vendor/github.com/samber/lo/channel.go @@ -0,0 +1,306 @@ +package lo + +import ( + "math/rand" + "sync" + "time" +) + +type DispatchingStrategy[T any] func(msg T, index uint64, channels []<-chan T) int + +// ChannelDispatcher distributes messages from input channels into N child channels. +// Close events are propagated to children. +// Underlying channels can have a fixed buffer capacity or be unbuffered when cap is 0. +func ChannelDispatcher[T any](stream <-chan T, count int, channelBufferCap int, strategy DispatchingStrategy[T]) []<-chan T { + children := createChannels[T](count, channelBufferCap) + + roChildren := channelsToReadOnly(children) + + go func() { + // propagate channel closing to children + defer closeChannels(children) + + var i uint64 = 0 + + for { + msg, ok := <-stream + if !ok { + return + } + + destination := strategy(msg, i, roChildren) % count + children[destination] <- msg + + i++ + } + }() + + return roChildren +} + +func createChannels[T any](count int, channelBufferCap int) []chan T { + children := make([]chan T, 0, count) + + for i := 0; i < count; i++ { + children = append(children, make(chan T, channelBufferCap)) + } + + return children +} + +func channelsToReadOnly[T any](children []chan T) []<-chan T { + roChildren := make([]<-chan T, 0, len(children)) + + for i := range children { + roChildren = append(roChildren, children[i]) + } + + return roChildren +} + +func closeChannels[T any](children []chan T) { + for i := 0; i < len(children); i++ { + close(children[i]) + } +} + +func channelIsNotFull[T any](ch <-chan T) bool { + return cap(ch) == 0 || len(ch) < cap(ch) +} + +// DispatchingStrategyRoundRobin distributes messages in a rotating sequential manner. +// If the channel capacity is exceeded, the next channel will be selected and so on. +func DispatchingStrategyRoundRobin[T any](msg T, index uint64, channels []<-chan T) int { + for { + i := int(index % uint64(len(channels))) + if channelIsNotFull(channels[i]) { + return i + } + + index++ + time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥 + } +} + +// DispatchingStrategyRandom distributes messages in a random manner. +// If the channel capacity is exceeded, another random channel will be selected and so on. +func DispatchingStrategyRandom[T any](msg T, index uint64, channels []<-chan T) int { + for { + i := rand.Intn(len(channels)) + if channelIsNotFull(channels[i]) { + return i + } + + time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥 + } +} + +// DispatchingStrategyWeightedRandom distributes messages in a weighted manner. +// If the channel capacity is exceeded, another random channel will be selected and so on. +func DispatchingStrategyWeightedRandom[T any](weights []int) DispatchingStrategy[T] { + seq := []int{} + + for i := 0; i < len(weights); i++ { + for j := 0; j < weights[i]; j++ { + seq = append(seq, i) + } + } + + return func(msg T, index uint64, channels []<-chan T) int { + for { + i := seq[rand.Intn(len(seq))] + if channelIsNotFull(channels[i]) { + return i + } + + time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥 + } + } +} + +// DispatchingStrategyFirst distributes messages in the first non-full channel. +// If the capacity of the first channel is exceeded, the second channel will be selected and so on. +func DispatchingStrategyFirst[T any](msg T, index uint64, channels []<-chan T) int { + for { + for i := range channels { + if channelIsNotFull(channels[i]) { + return i + } + } + + time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥 + } +} + +// DispatchingStrategyLeast distributes messages in the emptiest channel. +func DispatchingStrategyLeast[T any](msg T, index uint64, channels []<-chan T) int { + seq := Range(len(channels)) + + return MinBy(seq, func(item int, min int) bool { + return len(channels[item]) < len(channels[min]) + }) +} + +// DispatchingStrategyMost distributes messages in the fullest channel. +// If the channel capacity is exceeded, the next channel will be selected and so on. +func DispatchingStrategyMost[T any](msg T, index uint64, channels []<-chan T) int { + seq := Range(len(channels)) + + return MaxBy(seq, func(item int, max int) bool { + return len(channels[item]) > len(channels[max]) && channelIsNotFull(channels[item]) + }) +} + +// SliceToChannel returns a read-only channels of collection elements. +func SliceToChannel[T any](bufferSize int, collection []T) <-chan T { + ch := make(chan T, bufferSize) + + go func() { + for _, item := range collection { + ch <- item + } + + close(ch) + }() + + return ch +} + +// ChannelToSlice returns a slice built from channels items. Blocks until channel closes. +func ChannelToSlice[T any](ch <-chan T) []T { + collection := []T{} + + for item := range ch { + collection = append(collection, item) + } + + return collection +} + +// Generator implements the generator design pattern. +func Generator[T any](bufferSize int, generator func(yield func(T))) <-chan T { + ch := make(chan T, bufferSize) + + go func() { + // WARNING: infinite loop + generator(func(t T) { + ch <- t + }) + + close(ch) + }() + + return ch +} + +// Buffer creates a slice of n elements from a channel. Returns the slice and the slice length. +// @TODO: we should probably provide an helper that reuse the same buffer. +func Buffer[T any](ch <-chan T, size int) (collection []T, length int, readTime time.Duration, ok bool) { + buffer := make([]T, 0, size) + index := 0 + now := time.Now() + + for ; index < size; index++ { + item, ok := <-ch + if !ok { + return buffer, index, time.Since(now), false + } + + buffer = append(buffer, item) + } + + return buffer, index, time.Since(now), true +} + +// Buffer creates a slice of n elements from a channel. Returns the slice and the slice length. +// Deprecated: Use lo.Buffer instead. +func Batch[T any](ch <-chan T, size int) (collection []T, length int, readTime time.Duration, ok bool) { + return Buffer(ch, size) +} + +// BufferWithTimeout creates a slice of n elements from a channel, with timeout. Returns the slice and the slice length. +// @TODO: we should probably provide an helper that reuse the same buffer. +func BufferWithTimeout[T any](ch <-chan T, size int, timeout time.Duration) (collection []T, length int, readTime time.Duration, ok bool) { + expire := time.NewTimer(timeout) + defer expire.Stop() + + buffer := make([]T, 0, size) + index := 0 + now := time.Now() + + for ; index < size; index++ { + select { + case item, ok := <-ch: + if !ok { + return buffer, index, time.Since(now), false + } + + buffer = append(buffer, item) + + case <-expire.C: + return buffer, index, time.Since(now), true + } + } + + return buffer, index, time.Since(now), true +} + +// BufferWithTimeout creates a slice of n elements from a channel, with timeout. Returns the slice and the slice length. +// Deprecated: Use lo.BufferWithTimeout instead. +func BatchWithTimeout[T any](ch <-chan T, size int, timeout time.Duration) (collection []T, length int, readTime time.Duration, ok bool) { + return BufferWithTimeout(ch, size, timeout) +} + +// FanIn collects messages from multiple input channels into a single buffered channel. +// Output messages has no priority. When all upstream channels reach EOF, downstream channel closes. +func FanIn[T any](channelBufferCap int, upstreams ...<-chan T) <-chan T { + out := make(chan T, channelBufferCap) + var wg sync.WaitGroup + + // Start an output goroutine for each input channel in upstreams. + wg.Add(len(upstreams)) + for _, c := range upstreams { + go func(c <-chan T) { + for n := range c { + out <- n + } + wg.Done() + }(c) + } + + // Start a goroutine to close out once all the output goroutines are done. + go func() { + wg.Wait() + close(out) + }() + return out +} + +// ChannelMerge collects messages from multiple input channels into a single buffered channel. +// Output messages has no priority. When all upstream channels reach EOF, downstream channel closes. +// Deprecated: Use lo.FanIn instead. +func ChannelMerge[T any](channelBufferCap int, upstreams ...<-chan T) <-chan T { + return FanIn(channelBufferCap, upstreams...) +} + +// FanOut broadcasts all the upstream messages to multiple downstream channels. +// When upstream channel reach EOF, downstream channels close. If any downstream +// channels is full, broadcasting is paused. +func FanOut[T any](count int, channelsBufferCap int, upstream <-chan T) []<-chan T { + downstreams := createChannels[T](count, channelsBufferCap) + + go func() { + for msg := range upstream { + for i := range downstreams { + downstreams[i] <- msg + } + } + + // Close out once all the output goroutines are done. + for i := range downstreams { + close(downstreams[i]) + } + }() + + return channelsToReadOnly(downstreams) +} diff --git a/vendor/github.com/samber/lo/concurrency.go b/vendor/github.com/samber/lo/concurrency.go new file mode 100644 index 00000000000..2d6fd871a4b --- /dev/null +++ b/vendor/github.com/samber/lo/concurrency.go @@ -0,0 +1,95 @@ +package lo + +import "sync" + +type synchronize struct { + locker sync.Locker +} + +func (s *synchronize) Do(cb func()) { + s.locker.Lock() + Try0(cb) + s.locker.Unlock() +} + +// Synchronize wraps the underlying callback in a mutex. It receives an optional mutex. +func Synchronize(opt ...sync.Locker) *synchronize { + if len(opt) > 1 { + panic("unexpected arguments") + } else if len(opt) == 0 { + opt = append(opt, &sync.Mutex{}) + } + + return &synchronize{ + locker: opt[0], + } +} + +// Async executes a function in a goroutine and returns the result in a channel. +func Async[A any](f func() A) chan A { + ch := make(chan A) + go func() { + ch <- f() + }() + return ch +} + +// Async0 executes a function in a goroutine and returns a channel set once the function finishes. +func Async0(f func()) chan struct{} { + ch := make(chan struct{}) + go func() { + f() + ch <- struct{}{} + }() + return ch +} + +// Async1 is an alias to Async. +func Async1[A any](f func() A) chan A { + return Async(f) +} + +// Async2 has the same behavior as Async, but returns the 2 results as a tuple inside the channel. +func Async2[A any, B any](f func() (A, B)) chan Tuple2[A, B] { + ch := make(chan Tuple2[A, B]) + go func() { + ch <- T2(f()) + }() + return ch +} + +// Async3 has the same behavior as Async, but returns the 3 results as a tuple inside the channel. +func Async3[A any, B any, C any](f func() (A, B, C)) chan Tuple3[A, B, C] { + ch := make(chan Tuple3[A, B, C]) + go func() { + ch <- T3(f()) + }() + return ch +} + +// Async4 has the same behavior as Async, but returns the 4 results as a tuple inside the channel. +func Async4[A any, B any, C any, D any](f func() (A, B, C, D)) chan Tuple4[A, B, C, D] { + ch := make(chan Tuple4[A, B, C, D]) + go func() { + ch <- T4(f()) + }() + return ch +} + +// Async5 has the same behavior as Async, but returns the 5 results as a tuple inside the channel. +func Async5[A any, B any, C any, D any, E any](f func() (A, B, C, D, E)) chan Tuple5[A, B, C, D, E] { + ch := make(chan Tuple5[A, B, C, D, E]) + go func() { + ch <- T5(f()) + }() + return ch +} + +// Async6 has the same behavior as Async, but returns the 6 results as a tuple inside the channel. +func Async6[A any, B any, C any, D any, E any, F any](f func() (A, B, C, D, E, F)) chan Tuple6[A, B, C, D, E, F] { + ch := make(chan Tuple6[A, B, C, D, E, F]) + go func() { + ch <- T6(f()) + }() + return ch +} diff --git a/vendor/github.com/samber/lo/condition.go b/vendor/github.com/samber/lo/condition.go new file mode 100644 index 00000000000..1d4e75d25ed --- /dev/null +++ b/vendor/github.com/samber/lo/condition.go @@ -0,0 +1,150 @@ +package lo + +// Ternary is a 1 line if/else statement. +// Play: https://go.dev/play/p/t-D7WBL44h2 +func Ternary[T any](condition bool, ifOutput T, elseOutput T) T { + if condition { + return ifOutput + } + + return elseOutput +} + +// TernaryF is a 1 line if/else statement whose options are functions +// Play: https://go.dev/play/p/AO4VW20JoqM +func TernaryF[T any](condition bool, ifFunc func() T, elseFunc func() T) T { + if condition { + return ifFunc() + } + + return elseFunc() +} + +type ifElse[T any] struct { + result T + done bool +} + +// If. +// Play: https://go.dev/play/p/WSw3ApMxhyW +func If[T any](condition bool, result T) *ifElse[T] { + if condition { + return &ifElse[T]{result, true} + } + + var t T + return &ifElse[T]{t, false} +} + +// IfF. +// Play: https://go.dev/play/p/WSw3ApMxhyW +func IfF[T any](condition bool, resultF func() T) *ifElse[T] { + if condition { + return &ifElse[T]{resultF(), true} + } + + var t T + return &ifElse[T]{t, false} +} + +// ElseIf. +// Play: https://go.dev/play/p/WSw3ApMxhyW +func (i *ifElse[T]) ElseIf(condition bool, result T) *ifElse[T] { + if !i.done && condition { + i.result = result + i.done = true + } + + return i +} + +// ElseIfF. +// Play: https://go.dev/play/p/WSw3ApMxhyW +func (i *ifElse[T]) ElseIfF(condition bool, resultF func() T) *ifElse[T] { + if !i.done && condition { + i.result = resultF() + i.done = true + } + + return i +} + +// Else. +// Play: https://go.dev/play/p/WSw3ApMxhyW +func (i *ifElse[T]) Else(result T) T { + if i.done { + return i.result + } + + return result +} + +// ElseF. +// Play: https://go.dev/play/p/WSw3ApMxhyW +func (i *ifElse[T]) ElseF(resultF func() T) T { + if i.done { + return i.result + } + + return resultF() +} + +type switchCase[T comparable, R any] struct { + predicate T + result R + done bool +} + +// Switch is a pure functional switch/case/default statement. +// Play: https://go.dev/play/p/TGbKUMAeRUd +func Switch[T comparable, R any](predicate T) *switchCase[T, R] { + var result R + + return &switchCase[T, R]{ + predicate, + result, + false, + } +} + +// Case. +// Play: https://go.dev/play/p/TGbKUMAeRUd +func (s *switchCase[T, R]) Case(val T, result R) *switchCase[T, R] { + if !s.done && s.predicate == val { + s.result = result + s.done = true + } + + return s +} + +// CaseF. +// Play: https://go.dev/play/p/TGbKUMAeRUd +func (s *switchCase[T, R]) CaseF(val T, cb func() R) *switchCase[T, R] { + if !s.done && s.predicate == val { + s.result = cb() + s.done = true + } + + return s +} + +// Default. +// Play: https://go.dev/play/p/TGbKUMAeRUd +func (s *switchCase[T, R]) Default(result R) R { + if !s.done { + s.result = result + } + + return s.result +} + +// DefaultF. +// Play: https://go.dev/play/p/TGbKUMAeRUd +func (s *switchCase[T, R]) DefaultF(cb func() R) R { + if !s.done { + s.result = cb() + } + + return s.result +} diff --git a/vendor/github.com/samber/lo/constraints.go b/vendor/github.com/samber/lo/constraints.go new file mode 100644 index 00000000000..c1f3529685e --- /dev/null +++ b/vendor/github.com/samber/lo/constraints.go @@ -0,0 +1,6 @@ +package lo + +// Clonable defines a constraint of types having Clone() T method. +type Clonable[T any] interface { + Clone() T +} diff --git a/vendor/github.com/samber/lo/errors.go b/vendor/github.com/samber/lo/errors.go new file mode 100644 index 00000000000..a99013d950b --- /dev/null +++ b/vendor/github.com/samber/lo/errors.go @@ -0,0 +1,354 @@ +package lo + +import ( + "errors" + "fmt" + "reflect" +) + +// Validate is a helper that creates an error when a condition is not met. +// Play: https://go.dev/play/p/vPyh51XpCBt +func Validate(ok bool, format string, args ...any) error { + if !ok { + return fmt.Errorf(fmt.Sprintf(format, args...)) + } + return nil +} + +func messageFromMsgAndArgs(msgAndArgs ...interface{}) string { + if len(msgAndArgs) == 1 { + if msgAsStr, ok := msgAndArgs[0].(string); ok { + return msgAsStr + } + return fmt.Sprintf("%+v", msgAndArgs[0]) + } + if len(msgAndArgs) > 1 { + return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) + } + return "" +} + +// must panics if err is error or false. +func must(err any, messageArgs ...interface{}) { + if err == nil { + return + } + + switch e := err.(type) { + case bool: + if !e { + message := messageFromMsgAndArgs(messageArgs...) + if message == "" { + message = "not ok" + } + + panic(message) + } + + case error: + message := messageFromMsgAndArgs(messageArgs...) + if message != "" { + panic(message + ": " + e.Error()) + } else { + panic(e.Error()) + } + + default: + panic("must: invalid err type '" + reflect.TypeOf(err).Name() + "', should either be a bool or an error") + } +} + +// Must is a helper that wraps a call to a function returning a value and an error +// and panics if err is error or false. +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must[T any](val T, err any, messageArgs ...interface{}) T { + must(err, messageArgs...) + return val +} + +// Must0 has the same behavior as Must, but callback returns no variable. +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must0(err any, messageArgs ...interface{}) { + must(err, messageArgs...) +} + +// Must1 is an alias to Must +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must1[T any](val T, err any, messageArgs ...interface{}) T { + return Must(val, err, messageArgs...) +} + +// Must2 has the same behavior as Must, but callback returns 2 variables. +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must2[T1 any, T2 any](val1 T1, val2 T2, err any, messageArgs ...interface{}) (T1, T2) { + must(err, messageArgs...) + return val1, val2 +} + +// Must3 has the same behavior as Must, but callback returns 3 variables. +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must3[T1 any, T2 any, T3 any](val1 T1, val2 T2, val3 T3, err any, messageArgs ...interface{}) (T1, T2, T3) { + must(err, messageArgs...) + return val1, val2, val3 +} + +// Must4 has the same behavior as Must, but callback returns 4 variables. +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must4[T1 any, T2 any, T3 any, T4 any](val1 T1, val2 T2, val3 T3, val4 T4, err any, messageArgs ...interface{}) (T1, T2, T3, T4) { + must(err, messageArgs...) + return val1, val2, val3, val4 +} + +// Must5 has the same behavior as Must, but callback returns 5 variables. +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must5[T1 any, T2 any, T3 any, T4 any, T5 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, err any, messageArgs ...interface{}) (T1, T2, T3, T4, T5) { + must(err, messageArgs...) + return val1, val2, val3, val4, val5 +} + +// Must6 has the same behavior as Must, but callback returns 6 variables. +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must6[T1 any, T2 any, T3 any, T4 any, T5 any, T6 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, val6 T6, err any, messageArgs ...interface{}) (T1, T2, T3, T4, T5, T6) { + must(err, messageArgs...) + return val1, val2, val3, val4, val5, val6 +} + +// Try calls the function and return false in case of error. +func Try(callback func() error) (ok bool) { + ok = true + + defer func() { + if r := recover(); r != nil { + ok = false + } + }() + + err := callback() + if err != nil { + ok = false + } + + return +} + +// Try0 has the same behavior as Try, but callback returns no variable. +// Play: https://go.dev/play/p/mTyyWUvn9u4 +func Try0(callback func()) bool { + return Try(func() error { + callback() + return nil + }) +} + +// Try1 is an alias to Try. +// Play: https://go.dev/play/p/mTyyWUvn9u4 +func Try1(callback func() error) bool { + return Try(callback) +} + +// Try2 has the same behavior as Try, but callback returns 2 variables. +// Play: https://go.dev/play/p/mTyyWUvn9u4 +func Try2[T any](callback func() (T, error)) bool { + return Try(func() error { + _, err := callback() + return err + }) +} + +// Try3 has the same behavior as Try, but callback returns 3 variables. +// Play: https://go.dev/play/p/mTyyWUvn9u4 +func Try3[T, R any](callback func() (T, R, error)) bool { + return Try(func() error { + _, _, err := callback() + return err + }) +} + +// Try4 has the same behavior as Try, but callback returns 4 variables. +// Play: https://go.dev/play/p/mTyyWUvn9u4 +func Try4[T, R, S any](callback func() (T, R, S, error)) bool { + return Try(func() error { + _, _, _, err := callback() + return err + }) +} + +// Try5 has the same behavior as Try, but callback returns 5 variables. +// Play: https://go.dev/play/p/mTyyWUvn9u4 +func Try5[T, R, S, Q any](callback func() (T, R, S, Q, error)) bool { + return Try(func() error { + _, _, _, _, err := callback() + return err + }) +} + +// Try6 has the same behavior as Try, but callback returns 6 variables. +// Play: https://go.dev/play/p/mTyyWUvn9u4 +func Try6[T, R, S, Q, U any](callback func() (T, R, S, Q, U, error)) bool { + return Try(func() error { + _, _, _, _, _, err := callback() + return err + }) +} + +// TryOr has the same behavior as Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr[A any](callback func() (A, error), fallbackA A) (A, bool) { + return TryOr1(callback, fallbackA) +} + +// TryOr1 has the same behavior as Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr1[A any](callback func() (A, error), fallbackA A) (A, bool) { + ok := false + + Try0(func() { + a, err := callback() + if err == nil { + fallbackA = a + ok = true + } + }) + + return fallbackA, ok +} + +// TryOr2 has the same behavior as Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr2[A any, B any](callback func() (A, B, error), fallbackA A, fallbackB B) (A, B, bool) { + ok := false + + Try0(func() { + a, b, err := callback() + if err == nil { + fallbackA = a + fallbackB = b + ok = true + } + }) + + return fallbackA, fallbackB, ok +} + +// TryOr3 has the same behavior as Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr3[A any, B any, C any](callback func() (A, B, C, error), fallbackA A, fallbackB B, fallbackC C) (A, B, C, bool) { + ok := false + + Try0(func() { + a, b, c, err := callback() + if err == nil { + fallbackA = a + fallbackB = b + fallbackC = c + ok = true + } + }) + + return fallbackA, fallbackB, fallbackC, ok +} + +// TryOr4 has the same behavior as Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr4[A any, B any, C any, D any](callback func() (A, B, C, D, error), fallbackA A, fallbackB B, fallbackC C, fallbackD D) (A, B, C, D, bool) { + ok := false + + Try0(func() { + a, b, c, d, err := callback() + if err == nil { + fallbackA = a + fallbackB = b + fallbackC = c + fallbackD = d + ok = true + } + }) + + return fallbackA, fallbackB, fallbackC, fallbackD, ok +} + +// TryOr5 has the same behavior as Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr5[A any, B any, C any, D any, E any](callback func() (A, B, C, D, E, error), fallbackA A, fallbackB B, fallbackC C, fallbackD D, fallbackE E) (A, B, C, D, E, bool) { + ok := false + + Try0(func() { + a, b, c, d, e, err := callback() + if err == nil { + fallbackA = a + fallbackB = b + fallbackC = c + fallbackD = d + fallbackE = e + ok = true + } + }) + + return fallbackA, fallbackB, fallbackC, fallbackD, fallbackE, ok +} + +// TryOr6 has the same behavior as Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr6[A any, B any, C any, D any, E any, F any](callback func() (A, B, C, D, E, F, error), fallbackA A, fallbackB B, fallbackC C, fallbackD D, fallbackE E, fallbackF F) (A, B, C, D, E, F, bool) { + ok := false + + Try0(func() { + a, b, c, d, e, f, err := callback() + if err == nil { + fallbackA = a + fallbackB = b + fallbackC = c + fallbackD = d + fallbackE = e + fallbackF = f + ok = true + } + }) + + return fallbackA, fallbackB, fallbackC, fallbackD, fallbackE, fallbackF, ok +} + +// TryWithErrorValue has the same behavior as Try, but also returns value passed to panic. +// Play: https://go.dev/play/p/Kc7afQIT2Fs +func TryWithErrorValue(callback func() error) (errorValue any, ok bool) { + ok = true + + defer func() { + if r := recover(); r != nil { + ok = false + errorValue = r + } + }() + + err := callback() + if err != nil { + ok = false + errorValue = err + } + + return +} + +// TryCatch has the same behavior as Try, but calls the catch function in case of error. +// Play: https://go.dev/play/p/PnOON-EqBiU +func TryCatch(callback func() error, catch func()) { + if !Try(callback) { + catch() + } +} + +// TryCatchWithErrorValue has the same behavior as TryWithErrorValue, but calls the catch function in case of error. +// Play: https://go.dev/play/p/8Pc9gwX_GZO +func TryCatchWithErrorValue(callback func() error, catch func(any)) { + if err, ok := TryWithErrorValue(callback); !ok { + catch(err) + } +} + +// ErrorsAs is a shortcut for errors.As(err, &&T). +// Play: https://go.dev/play/p/8wk5rH8UfrE +func ErrorsAs[T error](err error) (T, bool) { + var t T + ok := errors.As(err, &t) + return t, ok +} diff --git a/vendor/github.com/samber/lo/find.go b/vendor/github.com/samber/lo/find.go new file mode 100644 index 00000000000..f8caeb89598 --- /dev/null +++ b/vendor/github.com/samber/lo/find.go @@ -0,0 +1,372 @@ +package lo + +import ( + "fmt" + "math/rand" + + "golang.org/x/exp/constraints" +) + +// import "golang.org/x/exp/constraints" + +// IndexOf returns the index at which the first occurrence of a value is found in an array or return -1 +// if the value cannot be found. +func IndexOf[T comparable](collection []T, element T) int { + for i, item := range collection { + if item == element { + return i + } + } + + return -1 +} + +// LastIndexOf returns the index at which the last occurrence of a value is found in an array or return -1 +// if the value cannot be found. +func LastIndexOf[T comparable](collection []T, element T) int { + length := len(collection) + + for i := length - 1; i >= 0; i-- { + if collection[i] == element { + return i + } + } + + return -1 +} + +// Find search an element in a slice based on a predicate. It returns element and true if element was found. +func Find[T any](collection []T, predicate func(item T) bool) (T, bool) { + for _, item := range collection { + if predicate(item) { + return item, true + } + } + + var result T + return result, false +} + +// FindIndexOf searches an element in a slice based on a predicate and returns the index and true. +// It returns -1 and false if the element is not found. +func FindIndexOf[T any](collection []T, predicate func(item T) bool) (T, int, bool) { + for i, item := range collection { + if predicate(item) { + return item, i, true + } + } + + var result T + return result, -1, false +} + +// FindLastIndexOf searches last element in a slice based on a predicate and returns the index and true. +// It returns -1 and false if the element is not found. +func FindLastIndexOf[T any](collection []T, predicate func(item T) bool) (T, int, bool) { + length := len(collection) + + for i := length - 1; i >= 0; i-- { + if predicate(collection[i]) { + return collection[i], i, true + } + } + + var result T + return result, -1, false +} + +// FindOrElse search an element in a slice based on a predicate. It returns the element if found or a given fallback value otherwise. +func FindOrElse[T any](collection []T, fallback T, predicate func(item T) bool) T { + for _, item := range collection { + if predicate(item) { + return item + } + } + + return fallback +} + +// FindKey returns the key of the first value matching. +func FindKey[K comparable, V comparable](object map[K]V, value V) (K, bool) { + for k, v := range object { + if v == value { + return k, true + } + } + + return Empty[K](), false +} + +// FindKeyBy returns the key of the first element predicate returns truthy for. +func FindKeyBy[K comparable, V any](object map[K]V, predicate func(key K, value V) bool) (K, bool) { + for k, v := range object { + if predicate(k, v) { + return k, true + } + } + + return Empty[K](), false +} + +// FindUniques returns a slice with all the unique elements of the collection. +// The order of result values is determined by the order they occur in the collection. +func FindUniques[T comparable](collection []T) []T { + isDupl := make(map[T]bool, len(collection)) + + for _, item := range collection { + duplicated, ok := isDupl[item] + if !ok { + isDupl[item] = false + } else if !duplicated { + isDupl[item] = true + } + } + + result := make([]T, 0, len(collection)-len(isDupl)) + + for _, item := range collection { + if duplicated := isDupl[item]; !duplicated { + result = append(result, item) + } + } + + return result +} + +// FindUniquesBy returns a slice with all the unique elements of the collection. +// The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is +// invoked for each element in array to generate the criterion by which uniqueness is computed. +func FindUniquesBy[T any, U comparable](collection []T, iteratee func(item T) U) []T { + isDupl := make(map[U]bool, len(collection)) + + for _, item := range collection { + key := iteratee(item) + + duplicated, ok := isDupl[key] + if !ok { + isDupl[key] = false + } else if !duplicated { + isDupl[key] = true + } + } + + result := make([]T, 0, len(collection)-len(isDupl)) + + for _, item := range collection { + key := iteratee(item) + + if duplicated := isDupl[key]; !duplicated { + result = append(result, item) + } + } + + return result +} + +// FindDuplicates returns a slice with the first occurrence of each duplicated elements of the collection. +// The order of result values is determined by the order they occur in the collection. +func FindDuplicates[T comparable](collection []T) []T { + isDupl := make(map[T]bool, len(collection)) + + for _, item := range collection { + duplicated, ok := isDupl[item] + if !ok { + isDupl[item] = false + } else if !duplicated { + isDupl[item] = true + } + } + + result := make([]T, 0, len(collection)-len(isDupl)) + + for _, item := range collection { + if duplicated := isDupl[item]; duplicated { + result = append(result, item) + isDupl[item] = false + } + } + + return result +} + +// FindDuplicatesBy returns a slice with the first occurrence of each duplicated elements of the collection. +// The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is +// invoked for each element in array to generate the criterion by which uniqueness is computed. +func FindDuplicatesBy[T any, U comparable](collection []T, iteratee func(item T) U) []T { + isDupl := make(map[U]bool, len(collection)) + + for _, item := range collection { + key := iteratee(item) + + duplicated, ok := isDupl[key] + if !ok { + isDupl[key] = false + } else if !duplicated { + isDupl[key] = true + } + } + + result := make([]T, 0, len(collection)-len(isDupl)) + + for _, item := range collection { + key := iteratee(item) + + if duplicated := isDupl[key]; duplicated { + result = append(result, item) + isDupl[key] = false + } + } + + return result +} + +// Min search the minimum value of a collection. +// Returns zero value when collection is empty. +func Min[T constraints.Ordered](collection []T) T { + var min T + + if len(collection) == 0 { + return min + } + + min = collection[0] + + for i := 1; i < len(collection); i++ { + item := collection[i] + + if item < min { + min = item + } + } + + return min +} + +// MinBy search the minimum value of a collection using the given comparison function. +// If several values of the collection are equal to the smallest value, returns the first such value. +// Returns zero value when collection is empty. +func MinBy[T any](collection []T, comparison func(a T, b T) bool) T { + var min T + + if len(collection) == 0 { + return min + } + + min = collection[0] + + for i := 1; i < len(collection); i++ { + item := collection[i] + + if comparison(item, min) { + min = item + } + } + + return min +} + +// Max searches the maximum value of a collection. +// Returns zero value when collection is empty. +func Max[T constraints.Ordered](collection []T) T { + var max T + + if len(collection) == 0 { + return max + } + + max = collection[0] + + for i := 1; i < len(collection); i++ { + item := collection[i] + + if item > max { + max = item + } + } + + return max +} + +// MaxBy search the maximum value of a collection using the given comparison function. +// If several values of the collection are equal to the greatest value, returns the first such value. +// Returns zero value when collection is empty. +func MaxBy[T any](collection []T, comparison func(a T, b T) bool) T { + var max T + + if len(collection) == 0 { + return max + } + + max = collection[0] + + for i := 1; i < len(collection); i++ { + item := collection[i] + + if comparison(item, max) { + max = item + } + } + + return max +} + +// Last returns the last element of a collection or error if empty. +func Last[T any](collection []T) (T, error) { + length := len(collection) + + if length == 0 { + var t T + return t, fmt.Errorf("last: cannot extract the last element of an empty slice") + } + + return collection[length-1], nil +} + +// Nth returns the element at index `nth` of collection. If `nth` is negative, the nth element +// from the end is returned. An error is returned when nth is out of slice bounds. +func Nth[T any, N constraints.Integer](collection []T, nth N) (T, error) { + n := int(nth) + l := len(collection) + if n >= l || -n > l { + var t T + return t, fmt.Errorf("nth: %d out of slice bounds", n) + } + + if n >= 0 { + return collection[n], nil + } + return collection[l+n], nil +} + +// Sample returns a random item from collection. +func Sample[T any](collection []T) T { + size := len(collection) + if size == 0 { + return Empty[T]() + } + + return collection[rand.Intn(size)] +} + +// Samples returns N random unique items from collection. +func Samples[T any](collection []T, count int) []T { + size := len(collection) + + copy := append([]T{}, collection...) + + results := []T{} + + for i := 0; i < size && i < count; i++ { + copyLength := size - i + + index := rand.Intn(size - i) + results = append(results, copy[index]) + + // Removes element. + // It is faster to swap with last element and remove it. + copy[index] = copy[copyLength-1] + copy = copy[:copyLength-1] + } + + return results +} diff --git a/vendor/github.com/samber/lo/func.go b/vendor/github.com/samber/lo/func.go new file mode 100644 index 00000000000..5fa1cbc8718 --- /dev/null +++ b/vendor/github.com/samber/lo/func.go @@ -0,0 +1,41 @@ +package lo + +// Partial returns new function that, when called, has its first argument set to the provided value. +func Partial[T1, T2, R any](f func(a T1, b T2) R, arg1 T1) func(T2) R { + return func(t2 T2) R { + return f(arg1, t2) + } +} + +// Partial1 returns new function that, when called, has its first argument set to the provided value. +func Partial1[T1, T2, R any](f func(T1, T2) R, arg1 T1) func(T2) R { + return Partial(f, arg1) +} + +// Partial2 returns new function that, when called, has its first argument set to the provided value. +func Partial2[T1, T2, T3, R any](f func(T1, T2, T3) R, arg1 T1) func(T2, T3) R { + return func(t2 T2, t3 T3) R { + return f(arg1, t2, t3) + } +} + +// Partial3 returns new function that, when called, has its first argument set to the provided value. +func Partial3[T1, T2, T3, T4, R any](f func(T1, T2, T3, T4) R, arg1 T1) func(T2, T3, T4) R { + return func(t2 T2, t3 T3, t4 T4) R { + return f(arg1, t2, t3, t4) + } +} + +// Partial4 returns new function that, when called, has its first argument set to the provided value. +func Partial4[T1, T2, T3, T4, T5, R any](f func(T1, T2, T3, T4, T5) R, arg1 T1) func(T2, T3, T4, T5) R { + return func(t2 T2, t3 T3, t4 T4, t5 T5) R { + return f(arg1, t2, t3, t4, t5) + } +} + +// Partial5 returns new function that, when called, has its first argument set to the provided value +func Partial5[T1, T2, T3, T4, T5, T6, R any](f func(T1, T2, T3, T4, T5, T6) R, arg1 T1) func(T2, T3, T4, T5, T6) R { + return func(t2 T2, t3 T3, t4 T4, t5 T5, t6 T6) R { + return f(arg1, t2, t3, t4, t5, t6) + } +} diff --git a/vendor/github.com/samber/lo/intersect.go b/vendor/github.com/samber/lo/intersect.go new file mode 100644 index 00000000000..cf6cab3d138 --- /dev/null +++ b/vendor/github.com/samber/lo/intersect.go @@ -0,0 +1,185 @@ +package lo + +// Contains returns true if an element is present in a collection. +func Contains[T comparable](collection []T, element T) bool { + for _, item := range collection { + if item == element { + return true + } + } + + return false +} + +// ContainsBy returns true if predicate function return true. +func ContainsBy[T any](collection []T, predicate func(item T) bool) bool { + for _, item := range collection { + if predicate(item) { + return true + } + } + + return false +} + +// Every returns true if all elements of a subset are contained into a collection or if the subset is empty. +func Every[T comparable](collection []T, subset []T) bool { + for _, elem := range subset { + if !Contains(collection, elem) { + return false + } + } + + return true +} + +// EveryBy returns true if the predicate returns true for all of the elements in the collection or if the collection is empty. +func EveryBy[T any](collection []T, predicate func(item T) bool) bool { + for _, v := range collection { + if !predicate(v) { + return false + } + } + + return true +} + +// Some returns true if at least 1 element of a subset is contained into a collection. +// If the subset is empty Some returns false. +func Some[T comparable](collection []T, subset []T) bool { + for _, elem := range subset { + if Contains(collection, elem) { + return true + } + } + + return false +} + +// SomeBy returns true if the predicate returns true for any of the elements in the collection. +// If the collection is empty SomeBy returns false. +func SomeBy[T any](collection []T, predicate func(item T) bool) bool { + for _, v := range collection { + if predicate(v) { + return true + } + } + + return false +} + +// None returns true if no element of a subset are contained into a collection or if the subset is empty. +func None[T comparable](collection []T, subset []T) bool { + for _, elem := range subset { + if Contains(collection, elem) { + return false + } + } + + return true +} + +// NoneBy returns true if the predicate returns true for none of the elements in the collection or if the collection is empty. +func NoneBy[T any](collection []T, predicate func(item T) bool) bool { + for _, v := range collection { + if predicate(v) { + return false + } + } + + return true +} + +// Intersect returns the intersection between two collections. +func Intersect[T comparable](list1 []T, list2 []T) []T { + result := []T{} + seen := map[T]struct{}{} + + for _, elem := range list1 { + seen[elem] = struct{}{} + } + + for _, elem := range list2 { + if _, ok := seen[elem]; ok { + result = append(result, elem) + } + } + + return result +} + +// Difference returns the difference between two collections. +// The first value is the collection of element absent of list2. +// The second value is the collection of element absent of list1. +func Difference[T comparable](list1 []T, list2 []T) ([]T, []T) { + left := []T{} + right := []T{} + + seenLeft := map[T]struct{}{} + seenRight := map[T]struct{}{} + + for _, elem := range list1 { + seenLeft[elem] = struct{}{} + } + + for _, elem := range list2 { + seenRight[elem] = struct{}{} + } + + for _, elem := range list1 { + if _, ok := seenRight[elem]; !ok { + left = append(left, elem) + } + } + + for _, elem := range list2 { + if _, ok := seenLeft[elem]; !ok { + right = append(right, elem) + } + } + + return left, right +} + +// Union returns all distinct elements from given collections. +// result returns will not change the order of elements relatively. +func Union[T comparable](lists ...[]T) []T { + result := []T{} + seen := map[T]struct{}{} + + for _, list := range lists { + for _, e := range list { + if _, ok := seen[e]; !ok { + seen[e] = struct{}{} + result = append(result, e) + } + } + } + + return result +} + +// Without returns slice excluding all given values. +func Without[T comparable](collection []T, exclude ...T) []T { + result := make([]T, 0, len(collection)) + for _, e := range collection { + if !Contains(exclude, e) { + result = append(result, e) + } + } + return result +} + +// WithoutEmpty returns slice excluding empty values. +func WithoutEmpty[T comparable](collection []T) []T { + var empty T + + result := make([]T, 0, len(collection)) + for _, e := range collection { + if e != empty { + result = append(result, e) + } + } + + return result +} diff --git a/vendor/github.com/samber/lo/map.go b/vendor/github.com/samber/lo/map.go new file mode 100644 index 00000000000..9ed68b4202e --- /dev/null +++ b/vendor/github.com/samber/lo/map.go @@ -0,0 +1,215 @@ +package lo + +// Keys creates an array of the map keys. +// Play: https://go.dev/play/p/Uu11fHASqrU +func Keys[K comparable, V any](in map[K]V) []K { + result := make([]K, 0, len(in)) + + for k := range in { + result = append(result, k) + } + + return result +} + +// Values creates an array of the map values. +// Play: https://go.dev/play/p/nnRTQkzQfF6 +func Values[K comparable, V any](in map[K]V) []V { + result := make([]V, 0, len(in)) + + for _, v := range in { + result = append(result, v) + } + + return result +} + +// PickBy returns same map type filtered by given predicate. +// Play: https://go.dev/play/p/kdg8GR_QMmf +func PickBy[K comparable, V any](in map[K]V, predicate func(key K, value V) bool) map[K]V { + r := map[K]V{} + for k, v := range in { + if predicate(k, v) { + r[k] = v + } + } + return r +} + +// PickByKeys returns same map type filtered by given keys. +// Play: https://go.dev/play/p/R1imbuci9qU +func PickByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V { + r := map[K]V{} + for k, v := range in { + if Contains(keys, k) { + r[k] = v + } + } + return r +} + +// PickByValues returns same map type filtered by given values. +// Play: https://go.dev/play/p/1zdzSvbfsJc +func PickByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V { + r := map[K]V{} + for k, v := range in { + if Contains(values, v) { + r[k] = v + } + } + return r +} + +// OmitBy returns same map type filtered by given predicate. +// Play: https://go.dev/play/p/EtBsR43bdsd +func OmitBy[K comparable, V any](in map[K]V, predicate func(key K, value V) bool) map[K]V { + r := map[K]V{} + for k, v := range in { + if !predicate(k, v) { + r[k] = v + } + } + return r +} + +// OmitByKeys returns same map type filtered by given keys. +// Play: https://go.dev/play/p/t1QjCrs-ysk +func OmitByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V { + r := map[K]V{} + for k, v := range in { + if !Contains(keys, k) { + r[k] = v + } + } + return r +} + +// OmitByValues returns same map type filtered by given values. +// Play: https://go.dev/play/p/9UYZi-hrs8j +func OmitByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V { + r := map[K]V{} + for k, v := range in { + if !Contains(values, v) { + r[k] = v + } + } + return r +} + +// Entries transforms a map into array of key/value pairs. +// Play: +func Entries[K comparable, V any](in map[K]V) []Entry[K, V] { + entries := make([]Entry[K, V], 0, len(in)) + + for k, v := range in { + entries = append(entries, Entry[K, V]{ + Key: k, + Value: v, + }) + } + + return entries +} + +// ToPairs transforms a map into array of key/value pairs. +// Alias of Entries(). +// Play: https://go.dev/play/p/3Dhgx46gawJ +func ToPairs[K comparable, V any](in map[K]V) []Entry[K, V] { + return Entries(in) +} + +// FromEntries transforms an array of key/value pairs into a map. +// Play: https://go.dev/play/p/oIr5KHFGCEN +func FromEntries[K comparable, V any](entries []Entry[K, V]) map[K]V { + out := map[K]V{} + + for _, v := range entries { + out[v.Key] = v.Value + } + + return out +} + +// FromPairs transforms an array of key/value pairs into a map. +// Alias of FromEntries(). +// Play: https://go.dev/play/p/oIr5KHFGCEN +func FromPairs[K comparable, V any](entries []Entry[K, V]) map[K]V { + return FromEntries(entries) +} + +// Invert creates a map composed of the inverted keys and values. If map +// contains duplicate values, subsequent values overwrite property assignments +// of previous values. +// Play: https://go.dev/play/p/rFQ4rak6iA1 +func Invert[K comparable, V comparable](in map[K]V) map[V]K { + out := map[V]K{} + + for k, v := range in { + out[v] = k + } + + return out +} + +// Assign merges multiple maps from left to right. +// Play: https://go.dev/play/p/VhwfJOyxf5o +func Assign[K comparable, V any](maps ...map[K]V) map[K]V { + out := map[K]V{} + + for _, m := range maps { + for k, v := range m { + out[k] = v + } + } + + return out +} + +// MapKeys manipulates a map keys and transforms it to a map of another type. +// Play: https://go.dev/play/p/9_4WPIqOetJ +func MapKeys[K comparable, V any, R comparable](in map[K]V, iteratee func(value V, key K) R) map[R]V { + result := map[R]V{} + + for k, v := range in { + result[iteratee(v, k)] = v + } + + return result +} + +// MapValues manipulates a map values and transforms it to a map of another type. +// Play: https://go.dev/play/p/T_8xAfvcf0W +func MapValues[K comparable, V any, R any](in map[K]V, iteratee func(value V, key K) R) map[K]R { + result := map[K]R{} + + for k, v := range in { + result[k] = iteratee(v, k) + } + + return result +} + +// MapEntries manipulates a map entries and transforms it to a map of another type. +// Play: https://go.dev/play/p/VuvNQzxKimT +func MapEntries[K1 comparable, V1 any, K2 comparable, V2 any](in map[K1]V1, iteratee func(key K1, value V1) (K2, V2)) map[K2]V2 { + result := make(map[K2]V2, len(in)) + + for k1, v1 := range in { + k2, v2 := iteratee(k1, v1) + result[k2] = v2 + } + + return result +} + +// MapToSlice transforms a map into a slice based on specific iteratee +// Play: https://go.dev/play/p/ZuiCZpDt6LD +func MapToSlice[K comparable, V any, R any](in map[K]V, iteratee func(key K, value V) R) []R { + result := make([]R, 0, len(in)) + + for k, v := range in { + result = append(result, iteratee(k, v)) + } + + return result +} diff --git a/vendor/github.com/samber/lo/math.go b/vendor/github.com/samber/lo/math.go new file mode 100644 index 00000000000..9dce28cf831 --- /dev/null +++ b/vendor/github.com/samber/lo/math.go @@ -0,0 +1,84 @@ +package lo + +import "golang.org/x/exp/constraints" + +// Range creates an array of numbers (positive and/or negative) with given length. +// Play: https://go.dev/play/p/0r6VimXAi9H +func Range(elementNum int) []int { + length := If(elementNum < 0, -elementNum).Else(elementNum) + result := make([]int, length) + step := If(elementNum < 0, -1).Else(1) + for i, j := 0, 0; i < length; i, j = i+1, j+step { + result[i] = j + } + return result +} + +// RangeFrom creates an array of numbers from start with specified length. +// Play: https://go.dev/play/p/0r6VimXAi9H +func RangeFrom[T constraints.Integer | constraints.Float](start T, elementNum int) []T { + length := If(elementNum < 0, -elementNum).Else(elementNum) + result := make([]T, length) + step := If(elementNum < 0, -1).Else(1) + for i, j := 0, start; i < length; i, j = i+1, j+T(step) { + result[i] = j + } + return result +} + +// RangeWithSteps creates an array of numbers (positive and/or negative) progressing from start up to, but not including end. +// step set to zero will return empty array. +// Play: https://go.dev/play/p/0r6VimXAi9H +func RangeWithSteps[T constraints.Integer | constraints.Float](start, end, step T) []T { + result := []T{} + if start == end || step == 0 { + return result + } + if start < end { + if step < 0 { + return result + } + for i := start; i < end; i += step { + result = append(result, i) + } + return result + } + if step > 0 { + return result + } + for i := start; i > end; i += step { + result = append(result, i) + } + return result +} + +// Clamp clamps number within the inclusive lower and upper bounds. +// Play: https://go.dev/play/p/RU4lJNC2hlI +func Clamp[T constraints.Ordered](value T, min T, max T) T { + if value < min { + return min + } else if value > max { + return max + } + return value +} + +// Sum sums the values in a collection. If collection is empty 0 is returned. +// Play: https://go.dev/play/p/upfeJVqs4Bt +func Sum[T constraints.Float | constraints.Integer | constraints.Complex](collection []T) T { + var sum T = 0 + for _, val := range collection { + sum += val + } + return sum +} + +// SumBy summarizes the values in a collection using the given return value from the iteration function. If collection is empty 0 is returned. +// Play: https://go.dev/play/p/Dz_a_7jN_ca +func SumBy[T any, R constraints.Float | constraints.Integer | constraints.Complex](collection []T, iteratee func(item T) R) R { + var sum R = 0 + for _, item := range collection { + sum = sum + iteratee(item) + } + return sum +} diff --git a/vendor/github.com/samber/lo/retry.go b/vendor/github.com/samber/lo/retry.go new file mode 100644 index 00000000000..0d46f265d55 --- /dev/null +++ b/vendor/github.com/samber/lo/retry.go @@ -0,0 +1,210 @@ +package lo + +import ( + "sync" + "time" +) + +type debounce struct { + after time.Duration + mu *sync.Mutex + timer *time.Timer + done bool + callbacks []func() +} + +func (d *debounce) reset() *debounce { + d.mu.Lock() + defer d.mu.Unlock() + + if d.done { + return d + } + + if d.timer != nil { + d.timer.Stop() + } + + d.timer = time.AfterFunc(d.after, func() { + for _, f := range d.callbacks { + f() + } + }) + return d +} + +func (d *debounce) cancel() { + d.mu.Lock() + defer d.mu.Unlock() + + if d.timer != nil { + d.timer.Stop() + d.timer = nil + } + + d.done = true +} + +// NewDebounce creates a debounced instance that delays invoking functions given until after wait milliseconds have elapsed. +// Play: https://go.dev/play/p/mz32VMK2nqe +func NewDebounce(duration time.Duration, f ...func()) (func(), func()) { + d := &debounce{ + after: duration, + mu: new(sync.Mutex), + timer: nil, + done: false, + callbacks: f, + } + + return func() { + d.reset() + }, d.cancel +} + +// Attempt invokes a function N times until it returns valid output. Returning either the caught error or nil. When first argument is less than `1`, the function runs until a successful response is returned. +// Play: https://go.dev/play/p/3ggJZ2ZKcMj +func Attempt(maxIteration int, f func(index int) error) (int, error) { + var err error + + for i := 0; maxIteration <= 0 || i < maxIteration; i++ { + // for retries >= 0 { + err = f(i) + if err == nil { + return i + 1, nil + } + } + + return maxIteration, err +} + +// AttemptWithDelay invokes a function N times until it returns valid output, +// with a pause between each call. Returning either the caught error or nil. +// When first argument is less than `1`, the function runs until a successful +// response is returned. +// Play: https://go.dev/play/p/tVs6CygC7m1 +func AttemptWithDelay(maxIteration int, delay time.Duration, f func(index int, duration time.Duration) error) (int, time.Duration, error) { + var err error + + start := time.Now() + + for i := 0; maxIteration <= 0 || i < maxIteration; i++ { + err = f(i, time.Since(start)) + if err == nil { + return i + 1, time.Since(start), nil + } + + if maxIteration <= 0 || i+1 < maxIteration { + time.Sleep(delay) + } + } + + return maxIteration, time.Since(start), err +} + +// AttemptWhile invokes a function N times until it returns valid output. +// Returning either the caught error or nil, and along with a bool value to identify +// whether it needs invoke function continuously. It will terminate the invoke +// immediately if second bool value is returned with falsy value. When first +// argument is less than `1`, the function runs until a successful response is +// returned. +func AttemptWhile(maxIteration int, f func(int) (error, bool)) (int, error) { + var err error + var shouldContinueInvoke bool + + for i := 0; maxIteration <= 0 || i < maxIteration; i++ { + // for retries >= 0 { + err, shouldContinueInvoke = f(i) + if !shouldContinueInvoke { // if shouldContinueInvoke is false, then return immediately + return i + 1, err + } + if err == nil { + return i + 1, nil + } + } + + return maxIteration, err +} + +// AttemptWhileWithDelay invokes a function N times until it returns valid output, +// with a pause between each call. Returning either the caught error or nil, and along +// with a bool value to identify whether it needs to invoke function continuously. +// It will terminate the invoke immediately if second bool value is returned with falsy +// value. When first argument is less than `1`, the function runs until a successful +// response is returned. +func AttemptWhileWithDelay(maxIteration int, delay time.Duration, f func(int, time.Duration) (error, bool)) (int, time.Duration, error) { + var err error + var shouldContinueInvoke bool + + start := time.Now() + + for i := 0; maxIteration <= 0 || i < maxIteration; i++ { + err, shouldContinueInvoke = f(i, time.Since(start)) + if !shouldContinueInvoke { // if shouldContinueInvoke is false, then return immediately + return i + 1, time.Since(start), err + } + if err == nil { + return i + 1, time.Since(start), nil + } + + if maxIteration <= 0 || i+1 < maxIteration { + time.Sleep(delay) + } + } + + return maxIteration, time.Since(start), err +} + +type transactionStep[T any] struct { + exec func(T) (T, error) + onRollback func(T) T +} + +// NewTransaction instanciate a new transaction. +func NewTransaction[T any]() *Transaction[T] { + return &Transaction[T]{ + steps: []transactionStep[T]{}, + } +} + +// Transaction implements a Saga pattern +type Transaction[T any] struct { + steps []transactionStep[T] +} + +// Then adds a step to the chain of callbacks. It returns the same Transaction. +func (t *Transaction[T]) Then(exec func(T) (T, error), onRollback func(T) T) *Transaction[T] { + t.steps = append(t.steps, transactionStep[T]{ + exec: exec, + onRollback: onRollback, + }) + + return t +} + +// Process runs the Transaction steps and rollbacks in case of errors. +func (t *Transaction[T]) Process(state T) (T, error) { + var i int + var err error + + for i < len(t.steps) { + state, err = t.steps[i].exec(state) + if err != nil { + break + } + + i++ + } + + if err == nil { + return state, nil + } + + for i > 0 { + i-- + state = t.steps[i].onRollback(state) + } + + return state, err +} + +// throttle ? diff --git a/vendor/github.com/samber/lo/slice.go b/vendor/github.com/samber/lo/slice.go new file mode 100644 index 00000000000..b2e92ad2f66 --- /dev/null +++ b/vendor/github.com/samber/lo/slice.go @@ -0,0 +1,592 @@ +package lo + +import ( + "math/rand" + + "golang.org/x/exp/constraints" +) + +// Filter iterates over elements of collection, returning an array of all elements predicate returns truthy for. +// Play: https://go.dev/play/p/Apjg3WeSi7K +func Filter[V any](collection []V, predicate func(item V, index int) bool) []V { + result := []V{} + + for i, item := range collection { + if predicate(item, i) { + result = append(result, item) + } + } + + return result +} + +// Map manipulates a slice and transforms it to a slice of another type. +// Play: https://go.dev/play/p/OkPcYAhBo0D +func Map[T any, R any](collection []T, iteratee func(item T, index int) R) []R { + result := make([]R, len(collection)) + + for i, item := range collection { + result[i] = iteratee(item, i) + } + + return result +} + +// FilterMap returns a slice which obtained after both filtering and mapping using the given callback function. +// The callback function should return two values: +// - the result of the mapping operation and +// - whether the result element should be included or not. +// +// Play: https://go.dev/play/p/-AuYXfy7opz +func FilterMap[T any, R any](collection []T, callback func(item T, index int) (R, bool)) []R { + result := []R{} + + for i, item := range collection { + if r, ok := callback(item, i); ok { + result = append(result, r) + } + } + + return result +} + +// FlatMap manipulates a slice and transforms and flattens it to a slice of another type. +// Play: https://go.dev/play/p/YSoYmQTA8-U +func FlatMap[T any, R any](collection []T, iteratee func(item T, index int) []R) []R { + result := []R{} + + for i, item := range collection { + result = append(result, iteratee(item, i)...) + } + + return result +} + +// Reduce reduces collection to a value which is the accumulated result of running each element in collection +// through accumulator, where each successive invocation is supplied the return value of the previous. +// Play: https://go.dev/play/p/R4UHXZNaaUG +func Reduce[T any, R any](collection []T, accumulator func(agg R, item T, index int) R, initial R) R { + for i, item := range collection { + initial = accumulator(initial, item, i) + } + + return initial +} + +// ReduceRight helper is like Reduce except that it iterates over elements of collection from right to left. +// Play: https://go.dev/play/p/Fq3W70l7wXF +func ReduceRight[T any, R any](collection []T, accumulator func(agg R, item T, index int) R, initial R) R { + for i := len(collection) - 1; i >= 0; i-- { + initial = accumulator(initial, collection[i], i) + } + + return initial +} + +// ForEach iterates over elements of collection and invokes iteratee for each element. +// Play: https://go.dev/play/p/oofyiUPRf8t +func ForEach[T any](collection []T, iteratee func(item T, index int)) { + for i, item := range collection { + iteratee(item, i) + } +} + +// Times invokes the iteratee n times, returning an array of the results of each invocation. +// The iteratee is invoked with index as argument. +// Play: https://go.dev/play/p/vgQj3Glr6lT +func Times[T any](count int, iteratee func(index int) T) []T { + result := make([]T, count) + + for i := 0; i < count; i++ { + result[i] = iteratee(i) + } + + return result +} + +// Uniq returns a duplicate-free version of an array, in which only the first occurrence of each element is kept. +// The order of result values is determined by the order they occur in the array. +// Play: https://go.dev/play/p/DTzbeXZ6iEN +func Uniq[T comparable](collection []T) []T { + result := make([]T, 0, len(collection)) + seen := make(map[T]struct{}, len(collection)) + + for _, item := range collection { + if _, ok := seen[item]; ok { + continue + } + + seen[item] = struct{}{} + result = append(result, item) + } + + return result +} + +// UniqBy returns a duplicate-free version of an array, in which only the first occurrence of each element is kept. +// The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is +// invoked for each element in array to generate the criterion by which uniqueness is computed. +// Play: https://go.dev/play/p/g42Z3QSb53u +func UniqBy[T any, U comparable](collection []T, iteratee func(item T) U) []T { + result := make([]T, 0, len(collection)) + seen := make(map[U]struct{}, len(collection)) + + for _, item := range collection { + key := iteratee(item) + + if _, ok := seen[key]; ok { + continue + } + + seen[key] = struct{}{} + result = append(result, item) + } + + return result +} + +// GroupBy returns an object composed of keys generated from the results of running each element of collection through iteratee. +// Play: https://go.dev/play/p/XnQBd_v6brd +func GroupBy[T any, U comparable](collection []T, iteratee func(item T) U) map[U][]T { + result := map[U][]T{} + + for _, item := range collection { + key := iteratee(item) + + result[key] = append(result[key], item) + } + + return result +} + +// Chunk returns an array of elements split into groups the length of size. If array can't be split evenly, +// the final chunk will be the remaining elements. +// Play: https://go.dev/play/p/EeKl0AuTehH +func Chunk[T any](collection []T, size int) [][]T { + if size <= 0 { + panic("Second parameter must be greater than 0") + } + + chunksNum := len(collection) / size + if len(collection)%size != 0 { + chunksNum += 1 + } + + result := make([][]T, 0, chunksNum) + + for i := 0; i < chunksNum; i++ { + last := (i + 1) * size + if last > len(collection) { + last = len(collection) + } + result = append(result, collection[i*size:last]) + } + + return result +} + +// PartitionBy returns an array of elements split into groups. The order of grouped values is +// determined by the order they occur in collection. The grouping is generated from the results +// of running each element of collection through iteratee. +// Play: https://go.dev/play/p/NfQ_nGjkgXW +func PartitionBy[T any, K comparable](collection []T, iteratee func(item T) K) [][]T { + result := [][]T{} + seen := map[K]int{} + + for _, item := range collection { + key := iteratee(item) + + resultIndex, ok := seen[key] + if !ok { + resultIndex = len(result) + seen[key] = resultIndex + result = append(result, []T{}) + } + + result[resultIndex] = append(result[resultIndex], item) + } + + return result + + // unordered: + // groups := GroupBy[T, K](collection, iteratee) + // return Values[K, []T](groups) +} + +// Flatten returns an array a single level deep. +// Play: https://go.dev/play/p/rbp9ORaMpjw +func Flatten[T any](collection [][]T) []T { + totalLen := 0 + for i := range collection { + totalLen += len(collection[i]) + } + + result := make([]T, 0, totalLen) + for i := range collection { + result = append(result, collection[i]...) + } + + return result +} + +// Interleave round-robin alternating input slices and sequentially appending value at index into result +// Play: https://go.dev/play/p/DDhlwrShbwe +func Interleave[T any](collections ...[]T) []T { + if len(collections) == 0 { + return []T{} + } + + maxSize := 0 + totalSize := 0 + for _, c := range collections { + size := len(c) + totalSize += size + if size > maxSize { + maxSize = size + } + } + + if maxSize == 0 { + return []T{} + } + + result := make([]T, totalSize) + + resultIdx := 0 + for i := 0; i < maxSize; i++ { + for j := range collections { + if len(collections[j])-1 < i { + continue + } + + result[resultIdx] = collections[j][i] + resultIdx++ + } + } + + return result +} + +// Shuffle returns an array of shuffled values. Uses the Fisher-Yates shuffle algorithm. +// Play: https://go.dev/play/p/Qp73bnTDnc7 +func Shuffle[T any](collection []T) []T { + rand.Shuffle(len(collection), func(i, j int) { + collection[i], collection[j] = collection[j], collection[i] + }) + + return collection +} + +// Reverse reverses array so that the first element becomes the last, the second element becomes the second to last, and so on. +// Play: https://go.dev/play/p/fhUMLvZ7vS6 +func Reverse[T any](collection []T) []T { + length := len(collection) + half := length / 2 + + for i := 0; i < half; i = i + 1 { + j := length - 1 - i + collection[i], collection[j] = collection[j], collection[i] + } + + return collection +} + +// Fill fills elements of array with `initial` value. +// Play: https://go.dev/play/p/VwR34GzqEub +func Fill[T Clonable[T]](collection []T, initial T) []T { + result := make([]T, 0, len(collection)) + + for range collection { + result = append(result, initial.Clone()) + } + + return result +} + +// Repeat builds a slice with N copies of initial value. +// Play: https://go.dev/play/p/g3uHXbmc3b6 +func Repeat[T Clonable[T]](count int, initial T) []T { + result := make([]T, 0, count) + + for i := 0; i < count; i++ { + result = append(result, initial.Clone()) + } + + return result +} + +// RepeatBy builds a slice with values returned by N calls of callback. +// Play: https://go.dev/play/p/ozZLCtX_hNU +func RepeatBy[T any](count int, predicate func(index int) T) []T { + result := make([]T, 0, count) + + for i := 0; i < count; i++ { + result = append(result, predicate(i)) + } + + return result +} + +// KeyBy transforms a slice or an array of structs to a map based on a pivot callback. +// Play: https://go.dev/play/p/mdaClUAT-zZ +func KeyBy[K comparable, V any](collection []V, iteratee func(item V) K) map[K]V { + result := make(map[K]V, len(collection)) + + for _, v := range collection { + k := iteratee(v) + result[k] = v + } + + return result +} + +// Associate returns a map containing key-value pairs provided by transform function applied to elements of the given slice. +// If any of two pairs would have the same key the last one gets added to the map. +// The order of keys in returned map is not specified and is not guaranteed to be the same from the original array. +// Play: https://go.dev/play/p/WHa2CfMO3Lr +func Associate[T any, K comparable, V any](collection []T, transform func(item T) (K, V)) map[K]V { + result := make(map[K]V, len(collection)) + + for _, t := range collection { + k, v := transform(t) + result[k] = v + } + + return result +} + +// SliceToMap returns a map containing key-value pairs provided by transform function applied to elements of the given slice. +// If any of two pairs would have the same key the last one gets added to the map. +// The order of keys in returned map is not specified and is not guaranteed to be the same from the original array. +// Alias of Associate(). +// Play: https://go.dev/play/p/WHa2CfMO3Lr +func SliceToMap[T any, K comparable, V any](collection []T, transform func(item T) (K, V)) map[K]V { + return Associate(collection, transform) +} + +// Drop drops n elements from the beginning of a slice or array. +// Play: https://go.dev/play/p/JswS7vXRJP2 +func Drop[T any](collection []T, n int) []T { + if len(collection) <= n { + return make([]T, 0) + } + + result := make([]T, 0, len(collection)-n) + + return append(result, collection[n:]...) +} + +// DropRight drops n elements from the end of a slice or array. +// Play: https://go.dev/play/p/GG0nXkSJJa3 +func DropRight[T any](collection []T, n int) []T { + if len(collection) <= n { + return []T{} + } + + result := make([]T, 0, len(collection)-n) + return append(result, collection[:len(collection)-n]...) +} + +// DropWhile drops elements from the beginning of a slice or array while the predicate returns true. +// Play: https://go.dev/play/p/7gBPYw2IK16 +func DropWhile[T any](collection []T, predicate func(item T) bool) []T { + i := 0 + for ; i < len(collection); i++ { + if !predicate(collection[i]) { + break + } + } + + result := make([]T, 0, len(collection)-i) + return append(result, collection[i:]...) +} + +// DropRightWhile drops elements from the end of a slice or array while the predicate returns true. +// Play: https://go.dev/play/p/3-n71oEC0Hz +func DropRightWhile[T any](collection []T, predicate func(item T) bool) []T { + i := len(collection) - 1 + for ; i >= 0; i-- { + if !predicate(collection[i]) { + break + } + } + + result := make([]T, 0, i+1) + return append(result, collection[:i+1]...) +} + +// Reject is the opposite of Filter, this method returns the elements of collection that predicate does not return truthy for. +// Play: https://go.dev/play/p/YkLMODy1WEL +func Reject[V any](collection []V, predicate func(item V, index int) bool) []V { + result := []V{} + + for i, item := range collection { + if !predicate(item, i) { + result = append(result, item) + } + } + + return result +} + +// Count counts the number of elements in the collection that compare equal to value. +// Play: https://go.dev/play/p/Y3FlK54yveC +func Count[T comparable](collection []T, value T) (count int) { + for _, item := range collection { + if item == value { + count++ + } + } + + return count +} + +// CountBy counts the number of elements in the collection for which predicate is true. +// Play: https://go.dev/play/p/ByQbNYQQi4X +func CountBy[T any](collection []T, predicate func(item T) bool) (count int) { + for _, item := range collection { + if predicate(item) { + count++ + } + } + + return count +} + +// CountValues counts the number of each element in the collection. +// Play: https://go.dev/play/p/-p-PyLT4dfy +func CountValues[T comparable](collection []T) map[T]int { + result := make(map[T]int) + + for _, item := range collection { + result[item]++ + } + + return result +} + +// CountValuesBy counts the number of each element return from mapper function. +// Is equivalent to chaining lo.Map and lo.CountValues. +// Play: https://go.dev/play/p/2U0dG1SnOmS +func CountValuesBy[T any, U comparable](collection []T, mapper func(item T) U) map[U]int { + result := make(map[U]int) + + for _, item := range collection { + result[mapper(item)]++ + } + + return result +} + +// Subset returns a copy of a slice from `offset` up to `length` elements. Like `slice[start:start+length]`, but does not panic on overflow. +// Play: https://go.dev/play/p/tOQu1GhFcog +func Subset[T any](collection []T, offset int, length uint) []T { + size := len(collection) + + if offset < 0 { + offset = size + offset + if offset < 0 { + offset = 0 + } + } + + if offset > size { + return []T{} + } + + if length > uint(size)-uint(offset) { + length = uint(size - offset) + } + + return collection[offset : offset+int(length)] +} + +// Slice returns a copy of a slice from `start` up to, but not including `end`. Like `slice[start:end]`, but does not panic on overflow. +// Play: https://go.dev/play/p/8XWYhfMMA1h +func Slice[T any](collection []T, start int, end int) []T { + size := len(collection) + + if start >= end { + return []T{} + } + + if start > size { + start = size + } + if start < 0 { + start = 0 + } + + if end > size { + end = size + } + if end < 0 { + end = 0 + } + + return collection[start:end] +} + +// Replace returns a copy of the slice with the first n non-overlapping instances of old replaced by new. +// Play: https://go.dev/play/p/XfPzmf9gql6 +func Replace[T comparable](collection []T, old T, new T, n int) []T { + result := make([]T, len(collection)) + copy(result, collection) + + for i := range result { + if result[i] == old && n != 0 { + result[i] = new + n-- + } + } + + return result +} + +// ReplaceAll returns a copy of the slice with all non-overlapping instances of old replaced by new. +// Play: https://go.dev/play/p/a9xZFUHfYcV +func ReplaceAll[T comparable](collection []T, old T, new T) []T { + return Replace(collection, old, new, -1) +} + +// Compact returns a slice of all non-zero elements. +// Play: https://go.dev/play/p/tXiy-iK6PAc +func Compact[T comparable](collection []T) []T { + var zero T + + result := []T{} + + for _, item := range collection { + if item != zero { + result = append(result, item) + } + } + + return result +} + +// IsSorted checks if a slice is sorted. +// Play: https://go.dev/play/p/mc3qR-t4mcx +func IsSorted[T constraints.Ordered](collection []T) bool { + for i := 1; i < len(collection); i++ { + if collection[i-1] > collection[i] { + return false + } + } + + return true +} + +// IsSortedByKey checks if a slice is sorted by iteratee. +// Play: https://go.dev/play/p/wiG6XyBBu49 +func IsSortedByKey[T any, K constraints.Ordered](collection []T, iteratee func(item T) K) bool { + size := len(collection) + + for i := 0; i < size-1; i++ { + if iteratee(collection[i]) > iteratee(collection[i+1]) { + return false + } + } + + return true +} diff --git a/vendor/github.com/samber/lo/string.go b/vendor/github.com/samber/lo/string.go new file mode 100644 index 00000000000..dfe1050b62e --- /dev/null +++ b/vendor/github.com/samber/lo/string.go @@ -0,0 +1,94 @@ +package lo + +import ( + "math/rand" + "unicode/utf8" +) + +var ( + LowerCaseLettersCharset = []rune("abcdefghijklmnopqrstuvwxyz") + UpperCaseLettersCharset = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ") + LettersCharset = append(LowerCaseLettersCharset, UpperCaseLettersCharset...) + NumbersCharset = []rune("0123456789") + AlphanumericCharset = append(LettersCharset, NumbersCharset...) + SpecialCharset = []rune("!@#$%^&*()_+-=[]{}|;':\",./<>?") + AllCharset = append(AlphanumericCharset, SpecialCharset...) +) + +// RandomString return a random string. +// Play: https://go.dev/play/p/rRseOQVVum4 +func RandomString(size int, charset []rune) string { + if size <= 0 { + panic("lo.RandomString: Size parameter must be greater than 0") + } + if len(charset) <= 0 { + panic("lo.RandomString: Charset parameter must not be empty") + } + + b := make([]rune, size) + possibleCharactersCount := len(charset) + for i := range b { + b[i] = charset[rand.Intn(possibleCharactersCount)] + } + return string(b) +} + +// Substring return part of a string. +// Play: https://go.dev/play/p/TQlxQi82Lu1 +func Substring[T ~string](str T, offset int, length uint) T { + size := len(str) + + if offset < 0 { + offset = size + offset + if offset < 0 { + offset = 0 + } + } + + if offset > size { + return Empty[T]() + } + + if length > uint(size)-uint(offset) { + length = uint(size - offset) + } + + return str[offset : offset+int(length)] +} + +// ChunkString returns an array of strings split into groups the length of size. If array can't be split evenly, +// the final chunk will be the remaining elements. +// Play: https://go.dev/play/p/__FLTuJVz54 +func ChunkString[T ~string](str T, size int) []T { + if size <= 0 { + panic("lo.ChunkString: Size parameter must be greater than 0") + } + + if len(str) == 0 { + return []T{""} + } + + if size >= len(str) { + return []T{str} + } + + var chunks []T = make([]T, 0, ((len(str)-1)/size)+1) + currentLen := 0 + currentStart := 0 + for i := range str { + if currentLen == size { + chunks = append(chunks, str[currentStart:i]) + currentLen = 0 + currentStart = i + } + currentLen++ + } + chunks = append(chunks, str[currentStart:]) + return chunks +} + +// RuneLength is an alias to utf8.RuneCountInString which returns the number of runes in string. +// Play: https://go.dev/play/p/tuhgW_lWY8l +func RuneLength(str string) int { + return utf8.RuneCountInString(str) +} diff --git a/vendor/github.com/samber/lo/tuples.go b/vendor/github.com/samber/lo/tuples.go new file mode 100644 index 00000000000..cdddf6afc10 --- /dev/null +++ b/vendor/github.com/samber/lo/tuples.go @@ -0,0 +1,513 @@ +package lo + +// T2 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm +func T2[A any, B any](a A, b B) Tuple2[A, B] { + return Tuple2[A, B]{A: a, B: b} +} + +// T3 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm +func T3[A any, B any, C any](a A, b B, c C) Tuple3[A, B, C] { + return Tuple3[A, B, C]{A: a, B: b, C: c} +} + +// T4 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm +func T4[A any, B any, C any, D any](a A, b B, c C, d D) Tuple4[A, B, C, D] { + return Tuple4[A, B, C, D]{A: a, B: b, C: c, D: d} +} + +// T5 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm +func T5[A any, B any, C any, D any, E any](a A, b B, c C, d D, e E) Tuple5[A, B, C, D, E] { + return Tuple5[A, B, C, D, E]{A: a, B: b, C: c, D: d, E: e} +} + +// T6 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm +func T6[A any, B any, C any, D any, E any, F any](a A, b B, c C, d D, e E, f F) Tuple6[A, B, C, D, E, F] { + return Tuple6[A, B, C, D, E, F]{A: a, B: b, C: c, D: d, E: e, F: f} +} + +// T7 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm +func T7[A any, B any, C any, D any, E any, F any, G any](a A, b B, c C, d D, e E, f F, g G) Tuple7[A, B, C, D, E, F, G] { + return Tuple7[A, B, C, D, E, F, G]{A: a, B: b, C: c, D: d, E: e, F: f, G: g} +} + +// T8 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm +func T8[A any, B any, C any, D any, E any, F any, G any, H any](a A, b B, c C, d D, e E, f F, g G, h H) Tuple8[A, B, C, D, E, F, G, H] { + return Tuple8[A, B, C, D, E, F, G, H]{A: a, B: b, C: c, D: d, E: e, F: f, G: g, H: h} +} + +// T9 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm +func T9[A any, B any, C any, D any, E any, F any, G any, H any, I any](a A, b B, c C, d D, e E, f F, g G, h H, i I) Tuple9[A, B, C, D, E, F, G, H, I] { + return Tuple9[A, B, C, D, E, F, G, H, I]{A: a, B: b, C: c, D: d, E: e, F: f, G: g, H: h, I: i} +} + +// Unpack2 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W +func Unpack2[A any, B any](tuple Tuple2[A, B]) (A, B) { + return tuple.A, tuple.B +} + +// Unpack3 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W +func Unpack3[A any, B any, C any](tuple Tuple3[A, B, C]) (A, B, C) { + return tuple.A, tuple.B, tuple.C +} + +// Unpack4 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W +func Unpack4[A any, B any, C any, D any](tuple Tuple4[A, B, C, D]) (A, B, C, D) { + return tuple.A, tuple.B, tuple.C, tuple.D +} + +// Unpack5 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W +func Unpack5[A any, B any, C any, D any, E any](tuple Tuple5[A, B, C, D, E]) (A, B, C, D, E) { + return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E +} + +// Unpack6 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W +func Unpack6[A any, B any, C any, D any, E any, F any](tuple Tuple6[A, B, C, D, E, F]) (A, B, C, D, E, F) { + return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F +} + +// Unpack7 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W +func Unpack7[A any, B any, C any, D any, E any, F any, G any](tuple Tuple7[A, B, C, D, E, F, G]) (A, B, C, D, E, F, G) { + return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F, tuple.G +} + +// Unpack8 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W +func Unpack8[A any, B any, C any, D any, E any, F any, G any, H any](tuple Tuple8[A, B, C, D, E, F, G, H]) (A, B, C, D, E, F, G, H) { + return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F, tuple.G, tuple.H +} + +// Unpack9 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W +func Unpack9[A any, B any, C any, D any, E any, F any, G any, H any, I any](tuple Tuple9[A, B, C, D, E, F, G, H, I]) (A, B, C, D, E, F, G, H, I) { + return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F, tuple.G, tuple.H, tuple.I +} + +// Zip2 creates a slice of grouped elements, the first of which contains the first elements +// of the given arrays, the second of which contains the second elements of the given arrays, and so on. +// When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp +func Zip2[A any, B any](a []A, b []B) []Tuple2[A, B] { + size := Max([]int{len(a), len(b)}) + + result := make([]Tuple2[A, B], 0, size) + + for index := 0; index < size; index++ { + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + + result = append(result, Tuple2[A, B]{ + A: _a, + B: _b, + }) + } + + return result +} + +// Zip3 creates a slice of grouped elements, the first of which contains the first elements +// of the given arrays, the second of which contains the second elements of the given arrays, and so on. +// When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp +func Zip3[A any, B any, C any](a []A, b []B, c []C) []Tuple3[A, B, C] { + size := Max([]int{len(a), len(b), len(c)}) + + result := make([]Tuple3[A, B, C], 0, size) + + for index := 0; index < size; index++ { + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + + result = append(result, Tuple3[A, B, C]{ + A: _a, + B: _b, + C: _c, + }) + } + + return result +} + +// Zip4 creates a slice of grouped elements, the first of which contains the first elements +// of the given arrays, the second of which contains the second elements of the given arrays, and so on. +// When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp +func Zip4[A any, B any, C any, D any](a []A, b []B, c []C, d []D) []Tuple4[A, B, C, D] { + size := Max([]int{len(a), len(b), len(c), len(d)}) + + result := make([]Tuple4[A, B, C, D], 0, size) + + for index := 0; index < size; index++ { + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) + + result = append(result, Tuple4[A, B, C, D]{ + A: _a, + B: _b, + C: _c, + D: _d, + }) + } + + return result +} + +// Zip5 creates a slice of grouped elements, the first of which contains the first elements +// of the given arrays, the second of which contains the second elements of the given arrays, and so on. +// When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp +func Zip5[A any, B any, C any, D any, E any](a []A, b []B, c []C, d []D, e []E) []Tuple5[A, B, C, D, E] { + size := Max([]int{len(a), len(b), len(c), len(d), len(e)}) + + result := make([]Tuple5[A, B, C, D, E], 0, size) + + for index := 0; index < size; index++ { + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) + _e, _ := Nth(e, index) + + result = append(result, Tuple5[A, B, C, D, E]{ + A: _a, + B: _b, + C: _c, + D: _d, + E: _e, + }) + } + + return result +} + +// Zip6 creates a slice of grouped elements, the first of which contains the first elements +// of the given arrays, the second of which contains the second elements of the given arrays, and so on. +// When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp +func Zip6[A any, B any, C any, D any, E any, F any](a []A, b []B, c []C, d []D, e []E, f []F) []Tuple6[A, B, C, D, E, F] { + size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f)}) + + result := make([]Tuple6[A, B, C, D, E, F], 0, size) + + for index := 0; index < size; index++ { + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) + _e, _ := Nth(e, index) + _f, _ := Nth(f, index) + + result = append(result, Tuple6[A, B, C, D, E, F]{ + A: _a, + B: _b, + C: _c, + D: _d, + E: _e, + F: _f, + }) + } + + return result +} + +// Zip7 creates a slice of grouped elements, the first of which contains the first elements +// of the given arrays, the second of which contains the second elements of the given arrays, and so on. +// When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp +func Zip7[A any, B any, C any, D any, E any, F any, G any](a []A, b []B, c []C, d []D, e []E, f []F, g []G) []Tuple7[A, B, C, D, E, F, G] { + size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g)}) + + result := make([]Tuple7[A, B, C, D, E, F, G], 0, size) + + for index := 0; index < size; index++ { + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) + _e, _ := Nth(e, index) + _f, _ := Nth(f, index) + _g, _ := Nth(g, index) + + result = append(result, Tuple7[A, B, C, D, E, F, G]{ + A: _a, + B: _b, + C: _c, + D: _d, + E: _e, + F: _f, + G: _g, + }) + } + + return result +} + +// Zip8 creates a slice of grouped elements, the first of which contains the first elements +// of the given arrays, the second of which contains the second elements of the given arrays, and so on. +// When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp +func Zip8[A any, B any, C any, D any, E any, F any, G any, H any](a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H) []Tuple8[A, B, C, D, E, F, G, H] { + size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g), len(h)}) + + result := make([]Tuple8[A, B, C, D, E, F, G, H], 0, size) + + for index := 0; index < size; index++ { + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) + _e, _ := Nth(e, index) + _f, _ := Nth(f, index) + _g, _ := Nth(g, index) + _h, _ := Nth(h, index) + + result = append(result, Tuple8[A, B, C, D, E, F, G, H]{ + A: _a, + B: _b, + C: _c, + D: _d, + E: _e, + F: _f, + G: _g, + H: _h, + }) + } + + return result +} + +// Zip9 creates a slice of grouped elements, the first of which contains the first elements +// of the given arrays, the second of which contains the second elements of the given arrays, and so on. +// When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp +func Zip9[A any, B any, C any, D any, E any, F any, G any, H any, I any](a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, i []I) []Tuple9[A, B, C, D, E, F, G, H, I] { + size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g), len(h), len(i)}) + + result := make([]Tuple9[A, B, C, D, E, F, G, H, I], 0, size) + + for index := 0; index < size; index++ { + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) + _e, _ := Nth(e, index) + _f, _ := Nth(f, index) + _g, _ := Nth(g, index) + _h, _ := Nth(h, index) + _i, _ := Nth(i, index) + + result = append(result, Tuple9[A, B, C, D, E, F, G, H, I]{ + A: _a, + B: _b, + C: _c, + D: _d, + E: _e, + F: _f, + G: _g, + H: _h, + I: _i, + }) + } + + return result +} + +// Unzip2 accepts an array of grouped elements and creates an array regrouping the elements +// to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW +func Unzip2[A any, B any](tuples []Tuple2[A, B]) ([]A, []B) { + size := len(tuples) + r1 := make([]A, 0, size) + r2 := make([]B, 0, size) + + for _, tuple := range tuples { + r1 = append(r1, tuple.A) + r2 = append(r2, tuple.B) + } + + return r1, r2 +} + +// Unzip3 accepts an array of grouped elements and creates an array regrouping the elements +// to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW +func Unzip3[A any, B any, C any](tuples []Tuple3[A, B, C]) ([]A, []B, []C) { + size := len(tuples) + r1 := make([]A, 0, size) + r2 := make([]B, 0, size) + r3 := make([]C, 0, size) + + for _, tuple := range tuples { + r1 = append(r1, tuple.A) + r2 = append(r2, tuple.B) + r3 = append(r3, tuple.C) + } + + return r1, r2, r3 +} + +// Unzip4 accepts an array of grouped elements and creates an array regrouping the elements +// to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW +func Unzip4[A any, B any, C any, D any](tuples []Tuple4[A, B, C, D]) ([]A, []B, []C, []D) { + size := len(tuples) + r1 := make([]A, 0, size) + r2 := make([]B, 0, size) + r3 := make([]C, 0, size) + r4 := make([]D, 0, size) + + for _, tuple := range tuples { + r1 = append(r1, tuple.A) + r2 = append(r2, tuple.B) + r3 = append(r3, tuple.C) + r4 = append(r4, tuple.D) + } + + return r1, r2, r3, r4 +} + +// Unzip5 accepts an array of grouped elements and creates an array regrouping the elements +// to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW +func Unzip5[A any, B any, C any, D any, E any](tuples []Tuple5[A, B, C, D, E]) ([]A, []B, []C, []D, []E) { + size := len(tuples) + r1 := make([]A, 0, size) + r2 := make([]B, 0, size) + r3 := make([]C, 0, size) + r4 := make([]D, 0, size) + r5 := make([]E, 0, size) + + for _, tuple := range tuples { + r1 = append(r1, tuple.A) + r2 = append(r2, tuple.B) + r3 = append(r3, tuple.C) + r4 = append(r4, tuple.D) + r5 = append(r5, tuple.E) + } + + return r1, r2, r3, r4, r5 +} + +// Unzip6 accepts an array of grouped elements and creates an array regrouping the elements +// to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW +func Unzip6[A any, B any, C any, D any, E any, F any](tuples []Tuple6[A, B, C, D, E, F]) ([]A, []B, []C, []D, []E, []F) { + size := len(tuples) + r1 := make([]A, 0, size) + r2 := make([]B, 0, size) + r3 := make([]C, 0, size) + r4 := make([]D, 0, size) + r5 := make([]E, 0, size) + r6 := make([]F, 0, size) + + for _, tuple := range tuples { + r1 = append(r1, tuple.A) + r2 = append(r2, tuple.B) + r3 = append(r3, tuple.C) + r4 = append(r4, tuple.D) + r5 = append(r5, tuple.E) + r6 = append(r6, tuple.F) + } + + return r1, r2, r3, r4, r5, r6 +} + +// Unzip7 accepts an array of grouped elements and creates an array regrouping the elements +// to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW +func Unzip7[A any, B any, C any, D any, E any, F any, G any](tuples []Tuple7[A, B, C, D, E, F, G]) ([]A, []B, []C, []D, []E, []F, []G) { + size := len(tuples) + r1 := make([]A, 0, size) + r2 := make([]B, 0, size) + r3 := make([]C, 0, size) + r4 := make([]D, 0, size) + r5 := make([]E, 0, size) + r6 := make([]F, 0, size) + r7 := make([]G, 0, size) + + for _, tuple := range tuples { + r1 = append(r1, tuple.A) + r2 = append(r2, tuple.B) + r3 = append(r3, tuple.C) + r4 = append(r4, tuple.D) + r5 = append(r5, tuple.E) + r6 = append(r6, tuple.F) + r7 = append(r7, tuple.G) + } + + return r1, r2, r3, r4, r5, r6, r7 +} + +// Unzip8 accepts an array of grouped elements and creates an array regrouping the elements +// to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW +func Unzip8[A any, B any, C any, D any, E any, F any, G any, H any](tuples []Tuple8[A, B, C, D, E, F, G, H]) ([]A, []B, []C, []D, []E, []F, []G, []H) { + size := len(tuples) + r1 := make([]A, 0, size) + r2 := make([]B, 0, size) + r3 := make([]C, 0, size) + r4 := make([]D, 0, size) + r5 := make([]E, 0, size) + r6 := make([]F, 0, size) + r7 := make([]G, 0, size) + r8 := make([]H, 0, size) + + for _, tuple := range tuples { + r1 = append(r1, tuple.A) + r2 = append(r2, tuple.B) + r3 = append(r3, tuple.C) + r4 = append(r4, tuple.D) + r5 = append(r5, tuple.E) + r6 = append(r6, tuple.F) + r7 = append(r7, tuple.G) + r8 = append(r8, tuple.H) + } + + return r1, r2, r3, r4, r5, r6, r7, r8 +} + +// Unzip9 accepts an array of grouped elements and creates an array regrouping the elements +// to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW +func Unzip9[A any, B any, C any, D any, E any, F any, G any, H any, I any](tuples []Tuple9[A, B, C, D, E, F, G, H, I]) ([]A, []B, []C, []D, []E, []F, []G, []H, []I) { + size := len(tuples) + r1 := make([]A, 0, size) + r2 := make([]B, 0, size) + r3 := make([]C, 0, size) + r4 := make([]D, 0, size) + r5 := make([]E, 0, size) + r6 := make([]F, 0, size) + r7 := make([]G, 0, size) + r8 := make([]H, 0, size) + r9 := make([]I, 0, size) + + for _, tuple := range tuples { + r1 = append(r1, tuple.A) + r2 = append(r2, tuple.B) + r3 = append(r3, tuple.C) + r4 = append(r4, tuple.D) + r5 = append(r5, tuple.E) + r6 = append(r6, tuple.F) + r7 = append(r7, tuple.G) + r8 = append(r8, tuple.H) + r9 = append(r9, tuple.I) + } + + return r1, r2, r3, r4, r5, r6, r7, r8, r9 +} diff --git a/vendor/github.com/samber/lo/type_manipulation.go b/vendor/github.com/samber/lo/type_manipulation.go new file mode 100644 index 00000000000..fe99ee1f040 --- /dev/null +++ b/vendor/github.com/samber/lo/type_manipulation.go @@ -0,0 +1,88 @@ +package lo + +// ToPtr returns a pointer copy of value. +func ToPtr[T any](x T) *T { + return &x +} + +// FromPtr returns the pointer value or empty. +func FromPtr[T any](x *T) T { + if x == nil { + return Empty[T]() + } + + return *x +} + +// FromPtrOr returns the pointer value or the fallback value. +func FromPtrOr[T any](x *T, fallback T) T { + if x == nil { + return fallback + } + + return *x +} + +// ToSlicePtr returns a slice of pointer copy of value. +func ToSlicePtr[T any](collection []T) []*T { + return Map(collection, func(x T, _ int) *T { + return &x + }) +} + +// ToAnySlice returns a slice with all elements mapped to `any` type +func ToAnySlice[T any](collection []T) []any { + result := make([]any, len(collection)) + for i, item := range collection { + result[i] = item + } + return result +} + +// FromAnySlice returns an `any` slice with all elements mapped to a type. +// Returns false in case of type conversion failure. +func FromAnySlice[T any](in []any) (out []T, ok bool) { + defer func() { + if r := recover(); r != nil { + out = []T{} + ok = false + } + }() + + result := make([]T, len(in)) + for i, item := range in { + result[i] = item.(T) + } + return result, true +} + +// Empty returns an empty value. +func Empty[T any]() T { + var zero T + return zero +} + +// IsEmpty returns true if argument is a zero value. +func IsEmpty[T comparable](v T) bool { + var zero T + return zero == v +} + +// IsNotEmpty returns true if argument is not a zero value. +func IsNotEmpty[T comparable](v T) bool { + var zero T + return zero != v +} + +// Coalesce returns the first non-empty arguments. Arguments must be comparable. +func Coalesce[T comparable](v ...T) (result T, ok bool) { + for _, e := range v { + if e != result { + result = e + ok = true + return + } + } + + return +} diff --git a/vendor/github.com/samber/lo/types.go b/vendor/github.com/samber/lo/types.go new file mode 100644 index 00000000000..271c5b4fdf7 --- /dev/null +++ b/vendor/github.com/samber/lo/types.go @@ -0,0 +1,123 @@ +package lo + +// Entry defines a key/value pairs. +type Entry[K comparable, V any] struct { + Key K + Value V +} + +// Tuple2 is a group of 2 elements (pair). +type Tuple2[A any, B any] struct { + A A + B B +} + +// Unpack returns values contained in tuple. +func (t Tuple2[A, B]) Unpack() (A, B) { + return t.A, t.B +} + +// Tuple3 is a group of 3 elements. +type Tuple3[A any, B any, C any] struct { + A A + B B + C C +} + +// Unpack returns values contained in tuple. +func (t Tuple3[A, B, C]) Unpack() (A, B, C) { + return t.A, t.B, t.C +} + +// Tuple4 is a group of 4 elements. +type Tuple4[A any, B any, C any, D any] struct { + A A + B B + C C + D D +} + +// Unpack returns values contained in tuple. +func (t Tuple4[A, B, C, D]) Unpack() (A, B, C, D) { + return t.A, t.B, t.C, t.D +} + +// Tuple5 is a group of 5 elements. +type Tuple5[A any, B any, C any, D any, E any] struct { + A A + B B + C C + D D + E E +} + +// Unpack returns values contained in tuple. +func (t Tuple5[A, B, C, D, E]) Unpack() (A, B, C, D, E) { + return t.A, t.B, t.C, t.D, t.E +} + +// Tuple6 is a group of 6 elements. +type Tuple6[A any, B any, C any, D any, E any, F any] struct { + A A + B B + C C + D D + E E + F F +} + +// Unpack returns values contained in tuple. +func (t Tuple6[A, B, C, D, E, F]) Unpack() (A, B, C, D, E, F) { + return t.A, t.B, t.C, t.D, t.E, t.F +} + +// Tuple7 is a group of 7 elements. +type Tuple7[A any, B any, C any, D any, E any, F any, G any] struct { + A A + B B + C C + D D + E E + F F + G G +} + +// Unpack returns values contained in tuple. +func (t Tuple7[A, B, C, D, E, F, G]) Unpack() (A, B, C, D, E, F, G) { + return t.A, t.B, t.C, t.D, t.E, t.F, t.G +} + +// Tuple8 is a group of 8 elements. +type Tuple8[A any, B any, C any, D any, E any, F any, G any, H any] struct { + A A + B B + C C + D D + E E + F F + G G + H H +} + +// Unpack returns values contained in tuple. +func (t Tuple8[A, B, C, D, E, F, G, H]) Unpack() (A, B, C, D, E, F, G, H) { + return t.A, t.B, t.C, t.D, t.E, t.F, t.G, t.H +} + +// Tuple9 is a group of 9 elements. +type Tuple9[A any, B any, C any, D any, E any, F any, G any, H any, I any] struct { + A A + B B + C C + D D + E E + F F + G G + H H + I I +} + +// Unpack returns values contained in tuple. +func (t Tuple9[A, B, C, D, E, F, G, H, I]) Unpack() (A, B, C, D, E, F, G, H, I) { + return t.A, t.B, t.C, t.D, t.E, t.F, t.G, t.H, t.I +} diff --git a/vendor/github.com/shopspring/decimal/.gitignore b/vendor/github.com/shopspring/decimal/.gitignore new file mode 100644 index 00000000000..ff36b987f07 --- /dev/null +++ b/vendor/github.com/shopspring/decimal/.gitignore @@ -0,0 +1,9 @@ +.git +*.swp + +# IntelliJ +.idea/ +*.iml + +# VS code +*.code-workspace diff --git a/vendor/github.com/shopspring/decimal/.travis.yml b/vendor/github.com/shopspring/decimal/.travis.yml new file mode 100644 index 00000000000..6326d40f0e9 --- /dev/null +++ b/vendor/github.com/shopspring/decimal/.travis.yml @@ -0,0 +1,19 @@ +language: go + +arch: + - amd64 + - ppc64le + +go: + - 1.7.x + - 1.14.x + - 1.15.x + - 1.16.x + - 1.17.x + - tip + +install: + - go build . + +script: + - go test -v diff --git a/vendor/github.com/shopspring/decimal/CHANGELOG.md b/vendor/github.com/shopspring/decimal/CHANGELOG.md new file mode 100644 index 00000000000..aea61154b8c --- /dev/null +++ b/vendor/github.com/shopspring/decimal/CHANGELOG.md @@ -0,0 +1,49 @@ +## Decimal v1.3.1 + +#### ENHANCEMENTS +- Reduce memory allocation in case of initialization from big.Int [#252](https://github.com/shopspring/decimal/pull/252) + +#### BUGFIXES +- Fix binary marshalling of decimal zero value [#253](https://github.com/shopspring/decimal/pull/253) + +## Decimal v1.3.0 + +#### FEATURES +- Add NewFromFormattedString initializer [#184](https://github.com/shopspring/decimal/pull/184) +- Add NewNullDecimal initializer [#234](https://github.com/shopspring/decimal/pull/234) +- Add implementation of natural exponent function (Taylor, Hull-Abraham) [#229](https://github.com/shopspring/decimal/pull/229) +- Add RoundUp, RoundDown, RoundCeil, RoundFloor methods [#196](https://github.com/shopspring/decimal/pull/196) [#202](https://github.com/shopspring/decimal/pull/202) [#220](https://github.com/shopspring/decimal/pull/220) +- Add XML support for NullDecimal [#192](https://github.com/shopspring/decimal/pull/192) +- Add IsInteger method [#179](https://github.com/shopspring/decimal/pull/179) +- Add Copy helper method [#123](https://github.com/shopspring/decimal/pull/123) +- Add InexactFloat64 helper method [#205](https://github.com/shopspring/decimal/pull/205) +- Add CoefficientInt64 helper method [#244](https://github.com/shopspring/decimal/pull/244) + +#### ENHANCEMENTS +- Performance optimization of NewFromString init method [#198](https://github.com/shopspring/decimal/pull/198) +- Performance optimization of Abs and Round methods [#240](https://github.com/shopspring/decimal/pull/240) +- Additional tests (CI) for ppc64le architecture [#188](https://github.com/shopspring/decimal/pull/188) + +#### BUGFIXES +- Fix rounding in FormatFloat fallback path (roundShortest method, fix taken from Go main repository) [#161](https://github.com/shopspring/decimal/pull/161) +- Add slice range checks to UnmarshalBinary method [#232](https://github.com/shopspring/decimal/pull/232) + +## Decimal v1.2.0 + +#### BREAKING +- Drop support for Go version older than 1.7 [#172](https://github.com/shopspring/decimal/pull/172) + +#### FEATURES +- Add NewFromInt and NewFromInt32 initializers [#72](https://github.com/shopspring/decimal/pull/72) +- Add support for Go modules [#157](https://github.com/shopspring/decimal/pull/157) +- Add BigInt, BigFloat helper methods [#171](https://github.com/shopspring/decimal/pull/171) + +#### ENHANCEMENTS +- Memory usage optimization [#160](https://github.com/shopspring/decimal/pull/160) +- Updated travis CI golang versions [#156](https://github.com/shopspring/decimal/pull/156) +- Update documentation [#173](https://github.com/shopspring/decimal/pull/173) +- Improve code quality [#174](https://github.com/shopspring/decimal/pull/174) + +#### BUGFIXES +- Revert remove insignificant digits [#159](https://github.com/shopspring/decimal/pull/159) +- Remove 15 interval for RoundCash [#166](https://github.com/shopspring/decimal/pull/166) diff --git a/vendor/github.com/shopspring/decimal/LICENSE b/vendor/github.com/shopspring/decimal/LICENSE new file mode 100644 index 00000000000..ad2148aaf93 --- /dev/null +++ b/vendor/github.com/shopspring/decimal/LICENSE @@ -0,0 +1,45 @@ +The MIT License (MIT) + +Copyright (c) 2015 Spring, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +- Based on https://github.com/oguzbilgic/fpd, which has the following license: +""" +The MIT License (MIT) + +Copyright (c) 2013 Oguz Bilgic + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" diff --git a/vendor/github.com/shopspring/decimal/README.md b/vendor/github.com/shopspring/decimal/README.md new file mode 100644 index 00000000000..2e35df068ea --- /dev/null +++ b/vendor/github.com/shopspring/decimal/README.md @@ -0,0 +1,130 @@ +# decimal + +[![Build Status](https://app.travis-ci.com/shopspring/decimal.svg?branch=master)](https://app.travis-ci.com/shopspring/decimal) [![GoDoc](https://godoc.org/github.com/shopspring/decimal?status.svg)](https://godoc.org/github.com/shopspring/decimal) [![Go Report Card](https://goreportcard.com/badge/github.com/shopspring/decimal)](https://goreportcard.com/report/github.com/shopspring/decimal) + +Arbitrary-precision fixed-point decimal numbers in go. + +_Note:_ Decimal library can "only" represent numbers with a maximum of 2^31 digits after the decimal point. + +## Features + + * The zero-value is 0, and is safe to use without initialization + * Addition, subtraction, multiplication with no loss of precision + * Division with specified precision + * Database/sql serialization/deserialization + * JSON and XML serialization/deserialization + +## Install + +Run `go get github.com/shopspring/decimal` + +## Requirements + +Decimal library requires Go version `>=1.7` + +## Usage + +```go +package main + +import ( + "fmt" + "github.com/shopspring/decimal" +) + +func main() { + price, err := decimal.NewFromString("136.02") + if err != nil { + panic(err) + } + + quantity := decimal.NewFromInt(3) + + fee, _ := decimal.NewFromString(".035") + taxRate, _ := decimal.NewFromString(".08875") + + subtotal := price.Mul(quantity) + + preTax := subtotal.Mul(fee.Add(decimal.NewFromFloat(1))) + + total := preTax.Mul(taxRate.Add(decimal.NewFromFloat(1))) + + fmt.Println("Subtotal:", subtotal) // Subtotal: 408.06 + fmt.Println("Pre-tax:", preTax) // Pre-tax: 422.3421 + fmt.Println("Taxes:", total.Sub(preTax)) // Taxes: 37.482861375 + fmt.Println("Total:", total) // Total: 459.824961375 + fmt.Println("Tax rate:", total.Sub(preTax).Div(preTax)) // Tax rate: 0.08875 +} +``` + +## Documentation + +http://godoc.org/github.com/shopspring/decimal + +## Production Usage + +* [Spring](https://shopspring.com/), since August 14, 2014. +* If you are using this in production, please let us know! + +## FAQ + +#### Why don't you just use float64? + +Because float64 (or any binary floating point type, actually) can't represent +numbers such as `0.1` exactly. + +Consider this code: http://play.golang.org/p/TQBd4yJe6B You might expect that +it prints out `10`, but it actually prints `9.999999999999831`. Over time, +these small errors can really add up! + +#### Why don't you just use big.Rat? + +big.Rat is fine for representing rational numbers, but Decimal is better for +representing money. Why? Here's a (contrived) example: + +Let's say you use big.Rat, and you have two numbers, x and y, both +representing 1/3, and you have `z = 1 - x - y = 1/3`. If you print each one +out, the string output has to stop somewhere (let's say it stops at 3 decimal +digits, for simplicity), so you'll get 0.333, 0.333, and 0.333. But where did +the other 0.001 go? + +Here's the above example as code: http://play.golang.org/p/lCZZs0w9KE + +With Decimal, the strings being printed out represent the number exactly. So, +if you have `x = y = 1/3` (with precision 3), they will actually be equal to +0.333, and when you do `z = 1 - x - y`, `z` will be equal to .334. No money is +unaccounted for! + +You still have to be careful. If you want to split a number `N` 3 ways, you +can't just send `N/3` to three different people. You have to pick one to send +`N - (2/3*N)` to. That person will receive the fraction of a penny remainder. + +But, it is much easier to be careful with Decimal than with big.Rat. + +#### Why isn't the API similar to big.Int's? + +big.Int's API is built to reduce the number of memory allocations for maximal +performance. This makes sense for its use-case, but the trade-off is that the +API is awkward and easy to misuse. + +For example, to add two big.Ints, you do: `z := new(big.Int).Add(x, y)`. A +developer unfamiliar with this API might try to do `z := a.Add(a, b)`. This +modifies `a` and sets `z` as an alias for `a`, which they might not expect. It +also modifies any other aliases to `a`. + +Here's an example of the subtle bugs you can introduce with big.Int's API: +https://play.golang.org/p/x2R_78pa8r + +In contrast, it's difficult to make such mistakes with decimal. Decimals +behave like other go numbers types: even though `a = b` will not deep copy +`b` into `a`, it is impossible to modify a Decimal, since all Decimal methods +return new Decimals and do not modify the originals. The downside is that +this causes extra allocations, so Decimal is less performant. My assumption +is that if you're using Decimals, you probably care more about correctness +than performance. + +## License + +The MIT License (MIT) + +This is a heavily modified fork of [fpd.Decimal](https://github.com/oguzbilgic/fpd), which was also released under the MIT License. diff --git a/vendor/github.com/shopspring/decimal/decimal-go.go b/vendor/github.com/shopspring/decimal/decimal-go.go new file mode 100644 index 00000000000..9958d690206 --- /dev/null +++ b/vendor/github.com/shopspring/decimal/decimal-go.go @@ -0,0 +1,415 @@ +// Copyright 2009 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. + +// Multiprecision decimal numbers. +// For floating-point formatting only; not general purpose. +// Only operations are assign and (binary) left/right shift. +// Can do binary floating point in multiprecision decimal precisely +// because 2 divides 10; cannot do decimal floating point +// in multiprecision binary precisely. + +package decimal + +type decimal struct { + d [800]byte // digits, big-endian representation + nd int // number of digits used + dp int // decimal point + neg bool // negative flag + trunc bool // discarded nonzero digits beyond d[:nd] +} + +func (a *decimal) String() string { + n := 10 + a.nd + if a.dp > 0 { + n += a.dp + } + if a.dp < 0 { + n += -a.dp + } + + buf := make([]byte, n) + w := 0 + switch { + case a.nd == 0: + return "0" + + case a.dp <= 0: + // zeros fill space between decimal point and digits + buf[w] = '0' + w++ + buf[w] = '.' + w++ + w += digitZero(buf[w : w+-a.dp]) + w += copy(buf[w:], a.d[0:a.nd]) + + case a.dp < a.nd: + // decimal point in middle of digits + w += copy(buf[w:], a.d[0:a.dp]) + buf[w] = '.' + w++ + w += copy(buf[w:], a.d[a.dp:a.nd]) + + default: + // zeros fill space between digits and decimal point + w += copy(buf[w:], a.d[0:a.nd]) + w += digitZero(buf[w : w+a.dp-a.nd]) + } + return string(buf[0:w]) +} + +func digitZero(dst []byte) int { + for i := range dst { + dst[i] = '0' + } + return len(dst) +} + +// trim trailing zeros from number. +// (They are meaningless; the decimal point is tracked +// independent of the number of digits.) +func trim(a *decimal) { + for a.nd > 0 && a.d[a.nd-1] == '0' { + a.nd-- + } + if a.nd == 0 { + a.dp = 0 + } +} + +// Assign v to a. +func (a *decimal) Assign(v uint64) { + var buf [24]byte + + // Write reversed decimal in buf. + n := 0 + for v > 0 { + v1 := v / 10 + v -= 10 * v1 + buf[n] = byte(v + '0') + n++ + v = v1 + } + + // Reverse again to produce forward decimal in a.d. + a.nd = 0 + for n--; n >= 0; n-- { + a.d[a.nd] = buf[n] + a.nd++ + } + a.dp = a.nd + trim(a) +} + +// Maximum shift that we can do in one pass without overflow. +// A uint has 32 or 64 bits, and we have to be able to accommodate 9<> 63) +const maxShift = uintSize - 4 + +// Binary shift right (/ 2) by k bits. k <= maxShift to avoid overflow. +func rightShift(a *decimal, k uint) { + r := 0 // read pointer + w := 0 // write pointer + + // Pick up enough leading digits to cover first shift. + var n uint + for ; n>>k == 0; r++ { + if r >= a.nd { + if n == 0 { + // a == 0; shouldn't get here, but handle anyway. + a.nd = 0 + return + } + for n>>k == 0 { + n = n * 10 + r++ + } + break + } + c := uint(a.d[r]) + n = n*10 + c - '0' + } + a.dp -= r - 1 + + var mask uint = (1 << k) - 1 + + // Pick up a digit, put down a digit. + for ; r < a.nd; r++ { + c := uint(a.d[r]) + dig := n >> k + n &= mask + a.d[w] = byte(dig + '0') + w++ + n = n*10 + c - '0' + } + + // Put down extra digits. + for n > 0 { + dig := n >> k + n &= mask + if w < len(a.d) { + a.d[w] = byte(dig + '0') + w++ + } else if dig > 0 { + a.trunc = true + } + n = n * 10 + } + + a.nd = w + trim(a) +} + +// Cheat sheet for left shift: table indexed by shift count giving +// number of new digits that will be introduced by that shift. +// +// For example, leftcheats[4] = {2, "625"}. That means that +// if we are shifting by 4 (multiplying by 16), it will add 2 digits +// when the string prefix is "625" through "999", and one fewer digit +// if the string prefix is "000" through "624". +// +// Credit for this trick goes to Ken. + +type leftCheat struct { + delta int // number of new digits + cutoff string // minus one digit if original < a. +} + +var leftcheats = []leftCheat{ + // Leading digits of 1/2^i = 5^i. + // 5^23 is not an exact 64-bit floating point number, + // so have to use bc for the math. + // Go up to 60 to be large enough for 32bit and 64bit platforms. + /* + seq 60 | sed 's/^/5^/' | bc | + awk 'BEGIN{ print "\t{ 0, \"\" }," } + { + log2 = log(2)/log(10) + printf("\t{ %d, \"%s\" },\t// * %d\n", + int(log2*NR+1), $0, 2**NR) + }' + */ + {0, ""}, + {1, "5"}, // * 2 + {1, "25"}, // * 4 + {1, "125"}, // * 8 + {2, "625"}, // * 16 + {2, "3125"}, // * 32 + {2, "15625"}, // * 64 + {3, "78125"}, // * 128 + {3, "390625"}, // * 256 + {3, "1953125"}, // * 512 + {4, "9765625"}, // * 1024 + {4, "48828125"}, // * 2048 + {4, "244140625"}, // * 4096 + {4, "1220703125"}, // * 8192 + {5, "6103515625"}, // * 16384 + {5, "30517578125"}, // * 32768 + {5, "152587890625"}, // * 65536 + {6, "762939453125"}, // * 131072 + {6, "3814697265625"}, // * 262144 + {6, "19073486328125"}, // * 524288 + {7, "95367431640625"}, // * 1048576 + {7, "476837158203125"}, // * 2097152 + {7, "2384185791015625"}, // * 4194304 + {7, "11920928955078125"}, // * 8388608 + {8, "59604644775390625"}, // * 16777216 + {8, "298023223876953125"}, // * 33554432 + {8, "1490116119384765625"}, // * 67108864 + {9, "7450580596923828125"}, // * 134217728 + {9, "37252902984619140625"}, // * 268435456 + {9, "186264514923095703125"}, // * 536870912 + {10, "931322574615478515625"}, // * 1073741824 + {10, "4656612873077392578125"}, // * 2147483648 + {10, "23283064365386962890625"}, // * 4294967296 + {10, "116415321826934814453125"}, // * 8589934592 + {11, "582076609134674072265625"}, // * 17179869184 + {11, "2910383045673370361328125"}, // * 34359738368 + {11, "14551915228366851806640625"}, // * 68719476736 + {12, "72759576141834259033203125"}, // * 137438953472 + {12, "363797880709171295166015625"}, // * 274877906944 + {12, "1818989403545856475830078125"}, // * 549755813888 + {13, "9094947017729282379150390625"}, // * 1099511627776 + {13, "45474735088646411895751953125"}, // * 2199023255552 + {13, "227373675443232059478759765625"}, // * 4398046511104 + {13, "1136868377216160297393798828125"}, // * 8796093022208 + {14, "5684341886080801486968994140625"}, // * 17592186044416 + {14, "28421709430404007434844970703125"}, // * 35184372088832 + {14, "142108547152020037174224853515625"}, // * 70368744177664 + {15, "710542735760100185871124267578125"}, // * 140737488355328 + {15, "3552713678800500929355621337890625"}, // * 281474976710656 + {15, "17763568394002504646778106689453125"}, // * 562949953421312 + {16, "88817841970012523233890533447265625"}, // * 1125899906842624 + {16, "444089209850062616169452667236328125"}, // * 2251799813685248 + {16, "2220446049250313080847263336181640625"}, // * 4503599627370496 + {16, "11102230246251565404236316680908203125"}, // * 9007199254740992 + {17, "55511151231257827021181583404541015625"}, // * 18014398509481984 + {17, "277555756156289135105907917022705078125"}, // * 36028797018963968 + {17, "1387778780781445675529539585113525390625"}, // * 72057594037927936 + {18, "6938893903907228377647697925567626953125"}, // * 144115188075855872 + {18, "34694469519536141888238489627838134765625"}, // * 288230376151711744 + {18, "173472347597680709441192448139190673828125"}, // * 576460752303423488 + {19, "867361737988403547205962240695953369140625"}, // * 1152921504606846976 +} + +// Is the leading prefix of b lexicographically less than s? +func prefixIsLessThan(b []byte, s string) bool { + for i := 0; i < len(s); i++ { + if i >= len(b) { + return true + } + if b[i] != s[i] { + return b[i] < s[i] + } + } + return false +} + +// Binary shift left (* 2) by k bits. k <= maxShift to avoid overflow. +func leftShift(a *decimal, k uint) { + delta := leftcheats[k].delta + if prefixIsLessThan(a.d[0:a.nd], leftcheats[k].cutoff) { + delta-- + } + + r := a.nd // read index + w := a.nd + delta // write index + + // Pick up a digit, put down a digit. + var n uint + for r--; r >= 0; r-- { + n += (uint(a.d[r]) - '0') << k + quo := n / 10 + rem := n - 10*quo + w-- + if w < len(a.d) { + a.d[w] = byte(rem + '0') + } else if rem != 0 { + a.trunc = true + } + n = quo + } + + // Put down extra digits. + for n > 0 { + quo := n / 10 + rem := n - 10*quo + w-- + if w < len(a.d) { + a.d[w] = byte(rem + '0') + } else if rem != 0 { + a.trunc = true + } + n = quo + } + + a.nd += delta + if a.nd >= len(a.d) { + a.nd = len(a.d) + } + a.dp += delta + trim(a) +} + +// Binary shift left (k > 0) or right (k < 0). +func (a *decimal) Shift(k int) { + switch { + case a.nd == 0: + // nothing to do: a == 0 + case k > 0: + for k > maxShift { + leftShift(a, maxShift) + k -= maxShift + } + leftShift(a, uint(k)) + case k < 0: + for k < -maxShift { + rightShift(a, maxShift) + k += maxShift + } + rightShift(a, uint(-k)) + } +} + +// If we chop a at nd digits, should we round up? +func shouldRoundUp(a *decimal, nd int) bool { + if nd < 0 || nd >= a.nd { + return false + } + if a.d[nd] == '5' && nd+1 == a.nd { // exactly halfway - round to even + // if we truncated, a little higher than what's recorded - always round up + if a.trunc { + return true + } + return nd > 0 && (a.d[nd-1]-'0')%2 != 0 + } + // not halfway - digit tells all + return a.d[nd] >= '5' +} + +// Round a to nd digits (or fewer). +// If nd is zero, it means we're rounding +// just to the left of the digits, as in +// 0.09 -> 0.1. +func (a *decimal) Round(nd int) { + if nd < 0 || nd >= a.nd { + return + } + if shouldRoundUp(a, nd) { + a.RoundUp(nd) + } else { + a.RoundDown(nd) + } +} + +// Round a down to nd digits (or fewer). +func (a *decimal) RoundDown(nd int) { + if nd < 0 || nd >= a.nd { + return + } + a.nd = nd + trim(a) +} + +// Round a up to nd digits (or fewer). +func (a *decimal) RoundUp(nd int) { + if nd < 0 || nd >= a.nd { + return + } + + // round up + for i := nd - 1; i >= 0; i-- { + c := a.d[i] + if c < '9' { // can stop after this digit + a.d[i]++ + a.nd = i + 1 + return + } + } + + // Number is all 9s. + // Change to single 1 with adjusted decimal point. + a.d[0] = '1' + a.nd = 1 + a.dp++ +} + +// Extract integer part, rounded appropriately. +// No guarantees about overflow. +func (a *decimal) RoundedInteger() uint64 { + if a.dp > 20 { + return 0xFFFFFFFFFFFFFFFF + } + var i int + n := uint64(0) + for i = 0; i < a.dp && i < a.nd; i++ { + n = n*10 + uint64(a.d[i]-'0') + } + for ; i < a.dp; i++ { + n *= 10 + } + if shouldRoundUp(a, a.dp) { + n++ + } + return n +} diff --git a/vendor/github.com/shopspring/decimal/decimal.go b/vendor/github.com/shopspring/decimal/decimal.go new file mode 100644 index 00000000000..84405ec1cf0 --- /dev/null +++ b/vendor/github.com/shopspring/decimal/decimal.go @@ -0,0 +1,1904 @@ +// Package decimal implements an arbitrary precision fixed-point decimal. +// +// The zero-value of a Decimal is 0, as you would expect. +// +// The best way to create a new Decimal is to use decimal.NewFromString, ex: +// +// n, err := decimal.NewFromString("-123.4567") +// n.String() // output: "-123.4567" +// +// To use Decimal as part of a struct: +// +// type Struct struct { +// Number Decimal +// } +// +// Note: This can "only" represent numbers with a maximum of 2^31 digits after the decimal point. +package decimal + +import ( + "database/sql/driver" + "encoding/binary" + "fmt" + "math" + "math/big" + "regexp" + "strconv" + "strings" +) + +// DivisionPrecision is the number of decimal places in the result when it +// doesn't divide exactly. +// +// Example: +// +// d1 := decimal.NewFromFloat(2).Div(decimal.NewFromFloat(3)) +// d1.String() // output: "0.6666666666666667" +// d2 := decimal.NewFromFloat(2).Div(decimal.NewFromFloat(30000)) +// d2.String() // output: "0.0000666666666667" +// d3 := decimal.NewFromFloat(20000).Div(decimal.NewFromFloat(3)) +// d3.String() // output: "6666.6666666666666667" +// decimal.DivisionPrecision = 3 +// d4 := decimal.NewFromFloat(2).Div(decimal.NewFromFloat(3)) +// d4.String() // output: "0.667" +// +var DivisionPrecision = 16 + +// MarshalJSONWithoutQuotes should be set to true if you want the decimal to +// be JSON marshaled as a number, instead of as a string. +// WARNING: this is dangerous for decimals with many digits, since many JSON +// unmarshallers (ex: Javascript's) will unmarshal JSON numbers to IEEE 754 +// double-precision floating point numbers, which means you can potentially +// silently lose precision. +var MarshalJSONWithoutQuotes = false + +// ExpMaxIterations specifies the maximum number of iterations needed to calculate +// precise natural exponent value using ExpHullAbrham method. +var ExpMaxIterations = 1000 + +// Zero constant, to make computations faster. +// Zero should never be compared with == or != directly, please use decimal.Equal or decimal.Cmp instead. +var Zero = New(0, 1) + +var zeroInt = big.NewInt(0) +var oneInt = big.NewInt(1) +var twoInt = big.NewInt(2) +var fourInt = big.NewInt(4) +var fiveInt = big.NewInt(5) +var tenInt = big.NewInt(10) +var twentyInt = big.NewInt(20) + +var factorials = []Decimal{New(1, 0)} + +// Decimal represents a fixed-point decimal. It is immutable. +// number = value * 10 ^ exp +type Decimal struct { + value *big.Int + + // NOTE(vadim): this must be an int32, because we cast it to float64 during + // calculations. If exp is 64 bit, we might lose precision. + // If we cared about being able to represent every possible decimal, we + // could make exp a *big.Int but it would hurt performance and numbers + // like that are unrealistic. + exp int32 +} + +// New returns a new fixed-point decimal, value * 10 ^ exp. +func New(value int64, exp int32) Decimal { + return Decimal{ + value: big.NewInt(value), + exp: exp, + } +} + +// NewFromInt converts a int64 to Decimal. +// +// Example: +// +// NewFromInt(123).String() // output: "123" +// NewFromInt(-10).String() // output: "-10" +func NewFromInt(value int64) Decimal { + return Decimal{ + value: big.NewInt(value), + exp: 0, + } +} + +// NewFromInt32 converts a int32 to Decimal. +// +// Example: +// +// NewFromInt(123).String() // output: "123" +// NewFromInt(-10).String() // output: "-10" +func NewFromInt32(value int32) Decimal { + return Decimal{ + value: big.NewInt(int64(value)), + exp: 0, + } +} + +// NewFromBigInt returns a new Decimal from a big.Int, value * 10 ^ exp +func NewFromBigInt(value *big.Int, exp int32) Decimal { + return Decimal{ + value: new(big.Int).Set(value), + exp: exp, + } +} + +// NewFromString returns a new Decimal from a string representation. +// Trailing zeroes are not trimmed. +// +// Example: +// +// d, err := NewFromString("-123.45") +// d2, err := NewFromString(".0001") +// d3, err := NewFromString("1.47000") +// +func NewFromString(value string) (Decimal, error) { + originalInput := value + var intString string + var exp int64 + + // Check if number is using scientific notation + eIndex := strings.IndexAny(value, "Ee") + if eIndex != -1 { + expInt, err := strconv.ParseInt(value[eIndex+1:], 10, 32) + if err != nil { + if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange { + return Decimal{}, fmt.Errorf("can't convert %s to decimal: fractional part too long", value) + } + return Decimal{}, fmt.Errorf("can't convert %s to decimal: exponent is not numeric", value) + } + value = value[:eIndex] + exp = expInt + } + + pIndex := -1 + vLen := len(value) + for i := 0; i < vLen; i++ { + if value[i] == '.' { + if pIndex > -1 { + return Decimal{}, fmt.Errorf("can't convert %s to decimal: too many .s", value) + } + pIndex = i + } + } + + if pIndex == -1 { + // There is no decimal point, we can just parse the original string as + // an int + intString = value + } else { + if pIndex+1 < vLen { + intString = value[:pIndex] + value[pIndex+1:] + } else { + intString = value[:pIndex] + } + expInt := -len(value[pIndex+1:]) + exp += int64(expInt) + } + + var dValue *big.Int + // strconv.ParseInt is faster than new(big.Int).SetString so this is just a shortcut for strings we know won't overflow + if len(intString) <= 18 { + parsed64, err := strconv.ParseInt(intString, 10, 64) + if err != nil { + return Decimal{}, fmt.Errorf("can't convert %s to decimal", value) + } + dValue = big.NewInt(parsed64) + } else { + dValue = new(big.Int) + _, ok := dValue.SetString(intString, 10) + if !ok { + return Decimal{}, fmt.Errorf("can't convert %s to decimal", value) + } + } + + if exp < math.MinInt32 || exp > math.MaxInt32 { + // NOTE(vadim): I doubt a string could realistically be this long + return Decimal{}, fmt.Errorf("can't convert %s to decimal: fractional part too long", originalInput) + } + + return Decimal{ + value: dValue, + exp: int32(exp), + }, nil +} + +// NewFromFormattedString returns a new Decimal from a formatted string representation. +// The second argument - replRegexp, is a regular expression that is used to find characters that should be +// removed from given decimal string representation. All matched characters will be replaced with an empty string. +// +// Example: +// +// r := regexp.MustCompile("[$,]") +// d1, err := NewFromFormattedString("$5,125.99", r) +// +// r2 := regexp.MustCompile("[_]") +// d2, err := NewFromFormattedString("1_000_000", r2) +// +// r3 := regexp.MustCompile("[USD\\s]") +// d3, err := NewFromFormattedString("5000 USD", r3) +// +func NewFromFormattedString(value string, replRegexp *regexp.Regexp) (Decimal, error) { + parsedValue := replRegexp.ReplaceAllString(value, "") + d, err := NewFromString(parsedValue) + if err != nil { + return Decimal{}, err + } + return d, nil +} + +// RequireFromString returns a new Decimal from a string representation +// or panics if NewFromString would have returned an error. +// +// Example: +// +// d := RequireFromString("-123.45") +// d2 := RequireFromString(".0001") +// +func RequireFromString(value string) Decimal { + dec, err := NewFromString(value) + if err != nil { + panic(err) + } + return dec +} + +// NewFromFloat converts a float64 to Decimal. +// +// The converted number will contain the number of significant digits that can be +// represented in a float with reliable roundtrip. +// This is typically 15 digits, but may be more in some cases. +// See https://www.exploringbinary.com/decimal-precision-of-binary-floating-point-numbers/ for more information. +// +// For slightly faster conversion, use NewFromFloatWithExponent where you can specify the precision in absolute terms. +// +// NOTE: this will panic on NaN, +/-inf +func NewFromFloat(value float64) Decimal { + if value == 0 { + return New(0, 0) + } + return newFromFloat(value, math.Float64bits(value), &float64info) +} + +// NewFromFloat32 converts a float32 to Decimal. +// +// The converted number will contain the number of significant digits that can be +// represented in a float with reliable roundtrip. +// This is typically 6-8 digits depending on the input. +// See https://www.exploringbinary.com/decimal-precision-of-binary-floating-point-numbers/ for more information. +// +// For slightly faster conversion, use NewFromFloatWithExponent where you can specify the precision in absolute terms. +// +// NOTE: this will panic on NaN, +/-inf +func NewFromFloat32(value float32) Decimal { + if value == 0 { + return New(0, 0) + } + // XOR is workaround for https://github.com/golang/go/issues/26285 + a := math.Float32bits(value) ^ 0x80808080 + return newFromFloat(float64(value), uint64(a)^0x80808080, &float32info) +} + +func newFromFloat(val float64, bits uint64, flt *floatInfo) Decimal { + if math.IsNaN(val) || math.IsInf(val, 0) { + panic(fmt.Sprintf("Cannot create a Decimal from %v", val)) + } + exp := int(bits>>flt.mantbits) & (1<>(flt.expbits+flt.mantbits) != 0 + + roundShortest(&d, mant, exp, flt) + // If less than 19 digits, we can do calculation in an int64. + if d.nd < 19 { + tmp := int64(0) + m := int64(1) + for i := d.nd - 1; i >= 0; i-- { + tmp += m * int64(d.d[i]-'0') + m *= 10 + } + if d.neg { + tmp *= -1 + } + return Decimal{value: big.NewInt(tmp), exp: int32(d.dp) - int32(d.nd)} + } + dValue := new(big.Int) + dValue, ok := dValue.SetString(string(d.d[:d.nd]), 10) + if ok { + return Decimal{value: dValue, exp: int32(d.dp) - int32(d.nd)} + } + + return NewFromFloatWithExponent(val, int32(d.dp)-int32(d.nd)) +} + +// NewFromFloatWithExponent converts a float64 to Decimal, with an arbitrary +// number of fractional digits. +// +// Example: +// +// NewFromFloatWithExponent(123.456, -2).String() // output: "123.46" +// +func NewFromFloatWithExponent(value float64, exp int32) Decimal { + if math.IsNaN(value) || math.IsInf(value, 0) { + panic(fmt.Sprintf("Cannot create a Decimal from %v", value)) + } + + bits := math.Float64bits(value) + mant := bits & (1<<52 - 1) + exp2 := int32((bits >> 52) & (1<<11 - 1)) + sign := bits >> 63 + + if exp2 == 0 { + // specials + if mant == 0 { + return Decimal{} + } + // subnormal + exp2++ + } else { + // normal + mant |= 1 << 52 + } + + exp2 -= 1023 + 52 + + // normalizing base-2 values + for mant&1 == 0 { + mant = mant >> 1 + exp2++ + } + + // maximum number of fractional base-10 digits to represent 2^N exactly cannot be more than -N if N<0 + if exp < 0 && exp < exp2 { + if exp2 < 0 { + exp = exp2 + } else { + exp = 0 + } + } + + // representing 10^M * 2^N as 5^M * 2^(M+N) + exp2 -= exp + + temp := big.NewInt(1) + dMant := big.NewInt(int64(mant)) + + // applying 5^M + if exp > 0 { + temp = temp.SetInt64(int64(exp)) + temp = temp.Exp(fiveInt, temp, nil) + } else if exp < 0 { + temp = temp.SetInt64(-int64(exp)) + temp = temp.Exp(fiveInt, temp, nil) + dMant = dMant.Mul(dMant, temp) + temp = temp.SetUint64(1) + } + + // applying 2^(M+N) + if exp2 > 0 { + dMant = dMant.Lsh(dMant, uint(exp2)) + } else if exp2 < 0 { + temp = temp.Lsh(temp, uint(-exp2)) + } + + // rounding and downscaling + if exp > 0 || exp2 < 0 { + halfDown := new(big.Int).Rsh(temp, 1) + dMant = dMant.Add(dMant, halfDown) + dMant = dMant.Quo(dMant, temp) + } + + if sign == 1 { + dMant = dMant.Neg(dMant) + } + + return Decimal{ + value: dMant, + exp: exp, + } +} + +// Copy returns a copy of decimal with the same value and exponent, but a different pointer to value. +func (d Decimal) Copy() Decimal { + d.ensureInitialized() + return Decimal{ + value: &(*d.value), + exp: d.exp, + } +} + +// rescale returns a rescaled version of the decimal. Returned +// decimal may be less precise if the given exponent is bigger +// than the initial exponent of the Decimal. +// NOTE: this will truncate, NOT round +// +// Example: +// +// d := New(12345, -4) +// d2 := d.rescale(-1) +// d3 := d2.rescale(-4) +// println(d1) +// println(d2) +// println(d3) +// +// Output: +// +// 1.2345 +// 1.2 +// 1.2000 +// +func (d Decimal) rescale(exp int32) Decimal { + d.ensureInitialized() + + if d.exp == exp { + return Decimal{ + new(big.Int).Set(d.value), + d.exp, + } + } + + // NOTE(vadim): must convert exps to float64 before - to prevent overflow + diff := math.Abs(float64(exp) - float64(d.exp)) + value := new(big.Int).Set(d.value) + + expScale := new(big.Int).Exp(tenInt, big.NewInt(int64(diff)), nil) + if exp > d.exp { + value = value.Quo(value, expScale) + } else if exp < d.exp { + value = value.Mul(value, expScale) + } + + return Decimal{ + value: value, + exp: exp, + } +} + +// Abs returns the absolute value of the decimal. +func (d Decimal) Abs() Decimal { + if !d.IsNegative() { + return d + } + d.ensureInitialized() + d2Value := new(big.Int).Abs(d.value) + return Decimal{ + value: d2Value, + exp: d.exp, + } +} + +// Add returns d + d2. +func (d Decimal) Add(d2 Decimal) Decimal { + rd, rd2 := RescalePair(d, d2) + + d3Value := new(big.Int).Add(rd.value, rd2.value) + return Decimal{ + value: d3Value, + exp: rd.exp, + } +} + +// Sub returns d - d2. +func (d Decimal) Sub(d2 Decimal) Decimal { + rd, rd2 := RescalePair(d, d2) + + d3Value := new(big.Int).Sub(rd.value, rd2.value) + return Decimal{ + value: d3Value, + exp: rd.exp, + } +} + +// Neg returns -d. +func (d Decimal) Neg() Decimal { + d.ensureInitialized() + val := new(big.Int).Neg(d.value) + return Decimal{ + value: val, + exp: d.exp, + } +} + +// Mul returns d * d2. +func (d Decimal) Mul(d2 Decimal) Decimal { + d.ensureInitialized() + d2.ensureInitialized() + + expInt64 := int64(d.exp) + int64(d2.exp) + if expInt64 > math.MaxInt32 || expInt64 < math.MinInt32 { + // NOTE(vadim): better to panic than give incorrect results, as + // Decimals are usually used for money + panic(fmt.Sprintf("exponent %v overflows an int32!", expInt64)) + } + + d3Value := new(big.Int).Mul(d.value, d2.value) + return Decimal{ + value: d3Value, + exp: int32(expInt64), + } +} + +// Shift shifts the decimal in base 10. +// It shifts left when shift is positive and right if shift is negative. +// In simpler terms, the given value for shift is added to the exponent +// of the decimal. +func (d Decimal) Shift(shift int32) Decimal { + d.ensureInitialized() + return Decimal{ + value: new(big.Int).Set(d.value), + exp: d.exp + shift, + } +} + +// Div returns d / d2. If it doesn't divide exactly, the result will have +// DivisionPrecision digits after the decimal point. +func (d Decimal) Div(d2 Decimal) Decimal { + return d.DivRound(d2, int32(DivisionPrecision)) +} + +// QuoRem does divsion with remainder +// d.QuoRem(d2,precision) returns quotient q and remainder r such that +// d = d2 * q + r, q an integer multiple of 10^(-precision) +// 0 <= r < abs(d2) * 10 ^(-precision) if d>=0 +// 0 >= r > -abs(d2) * 10 ^(-precision) if d<0 +// Note that precision<0 is allowed as input. +func (d Decimal) QuoRem(d2 Decimal, precision int32) (Decimal, Decimal) { + d.ensureInitialized() + d2.ensureInitialized() + if d2.value.Sign() == 0 { + panic("decimal division by 0") + } + scale := -precision + e := int64(d.exp - d2.exp - scale) + if e > math.MaxInt32 || e < math.MinInt32 { + panic("overflow in decimal QuoRem") + } + var aa, bb, expo big.Int + var scalerest int32 + // d = a 10^ea + // d2 = b 10^eb + if e < 0 { + aa = *d.value + expo.SetInt64(-e) + bb.Exp(tenInt, &expo, nil) + bb.Mul(d2.value, &bb) + scalerest = d.exp + // now aa = a + // bb = b 10^(scale + eb - ea) + } else { + expo.SetInt64(e) + aa.Exp(tenInt, &expo, nil) + aa.Mul(d.value, &aa) + bb = *d2.value + scalerest = scale + d2.exp + // now aa = a ^ (ea - eb - scale) + // bb = b + } + var q, r big.Int + q.QuoRem(&aa, &bb, &r) + dq := Decimal{value: &q, exp: scale} + dr := Decimal{value: &r, exp: scalerest} + return dq, dr +} + +// DivRound divides and rounds to a given precision +// i.e. to an integer multiple of 10^(-precision) +// for a positive quotient digit 5 is rounded up, away from 0 +// if the quotient is negative then digit 5 is rounded down, away from 0 +// Note that precision<0 is allowed as input. +func (d Decimal) DivRound(d2 Decimal, precision int32) Decimal { + // QuoRem already checks initialization + q, r := d.QuoRem(d2, precision) + // the actual rounding decision is based on comparing r*10^precision and d2/2 + // instead compare 2 r 10 ^precision and d2 + var rv2 big.Int + rv2.Abs(r.value) + rv2.Lsh(&rv2, 1) + // now rv2 = abs(r.value) * 2 + r2 := Decimal{value: &rv2, exp: r.exp + precision} + // r2 is now 2 * r * 10 ^ precision + var c = r2.Cmp(d2.Abs()) + + if c < 0 { + return q + } + + if d.value.Sign()*d2.value.Sign() < 0 { + return q.Sub(New(1, -precision)) + } + + return q.Add(New(1, -precision)) +} + +// Mod returns d % d2. +func (d Decimal) Mod(d2 Decimal) Decimal { + quo := d.Div(d2).Truncate(0) + return d.Sub(d2.Mul(quo)) +} + +// Pow returns d to the power d2 +func (d Decimal) Pow(d2 Decimal) Decimal { + var temp Decimal + if d2.IntPart() == 0 { + return NewFromFloat(1) + } + temp = d.Pow(d2.Div(NewFromFloat(2))) + if d2.IntPart()%2 == 0 { + return temp.Mul(temp) + } + if d2.IntPart() > 0 { + return temp.Mul(temp).Mul(d) + } + return temp.Mul(temp).Div(d) +} + +// ExpHullAbrham calculates the natural exponent of decimal (e to the power of d) using Hull-Abraham algorithm. +// OverallPrecision argument specifies the overall precision of the result (integer part + decimal part). +// +// ExpHullAbrham is faster than ExpTaylor for small precision values, but it is much slower for large precision values. +// +// Example: +// +// NewFromFloat(26.1).ExpHullAbrham(2).String() // output: "220000000000" +// NewFromFloat(26.1).ExpHullAbrham(20).String() // output: "216314672147.05767284" +// +func (d Decimal) ExpHullAbrham(overallPrecision uint32) (Decimal, error) { + // Algorithm based on Variable precision exponential function. + // ACM Transactions on Mathematical Software by T. E. Hull & A. Abrham. + if d.IsZero() { + return Decimal{oneInt, 0}, nil + } + + currentPrecision := overallPrecision + + // Algorithm does not work if currentPrecision * 23 < |x|. + // Precision is automatically increased in such cases, so the value can be calculated precisely. + // If newly calculated precision is higher than ExpMaxIterations the currentPrecision will not be changed. + f := d.Abs().InexactFloat64() + if ncp := f / 23; ncp > float64(currentPrecision) && ncp < float64(ExpMaxIterations) { + currentPrecision = uint32(math.Ceil(ncp)) + } + + // fail if abs(d) beyond an over/underflow threshold + overflowThreshold := New(23*int64(currentPrecision), 0) + if d.Abs().Cmp(overflowThreshold) > 0 { + return Decimal{}, fmt.Errorf("over/underflow threshold, exp(x) cannot be calculated precisely") + } + + // Return 1 if abs(d) small enough; this also avoids later over/underflow + overflowThreshold2 := New(9, -int32(currentPrecision)-1) + if d.Abs().Cmp(overflowThreshold2) <= 0 { + return Decimal{oneInt, d.exp}, nil + } + + // t is the smallest integer >= 0 such that the corresponding abs(d/k) < 1 + t := d.exp + int32(d.NumDigits()) // Add d.NumDigits because the paper assumes that d.value [0.1, 1) + + if t < 0 { + t = 0 + } + + k := New(1, t) // reduction factor + r := Decimal{new(big.Int).Set(d.value), d.exp - t} // reduced argument + p := int32(currentPrecision) + t + 2 // precision for calculating the sum + + // Determine n, the number of therms for calculating sum + // use first Newton step (1.435p - 1.182) / log10(p/abs(r)) + // for solving appropriate equation, along with directed + // roundings and simple rational bound for log10(p/abs(r)) + rf := r.Abs().InexactFloat64() + pf := float64(p) + nf := math.Ceil((1.453*pf - 1.182) / math.Log10(pf/rf)) + if nf > float64(ExpMaxIterations) || math.IsNaN(nf) { + return Decimal{}, fmt.Errorf("exact value cannot be calculated in <=ExpMaxIterations iterations") + } + n := int64(nf) + + tmp := New(0, 0) + sum := New(1, 0) + one := New(1, 0) + for i := n - 1; i > 0; i-- { + tmp.value.SetInt64(i) + sum = sum.Mul(r.DivRound(tmp, p)) + sum = sum.Add(one) + } + + ki := k.IntPart() + res := New(1, 0) + for i := ki; i > 0; i-- { + res = res.Mul(sum) + } + + resNumDigits := int32(res.NumDigits()) + + var roundDigits int32 + if resNumDigits > abs(res.exp) { + roundDigits = int32(currentPrecision) - resNumDigits - res.exp + } else { + roundDigits = int32(currentPrecision) + } + + res = res.Round(roundDigits) + + return res, nil +} + +// ExpTaylor calculates the natural exponent of decimal (e to the power of d) using Taylor series expansion. +// Precision argument specifies how precise the result must be (number of digits after decimal point). +// Negative precision is allowed. +// +// ExpTaylor is much faster for large precision values than ExpHullAbrham. +// +// Example: +// +// d, err := NewFromFloat(26.1).ExpTaylor(2).String() +// d.String() // output: "216314672147.06" +// +// NewFromFloat(26.1).ExpTaylor(20).String() +// d.String() // output: "216314672147.05767284062928674083" +// +// NewFromFloat(26.1).ExpTaylor(-10).String() +// d.String() // output: "220000000000" +// +func (d Decimal) ExpTaylor(precision int32) (Decimal, error) { + // Note(mwoss): Implementation can be optimized by exclusively using big.Int API only + if d.IsZero() { + return Decimal{oneInt, 0}.Round(precision), nil + } + + var epsilon Decimal + var divPrecision int32 + if precision < 0 { + epsilon = New(1, -1) + divPrecision = 8 + } else { + epsilon = New(1, -precision-1) + divPrecision = precision + 1 + } + + decAbs := d.Abs() + pow := d.Abs() + factorial := New(1, 0) + + result := New(1, 0) + + for i := int64(1); ; { + step := pow.DivRound(factorial, divPrecision) + result = result.Add(step) + + // Stop Taylor series when current step is smaller than epsilon + if step.Cmp(epsilon) < 0 { + break + } + + pow = pow.Mul(decAbs) + + i++ + + // Calculate next factorial number or retrieve cached value + if len(factorials) >= int(i) && !factorials[i-1].IsZero() { + factorial = factorials[i-1] + } else { + // To avoid any race conditions, firstly the zero value is appended to a slice to create + // a spot for newly calculated factorial. After that, the zero value is replaced by calculated + // factorial using the index notation. + factorial = factorials[i-2].Mul(New(i, 0)) + factorials = append(factorials, Zero) + factorials[i-1] = factorial + } + } + + if d.Sign() < 0 { + result = New(1, 0).DivRound(result, precision+1) + } + + result = result.Round(precision) + return result, nil +} + +// NumDigits returns the number of digits of the decimal coefficient (d.Value) +// Note: Current implementation is extremely slow for large decimals and/or decimals with large fractional part +func (d Decimal) NumDigits() int { + // Note(mwoss): It can be optimized, unnecessary cast of big.Int to string + if d.IsNegative() { + return len(d.value.String()) - 1 + } + return len(d.value.String()) +} + +// IsInteger returns true when decimal can be represented as an integer value, otherwise, it returns false. +func (d Decimal) IsInteger() bool { + // The most typical case, all decimal with exponent higher or equal 0 can be represented as integer + if d.exp >= 0 { + return true + } + // When the exponent is negative we have to check every number after the decimal place + // If all of them are zeroes, we are sure that given decimal can be represented as an integer + var r big.Int + q := new(big.Int).Set(d.value) + for z := abs(d.exp); z > 0; z-- { + q.QuoRem(q, tenInt, &r) + if r.Cmp(zeroInt) != 0 { + return false + } + } + return true +} + +// Abs calculates absolute value of any int32. Used for calculating absolute value of decimal's exponent. +func abs(n int32) int32 { + if n < 0 { + return -n + } + return n +} + +// Cmp compares the numbers represented by d and d2 and returns: +// +// -1 if d < d2 +// 0 if d == d2 +// +1 if d > d2 +// +func (d Decimal) Cmp(d2 Decimal) int { + d.ensureInitialized() + d2.ensureInitialized() + + if d.exp == d2.exp { + return d.value.Cmp(d2.value) + } + + rd, rd2 := RescalePair(d, d2) + + return rd.value.Cmp(rd2.value) +} + +// Equal returns whether the numbers represented by d and d2 are equal. +func (d Decimal) Equal(d2 Decimal) bool { + return d.Cmp(d2) == 0 +} + +// Equals is deprecated, please use Equal method instead +func (d Decimal) Equals(d2 Decimal) bool { + return d.Equal(d2) +} + +// GreaterThan (GT) returns true when d is greater than d2. +func (d Decimal) GreaterThan(d2 Decimal) bool { + return d.Cmp(d2) == 1 +} + +// GreaterThanOrEqual (GTE) returns true when d is greater than or equal to d2. +func (d Decimal) GreaterThanOrEqual(d2 Decimal) bool { + cmp := d.Cmp(d2) + return cmp == 1 || cmp == 0 +} + +// LessThan (LT) returns true when d is less than d2. +func (d Decimal) LessThan(d2 Decimal) bool { + return d.Cmp(d2) == -1 +} + +// LessThanOrEqual (LTE) returns true when d is less than or equal to d2. +func (d Decimal) LessThanOrEqual(d2 Decimal) bool { + cmp := d.Cmp(d2) + return cmp == -1 || cmp == 0 +} + +// Sign returns: +// +// -1 if d < 0 +// 0 if d == 0 +// +1 if d > 0 +// +func (d Decimal) Sign() int { + if d.value == nil { + return 0 + } + return d.value.Sign() +} + +// IsPositive return +// +// true if d > 0 +// false if d == 0 +// false if d < 0 +func (d Decimal) IsPositive() bool { + return d.Sign() == 1 +} + +// IsNegative return +// +// true if d < 0 +// false if d == 0 +// false if d > 0 +func (d Decimal) IsNegative() bool { + return d.Sign() == -1 +} + +// IsZero return +// +// true if d == 0 +// false if d > 0 +// false if d < 0 +func (d Decimal) IsZero() bool { + return d.Sign() == 0 +} + +// Exponent returns the exponent, or scale component of the decimal. +func (d Decimal) Exponent() int32 { + return d.exp +} + +// Coefficient returns the coefficient of the decimal. It is scaled by 10^Exponent() +func (d Decimal) Coefficient() *big.Int { + d.ensureInitialized() + // we copy the coefficient so that mutating the result does not mutate the Decimal. + return new(big.Int).Set(d.value) +} + +// CoefficientInt64 returns the coefficient of the decimal as int64. It is scaled by 10^Exponent() +// If coefficient cannot be represented in an int64, the result will be undefined. +func (d Decimal) CoefficientInt64() int64 { + d.ensureInitialized() + return d.value.Int64() +} + +// IntPart returns the integer component of the decimal. +func (d Decimal) IntPart() int64 { + scaledD := d.rescale(0) + return scaledD.value.Int64() +} + +// BigInt returns integer component of the decimal as a BigInt. +func (d Decimal) BigInt() *big.Int { + scaledD := d.rescale(0) + i := &big.Int{} + i.SetString(scaledD.String(), 10) + return i +} + +// BigFloat returns decimal as BigFloat. +// Be aware that casting decimal to BigFloat might cause a loss of precision. +func (d Decimal) BigFloat() *big.Float { + f := &big.Float{} + f.SetString(d.String()) + return f +} + +// Rat returns a rational number representation of the decimal. +func (d Decimal) Rat() *big.Rat { + d.ensureInitialized() + if d.exp <= 0 { + // NOTE(vadim): must negate after casting to prevent int32 overflow + denom := new(big.Int).Exp(tenInt, big.NewInt(-int64(d.exp)), nil) + return new(big.Rat).SetFrac(d.value, denom) + } + + mul := new(big.Int).Exp(tenInt, big.NewInt(int64(d.exp)), nil) + num := new(big.Int).Mul(d.value, mul) + return new(big.Rat).SetFrac(num, oneInt) +} + +// Float64 returns the nearest float64 value for d and a bool indicating +// whether f represents d exactly. +// For more details, see the documentation for big.Rat.Float64 +func (d Decimal) Float64() (f float64, exact bool) { + return d.Rat().Float64() +} + +// InexactFloat64 returns the nearest float64 value for d. +// It doesn't indicate if the returned value represents d exactly. +func (d Decimal) InexactFloat64() float64 { + f, _ := d.Float64() + return f +} + +// String returns the string representation of the decimal +// with the fixed point. +// +// Example: +// +// d := New(-12345, -3) +// println(d.String()) +// +// Output: +// +// -12.345 +// +func (d Decimal) String() string { + return d.string(true) +} + +// StringFixed returns a rounded fixed-point string with places digits after +// the decimal point. +// +// Example: +// +// NewFromFloat(0).StringFixed(2) // output: "0.00" +// NewFromFloat(0).StringFixed(0) // output: "0" +// NewFromFloat(5.45).StringFixed(0) // output: "5" +// NewFromFloat(5.45).StringFixed(1) // output: "5.5" +// NewFromFloat(5.45).StringFixed(2) // output: "5.45" +// NewFromFloat(5.45).StringFixed(3) // output: "5.450" +// NewFromFloat(545).StringFixed(-1) // output: "550" +// +func (d Decimal) StringFixed(places int32) string { + rounded := d.Round(places) + return rounded.string(false) +} + +// StringFixedBank returns a banker rounded fixed-point string with places digits +// after the decimal point. +// +// Example: +// +// NewFromFloat(0).StringFixedBank(2) // output: "0.00" +// NewFromFloat(0).StringFixedBank(0) // output: "0" +// NewFromFloat(5.45).StringFixedBank(0) // output: "5" +// NewFromFloat(5.45).StringFixedBank(1) // output: "5.4" +// NewFromFloat(5.45).StringFixedBank(2) // output: "5.45" +// NewFromFloat(5.45).StringFixedBank(3) // output: "5.450" +// NewFromFloat(545).StringFixedBank(-1) // output: "540" +// +func (d Decimal) StringFixedBank(places int32) string { + rounded := d.RoundBank(places) + return rounded.string(false) +} + +// StringFixedCash returns a Swedish/Cash rounded fixed-point string. For +// more details see the documentation at function RoundCash. +func (d Decimal) StringFixedCash(interval uint8) string { + rounded := d.RoundCash(interval) + return rounded.string(false) +} + +// Round rounds the decimal to places decimal places. +// If places < 0, it will round the integer part to the nearest 10^(-places). +// +// Example: +// +// NewFromFloat(5.45).Round(1).String() // output: "5.5" +// NewFromFloat(545).Round(-1).String() // output: "550" +// +func (d Decimal) Round(places int32) Decimal { + if d.exp == -places { + return d + } + // truncate to places + 1 + ret := d.rescale(-places - 1) + + // add sign(d) * 0.5 + if ret.value.Sign() < 0 { + ret.value.Sub(ret.value, fiveInt) + } else { + ret.value.Add(ret.value, fiveInt) + } + + // floor for positive numbers, ceil for negative numbers + _, m := ret.value.DivMod(ret.value, tenInt, new(big.Int)) + ret.exp++ + if ret.value.Sign() < 0 && m.Cmp(zeroInt) != 0 { + ret.value.Add(ret.value, oneInt) + } + + return ret +} + +// RoundCeil rounds the decimal towards +infinity. +// +// Example: +// +// NewFromFloat(545).RoundCeil(-2).String() // output: "600" +// NewFromFloat(500).RoundCeil(-2).String() // output: "500" +// NewFromFloat(1.1001).RoundCeil(2).String() // output: "1.11" +// NewFromFloat(-1.454).RoundCeil(1).String() // output: "-1.5" +// +func (d Decimal) RoundCeil(places int32) Decimal { + if d.exp >= -places { + return d + } + + rescaled := d.rescale(-places) + if d.Equal(rescaled) { + return d + } + + if d.value.Sign() > 0 { + rescaled.value.Add(rescaled.value, oneInt) + } + + return rescaled +} + +// RoundFloor rounds the decimal towards -infinity. +// +// Example: +// +// NewFromFloat(545).RoundFloor(-2).String() // output: "500" +// NewFromFloat(-500).RoundFloor(-2).String() // output: "-500" +// NewFromFloat(1.1001).RoundFloor(2).String() // output: "1.1" +// NewFromFloat(-1.454).RoundFloor(1).String() // output: "-1.4" +// +func (d Decimal) RoundFloor(places int32) Decimal { + if d.exp >= -places { + return d + } + + rescaled := d.rescale(-places) + if d.Equal(rescaled) { + return d + } + + if d.value.Sign() < 0 { + rescaled.value.Sub(rescaled.value, oneInt) + } + + return rescaled +} + +// RoundUp rounds the decimal away from zero. +// +// Example: +// +// NewFromFloat(545).RoundUp(-2).String() // output: "600" +// NewFromFloat(500).RoundUp(-2).String() // output: "500" +// NewFromFloat(1.1001).RoundUp(2).String() // output: "1.11" +// NewFromFloat(-1.454).RoundUp(1).String() // output: "-1.4" +// +func (d Decimal) RoundUp(places int32) Decimal { + if d.exp >= -places { + return d + } + + rescaled := d.rescale(-places) + if d.Equal(rescaled) { + return d + } + + if d.value.Sign() > 0 { + rescaled.value.Add(rescaled.value, oneInt) + } else if d.value.Sign() < 0 { + rescaled.value.Sub(rescaled.value, oneInt) + } + + return rescaled +} + +// RoundDown rounds the decimal towards zero. +// +// Example: +// +// NewFromFloat(545).RoundDown(-2).String() // output: "500" +// NewFromFloat(-500).RoundDown(-2).String() // output: "-500" +// NewFromFloat(1.1001).RoundDown(2).String() // output: "1.1" +// NewFromFloat(-1.454).RoundDown(1).String() // output: "-1.5" +// +func (d Decimal) RoundDown(places int32) Decimal { + if d.exp >= -places { + return d + } + + rescaled := d.rescale(-places) + if d.Equal(rescaled) { + return d + } + return rescaled +} + +// RoundBank rounds the decimal to places decimal places. +// If the final digit to round is equidistant from the nearest two integers the +// rounded value is taken as the even number +// +// If places < 0, it will round the integer part to the nearest 10^(-places). +// +// Examples: +// +// NewFromFloat(5.45).RoundBank(1).String() // output: "5.4" +// NewFromFloat(545).RoundBank(-1).String() // output: "540" +// NewFromFloat(5.46).RoundBank(1).String() // output: "5.5" +// NewFromFloat(546).RoundBank(-1).String() // output: "550" +// NewFromFloat(5.55).RoundBank(1).String() // output: "5.6" +// NewFromFloat(555).RoundBank(-1).String() // output: "560" +// +func (d Decimal) RoundBank(places int32) Decimal { + + round := d.Round(places) + remainder := d.Sub(round).Abs() + + half := New(5, -places-1) + if remainder.Cmp(half) == 0 && round.value.Bit(0) != 0 { + if round.value.Sign() < 0 { + round.value.Add(round.value, oneInt) + } else { + round.value.Sub(round.value, oneInt) + } + } + + return round +} + +// RoundCash aka Cash/Penny/öre rounding rounds decimal to a specific +// interval. The amount payable for a cash transaction is rounded to the nearest +// multiple of the minimum currency unit available. The following intervals are +// available: 5, 10, 25, 50 and 100; any other number throws a panic. +// 5: 5 cent rounding 3.43 => 3.45 +// 10: 10 cent rounding 3.45 => 3.50 (5 gets rounded up) +// 25: 25 cent rounding 3.41 => 3.50 +// 50: 50 cent rounding 3.75 => 4.00 +// 100: 100 cent rounding 3.50 => 4.00 +// For more details: https://en.wikipedia.org/wiki/Cash_rounding +func (d Decimal) RoundCash(interval uint8) Decimal { + var iVal *big.Int + switch interval { + case 5: + iVal = twentyInt + case 10: + iVal = tenInt + case 25: + iVal = fourInt + case 50: + iVal = twoInt + case 100: + iVal = oneInt + default: + panic(fmt.Sprintf("Decimal does not support this Cash rounding interval `%d`. Supported: 5, 10, 25, 50, 100", interval)) + } + dVal := Decimal{ + value: iVal, + } + + // TODO: optimize those calculations to reduce the high allocations (~29 allocs). + return d.Mul(dVal).Round(0).Div(dVal).Truncate(2) +} + +// Floor returns the nearest integer value less than or equal to d. +func (d Decimal) Floor() Decimal { + d.ensureInitialized() + + if d.exp >= 0 { + return d + } + + exp := big.NewInt(10) + + // NOTE(vadim): must negate after casting to prevent int32 overflow + exp.Exp(exp, big.NewInt(-int64(d.exp)), nil) + + z := new(big.Int).Div(d.value, exp) + return Decimal{value: z, exp: 0} +} + +// Ceil returns the nearest integer value greater than or equal to d. +func (d Decimal) Ceil() Decimal { + d.ensureInitialized() + + if d.exp >= 0 { + return d + } + + exp := big.NewInt(10) + + // NOTE(vadim): must negate after casting to prevent int32 overflow + exp.Exp(exp, big.NewInt(-int64(d.exp)), nil) + + z, m := new(big.Int).DivMod(d.value, exp, new(big.Int)) + if m.Cmp(zeroInt) != 0 { + z.Add(z, oneInt) + } + return Decimal{value: z, exp: 0} +} + +// Truncate truncates off digits from the number, without rounding. +// +// NOTE: precision is the last digit that will not be truncated (must be >= 0). +// +// Example: +// +// decimal.NewFromString("123.456").Truncate(2).String() // "123.45" +// +func (d Decimal) Truncate(precision int32) Decimal { + d.ensureInitialized() + if precision >= 0 && -precision > d.exp { + return d.rescale(-precision) + } + return d +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (d *Decimal) UnmarshalJSON(decimalBytes []byte) error { + if string(decimalBytes) == "null" { + return nil + } + + str, err := unquoteIfQuoted(decimalBytes) + if err != nil { + return fmt.Errorf("error decoding string '%s': %s", decimalBytes, err) + } + + decimal, err := NewFromString(str) + *d = decimal + if err != nil { + return fmt.Errorf("error decoding string '%s': %s", str, err) + } + return nil +} + +// MarshalJSON implements the json.Marshaler interface. +func (d Decimal) MarshalJSON() ([]byte, error) { + var str string + if MarshalJSONWithoutQuotes { + str = d.String() + } else { + str = "\"" + d.String() + "\"" + } + return []byte(str), nil +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. As a string representation +// is already used when encoding to text, this method stores that string as []byte +func (d *Decimal) UnmarshalBinary(data []byte) error { + // Verify we have at least 4 bytes for the exponent. The GOB encoded value + // may be empty. + if len(data) < 4 { + return fmt.Errorf("error decoding binary %v: expected at least 4 bytes, got %d", data, len(data)) + } + + // Extract the exponent + d.exp = int32(binary.BigEndian.Uint32(data[:4])) + + // Extract the value + d.value = new(big.Int) + if err := d.value.GobDecode(data[4:]); err != nil { + return fmt.Errorf("error decoding binary %v: %s", data, err) + } + + return nil +} + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (d Decimal) MarshalBinary() (data []byte, err error) { + // Write the exponent first since it's a fixed size + v1 := make([]byte, 4) + binary.BigEndian.PutUint32(v1, uint32(d.exp)) + + // Add the value + var v2 []byte + if v2, err = d.value.GobEncode(); err != nil { + return + } + + // Return the byte array + data = append(v1, v2...) + return +} + +// Scan implements the sql.Scanner interface for database deserialization. +func (d *Decimal) Scan(value interface{}) error { + // first try to see if the data is stored in database as a Numeric datatype + switch v := value.(type) { + + case float32: + *d = NewFromFloat(float64(v)) + return nil + + case float64: + // numeric in sqlite3 sends us float64 + *d = NewFromFloat(v) + return nil + + case int64: + // at least in sqlite3 when the value is 0 in db, the data is sent + // to us as an int64 instead of a float64 ... + *d = New(v, 0) + return nil + + default: + // default is trying to interpret value stored as string + str, err := unquoteIfQuoted(v) + if err != nil { + return err + } + *d, err = NewFromString(str) + return err + } +} + +// Value implements the driver.Valuer interface for database serialization. +func (d Decimal) Value() (driver.Value, error) { + return d.String(), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for XML +// deserialization. +func (d *Decimal) UnmarshalText(text []byte) error { + str := string(text) + + dec, err := NewFromString(str) + *d = dec + if err != nil { + return fmt.Errorf("error decoding string '%s': %s", str, err) + } + + return nil +} + +// MarshalText implements the encoding.TextMarshaler interface for XML +// serialization. +func (d Decimal) MarshalText() (text []byte, err error) { + return []byte(d.String()), nil +} + +// GobEncode implements the gob.GobEncoder interface for gob serialization. +func (d Decimal) GobEncode() ([]byte, error) { + return d.MarshalBinary() +} + +// GobDecode implements the gob.GobDecoder interface for gob serialization. +func (d *Decimal) GobDecode(data []byte) error { + return d.UnmarshalBinary(data) +} + +// StringScaled first scales the decimal then calls .String() on it. +// NOTE: buggy, unintuitive, and DEPRECATED! Use StringFixed instead. +func (d Decimal) StringScaled(exp int32) string { + return d.rescale(exp).String() +} + +func (d Decimal) string(trimTrailingZeros bool) string { + if d.exp >= 0 { + return d.rescale(0).value.String() + } + + abs := new(big.Int).Abs(d.value) + str := abs.String() + + var intPart, fractionalPart string + + // NOTE(vadim): this cast to int will cause bugs if d.exp == INT_MIN + // and you are on a 32-bit machine. Won't fix this super-edge case. + dExpInt := int(d.exp) + if len(str) > -dExpInt { + intPart = str[:len(str)+dExpInt] + fractionalPart = str[len(str)+dExpInt:] + } else { + intPart = "0" + + num0s := -dExpInt - len(str) + fractionalPart = strings.Repeat("0", num0s) + str + } + + if trimTrailingZeros { + i := len(fractionalPart) - 1 + for ; i >= 0; i-- { + if fractionalPart[i] != '0' { + break + } + } + fractionalPart = fractionalPart[:i+1] + } + + number := intPart + if len(fractionalPart) > 0 { + number += "." + fractionalPart + } + + if d.value.Sign() < 0 { + return "-" + number + } + + return number +} + +func (d *Decimal) ensureInitialized() { + if d.value == nil { + d.value = new(big.Int) + } +} + +// Min returns the smallest Decimal that was passed in the arguments. +// +// To call this function with an array, you must do: +// +// Min(arr[0], arr[1:]...) +// +// This makes it harder to accidentally call Min with 0 arguments. +func Min(first Decimal, rest ...Decimal) Decimal { + ans := first + for _, item := range rest { + if item.Cmp(ans) < 0 { + ans = item + } + } + return ans +} + +// Max returns the largest Decimal that was passed in the arguments. +// +// To call this function with an array, you must do: +// +// Max(arr[0], arr[1:]...) +// +// This makes it harder to accidentally call Max with 0 arguments. +func Max(first Decimal, rest ...Decimal) Decimal { + ans := first + for _, item := range rest { + if item.Cmp(ans) > 0 { + ans = item + } + } + return ans +} + +// Sum returns the combined total of the provided first and rest Decimals +func Sum(first Decimal, rest ...Decimal) Decimal { + total := first + for _, item := range rest { + total = total.Add(item) + } + + return total +} + +// Avg returns the average value of the provided first and rest Decimals +func Avg(first Decimal, rest ...Decimal) Decimal { + count := New(int64(len(rest)+1), 0) + sum := Sum(first, rest...) + return sum.Div(count) +} + +// RescalePair rescales two decimals to common exponential value (minimal exp of both decimals) +func RescalePair(d1 Decimal, d2 Decimal) (Decimal, Decimal) { + d1.ensureInitialized() + d2.ensureInitialized() + + if d1.exp == d2.exp { + return d1, d2 + } + + baseScale := min(d1.exp, d2.exp) + if baseScale != d1.exp { + return d1.rescale(baseScale), d2 + } + return d1, d2.rescale(baseScale) +} + +func min(x, y int32) int32 { + if x >= y { + return y + } + return x +} + +func unquoteIfQuoted(value interface{}) (string, error) { + var bytes []byte + + switch v := value.(type) { + case string: + bytes = []byte(v) + case []byte: + bytes = v + default: + return "", fmt.Errorf("could not convert value '%+v' to byte array of type '%T'", + value, value) + } + + // If the amount is quoted, strip the quotes + if len(bytes) > 2 && bytes[0] == '"' && bytes[len(bytes)-1] == '"' { + bytes = bytes[1 : len(bytes)-1] + } + return string(bytes), nil +} + +// NullDecimal represents a nullable decimal with compatibility for +// scanning null values from the database. +type NullDecimal struct { + Decimal Decimal + Valid bool +} + +func NewNullDecimal(d Decimal) NullDecimal { + return NullDecimal{ + Decimal: d, + Valid: true, + } +} + +// Scan implements the sql.Scanner interface for database deserialization. +func (d *NullDecimal) Scan(value interface{}) error { + if value == nil { + d.Valid = false + return nil + } + d.Valid = true + return d.Decimal.Scan(value) +} + +// Value implements the driver.Valuer interface for database serialization. +func (d NullDecimal) Value() (driver.Value, error) { + if !d.Valid { + return nil, nil + } + return d.Decimal.Value() +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (d *NullDecimal) UnmarshalJSON(decimalBytes []byte) error { + if string(decimalBytes) == "null" { + d.Valid = false + return nil + } + d.Valid = true + return d.Decimal.UnmarshalJSON(decimalBytes) +} + +// MarshalJSON implements the json.Marshaler interface. +func (d NullDecimal) MarshalJSON() ([]byte, error) { + if !d.Valid { + return []byte("null"), nil + } + return d.Decimal.MarshalJSON() +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for XML +// deserialization +func (d *NullDecimal) UnmarshalText(text []byte) error { + str := string(text) + + // check for empty XML or XML without body e.g., + if str == "" { + d.Valid = false + return nil + } + if err := d.Decimal.UnmarshalText(text); err != nil { + d.Valid = false + return err + } + d.Valid = true + return nil +} + +// MarshalText implements the encoding.TextMarshaler interface for XML +// serialization. +func (d NullDecimal) MarshalText() (text []byte, err error) { + if !d.Valid { + return []byte{}, nil + } + return d.Decimal.MarshalText() +} + +// Trig functions + +// Atan returns the arctangent, in radians, of x. +func (d Decimal) Atan() Decimal { + if d.Equal(NewFromFloat(0.0)) { + return d + } + if d.GreaterThan(NewFromFloat(0.0)) { + return d.satan() + } + return d.Neg().satan().Neg() +} + +func (d Decimal) xatan() Decimal { + P0 := NewFromFloat(-8.750608600031904122785e-01) + P1 := NewFromFloat(-1.615753718733365076637e+01) + P2 := NewFromFloat(-7.500855792314704667340e+01) + P3 := NewFromFloat(-1.228866684490136173410e+02) + P4 := NewFromFloat(-6.485021904942025371773e+01) + Q0 := NewFromFloat(2.485846490142306297962e+01) + Q1 := NewFromFloat(1.650270098316988542046e+02) + Q2 := NewFromFloat(4.328810604912902668951e+02) + Q3 := NewFromFloat(4.853903996359136964868e+02) + Q4 := NewFromFloat(1.945506571482613964425e+02) + z := d.Mul(d) + b1 := P0.Mul(z).Add(P1).Mul(z).Add(P2).Mul(z).Add(P3).Mul(z).Add(P4).Mul(z) + b2 := z.Add(Q0).Mul(z).Add(Q1).Mul(z).Add(Q2).Mul(z).Add(Q3).Mul(z).Add(Q4) + z = b1.Div(b2) + z = d.Mul(z).Add(d) + return z +} + +// satan reduces its argument (known to be positive) +// to the range [0, 0.66] and calls xatan. +func (d Decimal) satan() Decimal { + Morebits := NewFromFloat(6.123233995736765886130e-17) // pi/2 = PIO2 + Morebits + Tan3pio8 := NewFromFloat(2.41421356237309504880) // tan(3*pi/8) + pi := NewFromFloat(3.14159265358979323846264338327950288419716939937510582097494459) + + if d.LessThanOrEqual(NewFromFloat(0.66)) { + return d.xatan() + } + if d.GreaterThan(Tan3pio8) { + return pi.Div(NewFromFloat(2.0)).Sub(NewFromFloat(1.0).Div(d).xatan()).Add(Morebits) + } + return pi.Div(NewFromFloat(4.0)).Add((d.Sub(NewFromFloat(1.0)).Div(d.Add(NewFromFloat(1.0)))).xatan()).Add(NewFromFloat(0.5).Mul(Morebits)) +} + +// sin coefficients +var _sin = [...]Decimal{ + NewFromFloat(1.58962301576546568060e-10), // 0x3de5d8fd1fd19ccd + NewFromFloat(-2.50507477628578072866e-8), // 0xbe5ae5e5a9291f5d + NewFromFloat(2.75573136213857245213e-6), // 0x3ec71de3567d48a1 + NewFromFloat(-1.98412698295895385996e-4), // 0xbf2a01a019bfdf03 + NewFromFloat(8.33333333332211858878e-3), // 0x3f8111111110f7d0 + NewFromFloat(-1.66666666666666307295e-1), // 0xbfc5555555555548 +} + +// Sin returns the sine of the radian argument x. +func (d Decimal) Sin() Decimal { + PI4A := NewFromFloat(7.85398125648498535156e-1) // 0x3fe921fb40000000, Pi/4 split into three parts + PI4B := NewFromFloat(3.77489470793079817668e-8) // 0x3e64442d00000000, + PI4C := NewFromFloat(2.69515142907905952645e-15) // 0x3ce8469898cc5170, + M4PI := NewFromFloat(1.273239544735162542821171882678754627704620361328125) // 4/pi + + if d.Equal(NewFromFloat(0.0)) { + return d + } + // make argument positive but save the sign + sign := false + if d.LessThan(NewFromFloat(0.0)) { + d = d.Neg() + sign = true + } + + j := d.Mul(M4PI).IntPart() // integer part of x/(Pi/4), as integer for tests on the phase angle + y := NewFromFloat(float64(j)) // integer part of x/(Pi/4), as float + + // map zeros to origin + if j&1 == 1 { + j++ + y = y.Add(NewFromFloat(1.0)) + } + j &= 7 // octant modulo 2Pi radians (360 degrees) + // reflect in x axis + if j > 3 { + sign = !sign + j -= 4 + } + z := d.Sub(y.Mul(PI4A)).Sub(y.Mul(PI4B)).Sub(y.Mul(PI4C)) // Extended precision modular arithmetic + zz := z.Mul(z) + + if j == 1 || j == 2 { + w := zz.Mul(zz).Mul(_cos[0].Mul(zz).Add(_cos[1]).Mul(zz).Add(_cos[2]).Mul(zz).Add(_cos[3]).Mul(zz).Add(_cos[4]).Mul(zz).Add(_cos[5])) + y = NewFromFloat(1.0).Sub(NewFromFloat(0.5).Mul(zz)).Add(w) + } else { + y = z.Add(z.Mul(zz).Mul(_sin[0].Mul(zz).Add(_sin[1]).Mul(zz).Add(_sin[2]).Mul(zz).Add(_sin[3]).Mul(zz).Add(_sin[4]).Mul(zz).Add(_sin[5]))) + } + if sign { + y = y.Neg() + } + return y +} + +// cos coefficients +var _cos = [...]Decimal{ + NewFromFloat(-1.13585365213876817300e-11), // 0xbda8fa49a0861a9b + NewFromFloat(2.08757008419747316778e-9), // 0x3e21ee9d7b4e3f05 + NewFromFloat(-2.75573141792967388112e-7), // 0xbe927e4f7eac4bc6 + NewFromFloat(2.48015872888517045348e-5), // 0x3efa01a019c844f5 + NewFromFloat(-1.38888888888730564116e-3), // 0xbf56c16c16c14f91 + NewFromFloat(4.16666666666665929218e-2), // 0x3fa555555555554b +} + +// Cos returns the cosine of the radian argument x. +func (d Decimal) Cos() Decimal { + + PI4A := NewFromFloat(7.85398125648498535156e-1) // 0x3fe921fb40000000, Pi/4 split into three parts + PI4B := NewFromFloat(3.77489470793079817668e-8) // 0x3e64442d00000000, + PI4C := NewFromFloat(2.69515142907905952645e-15) // 0x3ce8469898cc5170, + M4PI := NewFromFloat(1.273239544735162542821171882678754627704620361328125) // 4/pi + + // make argument positive + sign := false + if d.LessThan(NewFromFloat(0.0)) { + d = d.Neg() + } + + j := d.Mul(M4PI).IntPart() // integer part of x/(Pi/4), as integer for tests on the phase angle + y := NewFromFloat(float64(j)) // integer part of x/(Pi/4), as float + + // map zeros to origin + if j&1 == 1 { + j++ + y = y.Add(NewFromFloat(1.0)) + } + j &= 7 // octant modulo 2Pi radians (360 degrees) + // reflect in x axis + if j > 3 { + sign = !sign + j -= 4 + } + if j > 1 { + sign = !sign + } + + z := d.Sub(y.Mul(PI4A)).Sub(y.Mul(PI4B)).Sub(y.Mul(PI4C)) // Extended precision modular arithmetic + zz := z.Mul(z) + + if j == 1 || j == 2 { + y = z.Add(z.Mul(zz).Mul(_sin[0].Mul(zz).Add(_sin[1]).Mul(zz).Add(_sin[2]).Mul(zz).Add(_sin[3]).Mul(zz).Add(_sin[4]).Mul(zz).Add(_sin[5]))) + } else { + w := zz.Mul(zz).Mul(_cos[0].Mul(zz).Add(_cos[1]).Mul(zz).Add(_cos[2]).Mul(zz).Add(_cos[3]).Mul(zz).Add(_cos[4]).Mul(zz).Add(_cos[5])) + y = NewFromFloat(1.0).Sub(NewFromFloat(0.5).Mul(zz)).Add(w) + } + if sign { + y = y.Neg() + } + return y +} + +var _tanP = [...]Decimal{ + NewFromFloat(-1.30936939181383777646e+4), // 0xc0c992d8d24f3f38 + NewFromFloat(1.15351664838587416140e+6), // 0x413199eca5fc9ddd + NewFromFloat(-1.79565251976484877988e+7), // 0xc1711fead3299176 +} +var _tanQ = [...]Decimal{ + NewFromFloat(1.00000000000000000000e+0), + NewFromFloat(1.36812963470692954678e+4), //0x40cab8a5eeb36572 + NewFromFloat(-1.32089234440210967447e+6), //0xc13427bc582abc96 + NewFromFloat(2.50083801823357915839e+7), //0x4177d98fc2ead8ef + NewFromFloat(-5.38695755929454629881e+7), //0xc189afe03cbe5a31 +} + +// Tan returns the tangent of the radian argument x. +func (d Decimal) Tan() Decimal { + + PI4A := NewFromFloat(7.85398125648498535156e-1) // 0x3fe921fb40000000, Pi/4 split into three parts + PI4B := NewFromFloat(3.77489470793079817668e-8) // 0x3e64442d00000000, + PI4C := NewFromFloat(2.69515142907905952645e-15) // 0x3ce8469898cc5170, + M4PI := NewFromFloat(1.273239544735162542821171882678754627704620361328125) // 4/pi + + if d.Equal(NewFromFloat(0.0)) { + return d + } + + // make argument positive but save the sign + sign := false + if d.LessThan(NewFromFloat(0.0)) { + d = d.Neg() + sign = true + } + + j := d.Mul(M4PI).IntPart() // integer part of x/(Pi/4), as integer for tests on the phase angle + y := NewFromFloat(float64(j)) // integer part of x/(Pi/4), as float + + // map zeros to origin + if j&1 == 1 { + j++ + y = y.Add(NewFromFloat(1.0)) + } + + z := d.Sub(y.Mul(PI4A)).Sub(y.Mul(PI4B)).Sub(y.Mul(PI4C)) // Extended precision modular arithmetic + zz := z.Mul(z) + + if zz.GreaterThan(NewFromFloat(1e-14)) { + w := zz.Mul(_tanP[0].Mul(zz).Add(_tanP[1]).Mul(zz).Add(_tanP[2])) + x := zz.Add(_tanQ[1]).Mul(zz).Add(_tanQ[2]).Mul(zz).Add(_tanQ[3]).Mul(zz).Add(_tanQ[4]) + y = z.Add(z.Mul(w.Div(x))) + } else { + y = z + } + if j&2 == 2 { + y = NewFromFloat(-1.0).Div(y) + } + if sign { + y = y.Neg() + } + return y +} diff --git a/vendor/github.com/shopspring/decimal/rounding.go b/vendor/github.com/shopspring/decimal/rounding.go new file mode 100644 index 00000000000..d4b0cd00795 --- /dev/null +++ b/vendor/github.com/shopspring/decimal/rounding.go @@ -0,0 +1,160 @@ +// Copyright 2009 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. + +// Multiprecision decimal numbers. +// For floating-point formatting only; not general purpose. +// Only operations are assign and (binary) left/right shift. +// Can do binary floating point in multiprecision decimal precisely +// because 2 divides 10; cannot do decimal floating point +// in multiprecision binary precisely. + +package decimal + +type floatInfo struct { + mantbits uint + expbits uint + bias int +} + +var float32info = floatInfo{23, 8, -127} +var float64info = floatInfo{52, 11, -1023} + +// roundShortest rounds d (= mant * 2^exp) to the shortest number of digits +// that will let the original floating point value be precisely reconstructed. +func roundShortest(d *decimal, mant uint64, exp int, flt *floatInfo) { + // If mantissa is zero, the number is zero; stop now. + if mant == 0 { + d.nd = 0 + return + } + + // Compute upper and lower such that any decimal number + // between upper and lower (possibly inclusive) + // will round to the original floating point number. + + // We may see at once that the number is already shortest. + // + // Suppose d is not denormal, so that 2^exp <= d < 10^dp. + // The closest shorter number is at least 10^(dp-nd) away. + // The lower/upper bounds computed below are at distance + // at most 2^(exp-mantbits). + // + // So the number is already shortest if 10^(dp-nd) > 2^(exp-mantbits), + // or equivalently log2(10)*(dp-nd) > exp-mantbits. + // It is true if 332/100*(dp-nd) >= exp-mantbits (log2(10) > 3.32). + minexp := flt.bias + 1 // minimum possible exponent + if exp > minexp && 332*(d.dp-d.nd) >= 100*(exp-int(flt.mantbits)) { + // The number is already shortest. + return + } + + // d = mant << (exp - mantbits) + // Next highest floating point number is mant+1 << exp-mantbits. + // Our upper bound is halfway between, mant*2+1 << exp-mantbits-1. + upper := new(decimal) + upper.Assign(mant*2 + 1) + upper.Shift(exp - int(flt.mantbits) - 1) + + // d = mant << (exp - mantbits) + // Next lowest floating point number is mant-1 << exp-mantbits, + // unless mant-1 drops the significant bit and exp is not the minimum exp, + // in which case the next lowest is mant*2-1 << exp-mantbits-1. + // Either way, call it mantlo << explo-mantbits. + // Our lower bound is halfway between, mantlo*2+1 << explo-mantbits-1. + var mantlo uint64 + var explo int + if mant > 1<= d.nd { + break + } + li := ui - upper.dp + lower.dp + l := byte('0') // lower digit + if li >= 0 && li < lower.nd { + l = lower.d[li] + } + m := byte('0') // middle digit + if mi >= 0 { + m = d.d[mi] + } + u := byte('0') // upper digit + if ui < upper.nd { + u = upper.d[ui] + } + + // Okay to round down (truncate) if lower has a different digit + // or if lower is inclusive and is exactly the result of rounding + // down (i.e., and we have reached the final digit of lower). + okdown := l != m || inclusive && li+1 == lower.nd + + switch { + case upperdelta == 0 && m+1 < u: + // Example: + // m = 12345xxx + // u = 12347xxx + upperdelta = 2 + case upperdelta == 0 && m != u: + // Example: + // m = 12345xxx + // u = 12346xxx + upperdelta = 1 + case upperdelta == 1 && (m != '9' || u != '0'): + // Example: + // m = 1234598x + // u = 1234600x + upperdelta = 2 + } + // Okay to round up if upper has a different digit and either upper + // is inclusive or upper is bigger than the result of rounding up. + okup := upperdelta > 0 && (inclusive || upperdelta > 1 || ui+1 < upper.nd) + + // If it's okay to do either, then round to the nearest one. + // If it's okay to do only one, do it. + switch { + case okdown && okup: + d.Round(mi + 1) + return + case okdown: + d.RoundDown(mi + 1) + return + case okup: + d.RoundUp(mi + 1) + return + } + } +} diff --git a/vendor/golang.org/x/exp/AUTHORS b/vendor/golang.org/x/exp/AUTHORS new file mode 100644 index 00000000000..15167cd746c --- /dev/null +++ b/vendor/golang.org/x/exp/AUTHORS @@ -0,0 +1,3 @@ +# This source code refers to The Go Authors for copyright purposes. +# The master list of authors is in the main Go distribution, +# visible at http://tip.golang.org/AUTHORS. diff --git a/vendor/golang.org/x/exp/CONTRIBUTORS b/vendor/golang.org/x/exp/CONTRIBUTORS new file mode 100644 index 00000000000..1c4577e9680 --- /dev/null +++ b/vendor/golang.org/x/exp/CONTRIBUTORS @@ -0,0 +1,3 @@ +# This source code was written by the Go contributors. +# The master list of contributors is in the main Go distribution, +# visible at http://tip.golang.org/CONTRIBUTORS. diff --git a/vendor/golang.org/x/exp/LICENSE b/vendor/golang.org/x/exp/LICENSE new file mode 100644 index 00000000000..6a66aea5eaf --- /dev/null +++ b/vendor/golang.org/x/exp/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. 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. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +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 +OWNER 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. diff --git a/vendor/golang.org/x/exp/PATENTS b/vendor/golang.org/x/exp/PATENTS new file mode 100644 index 00000000000..733099041f8 --- /dev/null +++ b/vendor/golang.org/x/exp/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the Go project. + +Google hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) +patent license to make, have made, use, offer to sell, sell, import, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/vendor/golang.org/x/exp/constraints/constraints.go b/vendor/golang.org/x/exp/constraints/constraints.go new file mode 100644 index 00000000000..2c033dff47e --- /dev/null +++ b/vendor/golang.org/x/exp/constraints/constraints.go @@ -0,0 +1,50 @@ +// Copyright 2021 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 constraints defines a set of useful constraints to be used +// with type parameters. +package constraints + +// Signed is a constraint that permits any signed integer type. +// If future releases of Go add new predeclared signed integer types, +// this constraint will be modified to include them. +type Signed interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 +} + +// Unsigned is a constraint that permits any unsigned integer type. +// If future releases of Go add new predeclared unsigned integer types, +// this constraint will be modified to include them. +type Unsigned interface { + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr +} + +// Integer is a constraint that permits any integer type. +// If future releases of Go add new predeclared integer types, +// this constraint will be modified to include them. +type Integer interface { + Signed | Unsigned +} + +// Float is a constraint that permits any floating-point type. +// If future releases of Go add new predeclared floating-point types, +// this constraint will be modified to include them. +type Float interface { + ~float32 | ~float64 +} + +// Complex is a constraint that permits any complex numeric type. +// If future releases of Go add new predeclared complex numeric types, +// this constraint will be modified to include them. +type Complex interface { + ~complex64 | ~complex128 +} + +// Ordered is a constraint that permits any ordered type: any type +// that supports the operators < <= >= >. +// If future releases of Go add new ordered types, +// this constraint will be modified to include them. +type Ordered interface { + Integer | Float | ~string +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 46d310f9fc5..0c4c58cdfb5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -45,8 +45,8 @@ github.com/Azure/azure-amqp-common-go/v4/uuid github.com/Azure/azure-event-hubs-go/v3 github.com/Azure/azure-event-hubs-go/v3/atom github.com/Azure/azure-event-hubs-go/v3/persist -# github.com/Azure/azure-kusto-go v0.9.2 -## explicit; go 1.16 +# github.com/Azure/azure-kusto-go v0.13.0 +## explicit; go 1.19 github.com/Azure/azure-kusto-go/kusto github.com/Azure/azure-kusto-go/kusto/data/errors github.com/Azure/azure-kusto-go/kusto/data/table @@ -60,7 +60,10 @@ github.com/Azure/azure-kusto-go/kusto/internal/frames/v2 github.com/Azure/azure-kusto-go/kusto/internal/log github.com/Azure/azure-kusto-go/kusto/internal/response github.com/Azure/azure-kusto-go/kusto/internal/version +github.com/Azure/azure-kusto-go/kusto/kql +github.com/Azure/azure-kusto-go/kusto/trusted_endpoints github.com/Azure/azure-kusto-go/kusto/unsafe +github.com/Azure/azure-kusto-go/kusto/utils # github.com/Azure/azure-pipeline-go v0.2.3 ## explicit; go 1.14 github.com/Azure/azure-pipeline-go/pipeline @@ -1015,6 +1018,12 @@ github.com/robfig/cron/v3 # github.com/ryanuber/go-glob v1.0.0 ## explicit github.com/ryanuber/go-glob +# github.com/samber/lo v1.37.0 +## explicit; go 1.18 +github.com/samber/lo +# github.com/shopspring/decimal v1.3.1 +## explicit; go 1.13 +github.com/shopspring/decimal # github.com/sirupsen/logrus v1.9.0 ## explicit; go 1.13 github.com/sirupsen/logrus @@ -1273,6 +1282,9 @@ golang.org/x/crypto/pkcs12/internal/rc2 golang.org/x/crypto/salsa20/salsa golang.org/x/crypto/scrypt golang.org/x/crypto/sha3 +# golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 +## explicit; go 1.18 +golang.org/x/exp/constraints # golang.org/x/mod v0.9.0 ## explicit; go 1.17 golang.org/x/mod/internal/lazyregexp