From 2ef26faa40938291229ea6d0139d244da75ad359 Mon Sep 17 00:00:00 2001 From: Captain Date: Sun, 21 Nov 2021 17:09:14 +0000 Subject: [PATCH] DERO-HE STARGATE Testnet Release27 --- blockchain/difficulty.go | 4 +- blockchain/mempool/mempool.go | 4 +- blockchain/mempool/mempool_test.go | 2 +- blockchain/miner_block.go | 7 +- config/config.go | 19 +- config/version.go | 2 +- p2p/connection_pool.go | 4 +- transaction/transaction_test.go | 2 +- .../caarlos0/env/v6/.github/FUNDING.yml | 1 + .../caarlos0/env/v6/.github/dependabot.yml | 18 + .../env/v6/.github/workflows/build.yml | 61 + .../env/v6/.github/workflows/lint.yml | 22 + vendor/github.com/caarlos0/env/v6/.gitignore | 4 + .../github.com/caarlos0/env/v6/.golangci.yml | 8 + .../caarlos0/env/v6/.goreleaser.yml | 3 + vendor/github.com/caarlos0/env/v6/LICENSE.md | 21 + vendor/github.com/caarlos0/env/v6/Makefile | 37 + vendor/github.com/caarlos0/env/v6/README.md | 459 +++++ vendor/github.com/caarlos0/env/v6/env.go | 477 ++++++ vendor/github.com/caarlos0/env/v6/env_test.go | 1477 +++++++++++++++++ vendor/github.com/caarlos0/env/v6/env_unix.go | 15 + .../github.com/caarlos0/env/v6/env_windows.go | 25 + .../caarlos0/env/v6/env_windows_test.go | 20 + vendor/github.com/caarlos0/env/v6/go.mod | 5 + vendor/github.com/caarlos0/env/v6/go.sum | 2 + 25 files changed, 2683 insertions(+), 16 deletions(-) create mode 100644 vendor/github.com/caarlos0/env/v6/.github/FUNDING.yml create mode 100644 vendor/github.com/caarlos0/env/v6/.github/dependabot.yml create mode 100644 vendor/github.com/caarlos0/env/v6/.github/workflows/build.yml create mode 100644 vendor/github.com/caarlos0/env/v6/.github/workflows/lint.yml create mode 100644 vendor/github.com/caarlos0/env/v6/.gitignore create mode 100644 vendor/github.com/caarlos0/env/v6/.golangci.yml create mode 100644 vendor/github.com/caarlos0/env/v6/.goreleaser.yml create mode 100644 vendor/github.com/caarlos0/env/v6/LICENSE.md create mode 100644 vendor/github.com/caarlos0/env/v6/Makefile create mode 100644 vendor/github.com/caarlos0/env/v6/README.md create mode 100644 vendor/github.com/caarlos0/env/v6/env.go create mode 100644 vendor/github.com/caarlos0/env/v6/env_test.go create mode 100644 vendor/github.com/caarlos0/env/v6/env_unix.go create mode 100644 vendor/github.com/caarlos0/env/v6/env_windows.go create mode 100644 vendor/github.com/caarlos0/env/v6/env_windows_test.go create mode 100644 vendor/github.com/caarlos0/env/v6/go.mod create mode 100644 vendor/github.com/caarlos0/env/v6/go.sum diff --git a/blockchain/difficulty.go b/blockchain/difficulty.go index 76cdc1ff..dbbd44ad 100644 --- a/blockchain/difficulty.go +++ b/blockchain/difficulty.go @@ -123,9 +123,9 @@ func (chain *Blockchain) Get_Difficulty_At_Tips(tips []crypto.Hash) *big.Int { step := new(big.Int) if globals.IsMainnet() { - MinimumDifficulty = new(big.Int).SetUint64(config.MAINNET_MINIMUM_DIFFICULTY) // this must be controllable parameter + MinimumDifficulty = new(big.Int).SetUint64(config.Settings.MAINNET_MINIMUM_DIFFICULTY) // this must be controllable parameter } else { - MinimumDifficulty = new(big.Int).SetUint64(config.TESTNET_MINIMUM_DIFFICULTY) // this must be controllable parameter + MinimumDifficulty = new(big.Int).SetUint64(config.Settings.TESTNET_MINIMUM_DIFFICULTY) // this must be controllable parameter } GenesisDifficulty := new(big.Int).SetUint64(1) diff --git a/blockchain/mempool/mempool.go b/blockchain/mempool/mempool.go index 22468d15..44f55cc9 100644 --- a/blockchain/mempool/mempool.go +++ b/blockchain/mempool/mempool.go @@ -327,12 +327,12 @@ func (pool *Mempool) Mempool_Print() { }) loggerpool.Info(fmt.Sprintf("Total TX in mempool = %d\n", len(klist))) - loggerpool.Info(fmt.Sprintf("%20s %14s %7s %7s %6s %32s\n", "Added", "Size", "Height", "TXID")) + loggerpool.Info(fmt.Sprintf("%20s %7s %6s %32s\n", "Added", "Size", "Height", "TXID")) for i := range klist { k := klist[i] v := vlist[i] - loggerpool.Info(fmt.Sprintf("%20s %14s %7d %7d %6d %32s\n", time.Unix(int64(v.Added), 0).UTC().Format(time.RFC3339), + loggerpool.Info(fmt.Sprintf("%20s %7d %6d %32s\n", time.Unix(int64(v.Added), 0).UTC().Format(time.RFC3339), len(v.Tx.Serialize()), v.Height, k)) } } diff --git a/blockchain/mempool/mempool_test.go b/blockchain/mempool/mempool_test.go index 427725cb..df10b7b3 100644 --- a/blockchain/mempool/mempool_test.go +++ b/blockchain/mempool/mempool_test.go @@ -28,7 +28,7 @@ func Test_mempool(t *testing.T) { // this tx is from internal testnet // tx_id 499002f3fb7fea8a71dac93dea65c0ff74be05b0858078a27a48d78b71eacf87 - tx_hex := "010000030101000000000000000000000000000000000000000000000000000000000000000000002aa937d8879cebc74425f4fee7bcc81b424dd6f2130d0548a0c4afa7f9e40b1c41ee8f9487971ee3ecf35cef40d26b6382b49b6315370f96696cda3394a550c4bd5d301a3b91737c354573ffa0d9e921a523af274b2ccc5d7078efd5c489c8deba43a0205aafd9f89b80cd432825f2f0bcbfa61f82bc30c2a83a7976e6743aad48fbdb8c7799cbf3131d156d2ac7b780c80202000d136ae832342f1ecf99a935e1dcdc255c7f33297b204383e7539f31278fd097001e261e85c34489c313aaa05ce4abb3772d31e5d192321dc2cce7fe45b21f4d114244b8fbf5a6cc1f0119b69ed785e7c8739c306b4a35278159bbb6977a28ea71ea0ffc5a6398ec935e000b8b5806e6e7a0bd8b57e24edd21cfafa79a54bf7d3bc74643294b88920b3b900113dad7529adb51a5b634140f01c8389c57c92af984d16cf4400a463a448fd7e7007ed6b651c46aab06f6f9677d50ee78cf98a3e121b325a0cab4bf67910f9a92d0153222d7f87cedc63a5b773a5c5482479e554d56d906eaf5d5f9b1c37d9f52170029bd3c397bcb89dca495d7ee8cab0e5e4cd8b204dff933e8673cacece063830d010676be97476c5028c633ca3160a7367352750304ee2be99bf6311c198b1207a20006364d038c0d479f5264c085c8f210ea88f8a7f08535240c3541c641aadae496002449b5477c26ff74be1159c36d71d9dbca3ab2c2a0804590637fb79e0d89e5d801053afa58bcdbf17aa0ce544e3ca12e9b5c204aaea6a0b5698fef75669c1d91690114acfe9439eff58fe532aa243187adadf7118ba78f9772429b3041a07cb203df0125ffa09b619faa1b890d0a5323a4f162b4d0d1a6864f9a0261820bfc901dea4501098ad5f3c3bb0629de269f3e01633b4e0ee6a8f74e648aa40f57dd2496ed7230010f100ede75074bafc160fc1327545a44fd8d34627b136fed6de0bfa72df0e1330116108dc5545f42b9f5b8e302d1546930d9840c18fc92e425bacd0856e842f60f011b7f689aa43b95bbfd02ee20aab2cac2077c9639b9e4f84b8b221b37cdaf13a2010b979e41274c4338395dfc447c0d4a548caf414ee425aab1748475cca9faef4e012051ff90139adccfbf0459e3a5c0638dfb1654ee728cba59c6803ab1daaddda9011ef927e921f956134892499c7513bc2e077986295c050633a1566d449fd0954a00071f56b24c65aaa4549be3c859ac8ec75fe302d89e8755166da3f7d0d08ae285011983fdc2aa2a0611187b0b86119508e3f7642dfe7380f5bdbe6ad27f0dc4ccf201127e3c3923518fec22a4ead80845bbe6008d9b790bd50ec6ef83b087d0490995011982ad7c49288a3115c638a04e14a0c74a75008359b40aff422e9972e276929800237a7bfd1fe105865abbc36e926de565530da393da1ddc0b1005bb0c31504402002569e7e95e54711903c29f63cb8c6b58d47ea3299c455c8738b543d552e8dffe012ea47467abedda0d168d06bcf2f45ab8fa090b90d6e4662d4052df0488084403057542fc7603a7d717dd8267dc036330e3083d2a1562bf5b1ecfbce99804c05100000000000000000000000000000000000000000000000000000000000000000e377bd9c8a2b6faf14c766e51fc9415ed979d604e3d5dcc800b3c5a9c6bec1f239a65bdeb2028926a6fa13fdf2caf0a09ba17e1420b74bef294aa4c9731d2c71f46ae2c57612f525f14cf49e86be5a984aab4d8c25ec3818e19612ba07722d500082c5b600665b3a718fad7516b976749ac3692a6ccf60225c04d1da272a95780012c932f36c1cca1753377ad16b2dbecabb58a2efbf8e88a066d80efba20addf8208d52649fc24ba86ecd87da10efd8ab480c48cadf4ee205e60d10f6e1f1527dd01171915999416a85d08a06e4a8ed18797fcd62910e6624222e120bdd60a00db22fb4d9f0a7aee7570f72b99c673195c67b7dc5e53aa5a32ffc04b409aa79cb52faa6cb8374cd242bd90fd2da9a891fc18022112ecb943b8b0f6c69a4b2e33ad0b16abbd22b38f11f1e76bfd6c1950864753ef84ce0ba869b82f909fbe2751ef0e02b170d107ec7cf6c25d9fdbdd86f5e4790e532254f89ad6cd477bf30816e207ce092de3148c4fd818812abfcadc03a325991fb7dd48b5a54266137709715a11556344ae9bbb2afd1e7ea7fa417f1308d20fdef8dd2168f89101d0643ce62217ee68724101b6e8644ad0860940f95b1674a1befa7ec45235d2c93043087e9001211d72b9f744364770a276f99a183dc184f90ad09d034119f5e4754249046bb201156984d6a418664bda88f98075679d6bb8cc4c644ff727278931797db12a2c65012698726eabb2580faecafa12befe8855a7074da2b4f2d2cd74fd481e818f2287001d445415f6abc3b4a9ff7043906d8f83d5c61ad853b12a72c152a21b9627d8bf00004d5a3deaf642e40e1401ead28babb7ce0263afcc3b3f1e7876a31d6fbea5e90125bff3189cf6ab0e68caa851e60fc92483d3bc2a5ecd511594e7f80f565301c101247b8928fb1866a98b99add332a7e6065cf27ed8a7d48d7775727299308f0ab9002cb7663076e2883fe31b3397f6f5460491835f1a23c173f858fb93cbc312cf360117c5b8e4a25c2e3228b6f4d06525f96e818772292b095b7d344eb3f9f18735bd000889c0fcc3c2f25fe74c915b178db653b0d7725efdc6e2e80a2770738cc3bff9002a578a39364fa16dc82fd7f15872da799f2ddc6422491b9e5c6447ea87adb01a00187461816522889fc2bcb1dc1b0b5d2537df3ae2bb58cbef2b58cb678ba6b9f30121f1cf89f1e9840aace8469b06edf81e9930ddfb8aef4a2c3d46e8551cc40ab400" + tx_hex := "01000003519b9e874a9dd7fcbfb7802268123dcf970c336891101a3a0705855b72b9eb08c80100000000000000000000000000000000000000000000000000000000000000000000f394d06566a81b024fd8624b4c8592f8b9344e58f227261eb08d821bd190b4ac9c7df0660038ddcf881602a14fe5ad8eebff83d3ce3541a1f232fe61acf71109e417160936863e8c0597c798b73ef08e76b8eeebcaf20260ba91fcdef5f626361f824dc6197b02c599d4511c624a933226d8fccc125611ec80d8ad5acea4baf2f1c77e4c5525d9859b40f43daaf3d4e4450203d80419dcb026b9adbd7d6912284ee9ce20f3721076947f16e1faefd9b8c35116f4cf0044ead0a12fda3f3bcf2ed91627b922c02c10d112df9e782d3750cda151f6ba2ebe6495065141f32800894566011198c4055458005a31d3e285037446ba14bac54420bcab87cbebeaccab1f158b011e90132b7310e6c866e348cab68d21bad1d9f811b4d667a1e40bdef1a19259d3000b972ae06da958658b12ca4a45c2c6ea50d305be86f7e979fc962c687eb012f401a2c4bf0beb6c713343b94d65ee6d937660480daf670ab5b0ed421a9103e8a25119c836dd799600e67e4ec66ef680833398a99b16956aed2ee5bd9e6c8e68009a01145636e5e448d6c8ab902db394aa9c5aeee92dd150e1150d5b8f6674f330c279010912df27232ae126525a130ed8a9b4dee62eb57ce1d8a42ae9bcef867b10ce94012f94aa78846fcdda504243f83f3122aee5dc38dd32fa2a90224b3a40b4cf79c301129028709e39591c55e6e477a36cc26dc2efd7183fb59f51635dbd114dfed9d5010d66f31b781fa181c2114600ee1f03d55439355265700fb9d35ac684a19db398001e12558cc17e59fb85fb825481dee6fef3612c043bb38ca4833183da5381364600077a99d4d2df2f06067359f178a40f98a3e0943309c5ce3ce5398b9909174254012aa49cfc78ebec51c00e7824db0d8d41ef43fbb0396824d2c31b7db64124cc61000a8b51bd6c94a502f7aea3e5001f00d76b165ad580e48f8aa72813da8240286e00055f3480aa7cdb68a20d4b1e567d42389b44cf4e345da9e5a5655a80418695bc012c8f1e1ef8af646b4fb08afaeb3febde17c4c0d04b30dfd22d96a32ced9c9439010c40484e1547d307ad56b79bb7450e4eaaed7ed79bd5392dffd7449043f8783a0015104264f8d80356176ca6fdf4777e4ebc8edcbab50f8b6c366ac75ebd6bb5c701078d55053d2d41f21f51d5e617f282ba9dfa2576f0231048dd73998ceaa699fd0105821b61addd6935c7d80e65c706f0f2aa2d73ad22c5548c9c8b92cd82689dc40115f576c413a37014b9ee5377b121c69ad6e0f00f2b4f6c99d1f68b00a5d923370119b29fe6e9cf35f1344e24d52ee1fdb6521de3176f3d2b2a2b9327ae6e6cfbb10017198369a6ff180253700361c40e7f378c8e7ead6e04f01dbf5b4c47e7e1e4ff001abaf84628764eccbc5d0f66dc0a99505bb0e598362c45312470526afad11caf00242c6a902c564b3b363eb4ae08f643f18571779428358bfbdaf9856b0ebf229f0100000000000000000000000000000000000000000000000000000000000000001b7a78b6f573d4ac5d3a8d7d10dafac0dbc92604474417b29d40448d9fbc821d0fe3f73da41b27752d5db0658fcc0bff7da21112417ac719e491fdd43c8e5d0b07bcca22c27367a276e6a4e0cfb1bfbc772842898356ad4723101d4ada5137f409bb3b290d7519da3af15feb09e183e2be809690099551f1cb984deb1295b7611dd0cfd9000482314a388512d278fc22b2f6e3fc6e95023b4bd228e22626b7440119fa2c6f45a33b008ef5b23a5522ebbf0bf5f26d8263613a7f9b5ad9a192357600107a023453f7f79f413756dfba0096ae5d13158bbbb147089a020e819ef6640728acf01c811b967d1a14d767ac20088c5ca1613aa5d94ebe7fe2840b6431d8d002137dae183071d12cd9fb14c4b4738ef750ff514ce26257d1245eaedfea625516bfcb075a33a8910dc4e40742bfaa70d95591711e9db33d0db0be06cc819817072c22a9c81f4369f68bb1ef71c7f7458671d647caef18b30cca91da4f1b201d082a03306b4a07d1f0e1a8cc8844038fc524201a6c3a346a971203fd45458d292987d44049eecdac25c5622465a8d683a43621612d2fb283724b17a7bfa3bad2266a6fb1cf669eef21c90e24e85d6ab48e8b237355e964407dde00d65fb442dd210ce0378349f533c0e8ab65293de5319e246a044ed22b1e6db28add1508dd6f0da22954eea8a431b211a8b54147e32fec6593f9fb731389f62f94d5047e0f3301216576c71a79629bc3879325280e216d59cf7e6ff2d6a0babe418f8f2ed603ce010e16785f8c855043221ed5a603d4ed4e8e90059669543738ceb2a04dc996079c01264e9beaf4bb82e1b8f47bb0c239167064001e306eeef2b5495ec42754806b65012357f738543b13594ff081a070dbc0575645ac5ce8e0f89d3afdeb3d49e9ddae012a17d5095b9cef08f6eee2ce733dbf2cfcdb8c23e39410ed973e68cf8505266d00280c52b45752dd12ba84499618898111a8f9b19f2b76c06917e57eef221f3387011cc3360b478d144118a82a3c4391814f34aac2fb0f2d043394304bed179689e501292ca5631820f5f465242b4cc517f06c153c176a43f77daca463b7e2d3e2d7ba001aa0b2c90c3172cd2556137d78748f47924432449210d8b2f5e27adb6210bc77012c92a12526a5b0b1ef60994eb4d08e9bb5a3ea330d417838a52ad8636d37fa9e00131153c9affeefd6b98ecb6a80f5e6ea97417376030325182c064e12dd353c32011f4e25c49eea09adaa46d56642e2ea0afced2d75036acd3839435863f1ec3bf1002e19fe8456a7236c8fb77a15f87164debaad865e85c3468152a5deaffde3da6a01" var tx, dup_tx transaction.Transaction diff --git a/blockchain/miner_block.go b/blockchain/miner_block.go index fe37a6b3..45619df4 100644 --- a/blockchain/miner_block.go +++ b/blockchain/miner_block.go @@ -166,12 +166,15 @@ func (chain *Blockchain) Create_new_miner_block(miner_address rpc.Address) (cbl } height := chain.Calculate_Height_At_Tips(bl.Tips) // we are 1 higher than previous highest tip - history := map[crypto.Hash]bool{} var history_array []crypto.Hash for i := range bl.Tips { - history_array = append(history_array, chain.get_ordered_past(bl.Tips[i], 26)...) + h := height - 20 + if h < 0 { + h = 0 + } + history_array = append(history_array, chain.get_ordered_past(bl.Tips[i], h)...) } for _, h := range history_array { history[h] = true diff --git a/config/config.go b/config/config.go index 5719ec34..25d511a2 100644 --- a/config/config.go +++ b/config/config.go @@ -17,6 +17,7 @@ package config import "github.com/satori/go.uuid" +import "github.com/caarlos0/env/v6" import "github.com/deroproject/derohe/cryptography/crypto" // all global configuration variables are picked from here @@ -52,12 +53,20 @@ const MAX_RINGSIZE = 128 // <= 128, ringsize will be accepted // Minimum FEE calculation constants are here const FEE_PER_KB = uint64(100) // .00100 dero per kb -const MAINNET_BOOTSTRAP_DIFFICULTY = uint64(80000000) // atlantis mainnet botstrapped at 80 MH/s -const MAINNET_MINIMUM_DIFFICULTY = uint64(800000000) // 80 MH/s -// testnet bootstraps at 1 MH -const TESTNET_BOOTSTRAP_DIFFICULTY = uint64(10000) // testnet bootstrap at 50KH/s -const TESTNET_MINIMUM_DIFFICULTY = uint64(10000) // 10KH/s +type SettingsStruct struct { + MAINNET_BOOTSTRAP_DIFFICULTY uint64 `env:"MAINNET_BOOTSTRAP_DIFFICULTY" envDefault:"80000000"` + MAINNET_MINIMUM_DIFFICULTY uint64 `env:"MAINNET_MINIMUM_DIFFICULTY" envDefault:"80000000"` + + TESTNET_BOOTSTRAP_DIFFICULTY uint64 `env:"TESTNET_BOOTSTRAP_DIFFICULTY" envDefault:"10000"` + TESTNET_MINIMUM_DIFFICULTY uint64 `env:"TESTNET_MINIMUM_DIFFICULTY" envDefault:"10000"` +} + +var Settings SettingsStruct + +var _ = env.Parse(&Settings) + + // this single parameter controls lots of various parameters // within the consensus, it should never go below 7 diff --git a/config/version.go b/config/version.go index 2ebb26fa..2b85a197 100644 --- a/config/version.go +++ b/config/version.go @@ -20,4 +20,4 @@ import "github.com/blang/semver/v4" // right now it has to be manually changed // do we need to include git commitsha?? -var Version = semver.MustParse("3.4.80-1.DEROHE.STARGATE+20112021") +var Version = semver.MustParse("3.4.82-1.DEROHE.STARGATE+20112021") diff --git a/p2p/connection_pool.go b/p2p/connection_pool.go index ec5e60c7..3d548357 100644 --- a/p2p/connection_pool.go +++ b/p2p/connection_pool.go @@ -146,13 +146,13 @@ func Connection_Pending_Clear() { if time.Now().Sub(v.update_received).Round(time.Second).Seconds() > 20 { v.exit() Connection_Delete(v) - v.logger.Info("Purging connection due since idle") + v.logger.V(1).Info("Purging connection due since idle") } if IsAddressInBanList(Address(v)) { v.exit() Connection_Delete(v) - v.logger.Info("Purging connection due to ban list") + v.logger.V(1).Info("Purging connection due to ban list") } return true }) diff --git a/transaction/transaction_test.go b/transaction/transaction_test.go index c91f35e6..3011fc68 100644 --- a/transaction/transaction_test.go +++ b/transaction/transaction_test.go @@ -112,7 +112,7 @@ func Test_TX_Valid(t *testing.T) { }{ { name: "4 ring test", - txhex: "010000030101000000000000000000000000000000000000000000000000000000000000000000002aa937d8879cebc74425f4fee7bcc81b424dd6f2130d0548a0c4afa7f9e40b1c41ee8f9487971ee3ecf35cef40d26b6382b49b6315370f96696cda3394a550c4bd5d301a3b91737c354573ffa0d9e921a523af274b2ccc5d7078efd5c489c8deba43a0205aafd9f89b80cd432825f2f0bcbfa61f82bc30c2a83a7976e6743aad48fbdb8c7799cbf3131d156d2ac7b780c80202000d136ae832342f1ecf99a935e1dcdc255c7f33297b204383e7539f31278fd097001e261e85c34489c313aaa05ce4abb3772d31e5d192321dc2cce7fe45b21f4d114244b8fbf5a6cc1f0119b69ed785e7c8739c306b4a35278159bbb6977a28ea71ea0ffc5a6398ec935e000b8b5806e6e7a0bd8b57e24edd21cfafa79a54bf7d3bc74643294b88920b3b900113dad7529adb51a5b634140f01c8389c57c92af984d16cf4400a463a448fd7e7007ed6b651c46aab06f6f9677d50ee78cf98a3e121b325a0cab4bf67910f9a92d0153222d7f87cedc63a5b773a5c5482479e554d56d906eaf5d5f9b1c37d9f52170029bd3c397bcb89dca495d7ee8cab0e5e4cd8b204dff933e8673cacece063830d010676be97476c5028c633ca3160a7367352750304ee2be99bf6311c198b1207a20006364d038c0d479f5264c085c8f210ea88f8a7f08535240c3541c641aadae496002449b5477c26ff74be1159c36d71d9dbca3ab2c2a0804590637fb79e0d89e5d801053afa58bcdbf17aa0ce544e3ca12e9b5c204aaea6a0b5698fef75669c1d91690114acfe9439eff58fe532aa243187adadf7118ba78f9772429b3041a07cb203df0125ffa09b619faa1b890d0a5323a4f162b4d0d1a6864f9a0261820bfc901dea4501098ad5f3c3bb0629de269f3e01633b4e0ee6a8f74e648aa40f57dd2496ed7230010f100ede75074bafc160fc1327545a44fd8d34627b136fed6de0bfa72df0e1330116108dc5545f42b9f5b8e302d1546930d9840c18fc92e425bacd0856e842f60f011b7f689aa43b95bbfd02ee20aab2cac2077c9639b9e4f84b8b221b37cdaf13a2010b979e41274c4338395dfc447c0d4a548caf414ee425aab1748475cca9faef4e012051ff90139adccfbf0459e3a5c0638dfb1654ee728cba59c6803ab1daaddda9011ef927e921f956134892499c7513bc2e077986295c050633a1566d449fd0954a00071f56b24c65aaa4549be3c859ac8ec75fe302d89e8755166da3f7d0d08ae285011983fdc2aa2a0611187b0b86119508e3f7642dfe7380f5bdbe6ad27f0dc4ccf201127e3c3923518fec22a4ead80845bbe6008d9b790bd50ec6ef83b087d0490995011982ad7c49288a3115c638a04e14a0c74a75008359b40aff422e9972e276929800237a7bfd1fe105865abbc36e926de565530da393da1ddc0b1005bb0c31504402002569e7e95e54711903c29f63cb8c6b58d47ea3299c455c8738b543d552e8dffe012ea47467abedda0d168d06bcf2f45ab8fa090b90d6e4662d4052df0488084403057542fc7603a7d717dd8267dc036330e3083d2a1562bf5b1ecfbce99804c05100000000000000000000000000000000000000000000000000000000000000000e377bd9c8a2b6faf14c766e51fc9415ed979d604e3d5dcc800b3c5a9c6bec1f239a65bdeb2028926a6fa13fdf2caf0a09ba17e1420b74bef294aa4c9731d2c71f46ae2c57612f525f14cf49e86be5a984aab4d8c25ec3818e19612ba07722d500082c5b600665b3a718fad7516b976749ac3692a6ccf60225c04d1da272a95780012c932f36c1cca1753377ad16b2dbecabb58a2efbf8e88a066d80efba20addf8208d52649fc24ba86ecd87da10efd8ab480c48cadf4ee205e60d10f6e1f1527dd01171915999416a85d08a06e4a8ed18797fcd62910e6624222e120bdd60a00db22fb4d9f0a7aee7570f72b99c673195c67b7dc5e53aa5a32ffc04b409aa79cb52faa6cb8374cd242bd90fd2da9a891fc18022112ecb943b8b0f6c69a4b2e33ad0b16abbd22b38f11f1e76bfd6c1950864753ef84ce0ba869b82f909fbe2751ef0e02b170d107ec7cf6c25d9fdbdd86f5e4790e532254f89ad6cd477bf30816e207ce092de3148c4fd818812abfcadc03a325991fb7dd48b5a54266137709715a11556344ae9bbb2afd1e7ea7fa417f1308d20fdef8dd2168f89101d0643ce62217ee68724101b6e8644ad0860940f95b1674a1befa7ec45235d2c93043087e9001211d72b9f744364770a276f99a183dc184f90ad09d034119f5e4754249046bb201156984d6a418664bda88f98075679d6bb8cc4c644ff727278931797db12a2c65012698726eabb2580faecafa12befe8855a7074da2b4f2d2cd74fd481e818f2287001d445415f6abc3b4a9ff7043906d8f83d5c61ad853b12a72c152a21b9627d8bf00004d5a3deaf642e40e1401ead28babb7ce0263afcc3b3f1e7876a31d6fbea5e90125bff3189cf6ab0e68caa851e60fc92483d3bc2a5ecd511594e7f80f565301c101247b8928fb1866a98b99add332a7e6065cf27ed8a7d48d7775727299308f0ab9002cb7663076e2883fe31b3397f6f5460491835f1a23c173f858fb93cbc312cf360117c5b8e4a25c2e3228b6f4d06525f96e818772292b095b7d344eb3f9f18735bd000889c0fcc3c2f25fe74c915b178db653b0d7725efdc6e2e80a2770738cc3bff9002a578a39364fa16dc82fd7f15872da799f2ddc6422491b9e5c6447ea87adb01a00187461816522889fc2bcb1dc1b0b5d2537df3ae2bb58cbef2b58cb678ba6b9f30121f1cf89f1e9840aace8469b06edf81e9930ddfb8aef4a2c3d46e8551cc40ab400", + txhex: "01000003519b9e874a9dd7fcbfb7802268123dcf970c336891101a3a0705855b72b9eb08c80100000000000000000000000000000000000000000000000000000000000000000000f394d06566a81b024fd8624b4c8592f8b9344e58f227261eb08d821bd190b4ac9c7df0660038ddcf881602a14fe5ad8eebff83d3ce3541a1f232fe61acf71109e417160936863e8c0597c798b73ef08e76b8eeebcaf20260ba91fcdef5f626361f824dc6197b02c599d4511c624a933226d8fccc125611ec80d8ad5acea4baf2f1c77e4c5525d9859b40f43daaf3d4e4450203d80419dcb026b9adbd7d6912284ee9ce20f3721076947f16e1faefd9b8c35116f4cf0044ead0a12fda3f3bcf2ed91627b922c02c10d112df9e782d3750cda151f6ba2ebe6495065141f32800894566011198c4055458005a31d3e285037446ba14bac54420bcab87cbebeaccab1f158b011e90132b7310e6c866e348cab68d21bad1d9f811b4d667a1e40bdef1a19259d3000b972ae06da958658b12ca4a45c2c6ea50d305be86f7e979fc962c687eb012f401a2c4bf0beb6c713343b94d65ee6d937660480daf670ab5b0ed421a9103e8a25119c836dd799600e67e4ec66ef680833398a99b16956aed2ee5bd9e6c8e68009a01145636e5e448d6c8ab902db394aa9c5aeee92dd150e1150d5b8f6674f330c279010912df27232ae126525a130ed8a9b4dee62eb57ce1d8a42ae9bcef867b10ce94012f94aa78846fcdda504243f83f3122aee5dc38dd32fa2a90224b3a40b4cf79c301129028709e39591c55e6e477a36cc26dc2efd7183fb59f51635dbd114dfed9d5010d66f31b781fa181c2114600ee1f03d55439355265700fb9d35ac684a19db398001e12558cc17e59fb85fb825481dee6fef3612c043bb38ca4833183da5381364600077a99d4d2df2f06067359f178a40f98a3e0943309c5ce3ce5398b9909174254012aa49cfc78ebec51c00e7824db0d8d41ef43fbb0396824d2c31b7db64124cc61000a8b51bd6c94a502f7aea3e5001f00d76b165ad580e48f8aa72813da8240286e00055f3480aa7cdb68a20d4b1e567d42389b44cf4e345da9e5a5655a80418695bc012c8f1e1ef8af646b4fb08afaeb3febde17c4c0d04b30dfd22d96a32ced9c9439010c40484e1547d307ad56b79bb7450e4eaaed7ed79bd5392dffd7449043f8783a0015104264f8d80356176ca6fdf4777e4ebc8edcbab50f8b6c366ac75ebd6bb5c701078d55053d2d41f21f51d5e617f282ba9dfa2576f0231048dd73998ceaa699fd0105821b61addd6935c7d80e65c706f0f2aa2d73ad22c5548c9c8b92cd82689dc40115f576c413a37014b9ee5377b121c69ad6e0f00f2b4f6c99d1f68b00a5d923370119b29fe6e9cf35f1344e24d52ee1fdb6521de3176f3d2b2a2b9327ae6e6cfbb10017198369a6ff180253700361c40e7f378c8e7ead6e04f01dbf5b4c47e7e1e4ff001abaf84628764eccbc5d0f66dc0a99505bb0e598362c45312470526afad11caf00242c6a902c564b3b363eb4ae08f643f18571779428358bfbdaf9856b0ebf229f0100000000000000000000000000000000000000000000000000000000000000001b7a78b6f573d4ac5d3a8d7d10dafac0dbc92604474417b29d40448d9fbc821d0fe3f73da41b27752d5db0658fcc0bff7da21112417ac719e491fdd43c8e5d0b07bcca22c27367a276e6a4e0cfb1bfbc772842898356ad4723101d4ada5137f409bb3b290d7519da3af15feb09e183e2be809690099551f1cb984deb1295b7611dd0cfd9000482314a388512d278fc22b2f6e3fc6e95023b4bd228e22626b7440119fa2c6f45a33b008ef5b23a5522ebbf0bf5f26d8263613a7f9b5ad9a192357600107a023453f7f79f413756dfba0096ae5d13158bbbb147089a020e819ef6640728acf01c811b967d1a14d767ac20088c5ca1613aa5d94ebe7fe2840b6431d8d002137dae183071d12cd9fb14c4b4738ef750ff514ce26257d1245eaedfea625516bfcb075a33a8910dc4e40742bfaa70d95591711e9db33d0db0be06cc819817072c22a9c81f4369f68bb1ef71c7f7458671d647caef18b30cca91da4f1b201d082a03306b4a07d1f0e1a8cc8844038fc524201a6c3a346a971203fd45458d292987d44049eecdac25c5622465a8d683a43621612d2fb283724b17a7bfa3bad2266a6fb1cf669eef21c90e24e85d6ab48e8b237355e964407dde00d65fb442dd210ce0378349f533c0e8ab65293de5319e246a044ed22b1e6db28add1508dd6f0da22954eea8a431b211a8b54147e32fec6593f9fb731389f62f94d5047e0f3301216576c71a79629bc3879325280e216d59cf7e6ff2d6a0babe418f8f2ed603ce010e16785f8c855043221ed5a603d4ed4e8e90059669543738ceb2a04dc996079c01264e9beaf4bb82e1b8f47bb0c239167064001e306eeef2b5495ec42754806b65012357f738543b13594ff081a070dbc0575645ac5ce8e0f89d3afdeb3d49e9ddae012a17d5095b9cef08f6eee2ce733dbf2cfcdb8c23e39410ed973e68cf8505266d00280c52b45752dd12ba84499618898111a8f9b19f2b76c06917e57eef221f3387011cc3360b478d144118a82a3c4391814f34aac2fb0f2d043394304bed179689e501292ca5631820f5f465242b4cc517f06c153c176a43f77daca463b7e2d3e2d7ba001aa0b2c90c3172cd2556137d78748f47924432449210d8b2f5e27adb6210bc77012c92a12526a5b0b1ef60994eb4d08e9bb5a3ea330d417838a52ad8636d37fa9e00131153c9affeefd6b98ecb6a80f5e6ea97417376030325182c064e12dd353c32011f4e25c49eea09adaa46d56642e2ea0afced2d75036acd3839435863f1ec3bf1002e19fe8456a7236c8fb77a15f87164debaad865e85c3468152a5deaffde3da6a01", }, } diff --git a/vendor/github.com/caarlos0/env/v6/.github/FUNDING.yml b/vendor/github.com/caarlos0/env/v6/.github/FUNDING.yml new file mode 100644 index 00000000..f87b4ac2 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [caarlos0] diff --git a/vendor/github.com/caarlos0/env/v6/.github/dependabot.yml b/vendor/github.com/caarlos0/env/v6/.github/dependabot.yml new file mode 100644 index 00000000..ecc1aa84 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/.github/dependabot.yml @@ -0,0 +1,18 @@ +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" + time: "08:00" + labels: + - "dependencies" + - "automerge" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + time: "08:00" + labels: + - "dependencies" + - "automerge" diff --git a/vendor/github.com/caarlos0/env/v6/.github/workflows/build.yml b/vendor/github.com/caarlos0/env/v6/.github/workflows/build.yml new file mode 100644 index 00000000..12c54268 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/.github/workflows/build.yml @@ -0,0 +1,61 @@ +name: build + +on: + push: + branches: + - 'master' + tags: + - 'v*' + pull_request: + +jobs: + build: + strategy: + matrix: + go-version: [~1.17] + os: [ ubuntu-latest, macos-latest, windows-latest ] + runs-on: ${{ matrix.os }} + steps: + - + name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - + name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + - + name: Cache Go modules + uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - + name: CI + run: make setup ci + - + name: Upload coverage + uses: codecov/codecov-action@v2 + if: matrix.os == 'ubuntu-latest' + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.txt + - + name: Run GoReleaser + uses: goreleaser/goreleaser-action@v2 + if: success() && startsWith(github.ref, 'refs/tags/') && matrix.os == 'ubuntu-latest' + with: + version: latest + distribution: goreleaser-pro + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} + TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} + TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} + TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} + TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} diff --git a/vendor/github.com/caarlos0/env/v6/.github/workflows/lint.yml b/vendor/github.com/caarlos0/env/v6/.github/workflows/lint.yml new file mode 100644 index 00000000..1c634c4d --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/.github/workflows/lint.yml @@ -0,0 +1,22 @@ +name: golangci-lint +on: + push: + tags: + - v* + branches: + - master + - main + pull_request: +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v2 + with: + go-version: ~1.16 + - uses: actions/checkout@v2 + - name: golangci-lint + uses: golangci/golangci-lint-action@v2 + with: + skip-go-installation: true diff --git a/vendor/github.com/caarlos0/env/v6/.gitignore b/vendor/github.com/caarlos0/env/v6/.gitignore new file mode 100644 index 00000000..ca6a0ff8 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/.gitignore @@ -0,0 +1,4 @@ +coverage.txt +bin +card.png +dist diff --git a/vendor/github.com/caarlos0/env/v6/.golangci.yml b/vendor/github.com/caarlos0/env/v6/.golangci.yml new file mode 100644 index 00000000..ff791f86 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/.golangci.yml @@ -0,0 +1,8 @@ +linters: + enable: + - thelper + - gofumpt + - tparallel + - unconvert + - unparam + - wastedassign diff --git a/vendor/github.com/caarlos0/env/v6/.goreleaser.yml b/vendor/github.com/caarlos0/env/v6/.goreleaser.yml new file mode 100644 index 00000000..4688983c --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/.goreleaser.yml @@ -0,0 +1,3 @@ +includes: + - from_url: + url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/lib.yml diff --git a/vendor/github.com/caarlos0/env/v6/LICENSE.md b/vendor/github.com/caarlos0/env/v6/LICENSE.md new file mode 100644 index 00000000..a05f258e --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-2019 Carlos Alexandro Becker + +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/caarlos0/env/v6/Makefile b/vendor/github.com/caarlos0/env/v6/Makefile new file mode 100644 index 00000000..d54d31f7 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/Makefile @@ -0,0 +1,37 @@ +SOURCE_FILES?=./... +TEST_PATTERN?=. + +export GO111MODULE := on + +setup: + go mod tidy +.PHONY: setup + +build: + go build +.PHONY: build + +test: + go test -v -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=2m +.PHONY: test + +cover: test + go tool cover -html=coverage.txt +.PHONY: cover + +fmt: + find . -name '*.go' -not -wholename './vendor/*' | while read -r file; do gofmt -w -s "$$file"; goimports -w "$$file"; done +.PHONY: fmt + +lint: + ./bin/golangci-lint run ./... +.PHONY: lint + +ci: build test +.PHONY: ci + +card: + wget -O card.png -c "https://og.caarlos0.dev/**env**: parse envs to structs.png?theme=light&md=1&fontSize=100px&images=https://github.com/caarlos0.png" +.PHONY: card + +.DEFAULT_GOAL := ci diff --git a/vendor/github.com/caarlos0/env/v6/README.md b/vendor/github.com/caarlos0/env/v6/README.md new file mode 100644 index 00000000..a0281009 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/README.md @@ -0,0 +1,459 @@ +# env + +[![Build Status](https://img.shields.io/github/workflow/status/caarlos0/env/build?style=for-the-badge)](https://github.com/caarlos0/env/actions?workflow=build) +[![Coverage Status](https://img.shields.io/codecov/c/gh/caarlos0/env.svg?logo=codecov&style=for-the-badge)](https://codecov.io/gh/caarlos0/env) +[![](http://img.shields.io/badge/godoc-reference-5272B4.svg?style=for-the-badge)](https://pkg.go.dev/github.com/caarlos0/env/v6) + +Simple lib to parse envs to structs in Go. + +## Example + +Get the module with: + +```sh +go get github.com/caarlos0/env/v6 +``` + +The usage looks like this: + +```go +package main + +import ( + "fmt" + "time" + + "github.com/caarlos0/env/v6" +) + +type config struct { + Home string `env:"HOME"` + Port int `env:"PORT" envDefault:"3000"` + Password string `env:"PASSWORD,unset"` + IsProduction bool `env:"PRODUCTION"` + Hosts []string `env:"HOSTS" envSeparator:":"` + Duration time.Duration `env:"DURATION"` + TempFolder string `env:"TEMP_FOLDER" envDefault:"${HOME}/tmp" envExpand:"true"` +} + +func main() { + cfg := config{} + if err := env.Parse(&cfg); err != nil { + fmt.Printf("%+v\n", err) + } + + fmt.Printf("%+v\n", cfg) +} +``` + +You can run it like this: + +```sh +$ PRODUCTION=true HOSTS="host1:host2:host3" DURATION=1s go run main.go +{Home:/your/home Port:3000 IsProduction:true Hosts:[host1 host2 host3] Duration:1s} +``` + +⚠️⚠️⚠️ **Attention:** _unexported fields_ will be **ignored**. + +## Supported types and defaults + +Out of the box all built-in types are supported, plus a few others that +are commonly used. + +Complete list: + +- `string` +- `bool` +- `int` +- `int8` +- `int16` +- `int32` +- `int64` +- `uint` +- `uint8` +- `uint16` +- `uint32` +- `uint64` +- `float32` +- `float64` +- `string` +- `time.Duration` +- `encoding.TextUnmarshaler` +- `url.URL` + +Pointers, slices and slices of pointers of those types are also supported. + +You can also use/define a [custom parser func](#custom-parser-funcs) for any +other type you want. + +If you set the `envDefault` tag for something, this value will be used in the +case of absence of it in the environment. + +By default, slice types will split the environment value on `,`; you can change +this behavior by setting the `envSeparator` tag. + +If you set the `envExpand` tag, environment variables (either in `${var}` or +`$var` format) in the string will be replaced according with the actual value +of the variable. + +## Custom Parser Funcs + +If you have a type that is not supported out of the box by the lib, you are able +to use (or define) and pass custom parsers (and their associated `reflect.Type`) +to the `env.ParseWithFuncs()` function. + +In addition to accepting a struct pointer (same as `Parse()`), this function +also accepts a `map[reflect.Type]env.ParserFunc`. + +`env` also ships with some pre-built custom parser funcs for common types. You +can check them out [here](parsers/). + +If you add a custom parser for, say `Foo`, it will also be used to parse +`*Foo` and `[]Foo` types. + +This directory contains pre-built, custom parsers that can be used with `env.ParseWithFuncs` +to facilitate the parsing of envs that are not basic types. + +Check the example in the [go doc](http://godoc.org/github.com/caarlos0/env) +for more info. + +### A note about `TextUnmarshaler` and `time.Time` + +Env supports by default anything that implements the `TextUnmarshaler` interface. +That includes things like `time.Time` for example. +The upside is that depending on the format you need, you don't need to change anything. +The downside is that if you do need time in another format, you'll need to create your own type. + +Its fairly straightforward: + +```go +type MyTime time.Time + +func (t *MyTime) UnmarshalText(text []byte) error { + tt, err := time.Parse("2006-01-02", string(text)) + *t = MyTime(tt) + return err +} + +type Config struct { + SomeTime MyTime `env:"SOME_TIME"` +} +``` + +And then you can parse `Config` with `env.Parse`. + +## Required fields + +The `env` tag option `required` (e.g., `env:"tagKey,required"`) can be added to ensure that some environment variable is set. +In the example above, an error is returned if the `config` struct is changed to: + +```go +type config struct { + SecretKey string `env:"SECRET_KEY,required"` +} +``` + +## Not Empty fields + +While `required` demands the environment variable to be check, it doesn't check its value. +If you want to make sure the environment is set and not empty, you need to use the `notEmpty` tag option instead (`env:"SOME_ENV,notEmpty"`). + +Example: + +```go +type config struct { + SecretKey string `env:"SECRET_KEY,notEmpty"` +} +``` + +## Unset environment variable after reading it + +The `env` tag option `unset` (e.g., `env:"tagKey,unset"`) can be added +to ensure that some environment variable is unset after reading it. + +Example: + +```go +type config struct { + SecretKey string `env:"SECRET_KEY,unset"` +} +``` + +## From file + +The `env` tag option `file` (e.g., `env:"tagKey,file"`) can be added +to in order to indicate that the value of the variable shall be loaded from a file. The path of that file is given +by the environment variable associated with it +Example below + +```go +package main + +import ( + "fmt" + "time" + "github.com/caarlos0/env/v6" +) + +type config struct { + Secret string `env:"SECRET,file"` + Password string `env:"PASSWORD,file" envDefault:"/tmp/password"` + Certificate string `env:"CERTIFICATE,file" envDefault:"${CERTIFICATE_FILE}" envExpand:"true"` +} + +func main() { + cfg := config{} + if err := env.Parse(&cfg); err != nil { + fmt.Printf("%+v\n", err) + } + + fmt.Printf("%+v\n", cfg) +} +``` + +```sh +$ echo qwerty > /tmp/secret +$ echo dvorak > /tmp/password +$ echo coleman > /tmp/certificate + +$ SECRET=/tmp/secret \ + CERTIFICATE_FILE=/tmp/certificate \ + go run main.go +{Secret:qwerty Password:dvorak Certificate:coleman} +``` + +## Options + +### Environment + +By setting the `Options.Environment` map you can tell `Parse` to add those `keys` and `values` +as env vars before parsing is done. These envs are stored in the map and never actually set by `os.Setenv`. +This option effectively makes `env` ignore the OS environment variables: only the ones provided in the option are used. + +This can make your testing scenarios a bit more clean and easy to handle. + +```go +package main + +import ( + "fmt" + "log" + + "github.com/caarlos0/env/v6" +) + +type Config struct { + Password string `env:"PASSWORD"` +} + +func main() { + cfg := &Config{} + opts := &env.Options{Environment: map[string]string{ + "PASSWORD": "MY_PASSWORD", + }} + + // Load env vars. + if err := env.Parse(cfg, opts); err != nil { + log.Fatal(err) + } + + // Print the loaded data. + fmt.Printf("%+v\n", cfg.envData) +} +``` + +### Changing default tag name + +You can change what tag name to use for setting the env vars by setting the `Options.TagName` +variable. + +For example +```go +package main + +import ( + "fmt" + "log" + + "github.com/caarlos0/env/v6" +) + +type Config struct { + Password string `json:"PASSWORD"` +} + +func main() { + cfg := &Config{} + opts := &env.Options{TagName: "json"} + + // Load env vars. + if err := env.Parse(cfg, opts); err != nil { + log.Fatal(err) + } + + // Print the loaded data. + fmt.Printf("%+v\n", cfg.envData) +} +``` + +### Prefixes + +You can prefix sub-structs env tags, as well as a whole `env.Parse` call. + +Here's an example flexing it a bit: + +```go +package main + +import ( + "fmt" + "log" + + "github.com/caarlos0/env/v6" +) + +type Config struct { + Home string `env:"HOME"` +} + +type ComplexConfig struct { + Foo Config `envPrefix:"FOO_"` + Clean Config + Bar Config `envPrefix:"BAR_"` + Blah string `env:"BLAH"` +} + +func main() { + cfg := ComplexConfig{} + if err := Parse(&cfg, Options{ + Prefix: "T_", + Environment: map[string]string{ + "T_FOO_HOME": "/foo", + "T_BAR_HOME": "/bar", + "T_BLAH": "blahhh", + "T_HOME": "/clean", + }, + }); err != nil { + log.Fatal(err) + } + + // Load env vars. + if err := env.Parse(cfg, opts); err != nil { + log.Fatal(err) + } + + // Print the loaded data. + fmt.Printf("%+v\n", cfg.envData) +} +``` + +### On set hooks + +You might want to listen to value sets and, for example, log something or do some other kind of logic. +You can do this by passing a `OnSet` option: + +```go +package main + +import ( + "fmt" + "log" + + "github.com/caarlos0/env/v6" +) + +type Config struct { + Username string `env:"USERNAME" envDefault:"admin"` + Password string `env:"PASSWORD"` +} + +func main() { + cfg := &Config{} + opts := &env.Options{ + OnSet: func(tag string, value interface{}, isDefault bool) { + fmt.Printf("Set %s to %v (default? %v)\n", tag, value, isDefault) + }, + } + + // Load env vars. + if err := env.Parse(cfg, opts); err != nil { + log.Fatal(err) + } + + // Print the loaded data. + fmt.Printf("%+v\n", cfg.envData) +} +``` + +## Making all fields to required + +You can make all fields that don't have a default value be required by setting the `RequiredIfNoDef: true` in the `Options`. + +For example + +```go +package main + +import ( + "fmt" + "log" + + "github.com/caarlos0/env/v6" +) + +type Config struct { + Username string `env:"USERNAME" envDefault:"admin"` + Password string `env:"PASSWORD"` +} + +func main() { + cfg := &Config{} + opts := &env.Options{RequiredIfNoDef: true} + + // Load env vars. + if err := env.Parse(cfg, opts); err != nil { + log.Fatal(err) + } + + // Print the loaded data. + fmt.Printf("%+v\n", cfg.envData) +} +``` + +## Defaults from code + +You may define default value also in code, by initialising the config data before it's filled by `env.Parse`. +Default values defined as struct tags will overwrite existing values during Parse. + +```go +package main + +import ( + "fmt" + "log" + + "github.com/caarlos0/env/v6" +) + +type Config struct { + Username string `env:"USERNAME" envDefault:"admin"` + Password string `env:"PASSWORD"` +} + +func main() { + var cfg = Config{ + Username: "test", + Password: "123456", + } + + if err := env.Parse(&cfg); err != nil { + fmt.Println("failed:", err) + } + + fmt.Printf("%+v", cfg) // {Username:admin Password:123456} +} +``` + +## Stargazers over time + +[![Stargazers over time](https://starchart.cc/caarlos0/env.svg)](https://starchart.cc/caarlos0/env) diff --git a/vendor/github.com/caarlos0/env/v6/env.go b/vendor/github.com/caarlos0/env/v6/env.go new file mode 100644 index 00000000..53e36c42 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/env.go @@ -0,0 +1,477 @@ +package env + +import ( + "encoding" + "errors" + "fmt" + "net/url" + "os" + "reflect" + "strconv" + "strings" + "time" +) + +// nolint: gochecknoglobals +var ( + // ErrNotAStructPtr is returned if you pass something that is not a pointer to a + // Struct to Parse. + ErrNotAStructPtr = errors.New("env: expected a pointer to a Struct") + + defaultBuiltInParsers = map[reflect.Kind]ParserFunc{ + reflect.Bool: func(v string) (interface{}, error) { + return strconv.ParseBool(v) + }, + reflect.String: func(v string) (interface{}, error) { + return v, nil + }, + reflect.Int: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 32) + return int(i), err + }, + reflect.Int16: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 16) + return int16(i), err + }, + reflect.Int32: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 32) + return int32(i), err + }, + reflect.Int64: func(v string) (interface{}, error) { + return strconv.ParseInt(v, 10, 64) + }, + reflect.Int8: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 8) + return int8(i), err + }, + reflect.Uint: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 32) + return uint(i), err + }, + reflect.Uint16: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 16) + return uint16(i), err + }, + reflect.Uint32: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 32) + return uint32(i), err + }, + reflect.Uint64: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 64) + return i, err + }, + reflect.Uint8: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 8) + return uint8(i), err + }, + reflect.Float64: func(v string) (interface{}, error) { + return strconv.ParseFloat(v, 64) + }, + reflect.Float32: func(v string) (interface{}, error) { + f, err := strconv.ParseFloat(v, 32) + return float32(f), err + }, + } + + defaultTypeParsers = map[reflect.Type]ParserFunc{ + reflect.TypeOf(url.URL{}): func(v string) (interface{}, error) { + u, err := url.Parse(v) + if err != nil { + return nil, fmt.Errorf("unable to parse URL: %v", err) + } + return *u, nil + }, + reflect.TypeOf(time.Nanosecond): func(v string) (interface{}, error) { + s, err := time.ParseDuration(v) + if err != nil { + return nil, fmt.Errorf("unable to parse duration: %v", err) + } + return s, err + }, + } +) + +// ParserFunc defines the signature of a function that can be used within `CustomParsers`. +type ParserFunc func(v string) (interface{}, error) + +// OnSetFn is a hook that can be run when a value is set. +type OnSetFn func(tag string, value interface{}, isDefault bool) + +// Options for the parser. +type Options struct { + // Environment keys and values that will be accessible for the service. + Environment map[string]string + + // TagName specifies another tagname to use rather than the default env. + TagName string + + // RequiredIfNoDef automatically sets all env as required if they do not declare 'envDefault' + RequiredIfNoDef bool + + // OnSet allows to run a function when a value is set + OnSet OnSetFn + + // Prefix define a prefix for each key + Prefix string + + // Sets to true if we have already configured once. + configured bool +} + +// configure will do the basic configurations and defaults. +func configure(opts []Options) []Options { + // If we have already configured the first item + // of options will have been configured set to true. + if len(opts) > 0 && opts[0].configured { + return opts + } + + // Created options with defaults. + opt := Options{ + TagName: "env", + Environment: toMap(os.Environ()), + configured: true, + } + + // Loop over all opts structs and set + // to opt if value is not default/empty. + for _, item := range opts { + if item.Environment != nil { + opt.Environment = item.Environment + } + if item.TagName != "" { + opt.TagName = item.TagName + } + if item.OnSet != nil { + opt.OnSet = item.OnSet + } + if item.Prefix != "" { + opt.Prefix = item.Prefix + } + opt.RequiredIfNoDef = item.RequiredIfNoDef + } + + return []Options{opt} +} + +func getOnSetFn(opts []Options) OnSetFn { + return opts[0].OnSet +} + +// getTagName returns the tag name. +func getTagName(opts []Options) string { + return opts[0].TagName +} + +// getEnvironment returns the environment map. +func getEnvironment(opts []Options) map[string]string { + return opts[0].Environment +} + +// Parse parses a struct containing `env` tags and loads its values from +// environment variables. +func Parse(v interface{}, opts ...Options) error { + return ParseWithFuncs(v, map[reflect.Type]ParserFunc{}, opts...) +} + +// ParseWithFuncs is the same as `Parse` except it also allows the user to pass +// in custom parsers. +func ParseWithFuncs(v interface{}, funcMap map[reflect.Type]ParserFunc, opts ...Options) error { + opts = configure(opts) + + ptrRef := reflect.ValueOf(v) + if ptrRef.Kind() != reflect.Ptr { + return ErrNotAStructPtr + } + ref := ptrRef.Elem() + if ref.Kind() != reflect.Struct { + return ErrNotAStructPtr + } + parsers := defaultTypeParsers + for k, v := range funcMap { + parsers[k] = v + } + + return doParse(ref, parsers, opts) +} + +func doParse(ref reflect.Value, funcMap map[reflect.Type]ParserFunc, opts []Options) error { + refType := ref.Type() + + for i := 0; i < refType.NumField(); i++ { + refField := ref.Field(i) + if !refField.CanSet() { + continue + } + if reflect.Ptr == refField.Kind() && !refField.IsNil() { + err := ParseWithFuncs(refField.Interface(), funcMap, opts...) + if err != nil { + return err + } + continue + } + if reflect.Struct == refField.Kind() && refField.CanAddr() && refField.Type().Name() == "" { + err := Parse(refField.Addr().Interface(), opts...) + if err != nil { + return err + } + continue + } + refTypeField := refType.Field(i) + value, err := get(refTypeField, opts) + if err != nil { + return err + } + if value == "" { + if reflect.Struct == refField.Kind() { + subOpts := make([]Options, len(opts)) + copy(subOpts, opts) + if prefix := refType.Field(i).Tag.Get("envPrefix"); prefix != "" { + subOpts[0].Prefix += prefix + } + if err := doParse(refField, funcMap, subOpts); err != nil { + return err + } + } + continue + } + if err := set(refField, refTypeField, value, funcMap); err != nil { + return err + } + } + return nil +} + +func get(field reflect.StructField, opts []Options) (val string, err error) { + var exists bool + var isDefault bool + var loadFile bool + var unset bool + var notEmpty bool + + required := opts[0].RequiredIfNoDef + prefix := opts[0].Prefix + key, tags := parseKeyForOption(field.Tag.Get(getTagName(opts))) + key = prefix + key + for _, tag := range tags { + switch tag { + case "": + continue + case "file": + loadFile = true + case "required": + required = true + case "unset": + unset = true + case "notEmpty": + notEmpty = true + default: + return "", fmt.Errorf("env: tag option %q not supported", tag) + } + } + expand := strings.EqualFold(field.Tag.Get("envExpand"), "true") + defaultValue, defExists := field.Tag.Lookup("envDefault") + val, exists, isDefault = getOr(key, defaultValue, defExists, getEnvironment(opts)) + + if expand { + val = os.ExpandEnv(val) + } + + if unset { + defer os.Unsetenv(key) + } + + if required && !exists && len(key) > 0 { + return "", fmt.Errorf(`env: required environment variable %q is not set`, key) + } + + if notEmpty && val == "" { + return "", fmt.Errorf("env: environment variable %q should not be empty", key) + } + + if loadFile && val != "" { + filename := val + val, err = getFromFile(filename) + if err != nil { + return "", fmt.Errorf(`env: could not load content of file "%s" from variable %s: %v`, filename, key, err) + } + } + + if onSetFn := getOnSetFn(opts); onSetFn != nil { + onSetFn(key, val, isDefault) + } + return val, err +} + +// split the env tag's key into the expected key and desired option, if any. +func parseKeyForOption(key string) (string, []string) { + opts := strings.Split(key, ",") + return opts[0], opts[1:] +} + +func getFromFile(filename string) (value string, err error) { + b, err := os.ReadFile(filename) + return string(b), err +} + +func getOr(key, defaultValue string, defExists bool, envs map[string]string) (string, bool, bool) { + value, exists := envs[key] + switch { + case (!exists || key == "") && defExists: + return defaultValue, true, true + case !exists: + return "", false, false + } + + return value, true, false +} + +func set(field reflect.Value, sf reflect.StructField, value string, funcMap map[reflect.Type]ParserFunc) error { + if tm := asTextUnmarshaler(field); tm != nil { + if err := tm.UnmarshalText([]byte(value)); err != nil { + return newParseError(sf, err) + } + return nil + } + + typee := sf.Type + fieldee := field + if typee.Kind() == reflect.Ptr { + typee = typee.Elem() + fieldee = field.Elem() + } + + parserFunc, ok := funcMap[typee] + if ok { + val, err := parserFunc(value) + if err != nil { + return newParseError(sf, err) + } + + fieldee.Set(reflect.ValueOf(val)) + return nil + } + + parserFunc, ok = defaultBuiltInParsers[typee.Kind()] + if ok { + val, err := parserFunc(value) + if err != nil { + return newParseError(sf, err) + } + + fieldee.Set(reflect.ValueOf(val).Convert(typee)) + return nil + } + + if field.Kind() == reflect.Slice { + return handleSlice(field, value, sf, funcMap) + } + + return newNoParserError(sf) +} + +func handleSlice(field reflect.Value, value string, sf reflect.StructField, funcMap map[reflect.Type]ParserFunc) error { + separator := sf.Tag.Get("envSeparator") + if separator == "" { + separator = "," + } + parts := strings.Split(value, separator) + + typee := sf.Type.Elem() + if typee.Kind() == reflect.Ptr { + typee = typee.Elem() + } + + if _, ok := reflect.New(typee).Interface().(encoding.TextUnmarshaler); ok { + return parseTextUnmarshalers(field, parts, sf) + } + + parserFunc, ok := funcMap[typee] + if !ok { + parserFunc, ok = defaultBuiltInParsers[typee.Kind()] + if !ok { + return newNoParserError(sf) + } + } + + result := reflect.MakeSlice(sf.Type, 0, len(parts)) + for _, part := range parts { + r, err := parserFunc(part) + if err != nil { + return newParseError(sf, err) + } + v := reflect.ValueOf(r).Convert(typee) + if sf.Type.Elem().Kind() == reflect.Ptr { + v = reflect.New(typee) + v.Elem().Set(reflect.ValueOf(r).Convert(typee)) + } + result = reflect.Append(result, v) + } + field.Set(result) + return nil +} + +func asTextUnmarshaler(field reflect.Value) encoding.TextUnmarshaler { + if reflect.Ptr == field.Kind() { + if field.IsNil() { + field.Set(reflect.New(field.Type().Elem())) + } + } else if field.CanAddr() { + field = field.Addr() + } + + tm, ok := field.Interface().(encoding.TextUnmarshaler) + if !ok { + return nil + } + return tm +} + +func parseTextUnmarshalers(field reflect.Value, data []string, sf reflect.StructField) error { + s := len(data) + elemType := field.Type().Elem() + slice := reflect.MakeSlice(reflect.SliceOf(elemType), s, s) + for i, v := range data { + sv := slice.Index(i) + kind := sv.Kind() + if kind == reflect.Ptr { + sv = reflect.New(elemType.Elem()) + } else { + sv = sv.Addr() + } + tm := sv.Interface().(encoding.TextUnmarshaler) + if err := tm.UnmarshalText([]byte(v)); err != nil { + return newParseError(sf, err) + } + if kind == reflect.Ptr { + slice.Index(i).Set(sv) + } + } + + field.Set(slice) + + return nil +} + +func newParseError(sf reflect.StructField, err error) error { + if err == nil { + return nil + } + return parseError{ + sf: sf, + err: err, + } +} + +type parseError struct { + sf reflect.StructField + err error +} + +func (e parseError) Error() string { + return fmt.Sprintf(`env: parse error on field "%s" of type "%s": %v`, e.sf.Name, e.sf.Type, e.err) +} + +func newNoParserError(sf reflect.StructField) error { + return fmt.Errorf(`env: no parser found for field "%s" of type "%s"`, sf.Name, sf.Type) +} diff --git a/vendor/github.com/caarlos0/env/v6/env_test.go b/vendor/github.com/caarlos0/env/v6/env_test.go new file mode 100644 index 00000000..3e04dd3c --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/env_test.go @@ -0,0 +1,1477 @@ +package env + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "os" + "path/filepath" + "reflect" + "runtime" + "strconv" + "strings" + "testing" + "time" + + "github.com/matryer/is" +) + +type unmarshaler struct { + time.Duration +} + +// TextUnmarshaler implements encoding.TextUnmarshaler. +func (d *unmarshaler) UnmarshalText(data []byte) (err error) { + if len(data) != 0 { + d.Duration, err = time.ParseDuration(string(data)) + } else { + d.Duration = 0 + } + return err +} + +// nolint: maligned +type Config struct { + String string `env:"STRING"` + StringPtr *string `env:"STRING"` + Strings []string `env:"STRINGS"` + StringPtrs []*string `env:"STRINGS"` + + Bool bool `env:"BOOL"` + BoolPtr *bool `env:"BOOL"` + Bools []bool `env:"BOOLS"` + BoolPtrs []*bool `env:"BOOLS"` + + Int int `env:"INT"` + IntPtr *int `env:"INT"` + Ints []int `env:"INTS"` + IntPtrs []*int `env:"INTS"` + + Int8 int8 `env:"INT8"` + Int8Ptr *int8 `env:"INT8"` + Int8s []int8 `env:"INT8S"` + Int8Ptrs []*int8 `env:"INT8S"` + + Int16 int16 `env:"INT16"` + Int16Ptr *int16 `env:"INT16"` + Int16s []int16 `env:"INT16S"` + Int16Ptrs []*int16 `env:"INT16S"` + + Int32 int32 `env:"INT32"` + Int32Ptr *int32 `env:"INT32"` + Int32s []int32 `env:"INT32S"` + Int32Ptrs []*int32 `env:"INT32S"` + + Int64 int64 `env:"INT64"` + Int64Ptr *int64 `env:"INT64"` + Int64s []int64 `env:"INT64S"` + Int64Ptrs []*int64 `env:"INT64S"` + + Uint uint `env:"UINT"` + UintPtr *uint `env:"UINT"` + Uints []uint `env:"UINTS"` + UintPtrs []*uint `env:"UINTS"` + + Uint8 uint8 `env:"UINT8"` + Uint8Ptr *uint8 `env:"UINT8"` + Uint8s []uint8 `env:"UINT8S"` + Uint8Ptrs []*uint8 `env:"UINT8S"` + + Uint16 uint16 `env:"UINT16"` + Uint16Ptr *uint16 `env:"UINT16"` + Uint16s []uint16 `env:"UINT16S"` + Uint16Ptrs []*uint16 `env:"UINT16S"` + + Uint32 uint32 `env:"UINT32"` + Uint32Ptr *uint32 `env:"UINT32"` + Uint32s []uint32 `env:"UINT32S"` + Uint32Ptrs []*uint32 `env:"UINT32S"` + + Uint64 uint64 `env:"UINT64"` + Uint64Ptr *uint64 `env:"UINT64"` + Uint64s []uint64 `env:"UINT64S"` + Uint64Ptrs []*uint64 `env:"UINT64S"` + + Float32 float32 `env:"FLOAT32"` + Float32Ptr *float32 `env:"FLOAT32"` + Float32s []float32 `env:"FLOAT32S"` + Float32Ptrs []*float32 `env:"FLOAT32S"` + + Float64 float64 `env:"FLOAT64"` + Float64Ptr *float64 `env:"FLOAT64"` + Float64s []float64 `env:"FLOAT64S"` + Float64Ptrs []*float64 `env:"FLOAT64S"` + + Duration time.Duration `env:"DURATION"` + Durations []time.Duration `env:"DURATIONS"` + DurationPtr *time.Duration `env:"DURATION"` + DurationPtrs []*time.Duration `env:"DURATIONS"` + + Unmarshaler unmarshaler `env:"UNMARSHALER"` + UnmarshalerPtr *unmarshaler `env:"UNMARSHALER"` + Unmarshalers []unmarshaler `env:"UNMARSHALERS"` + UnmarshalerPtrs []*unmarshaler `env:"UNMARSHALERS"` + + URL url.URL `env:"URL"` + URLPtr *url.URL `env:"URL"` + URLs []url.URL `env:"URLS"` + URLPtrs []*url.URL `env:"URLS"` + + StringWithdefault string `env:"DATABASE_URL" envDefault:"postgres://localhost:5432/db"` + + CustomSeparator []string `env:"SEPSTRINGS" envSeparator:":"` + + NonDefined struct { + String string `env:"NONDEFINED_STR"` + } + + NotAnEnv string + unexported string `env:"FOO"` +} + +type ParentStruct struct { + InnerStruct *InnerStruct + unexported *InnerStruct + Ignored *http.Client +} + +type InnerStruct struct { + Inner string `env:"innervar"` + Number uint `env:"innernum"` +} + +type ForNestedStruct struct { + NestedStruct +} + +type NestedStruct struct { + NestedVar string `env:"nestedvar"` +} + +func TestParsesEnv(t *testing.T) { + is := is.New(t) + + defer os.Clearenv() + + tos := func(v interface{}) string { + return fmt.Sprintf("%v", v) + } + + toss := func(v ...interface{}) string { + ss := []string{} + for _, s := range v { + ss = append(ss, tos(s)) + } + return strings.Join(ss, ",") + } + + str1 := "str1" + str2 := "str2" + os.Setenv("STRING", str1) + os.Setenv("STRINGS", toss(str1, str2)) + + bool1 := true + bool2 := false + os.Setenv("BOOL", tos(bool1)) + os.Setenv("BOOLS", toss(bool1, bool2)) + + int1 := -1 + int2 := 2 + os.Setenv("INT", tos(int1)) + os.Setenv("INTS", toss(int1, int2)) + + var int81 int8 = -2 + var int82 int8 = 5 + os.Setenv("INT8", tos(int81)) + os.Setenv("INT8S", toss(int81, int82)) + + var int161 int16 = -24 + var int162 int16 = 15 + os.Setenv("INT16", tos(int161)) + os.Setenv("INT16S", toss(int161, int162)) + + var int321 int32 = -14 + var int322 int32 = 154 + os.Setenv("INT32", tos(int321)) + os.Setenv("INT32S", toss(int321, int322)) + + var int641 int64 = -12 + var int642 int64 = 150 + os.Setenv("INT64", tos(int641)) + os.Setenv("INT64S", toss(int641, int642)) + + var uint1 uint = 1 + var uint2 uint = 2 + os.Setenv("UINT", tos(uint1)) + os.Setenv("UINTS", toss(uint1, uint2)) + + var uint81 uint8 = 15 + var uint82 uint8 = 51 + os.Setenv("UINT8", tos(uint81)) + os.Setenv("UINT8S", toss(uint81, uint82)) + + var uint161 uint16 = 532 + var uint162 uint16 = 123 + os.Setenv("UINT16", tos(uint161)) + os.Setenv("UINT16S", toss(uint161, uint162)) + + var uint321 uint32 = 93 + var uint322 uint32 = 14 + os.Setenv("UINT32", tos(uint321)) + os.Setenv("UINT32S", toss(uint321, uint322)) + + var uint641 uint64 = 5 + var uint642 uint64 = 43 + os.Setenv("UINT64", tos(uint641)) + os.Setenv("UINT64S", toss(uint641, uint642)) + + var float321 float32 = 9.3 + var float322 float32 = 1.1 + os.Setenv("FLOAT32", tos(float321)) + os.Setenv("FLOAT32S", toss(float321, float322)) + + float641 := 1.53 + float642 := 0.5 + os.Setenv("FLOAT64", tos(float641)) + os.Setenv("FLOAT64S", toss(float641, float642)) + + duration1 := time.Second + duration2 := time.Second * 4 + os.Setenv("DURATION", tos(duration1)) + os.Setenv("DURATIONS", toss(duration1, duration2)) + + unmarshaler1 := unmarshaler{time.Minute} + unmarshaler2 := unmarshaler{time.Millisecond * 1232} + os.Setenv("UNMARSHALER", tos(unmarshaler1.Duration)) + os.Setenv("UNMARSHALERS", toss(unmarshaler1.Duration, unmarshaler2.Duration)) + + url1 := "https://goreleaser.com" + url2 := "https://caarlos0.dev" + os.Setenv("URL", tos(url1)) + os.Setenv("URLS", toss(url1, url2)) + + os.Setenv("SEPSTRINGS", strings.Join([]string{str1, str2}, ":")) + + nonDefinedStr := "nonDefinedStr" + os.Setenv("NONDEFINED_STR", nonDefinedStr) + + cfg := Config{} + is.NoErr(Parse(&cfg)) + + is.Equal(str1, cfg.String) + is.Equal(&str1, cfg.StringPtr) + is.Equal(str1, cfg.Strings[0]) + is.Equal(str2, cfg.Strings[1]) + is.Equal(&str1, cfg.StringPtrs[0]) + is.Equal(&str2, cfg.StringPtrs[1]) + + is.Equal(bool1, cfg.Bool) + is.Equal(&bool1, cfg.BoolPtr) + is.Equal(bool1, cfg.Bools[0]) + is.Equal(bool2, cfg.Bools[1]) + is.Equal(&bool1, cfg.BoolPtrs[0]) + is.Equal(&bool2, cfg.BoolPtrs[1]) + + is.Equal(int1, cfg.Int) + is.Equal(&int1, cfg.IntPtr) + is.Equal(int1, cfg.Ints[0]) + is.Equal(int2, cfg.Ints[1]) + is.Equal(&int1, cfg.IntPtrs[0]) + is.Equal(&int2, cfg.IntPtrs[1]) + + is.Equal(int81, cfg.Int8) + is.Equal(&int81, cfg.Int8Ptr) + is.Equal(int81, cfg.Int8s[0]) + is.Equal(int82, cfg.Int8s[1]) + is.Equal(&int81, cfg.Int8Ptrs[0]) + is.Equal(&int82, cfg.Int8Ptrs[1]) + + is.Equal(int161, cfg.Int16) + is.Equal(&int161, cfg.Int16Ptr) + is.Equal(int161, cfg.Int16s[0]) + is.Equal(int162, cfg.Int16s[1]) + is.Equal(&int161, cfg.Int16Ptrs[0]) + is.Equal(&int162, cfg.Int16Ptrs[1]) + + is.Equal(int321, cfg.Int32) + is.Equal(&int321, cfg.Int32Ptr) + is.Equal(int321, cfg.Int32s[0]) + is.Equal(int322, cfg.Int32s[1]) + is.Equal(&int321, cfg.Int32Ptrs[0]) + is.Equal(&int322, cfg.Int32Ptrs[1]) + + is.Equal(int641, cfg.Int64) + is.Equal(&int641, cfg.Int64Ptr) + is.Equal(int641, cfg.Int64s[0]) + is.Equal(int642, cfg.Int64s[1]) + is.Equal(&int641, cfg.Int64Ptrs[0]) + is.Equal(&int642, cfg.Int64Ptrs[1]) + + is.Equal(uint1, cfg.Uint) + is.Equal(&uint1, cfg.UintPtr) + is.Equal(uint1, cfg.Uints[0]) + is.Equal(uint2, cfg.Uints[1]) + is.Equal(&uint1, cfg.UintPtrs[0]) + is.Equal(&uint2, cfg.UintPtrs[1]) + + is.Equal(uint81, cfg.Uint8) + is.Equal(&uint81, cfg.Uint8Ptr) + is.Equal(uint81, cfg.Uint8s[0]) + is.Equal(uint82, cfg.Uint8s[1]) + is.Equal(&uint81, cfg.Uint8Ptrs[0]) + is.Equal(&uint82, cfg.Uint8Ptrs[1]) + + is.Equal(uint161, cfg.Uint16) + is.Equal(&uint161, cfg.Uint16Ptr) + is.Equal(uint161, cfg.Uint16s[0]) + is.Equal(uint162, cfg.Uint16s[1]) + is.Equal(&uint161, cfg.Uint16Ptrs[0]) + is.Equal(&uint162, cfg.Uint16Ptrs[1]) + + is.Equal(uint321, cfg.Uint32) + is.Equal(&uint321, cfg.Uint32Ptr) + is.Equal(uint321, cfg.Uint32s[0]) + is.Equal(uint322, cfg.Uint32s[1]) + is.Equal(&uint321, cfg.Uint32Ptrs[0]) + is.Equal(&uint322, cfg.Uint32Ptrs[1]) + + is.Equal(uint641, cfg.Uint64) + is.Equal(&uint641, cfg.Uint64Ptr) + is.Equal(uint641, cfg.Uint64s[0]) + is.Equal(uint642, cfg.Uint64s[1]) + is.Equal(&uint641, cfg.Uint64Ptrs[0]) + is.Equal(&uint642, cfg.Uint64Ptrs[1]) + + is.Equal(float321, cfg.Float32) + is.Equal(&float321, cfg.Float32Ptr) + is.Equal(float321, cfg.Float32s[0]) + is.Equal(float322, cfg.Float32s[1]) + is.Equal(&float321, cfg.Float32Ptrs[0]) + is.Equal(&float322, cfg.Float32Ptrs[1]) + + is.Equal(float641, cfg.Float64) + is.Equal(&float641, cfg.Float64Ptr) + is.Equal(float641, cfg.Float64s[0]) + is.Equal(float642, cfg.Float64s[1]) + is.Equal(&float641, cfg.Float64Ptrs[0]) + is.Equal(&float642, cfg.Float64Ptrs[1]) + + is.Equal(duration1, cfg.Duration) + is.Equal(&duration1, cfg.DurationPtr) + is.Equal(duration1, cfg.Durations[0]) + is.Equal(duration2, cfg.Durations[1]) + is.Equal(&duration1, cfg.DurationPtrs[0]) + is.Equal(&duration2, cfg.DurationPtrs[1]) + + is.Equal(unmarshaler1, cfg.Unmarshaler) + is.Equal(&unmarshaler1, cfg.UnmarshalerPtr) + is.Equal(unmarshaler1, cfg.Unmarshalers[0]) + is.Equal(unmarshaler2, cfg.Unmarshalers[1]) + is.Equal(&unmarshaler1, cfg.UnmarshalerPtrs[0]) + is.Equal(&unmarshaler2, cfg.UnmarshalerPtrs[1]) + + is.Equal(url1, cfg.URL.String()) + is.Equal(url1, cfg.URLPtr.String()) + is.Equal(url1, cfg.URLs[0].String()) + is.Equal(url2, cfg.URLs[1].String()) + is.Equal(url1, cfg.URLPtrs[0].String()) + is.Equal(url2, cfg.URLPtrs[1].String()) + + is.Equal("postgres://localhost:5432/db", cfg.StringWithdefault) + is.Equal(nonDefinedStr, cfg.NonDefined.String) + + is.Equal(str1, cfg.CustomSeparator[0]) + is.Equal(str2, cfg.CustomSeparator[1]) + + is.Equal(cfg.NotAnEnv, "") + + is.Equal(cfg.unexported, "") +} + +func TestSetEnvAndTagOptsChain(t *testing.T) { + is := is.New(t) + + defer os.Clearenv() + type config struct { + Key1 string `mytag:"KEY1,required"` + Key2 int `mytag:"KEY2,required"` + } + envs := map[string]string{ + "KEY1": "VALUE1", + "KEY2": "3", + } + + cfg := config{} + is.NoErr(Parse(&cfg, Options{TagName: "mytag"}, Options{Environment: envs})) + is.Equal("VALUE1", cfg.Key1) + is.Equal(3, cfg.Key2) +} + +func TestJSONTag(t *testing.T) { + is := is.New(t) + + defer os.Clearenv() + type config struct { + Key1 string `json:"KEY1"` + Key2 int `json:"KEY2"` + } + + os.Setenv("KEY1", "VALUE7") + os.Setenv("KEY2", "5") + + cfg := config{} + is.NoErr(Parse(&cfg, Options{TagName: "json"})) + is.Equal("VALUE7", cfg.Key1) + is.Equal(5, cfg.Key2) +} + +func TestParsesEnvInner(t *testing.T) { + is := is.New(t) + + os.Setenv("innervar", "someinnervalue") + os.Setenv("innernum", "8") + defer os.Clearenv() + cfg := ParentStruct{ + InnerStruct: &InnerStruct{}, + unexported: &InnerStruct{}, + } + is.NoErr(Parse(&cfg)) + is.Equal("someinnervalue", cfg.InnerStruct.Inner) + is.Equal(uint(8), cfg.InnerStruct.Number) +} + +func TestParsesEnvInnerFails(t *testing.T) { + defer os.Clearenv() + type config struct { + Foo struct { + Number int `env:"NUMBER"` + } + } + os.Setenv("NUMBER", "not-a-number") + isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "Number" of type "int": strconv.ParseInt: parsing "not-a-number": invalid syntax`) +} + +func TestParsesEnvInnerNil(t *testing.T) { + is := is.New(t) + + os.Setenv("innervar", "someinnervalue") + defer os.Clearenv() + cfg := ParentStruct{} + is.NoErr(Parse(&cfg)) +} + +func TestParsesEnvInnerInvalid(t *testing.T) { + os.Setenv("innernum", "-547") + defer os.Clearenv() + cfg := ParentStruct{ + InnerStruct: &InnerStruct{}, + } + isErrorWithMessage(t, Parse(&cfg), `env: parse error on field "Number" of type "uint": strconv.ParseUint: parsing "-547": invalid syntax`) +} + +func TestParsesEnvNested(t *testing.T) { + is := is.New(t) + + os.Setenv("nestedvar", "somenestedvalue") + defer os.Clearenv() + var cfg ForNestedStruct + is.NoErr(Parse(&cfg)) + is.Equal("somenestedvalue", cfg.NestedVar) +} + +func TestEmptyVars(t *testing.T) { + is := is.New(t) + + os.Clearenv() + cfg := Config{} + is.NoErr(Parse(&cfg)) + is.Equal("", cfg.String) + is.Equal(false, cfg.Bool) + is.Equal(0, cfg.Int) + is.Equal(uint(0), cfg.Uint) + is.Equal(uint64(0), cfg.Uint64) + is.Equal(int64(0), cfg.Int64) + is.Equal(0, len(cfg.Strings)) + is.Equal(0, len(cfg.CustomSeparator)) + is.Equal(0, len(cfg.Ints)) + is.Equal(0, len(cfg.Bools)) +} + +func TestPassAnInvalidPtr(t *testing.T) { + var thisShouldBreak int + isErrorWithMessage(t, Parse(&thisShouldBreak), "env: expected a pointer to a Struct") +} + +func TestPassReference(t *testing.T) { + cfg := Config{} + isErrorWithMessage(t, Parse(cfg), "env: expected a pointer to a Struct") +} + +func TestInvalidBool(t *testing.T) { + os.Setenv("BOOL", "should-be-a-bool") + defer os.Clearenv() + isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Bool" of type "bool": strconv.ParseBool: parsing "should-be-a-bool": invalid syntax`) +} + +func TestInvalidInt(t *testing.T) { + os.Setenv("INT", "should-be-an-int") + defer os.Clearenv() + isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Int" of type "int": strconv.ParseInt: parsing "should-be-an-int": invalid syntax`) +} + +func TestInvalidUint(t *testing.T) { + os.Setenv("UINT", "-44") + defer os.Clearenv() + isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Uint" of type "uint": strconv.ParseUint: parsing "-44": invalid syntax`) +} + +func TestInvalidFloat32(t *testing.T) { + os.Setenv("FLOAT32", "AAA") + defer os.Clearenv() + + isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Float32" of type "float32": strconv.ParseFloat: parsing "AAA": invalid syntax`) +} + +func TestInvalidFloat64(t *testing.T) { + os.Setenv("FLOAT64", "AAA") + defer os.Clearenv() + isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Float64" of type "float64": strconv.ParseFloat: parsing "AAA": invalid syntax`) +} + +func TestInvalidUint64(t *testing.T) { + os.Setenv("UINT64", "AAA") + defer os.Clearenv() + isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Uint64" of type "uint64": strconv.ParseUint: parsing "AAA": invalid syntax`) +} + +func TestInvalidInt64(t *testing.T) { + os.Setenv("INT64", "AAA") + defer os.Clearenv() + isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Int64" of type "int64": strconv.ParseInt: parsing "AAA": invalid syntax`) +} + +func TestInvalidInt64Slice(t *testing.T) { + os.Setenv("BADINTS", "A,2,3") + defer os.Clearenv() + type config struct { + BadFloats []int64 `env:"BADINTS"` + } + isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "BadFloats" of type "[]int64": strconv.ParseInt: parsing "A": invalid syntax`) +} + +func TestInvalidUInt64Slice(t *testing.T) { + os.Setenv("BADINTS", "A,2,3") + defer os.Clearenv() + type config struct { + BadFloats []uint64 `env:"BADINTS"` + } + isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "BadFloats" of type "[]uint64": strconv.ParseUint: parsing "A": invalid syntax`) +} + +func TestInvalidFloat32Slice(t *testing.T) { + os.Setenv("BADFLOATS", "A,2.0,3.0") + defer os.Clearenv() + type config struct { + BadFloats []float32 `env:"BADFLOATS"` + } + isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "BadFloats" of type "[]float32": strconv.ParseFloat: parsing "A": invalid syntax`) +} + +func TestInvalidFloat64Slice(t *testing.T) { + os.Setenv("BADFLOATS", "A,2.0,3.0") + defer os.Clearenv() + type config struct { + BadFloats []float64 `env:"BADFLOATS"` + } + isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "BadFloats" of type "[]float64": strconv.ParseFloat: parsing "A": invalid syntax`) +} + +func TestInvalidBoolsSlice(t *testing.T) { + os.Setenv("BADBOOLS", "t,f,TRUE,faaaalse") + defer os.Clearenv() + type config struct { + BadBools []bool `env:"BADBOOLS"` + } + isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "BadBools" of type "[]bool": strconv.ParseBool: parsing "faaaalse": invalid syntax`) +} + +func TestInvalidDuration(t *testing.T) { + os.Setenv("DURATION", "should-be-a-valid-duration") + defer os.Clearenv() + isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Duration" of type "time.Duration": unable to parse duration: time: invalid duration "should-be-a-valid-duration"`) +} + +func TestInvalidDurations(t *testing.T) { + os.Setenv("DURATIONS", "1s,contains-an-invalid-duration,3s") + defer os.Clearenv() + isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Durations" of type "[]time.Duration": unable to parse duration: time: invalid duration "contains-an-invalid-duration"`) +} + +func TestParseStructWithoutEnvTag(t *testing.T) { + is := is.New(t) + + cfg := Config{} + is.NoErr(Parse(&cfg)) + is.Equal(cfg.NotAnEnv, "") +} + +func TestParseStructWithInvalidFieldKind(t *testing.T) { + type config struct { + WontWorkByte byte `env:"BLAH"` + } + os.Setenv("BLAH", "a") + isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "WontWorkByte" of type "uint8": strconv.ParseUint: parsing "a": invalid syntax`) +} + +func TestUnsupportedSliceType(t *testing.T) { + type config struct { + WontWork []map[int]int `env:"WONTWORK"` + } + + os.Setenv("WONTWORK", "1,2,3") + defer os.Clearenv() + + isErrorWithMessage(t, Parse(&config{}), `env: no parser found for field "WontWork" of type "[]map[int]int"`) +} + +func TestBadSeparator(t *testing.T) { + type config struct { + WontWork []int `env:"WONTWORK" envSeparator:":"` + } + + os.Setenv("WONTWORK", "1,2,3,4") + defer os.Clearenv() + + isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "WontWork" of type "[]int": strconv.ParseInt: parsing "1,2,3,4": invalid syntax`) +} + +func TestNoErrorRequiredSet(t *testing.T) { + is := is.New(t) + + type config struct { + IsRequired string `env:"IS_REQUIRED,required"` + } + + cfg := &config{} + + os.Setenv("IS_REQUIRED", "") + defer os.Clearenv() + is.NoErr(Parse(cfg)) + is.Equal("", cfg.IsRequired) +} + +func TestHook(t *testing.T) { + is := is.New(t) + + type config struct { + Something string `env:"SOMETHING" envDefault:"important"` + Another string `env:"ANOTHER"` + } + + cfg := &config{} + + os.Setenv("ANOTHER", "1") + defer os.Clearenv() + + type onSetArgs struct { + tag string + key interface{} + isDefault bool + } + + var onSetCalled []onSetArgs + + is.NoErr(Parse(cfg, Options{ + OnSet: func(tag string, value interface{}, isDefault bool) { + onSetCalled = append(onSetCalled, onSetArgs{tag, value, isDefault}) + }, + })) + is.Equal("important", cfg.Something) + is.Equal("1", cfg.Another) + is.Equal(2, len(onSetCalled)) + is.Equal(onSetArgs{"SOMETHING", "important", true}, onSetCalled[0]) + is.Equal(onSetArgs{"ANOTHER", "1", false}, onSetCalled[1]) +} + +func TestErrorRequiredWithDefault(t *testing.T) { + is := is.New(t) + + type config struct { + IsRequired string `env:"IS_REQUIRED,required" envDefault:"important"` + } + + cfg := &config{} + + os.Setenv("IS_REQUIRED", "") + defer os.Clearenv() + is.NoErr(Parse(cfg)) + is.Equal("", cfg.IsRequired) +} + +func TestErrorRequiredNotSet(t *testing.T) { + type config struct { + IsRequired string `env:"IS_REQUIRED,required"` + } + isErrorWithMessage(t, Parse(&config{}), `env: required environment variable "IS_REQUIRED" is not set`) +} + +func TestNoErrorNotEmptySet(t *testing.T) { + is := is.New(t) + os.Setenv("IS_REQUIRED", "1") + defer os.Clearenv() + type config struct { + IsRequired string `env:"IS_REQUIRED,notEmpty"` + } + is.NoErr(Parse(&config{})) +} + +func TestNoErrorRequiredAndNotEmptySet(t *testing.T) { + is := is.New(t) + os.Setenv("IS_REQUIRED", "1") + defer os.Clearenv() + type config struct { + IsRequired string `env:"IS_REQUIRED,required,notEmpty"` + } + is.NoErr(Parse(&config{})) +} + +func TestErrorNotEmptySet(t *testing.T) { + os.Setenv("IS_REQUIRED", "") + defer os.Clearenv() + type config struct { + IsRequired string `env:"IS_REQUIRED,notEmpty"` + } + isErrorWithMessage(t, Parse(&config{}), `env: environment variable "IS_REQUIRED" should not be empty`) +} + +func TestErrorRequiredAndNotEmptySet(t *testing.T) { + os.Setenv("IS_REQUIRED", "") + defer os.Clearenv() + type config struct { + IsRequired string `env:"IS_REQUIRED,notEmpty,required"` + } + isErrorWithMessage(t, Parse(&config{}), `env: environment variable "IS_REQUIRED" should not be empty`) +} + +func TestErrorRequiredNotSetWithDefault(t *testing.T) { + is := is.New(t) + + type config struct { + IsRequired string `env:"IS_REQUIRED,required" envDefault:"important"` + } + + cfg := &config{} + + is.NoErr(Parse(cfg)) + is.Equal("important", cfg.IsRequired) +} + +func TestParseExpandOption(t *testing.T) { + is := is.New(t) + + type config struct { + Host string `env:"HOST" envDefault:"localhost"` + Port int `env:"PORT" envDefault:"3000" envExpand:"True"` + SecretKey string `env:"SECRET_KEY" envExpand:"True"` + ExpandKey string `env:"EXPAND_KEY"` + CompoundKey string `env:"HOST_PORT" envDefault:"${HOST}:${PORT}" envExpand:"True"` + Default string `env:"DEFAULT" envDefault:"def1" envExpand:"True"` + } + defer os.Clearenv() + + os.Setenv("HOST", "localhost") + os.Setenv("PORT", "3000") + os.Setenv("EXPAND_KEY", "qwerty12345") + os.Setenv("SECRET_KEY", "${EXPAND_KEY}") + + cfg := config{} + err := Parse(&cfg) + + is.NoErr(err) + is.Equal("localhost", cfg.Host) + is.Equal(3000, cfg.Port) + is.Equal("qwerty12345", cfg.SecretKey) + is.Equal("qwerty12345", cfg.ExpandKey) + is.Equal("localhost:3000", cfg.CompoundKey) + is.Equal("def1", cfg.Default) +} + +func TestParseUnsetRequireOptions(t *testing.T) { + is := is.New(t) + + type config struct { + Password string `env:"PASSWORD,unset,required"` + } + defer os.Clearenv() + cfg := config{} + + isErrorWithMessage(t, Parse(&cfg), `env: required environment variable "PASSWORD" is not set`) + os.Setenv("PASSWORD", "superSecret") + is.NoErr(Parse(&cfg)) + + is.Equal("superSecret", cfg.Password) + unset, exists := os.LookupEnv("PASSWORD") + is.Equal("", unset) + is.Equal(false, exists) +} + +func TestCustomParser(t *testing.T) { + is := is.New(t) + + type foo struct { + name string + } + + type bar struct { + Name string `env:"OTHER"` + Foo *foo `env:"BLAH"` + } + + type config struct { + Var foo `env:"VAR"` + Foo *foo `env:"BLAH"` + Other *bar + } + + os.Setenv("VAR", "test") + defer os.Unsetenv("VAR") + os.Setenv("OTHER", "test2") + defer os.Unsetenv("OTHER") + os.Setenv("BLAH", "test3") + defer os.Unsetenv("BLAH") + + cfg := &config{ + Other: &bar{}, + } + err := ParseWithFuncs(cfg, map[reflect.Type]ParserFunc{ + reflect.TypeOf(foo{}): func(v string) (interface{}, error) { + return foo{name: v}, nil + }, + }) + + is.NoErr(err) + is.Equal(cfg.Var.name, "test") + is.Equal(cfg.Foo.name, "test3") + is.Equal(cfg.Other.Name, "test2") + is.Equal(cfg.Other.Foo.name, "test3") +} + +func TestParseWithFuncsNoPtr(t *testing.T) { + type foo struct{} + isErrorWithMessage(t, ParseWithFuncs(foo{}, nil), "env: expected a pointer to a Struct") +} + +func TestParseWithFuncsInvalidType(t *testing.T) { + var c int + isErrorWithMessage(t, ParseWithFuncs(&c, nil), "env: expected a pointer to a Struct") +} + +func TestCustomParserError(t *testing.T) { + type foo struct { + name string + } + + customParserFunc := func(v string) (interface{}, error) { + return nil, errors.New("something broke") + } + + t.Run("single", func(t *testing.T) { + is := is.New(t) + + type config struct { + Var foo `env:"VAR"` + } + + os.Setenv("VAR", "single") + cfg := &config{} + err := ParseWithFuncs(cfg, map[reflect.Type]ParserFunc{ + reflect.TypeOf(foo{}): customParserFunc, + }) + + is.Equal(cfg.Var.name, "") + isErrorWithMessage(t, err, `env: parse error on field "Var" of type "env.foo": something broke`) + }) + + t.Run("slice", func(t *testing.T) { + is := is.New(t) + + type config struct { + Var []foo `env:"VAR2"` + } + os.Setenv("VAR2", "slice,slace") + + cfg := &config{} + err := ParseWithFuncs(cfg, map[reflect.Type]ParserFunc{ + reflect.TypeOf(foo{}): customParserFunc, + }) + + is.Equal(cfg.Var, nil) + isErrorWithMessage(t, err, `env: parse error on field "Var" of type "[]env.foo": something broke`) + }) +} + +func TestCustomParserBasicType(t *testing.T) { + is := is.New(t) + + type ConstT int32 + + type config struct { + Const ConstT `env:"CONST_"` + } + + exp := ConstT(123) + os.Setenv("CONST_", fmt.Sprintf("%d", exp)) + + customParserFunc := func(v string) (interface{}, error) { + i, err := strconv.Atoi(v) + if err != nil { + return nil, err + } + r := ConstT(i) + return r, nil + } + + cfg := &config{} + err := ParseWithFuncs(cfg, map[reflect.Type]ParserFunc{ + reflect.TypeOf(ConstT(0)): customParserFunc, + }) + + is.NoErr(err) + is.Equal(exp, cfg.Const) +} + +func TestCustomParserUint64Alias(t *testing.T) { + is := is.New(t) + + type T uint64 + + var one T = 1 + + type config struct { + Val T `env:"" envDefault:"1x"` + } + + parserCalled := false + + tParser := func(value string) (interface{}, error) { + parserCalled = true + trimmed := strings.TrimSuffix(value, "x") + i, err := strconv.Atoi(trimmed) + if err != nil { + return nil, err + } + return T(i), nil + } + + cfg := config{} + + err := ParseWithFuncs(&cfg, map[reflect.Type]ParserFunc{ + reflect.TypeOf(one): tParser, + }) + + is.True(parserCalled) // tParser should have been called + is.NoErr(err) + is.Equal(T(1), cfg.Val) +} + +func TestTypeCustomParserBasicInvalid(t *testing.T) { + is := is.New(t) + + type ConstT int32 + + type config struct { + Const ConstT `env:"CONST_"` + } + + os.Setenv("CONST_", "foobar") + + customParserFunc := func(_ string) (interface{}, error) { + return nil, errors.New("random error") + } + + cfg := &config{} + err := ParseWithFuncs(cfg, map[reflect.Type]ParserFunc{ + reflect.TypeOf(ConstT(0)): customParserFunc, + }) + + is.Equal(cfg.Const, ConstT(0)) + isErrorWithMessage(t, err, `env: parse error on field "Const" of type "env.ConstT": random error`) +} + +func TestCustomParserNotCalledForNonAlias(t *testing.T) { + is := is.New(t) + + type T uint64 + type U uint64 + + type config struct { + Val uint64 `env:"" envDefault:"33"` + Other U `env:"OTHER" envDefault:"44"` + } + + tParserCalled := false + + tParser := func(value string) (interface{}, error) { + tParserCalled = true + return T(99), nil + } + + cfg := config{} + + err := ParseWithFuncs(&cfg, map[reflect.Type]ParserFunc{ + reflect.TypeOf(T(0)): tParser, + }) + + is.True(!tParserCalled) // tParser should not have been called + is.NoErr(err) + is.Equal(uint64(33), cfg.Val) + is.Equal(U(44), cfg.Other) +} + +func TestCustomParserBasicUnsupported(t *testing.T) { + is := is.New(t) + + type ConstT struct { + A int + } + + type config struct { + Const ConstT `env:"CONST_"` + } + + os.Setenv("CONST_", "42") + + cfg := &config{} + err := Parse(cfg) + + is.Equal(cfg.Const, ConstT{0}) + isErrorWithMessage(t, err, `env: no parser found for field "Const" of type "env.ConstT"`) +} + +func TestUnsupportedStructType(t *testing.T) { + type config struct { + Foo http.Client `env:"FOO"` + } + os.Setenv("FOO", "foo") + defer os.Clearenv() + isErrorWithMessage(t, Parse(&config{}), `env: no parser found for field "Foo" of type "http.Client"`) +} + +func TestEmptyOption(t *testing.T) { + is := is.New(t) + + type config struct { + Var string `env:"VAR,"` + } + + cfg := &config{} + + os.Setenv("VAR", "") + defer os.Clearenv() + is.NoErr(Parse(cfg)) + is.Equal("", cfg.Var) +} + +func TestErrorOptionNotRecognized(t *testing.T) { + type config struct { + Var string `env:"VAR,not_supported!"` + } + isErrorWithMessage(t, Parse(&config{}), `env: tag option "not_supported!" not supported`) +} + +func TestTextUnmarshalerError(t *testing.T) { + type config struct { + Unmarshaler unmarshaler `env:"UNMARSHALER"` + } + os.Setenv("UNMARSHALER", "invalid") + isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "Unmarshaler" of type "env.unmarshaler": time: invalid duration "invalid"`) +} + +func TestTextUnmarshalersError(t *testing.T) { + type config struct { + Unmarshalers []unmarshaler `env:"UNMARSHALERS"` + } + os.Setenv("UNMARSHALERS", "1s,invalid") + isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "Unmarshalers" of type "[]env.unmarshaler": time: invalid duration "invalid"`) +} + +func TestParseURL(t *testing.T) { + is := is.New(t) + + type config struct { + ExampleURL url.URL `env:"EXAMPLE_URL" envDefault:"https://google.com"` + } + var cfg config + is.NoErr(Parse(&cfg)) + is.Equal("https://google.com", cfg.ExampleURL.String()) +} + +func TestParseInvalidURL(t *testing.T) { + type config struct { + ExampleURL url.URL `env:"EXAMPLE_URL_2"` + } + os.Setenv("EXAMPLE_URL_2", "nope://s s/") + + isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "ExampleURL" of type "url.URL": unable to parse URL: parse "nope://s s/": invalid character " " in host name`) +} + +func ExampleParse() { + type inner struct { + Foo string `env:"FOO" envDefault:"foobar"` + } + type config struct { + Home string `env:"HOME,required"` + Port int `env:"PORT" envDefault:"3000"` + IsProduction bool `env:"PRODUCTION"` + Inner inner + } + os.Setenv("HOME", "/tmp/fakehome") + var cfg config + if err := Parse(&cfg); err != nil { + fmt.Println("failed:", err) + } + fmt.Printf("%+v", cfg) + // Output: {Home:/tmp/fakehome Port:3000 IsProduction:false Inner:{Foo:foobar}} +} + +func ExampleParse_onSet() { + type config struct { + Home string `env:"HOME,required"` + Port int `env:"PORT" envDefault:"3000"` + IsProduction bool `env:"PRODUCTION"` + } + os.Setenv("HOME", "/tmp/fakehome") + var cfg config + if err := Parse(&cfg, Options{ + OnSet: func(tag string, value interface{}, isDefault bool) { + fmt.Printf("Set %s to %v (default? %v)\n", tag, value, isDefault) + }, + }); err != nil { + fmt.Println("failed:", err) + } + fmt.Printf("%+v", cfg) + // Output: Set HOME to /tmp/fakehome (default? false) + // Set PORT to 3000 (default? true) + // Set PRODUCTION to (default? false) + // {Home:/tmp/fakehome Port:3000 IsProduction:false} +} + +func ExampleParse_defaults() { + type config struct { + A string `env:"FOO" envDefault:"foo"` + B string `env:"FOO"` + } + + // env FOO is not set + + cfg := config{ + A: "A", + B: "B", + } + if err := Parse(&cfg); err != nil { + fmt.Println("failed:", err) + } + fmt.Printf("%+v", cfg) + // Output: {A:foo B:B} +} + +func TestIgnoresUnexported(t *testing.T) { + is := is.New(t) + + type unexportedConfig struct { + home string `env:"HOME"` + Home2 string `env:"HOME"` + } + cfg := unexportedConfig{} + + os.Setenv("HOME", "/tmp/fakehome") + is.NoErr(Parse(&cfg)) + is.Equal(cfg.home, "") + is.Equal("/tmp/fakehome", cfg.Home2) +} + +type LogLevel int8 + +func (l *LogLevel) UnmarshalText(text []byte) error { + txt := string(text) + switch txt { + case "debug": + *l = DebugLevel + case "info": + *l = InfoLevel + default: + return fmt.Errorf("unknown level: %q", txt) + } + + return nil +} + +const ( + DebugLevel LogLevel = iota - 1 + InfoLevel +) + +func TestPrecedenceUnmarshalText(t *testing.T) { + is := is.New(t) + + os.Setenv("LOG_LEVEL", "debug") + os.Setenv("LOG_LEVELS", "debug,info") + defer os.Unsetenv("LOG_LEVEL") + defer os.Unsetenv("LOG_LEVELS") + + type config struct { + LogLevel LogLevel `env:"LOG_LEVEL"` + LogLevels []LogLevel `env:"LOG_LEVELS"` + } + var cfg config + + is.NoErr(Parse(&cfg)) + is.Equal(DebugLevel, cfg.LogLevel) + is.Equal([]LogLevel{DebugLevel, InfoLevel}, cfg.LogLevels) +} + +func ExampleParseWithFuncs() { + type thing struct { + desc string + } + + type conf struct { + Thing thing `env:"THING"` + } + + os.Setenv("THING", "my thing") + + c := conf{} + + err := ParseWithFuncs(&c, map[reflect.Type]ParserFunc{ + reflect.TypeOf(thing{}): func(v string) (interface{}, error) { + return thing{desc: v}, nil + }, + }) + if err != nil { + fmt.Println(err) + } + fmt.Println(c.Thing.desc) + // Output: + // my thing +} + +func TestFile(t *testing.T) { + is := is.New(t) + + type config struct { + SecretKey string `env:"SECRET_KEY,file"` + } + + dir := t.TempDir() + file := filepath.Join(dir, "sec_key") + is.NoErr(os.WriteFile(file, []byte("secret"), 0o660)) + + defer os.Clearenv() + os.Setenv("SECRET_KEY", file) + + cfg := config{} + is.NoErr(Parse(&cfg)) + is.Equal("secret", cfg.SecretKey) +} + +func TestFileNoParam(t *testing.T) { + is := is.New(t) + + type config struct { + SecretKey string `env:"SECRET_KEY,file"` + } + defer os.Clearenv() + + cfg := config{} + is.NoErr(Parse(&cfg)) +} + +func TestFileNoParamRequired(t *testing.T) { + type config struct { + SecretKey string `env:"SECRET_KEY,file,required"` + } + isErrorWithMessage(t, Parse(&config{}), `env: required environment variable "SECRET_KEY" is not set`) +} + +func TestFileBadFile(t *testing.T) { + type config struct { + SecretKey string `env:"SECRET_KEY,file"` + } + + filename := "not-a-real-file" + defer os.Clearenv() + os.Setenv("SECRET_KEY", filename) + + oserr := "no such file or directory" + if runtime.GOOS == "windows" { + oserr = "The system cannot find the file specified." + } + isErrorWithMessage(t, Parse(&config{}), fmt.Sprintf(`env: could not load content of file "%s" from variable SECRET_KEY: open %s: %s`, filename, filename, oserr)) +} + +func TestFileWithDefault(t *testing.T) { + is := is.New(t) + + type config struct { + SecretKey string `env:"SECRET_KEY,file" envDefault:"${FILE}" envExpand:"true"` + } + defer os.Clearenv() + + dir := t.TempDir() + file := filepath.Join(dir, "sec_key") + is.NoErr(os.WriteFile(file, []byte("secret"), 0o660)) + + defer os.Clearenv() + os.Setenv("FILE", file) + + cfg := config{} + is.NoErr(Parse(&cfg)) + is.Equal("secret", cfg.SecretKey) +} + +func TestCustomSliceType(t *testing.T) { + is := is.New(t) + + type customslice []byte + + type config struct { + SecretKey customslice `env:"SECRET_KEY"` + } + + parsecustomsclice := func(value string) (interface{}, error) { + return customslice(value), nil + } + + defer os.Clearenv() + os.Setenv("SECRET_KEY", "somesecretkey") + + var cfg config + is.NoErr(ParseWithFuncs(&cfg, map[reflect.Type]ParserFunc{reflect.TypeOf(customslice{}): parsecustomsclice})) +} + +func TestBlankKey(t *testing.T) { + is := is.New(t) + + type testStruct struct { + Blank string + BlankWithTag string `env:""` + } + + val := testStruct{} + + defer os.Clearenv() + os.Setenv("", "You should not see this") + + is.NoErr(Parse(&val)) + is.Equal("", val.Blank) + is.Equal("", val.BlankWithTag) +} + +type MyTime time.Time + +func (t *MyTime) UnmarshalText(text []byte) error { + tt, err := time.Parse("2006-01-02", string(text)) + *t = MyTime(tt) + return err +} + +func TestCustomTimeParser(t *testing.T) { + is := is.New(t) + + type config struct { + SomeTime MyTime `env:"SOME_TIME"` + } + + os.Setenv("SOME_TIME", "2021-05-06") + defer os.Unsetenv("SOME_TIME") + + var cfg config + is.NoErr(Parse(&cfg)) + is.Equal(2021, time.Time(cfg.SomeTime).Year()) + is.Equal(time.Month(5), time.Time(cfg.SomeTime).Month()) + is.Equal(6, time.Time(cfg.SomeTime).Day()) +} + +func TestRequiredIfNoDefOption(t *testing.T) { + type Tree struct { + Fruit string `env:"FRUIT"` + } + type config struct { + Name string `env:"NAME"` + Genre string `env:"GENRE" envDefault:"Unknown"` + Tree + } + var cfg config + + t.Run("missing", func(t *testing.T) { + isErrorWithMessage(t, Parse(&cfg, Options{RequiredIfNoDef: true}), `env: required environment variable "NAME" is not set`) + os.Setenv("NAME", "John") + t.Cleanup(os.Clearenv) + isErrorWithMessage(t, Parse(&cfg, Options{RequiredIfNoDef: true}), `env: required environment variable "FRUIT" is not set`) + }) + + t.Run("all set", func(t *testing.T) { + os.Setenv("NAME", "John") + os.Setenv("FRUIT", "Apple") + t.Cleanup(os.Clearenv) + + // should not trigger an error for the missing 'GENRE' env because it has a default value. + is.New(t).NoErr(Parse(&cfg, Options{RequiredIfNoDef: true})) + }) +} + +func TestPrefix(t *testing.T) { + is := is.New(t) + type Config struct { + Home string `env:"HOME"` + } + type ComplexConfig struct { + Foo Config `envPrefix:"FOO_"` + Bar Config `envPrefix:"BAR_"` + Clean Config + } + cfg := ComplexConfig{} + err := Parse(&cfg, Options{Environment: map[string]string{"FOO_HOME": "/foo", "BAR_HOME": "/bar", "HOME": "/clean"}}) + is.NoErr(err) + is.Equal("/foo", cfg.Foo.Home) + is.Equal("/bar", cfg.Bar.Home) + is.Equal("/clean", cfg.Clean.Home) +} + +func TestComplePrefix(t *testing.T) { + is := is.New(t) + type Config struct { + Home string `env:"HOME"` + } + type ComplexConfig struct { + Foo Config `envPrefix:"FOO_"` + Clean Config + Bar Config `envPrefix:"BAR_"` + Blah string `env:"BLAH"` + } + cfg := ComplexConfig{} + err := Parse(&cfg, Options{ + Prefix: "T_", + Environment: map[string]string{ + "T_FOO_HOME": "/foo", + "T_BAR_HOME": "/bar", + "T_BLAH": "blahhh", + "T_HOME": "/clean", + }, + }) + is.NoErr(err) + is.Equal("/foo", cfg.Foo.Home) + is.Equal("/bar", cfg.Bar.Home) + is.Equal("/clean", cfg.Clean.Home) + is.Equal("blahhh", cfg.Blah) +} + +func isErrorWithMessage(tb testing.TB, err error, msg string) { + tb.Helper() + + is := is.New(tb) + is.True(err != nil) // should have failed + is.Equal(err.Error(), msg) // should have the expected message +} diff --git a/vendor/github.com/caarlos0/env/v6/env_unix.go b/vendor/github.com/caarlos0/env/v6/env_unix.go new file mode 100644 index 00000000..411d4385 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/env_unix.go @@ -0,0 +1,15 @@ +//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris +// +build darwin dragonfly freebsd linux netbsd openbsd solaris + +package env + +import "strings" + +func toMap(env []string) map[string]string { + r := map[string]string{} + for _, e := range env { + p := strings.SplitN(e, "=", 2) + r[p[0]] = p[1] + } + return r +} diff --git a/vendor/github.com/caarlos0/env/v6/env_windows.go b/vendor/github.com/caarlos0/env/v6/env_windows.go new file mode 100644 index 00000000..e12123cd --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/env_windows.go @@ -0,0 +1,25 @@ +package env + +import "strings" + +func toMap(env []string) map[string]string { + r := map[string]string{} + for _, e := range env { + p := strings.SplitN(e, "=", 2) + + // On Windows, environment variables can start with '='. If so, Split at next character. + // See env_windows.go in the Go source: https://github.com/golang/go/blob/master/src/syscall/env_windows.go#L58 + prefixEqualSign := false + if len(e) > 0 && e[0] == '=' { + e = e[1:] + prefixEqualSign = true + } + p = strings.SplitN(e, "=", 2) + if prefixEqualSign { + p[0] = "=" + p[0] + } + + r[p[0]] = p[1] + } + return r +} diff --git a/vendor/github.com/caarlos0/env/v6/env_windows_test.go b/vendor/github.com/caarlos0/env/v6/env_windows_test.go new file mode 100644 index 00000000..01bfdb20 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/env_windows_test.go @@ -0,0 +1,20 @@ +package env + +import ( + "testing" + + "github.com/matryer/is" +) + +// On Windows, environment variables can start with '='. This test verifies this behavior without relying on a Windows environment. +// See env_windows.go in the Go source: https://github.com/golang/go/blob/master/src/syscall/env_windows.go#L58 +func TestToMapWindows(t *testing.T) { + is := is.New(t) + envVars := []string{"=::=::\\", "=C:=C:\\test", "VAR=REGULARVAR"} + result := toMap(envVars) + is.Equal(map[string]string{ + "=::": "::\\", + "=C:": "C:\\test", + "VAR": "REGULARVAR", + }, result) +} diff --git a/vendor/github.com/caarlos0/env/v6/go.mod b/vendor/github.com/caarlos0/env/v6/go.mod new file mode 100644 index 00000000..6b8d3513 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/go.mod @@ -0,0 +1,5 @@ +module github.com/caarlos0/env/v6 + +require github.com/matryer/is v1.4.0 + +go 1.17 diff --git a/vendor/github.com/caarlos0/env/v6/go.sum b/vendor/github.com/caarlos0/env/v6/go.sum new file mode 100644 index 00000000..ddd6bbf7 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v6/go.sum @@ -0,0 +1,2 @@ +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=