From badc43b225d4379588cdadbf818e5cbdbc89e53b Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 9 Oct 2022 10:18:20 -0700 Subject: [PATCH] allow changing window size --- go.mod | 9 +- go.sum | 17 +- pkg/config/app_config.go | 21 +- pkg/gui/arrangement.go | 237 ++++ pkg/gui/gui.go | 17 + pkg/gui/keybindings.go | 18 + pkg/gui/layout.go | 225 +--- pkg/gui/theme.go | 4 +- pkg/gui/view_helpers.go | 52 + pkg/gui/views.go | 128 ++ pkg/gui/window.go | 7 + pkg/i18n/english.go | 6 + scripts/bump_gocui.sh | 5 + scripts/bump_lazycore.sh | 5 + .../github.com/jesseduffield/lazycore/LICENSE | 21 + .../lazycore/pkg/boxlayout/boxlayout.go | 211 +++ .../jesseduffield/lazycore/pkg/utils/utils.go | 16 + vendor/github.com/samber/lo/CHANGELOG.md | 124 +- vendor/github.com/samber/lo/Makefile | 11 +- vendor/github.com/samber/lo/README.md | 1200 ++++++++++++++--- vendor/github.com/samber/lo/channel.go | 228 ++++ vendor/github.com/samber/lo/condition.go | 22 + .../github.com/samber/lo/docker-compose.yml | 4 +- vendor/github.com/samber/lo/errors.go | 236 +++- vendor/github.com/samber/lo/find.go | 158 ++- vendor/github.com/samber/lo/func.go | 8 + vendor/github.com/samber/lo/intersect.go | 25 + vendor/github.com/samber/lo/map.go | 42 +- vendor/github.com/samber/lo/math.go | 7 +- vendor/github.com/samber/lo/pointers.go | 32 - vendor/github.com/samber/lo/retry.go | 9 +- vendor/github.com/samber/lo/slice.go | 219 ++- vendor/github.com/samber/lo/string.go | 37 +- vendor/github.com/samber/lo/test.go | 32 + vendor/github.com/samber/lo/tuples.go | 136 +- .../github.com/samber/lo/type_manipulation.go | 88 ++ .../testify/assert/assertion_compare.go | 76 +- .../assert/assertion_compare_can_convert.go | 16 + .../assert/assertion_compare_legacy.go | 16 + .../testify/assert/assertion_format.go | 22 + .../testify/assert/assertion_forward.go | 44 + .../testify/assert/assertion_order.go | 8 +- .../stretchr/testify/assert/assertions.go | 190 ++- vendor/gopkg.in/yaml.v3/decode.go | 78 +- vendor/gopkg.in/yaml.v3/parserc.go | 11 +- vendor/modules.txt | 10 +- 46 files changed, 3442 insertions(+), 646 deletions(-) create mode 100644 pkg/gui/arrangement.go create mode 100644 pkg/gui/views.go create mode 100644 pkg/gui/window.go create mode 100755 scripts/bump_gocui.sh create mode 100755 scripts/bump_lazycore.sh create mode 100644 vendor/github.com/jesseduffield/lazycore/LICENSE create mode 100644 vendor/github.com/jesseduffield/lazycore/pkg/boxlayout/boxlayout.go create mode 100644 vendor/github.com/jesseduffield/lazycore/pkg/utils/utils.go create mode 100644 vendor/github.com/samber/lo/channel.go create mode 100644 vendor/github.com/samber/lo/func.go delete mode 100644 vendor/github.com/samber/lo/pointers.go create mode 100644 vendor/github.com/samber/lo/test.go create mode 100644 vendor/github.com/samber/lo/type_manipulation.go create mode 100644 vendor/github.com/stretchr/testify/assert/assertion_compare_can_convert.go create mode 100644 vendor/github.com/stretchr/testify/assert/assertion_compare_legacy.go diff --git a/go.mod b/go.mod index c7925e4..b8a9dcd 100644 --- a/go.mod +++ b/go.mod @@ -16,13 +16,15 @@ require ( github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6 github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 + github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316 github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56 + github.com/mattn/go-runewidth v0.0.13 github.com/mcuadros/go-lookup v0.0.0-20171110082742-5650f26be767 github.com/mgutz/str v1.2.0 - github.com/samber/lo v1.20.0 + github.com/samber/lo v1.31.0 github.com/sirupsen/logrus v1.4.2 github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.8.0 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 ) @@ -39,7 +41,6 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.4 // indirect github.com/mattn/go-isatty v0.0.11 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect github.com/onsi/ginkgo v1.8.0 // indirect @@ -57,6 +58,6 @@ require ( golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect gopkg.in/yaml.v2 v2.2.2 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.2.0 // indirect ) diff --git a/go.sum b/go.sum index b72e4b1..7daa24f 100644 --- a/go.sum +++ b/go.sum @@ -54,6 +54,8 @@ github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6 h1:Fmay0Lz21 github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU= github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0= github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo= +github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316 h1:UzTg0utG+cLS94Qjb/ZFDzfMeZDFXErSbNgJOyj70EA= +github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316/go.mod h1:qxN4mHOAyeIDLP7IK7defgPClM/z1Kze8VVQiaEjzsQ= github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56 h1:33wSxJWU/f2TAozHYtJ8zqBxEnEVYM+22moLoiAkxvg= github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56/go.mod h1:FZJBwOhE+RXz8EVZfY+xnbCw2cVOwxlK3/aIi581z/s= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= @@ -99,8 +101,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/samber/lo v1.20.0 h1:20FtphdORvp4yxklurzZv2HX+g+0urEMQziODC5bV70= -github.com/samber/lo v1.20.0/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A= +github.com/samber/lo v1.31.0 h1:Sfa+/064Tdo4SvlohQUQzBhgSer9v/coGvKQI/XLWAM= +github.com/samber/lo v1.31.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -108,11 +110,14 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= @@ -177,8 +182,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index c087675..9094104 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -114,6 +114,16 @@ type GuiConfig struct { // By default, containers are now sorted by status. This setting allows users to // use legacy behaviour instead. LegacySortContainers bool `yaml:"legacySortContainers,omitempty"` + + // If 0.333, then the side panels will be 1/3 of the screen's width + SidePanelWidth float64 `yaml:"sidePanelWidth"` + + // Determines whether we show the bottom line (the one containing keybinding + // info and the status of the app). + ShowBottomLine bool `yaml:"showBottomLine"` + + // When true, ensures that the focused side panel always takes up the full vertical space + ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"` } // CommandTemplatesConfig determines what commands actually get called when we @@ -320,10 +330,13 @@ func GetDefaultConfig() UserConfig { InactiveBorderColor: []string{"default"}, OptionsTextColor: []string{"blue"}, }, - ShowAllContainers: false, - ReturnImmediately: false, - WrapMainPanel: true, - LegacySortContainers: false, + ShowAllContainers: false, + ReturnImmediately: false, + WrapMainPanel: true, + LegacySortContainers: false, + SidePanelWidth: 0.3333, + ShowBottomLine: true, + ExpandFocusedSidePanel: false, }, ConfirmOnQuit: false, Logs: LogsConfig{ diff --git a/pkg/gui/arrangement.go b/pkg/gui/arrangement.go new file mode 100644 index 0000000..58b5370 --- /dev/null +++ b/pkg/gui/arrangement.go @@ -0,0 +1,237 @@ +package gui + +import ( + "github.com/jesseduffield/lazycore/pkg/boxlayout" + "github.com/jesseduffield/lazydocker/pkg/utils" + "github.com/mattn/go-runewidth" + "github.com/samber/lo" +) + +// In this file we use the boxlayout package, along with knowledge about the app's state, +// to arrange the windows (i.e. panels) on the screen. + +const INFO_SECTION_PADDING = " " + +func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions { + minimumHeight := 9 + minimumWidth := 10 + width, height := gui.g.Size() + if width < minimumWidth || height < minimumHeight { + return boxlayout.ArrangeWindows(&boxlayout.Box{Window: "limit"}, 0, 0, width, height) + } + + sideSectionWeight, mainSectionWeight := gui.getMidSectionWeights() + + sidePanelsDirection := boxlayout.COLUMN + portraitMode := width <= 84 && height > 45 + if portraitMode { + sidePanelsDirection = boxlayout.ROW + } + + showInfoSection := gui.Config.UserConfig.Gui.ShowBottomLine + infoSectionSize := 0 + if showInfoSection { + infoSectionSize = 1 + } + + root := &boxlayout.Box{ + Direction: boxlayout.ROW, + Children: []*boxlayout.Box{ + { + Direction: sidePanelsDirection, + Weight: 1, + Children: []*boxlayout.Box{ + { + Direction: boxlayout.ROW, + Weight: sideSectionWeight, + ConditionalChildren: gui.sidePanelChildren, + }, + { + Window: "main", + Weight: mainSectionWeight, + }, + }, + }, + { + Direction: boxlayout.COLUMN, + Size: infoSectionSize, + Children: gui.infoSectionChildren(informationStr, appStatus), + }, + }, + } + + return boxlayout.ArrangeWindows(root, 0, 0, width, height) +} + +func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V { + result := map[K]V{} + for _, currMap := range maps { + for key, value := range currMap { + result[key] = value + } + } + + return result +} + +func (gui *Gui) getMidSectionWeights() (int, int) { + currentWindow := gui.currentWindow() + + // we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4 + sidePanelWidthRatio := gui.Config.UserConfig.Gui.SidePanelWidth + // we could make this better by creating ratios like 2:3 rather than always 1:something + mainSectionWeight := int(1/sidePanelWidthRatio) - 1 + sideSectionWeight := 1 + + if currentWindow == "main" && gui.State.ScreenMode == SCREEN_FULL { + mainSectionWeight = 1 + sideSectionWeight = 0 + } else { + if gui.State.ScreenMode == SCREEN_HALF { + mainSectionWeight = 1 + } else if gui.State.ScreenMode == SCREEN_FULL { + mainSectionWeight = 0 + } + } + + return sideSectionWeight, mainSectionWeight +} + +func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box { + result := []*boxlayout.Box{} + + if len(appStatus) > 0 { + result = append(result, + &boxlayout.Box{ + Window: "appStatus", + Size: runewidth.StringWidth(appStatus) + runewidth.StringWidth(INFO_SECTION_PADDING), + }, + ) + } + + result = append(result, + []*boxlayout.Box{ + { + Window: "options", + Weight: 1, + }, + { + Window: "information", + // unlike appStatus, informationStr has various colors so we need to decolorise before taking the length + Size: runewidth.StringWidth(INFO_SECTION_PADDING) + runewidth.StringWidth(utils.Decolorise(informationStr)), + }, + }..., + ) + + return result +} + +func (gui *Gui) sideViewNames() []string { + if gui.DockerCommand.InDockerComposeProject { + return []string{"project", "services", "containers", "images", "volumes"} + } else { + return []string{"project", "containers", "images", "volumes"} + } +} + +func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box { + currentWindow := gui.currentSideWindowName() + sideWindowNames := gui.sideViewNames() + + if gui.State.ScreenMode == SCREEN_FULL || gui.State.ScreenMode == SCREEN_HALF { + fullHeightBox := func(window string) *boxlayout.Box { + if window == currentWindow { + return &boxlayout.Box{ + Window: window, + Weight: 1, + } + } else { + return &boxlayout.Box{ + Window: window, + Size: 0, + } + } + } + + return lo.Map(sideWindowNames, func(window string, _ int) *boxlayout.Box { + return fullHeightBox(window) + }) + + } else if height >= 28 { + accordionMode := gui.Config.UserConfig.Gui.ExpandFocusedSidePanel + accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box { + if accordionMode && defaultBox.Window == currentWindow { + return &boxlayout.Box{ + Window: defaultBox.Window, + Weight: 2, + } + } + + return defaultBox + } + + return append([]*boxlayout.Box{ + { + Window: sideWindowNames[0], + Size: 3, + }, + }, lo.Map(sideWindowNames[1:], func(window string, _ int) *boxlayout.Box { + return accordionBox(&boxlayout.Box{Window: window, Weight: 1}) + })...) + } else { + squashedHeight := 1 + if height >= 21 { + squashedHeight = 3 + } + + squashedSidePanelBox := func(window string) *boxlayout.Box { + if window == currentWindow { + return &boxlayout.Box{ + Window: window, + Weight: 1, + } + } else { + return &boxlayout.Box{ + Window: window, + Size: squashedHeight, + } + } + } + + return lo.Map(sideWindowNames, func(window string, _ int) *boxlayout.Box { + return squashedSidePanelBox(window) + }) + } +} + +// TODO: reintroduce +// func (gui *Gui) currentSideWindowName() string { +// // there is always one and only one cyclable context in the context stack. We'll look from top to bottom +// gui.State.ContextManager.RLock() +// defer gui.State.ContextManager.RUnlock() + +// for idx := range gui.State.ContextManager.ContextStack { +// reversedIdx := len(gui.State.ContextManager.ContextStack) - 1 - idx +// context := gui.State.ContextManager.ContextStack[reversedIdx] + +// if context.GetKind() == types.SIDE_CONTEXT { +// return context.GetWindowName() +// } +// } + +// return "files" // default +// } + +func (gui *Gui) currentNonPopupWindowName() string { + return gui.peekPreviousView() +} + +// TODO: do this better. +func (gui *Gui) currentSideWindowName() string { + windowName := gui.currentWindow() + if !lo.Contains(gui.sideViewNames(), windowName) { + return gui.peekPreviousView() + } + + return windowName +} diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 2818e56..ccafc9e 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -73,6 +73,9 @@ type Gui struct { T *tasks.TaskManager ErrorChan chan error CyclableViews []string + Views Views + + ViewsSetup bool } type servicePanelState struct { @@ -131,8 +134,22 @@ type guiState struct { // We increment it each time we switch to a new subprocess // Every time we go to a subprocess we need to close a few goroutines so this index is used for that purpose SessionIndex int + + ScreenMode WindowMaximisation } +// screen sizing determines how much space your selected window takes up (window +// as in panel, not your terminal's window). Sometimes you want a bit more space +// to see the contents of a panel, and this keeps track of how much maximisation +// you've set +type WindowMaximisation int + +const ( + SCREEN_NORMAL WindowMaximisation = iota + SCREEN_HALF + SCREEN_FULL +) + // NewGui builds a new gui handler func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand *commands.OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*Gui, error) { initialState := guiState{ diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 9a25f3d..05ce349 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -506,6 +506,18 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Modifier: gocui.ModNone, Handler: gui.scrollRightMain, }, + { + ViewName: "", + Key: '+', + Handler: wrappedHandler(gui.nextScreenMode), + Description: gui.Tr.LcNextScreenMode, + }, + { + ViewName: "", + Key: '_', + Handler: wrappedHandler(gui.prevScreenMode), + Description: gui.Tr.LcPrevScreenMode, + }, } // TODO: add more views here @@ -573,3 +585,9 @@ func (gui *Gui) keybindings(g *gocui.Gui) error { return nil } + +func wrappedHandler(f func() error) func(*gocui.Gui, *gocui.View) error { + return func(g *gocui.Gui, v *gocui.View) error { + return f() + } +} diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index d4765f5..22f5fd8 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -1,11 +1,11 @@ package gui import ( - "github.com/fatih/color" "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazydocker/pkg/utils" ) +const UNKNOWN_VIEW_ERROR_MSG = "unknown view" + // getFocusLayout returns a manager function for when view gain and lose focus func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error { var previousView *gocui.View @@ -59,204 +59,61 @@ func (gui *Gui) onFocus(v *gocui.View) { // layout is called for every screen re-render e.g. when the screen is resized func (gui *Gui) layout(g *gocui.Gui) error { - g.Highlight = true - width, height := g.Size() - - information := gui.Config.Version - if gui.g.Mouse { - donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.Donate) - information = donate + " " + information - } - - minimumHeight := 9 - minimumWidth := 10 - if height < minimumHeight || width < minimumWidth { - v, err := g.SetView("limit", 0, 0, width-1, height-1, 0) - if err != nil { - if err.Error() != "unknown view" { - return err - } - v.Title = gui.Tr.NotEnoughSpace - v.Wrap = true - _, _ = g.SetViewOnTop("limit") - } - return nil - } - - currView := gui.g.CurrentView() - currentCyclebleView := gui.peekPreviousView() - if currView != nil { - viewName := currView.Name() - usePreviouseView := true - for _, view := range gui.CyclableViews { - if view == viewName { - currentCyclebleView = viewName - usePreviouseView = false - break - } - } - if usePreviouseView { - currentCyclebleView = gui.peekPreviousView() - } - } - - usableSpace := height - 4 - - tallPanels := 3 - var vHeights map[string]int - if gui.DockerCommand.InDockerComposeProject { - tallPanels++ - vHeights = map[string]int{ - "project": 3, - "services": usableSpace/tallPanels + usableSpace%tallPanels, - "containers": usableSpace / tallPanels, - "images": usableSpace / tallPanels, - "volumes": usableSpace / tallPanels, - "options": 1, - } - } else { - vHeights = map[string]int{ - "project": 3, - "containers": usableSpace/tallPanels + usableSpace%tallPanels, - "images": usableSpace / tallPanels, - "volumes": usableSpace / tallPanels, - "options": 1, + if !gui.ViewsSetup { + if err := gui.createAllViews(); err != nil { + return err } - } - if height < 28 { - defaultHeight := 3 - if height < 21 { - defaultHeight = 1 - } - vHeights = map[string]int{ - "project": defaultHeight, - "containers": defaultHeight, - "images": defaultHeight, - "volumes": defaultHeight, - "options": defaultHeight, - } - if gui.DockerCommand.InDockerComposeProject { - vHeights["services"] = defaultHeight - } - vHeights[currentCyclebleView] = height - defaultHeight*tallPanels - 1 + gui.ViewsSetup = true } - optionsVersionBoundary := width - max(len(utils.Decolorise(information)), 1) - leftSideWidth := width / 3 + g.Highlight = true + width, height := g.Size() appStatus := gui.statusManager.getStatusString() - appStatusOptionsBoundary := 0 - if appStatus != "" { - appStatusOptionsBoundary = len(appStatus) + 2 - } - - _, _ = g.SetViewOnBottom("limit") - _ = g.DeleteView("limit") - - v, err := g.SetView("main", leftSideWidth+1, 0, width-1, height-2, gocui.LEFT) - if err != nil { - if err.Error() != "unknown view" { - return err - } - v.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel - v.FgColor = gocui.ColorDefault - // when you run a docker container with the -it flags (interactive mode) it adds carriage returns for some reason. This is not docker's fault, it's an os-level default. - v.IgnoreCarriageReturns = true - } - - if v, err := g.SetView("project", 0, 0, leftSideWidth, vHeights["project"]-1, gocui.BOTTOM|gocui.RIGHT); err != nil { - if err.Error() != "unknown view" { - return err - } - v.Title = gui.Tr.ProjectTitle - v.FgColor = gocui.ColorDefault - } + viewDimensions := gui.getWindowDimensions(gui.getInformationContent(), appStatus) + // we assume that the view has already been created. + setViewFromDimensions := func(viewName string, windowName string) (*gocui.View, error) { + dimensionsObj, ok := viewDimensions[windowName] - var servicesView *gocui.View - aboveContainersView := "project" - if gui.DockerCommand.InDockerComposeProject { - aboveContainersView = "services" - servicesView, err = g.SetViewBeneath("services", "project", vHeights["services"]) + view, err := g.View(viewName) if err != nil { - if err.Error() != "unknown view" { - return err - } - servicesView.Highlight = true - servicesView.Title = gui.Tr.ServicesTitle - servicesView.FgColor = gocui.ColorDefault - } - } - - containersView, err := g.SetViewBeneath("containers", aboveContainersView, vHeights["containers"]) - if err != nil { - if err.Error() != "unknown view" { - return err - } - containersView.Highlight = true - if gui.Config.UserConfig.Gui.ShowAllContainers || !gui.DockerCommand.InDockerComposeProject { - containersView.Title = gui.Tr.ContainersTitle - } else { - containersView.Title = gui.Tr.StandaloneContainersTitle - } - containersView.FgColor = gocui.ColorDefault - } - - imagesView, err := g.SetViewBeneath("images", "containers", vHeights["images"]) - if err != nil { - if err.Error() != "unknown view" { - return err + return nil, err } - imagesView.Highlight = true - imagesView.Title = gui.Tr.ImagesTitle - imagesView.FgColor = gocui.ColorDefault - } - volumesView, err := g.SetViewBeneath("volumes", "images", vHeights["volumes"]) - if err != nil { - if err.Error() != "unknown view" { - return err + if !ok { + // view not specified in dimensions object: so create the view and hide it + // making the view take up the whole space in the background in case it needs + // to render content as soon as it appears, because lazyloaded content (via a pty task) + // cares about the size of the view. + _, err := g.SetView(viewName, 0, 0, width, height, 0) + view.Visible = false + return view, err } - volumesView.Highlight = true - volumesView.Title = gui.Tr.VolumesTitle - volumesView.FgColor = gocui.ColorDefault - } - if v, err := g.SetView("options", appStatusOptionsBoundary-1, height-2, optionsVersionBoundary-1, height, 0); err != nil { - if err.Error() != "unknown view" { - return err + frameOffset := 1 + if view.Frame { + frameOffset = 0 } - v.Frame = false - if v.FgColor, err = gui.GetOptionsPanelTextColor(); err != nil { - return err - } - } + _, err = g.SetView( + viewName, + dimensionsObj.X0-frameOffset, + dimensionsObj.Y0-frameOffset, + dimensionsObj.X1+frameOffset, + dimensionsObj.Y1+frameOffset, + 0, + ) + view.Visible = true - if appStatusView, err := g.SetView("appStatus", -1, height-2, width, height, 0); err != nil { - if err.Error() != "unknown view" { - return err - } - appStatusView.BgColor = gocui.ColorDefault - appStatusView.FgColor = gocui.ColorCyan - appStatusView.Frame = false - if _, err := g.SetViewOnBottom("appStatus"); err != nil { - return err - } + return view, err } - if v, err := g.SetView("information", optionsVersionBoundary-1, height-2, width, height, 0); err != nil { - if err.Error() != "unknown view" { - return err - } - v.BgColor = gocui.ColorDefault - v.FgColor = gocui.ColorGreen - v.Frame = false - if err := gui.renderString(g, "information", information); err != nil { + for _, viewName := range []string{"project", "services", "containers", "images", "volumes", "options", "information", "appStatus", "main", "limit"} { + _, err := setViewFromDimensions(viewName, viewName) + if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG { return err } - - gui.waitForIntro.Done() } if gui.g.CurrentView() == nil { @@ -303,3 +160,9 @@ func (gui *Gui) focusPointInView(view *gocui.View) { gui.focusY(state.selectedLine, state.lineCount, view) } } + +func (gui *Gui) prepareView(viewName string) (*gocui.View, error) { + // arbitrarily giving the view enough size so that we don't get an error, but + // it's expected that the view will be given the correct size before being shown + return gui.g.SetView(viewName, 0, 0, 10, 10, 0) +} diff --git a/pkg/gui/theme.go b/pkg/gui/theme.go index b9b28bc..0fb740d 100644 --- a/pkg/gui/theme.go +++ b/pkg/gui/theme.go @@ -5,8 +5,8 @@ import ( ) // GetOptionsPanelTextColor gets the color of the options panel text -func (gui *Gui) GetOptionsPanelTextColor() (gocui.Attribute, error) { - return GetGocuiStyle(gui.Config.UserConfig.Gui.Theme.OptionsTextColor), nil +func (gui *Gui) GetOptionsPanelTextColor() gocui.Attribute { + return GetGocuiStyle(gui.Config.UserConfig.Gui.Theme.OptionsTextColor) } // SetColorScheme sets the color scheme for the app based on the user config diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index a08b3a7..6e1a62c 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -291,6 +291,10 @@ func (gui *Gui) trimmedContent(v *gocui.View) string { func (gui *Gui) currentViewName() string { currentView := gui.g.CurrentView() + // this can happen when the app is first starting up + if currentView == nil { + return gui.initiallyFocusedViewName() + } return currentView.Name() } @@ -383,3 +387,51 @@ func (gui *Gui) handleClick(v *gocui.View, itemCount int, selectedLine *int, han return handleSelect(gui.g, v) } + +func (gui *Gui) nextScreenMode() error { + if gui.currentViewName() == "main" { + gui.State.ScreenMode = prevIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode) + + return nil + } + + gui.State.ScreenMode = nextIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode) + + return nil +} + +func (gui *Gui) prevScreenMode() error { + if gui.currentViewName() == "main" { + gui.State.ScreenMode = nextIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode) + + return nil + } + + gui.State.ScreenMode = prevIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode) + + return nil +} + +func nextIntInCycle(sl []WindowMaximisation, current WindowMaximisation) WindowMaximisation { + for i, val := range sl { + if val == current { + if i == len(sl)-1 { + return sl[0] + } + return sl[i+1] + } + } + return sl[0] +} + +func prevIntInCycle(sl []WindowMaximisation, current WindowMaximisation) WindowMaximisation { + for i, val := range sl { + if val == current { + if i > 0 { + return sl[i-1] + } + return sl[len(sl)-1] + } + } + return sl[len(sl)-1] +} diff --git a/pkg/gui/views.go b/pkg/gui/views.go new file mode 100644 index 0000000..3f83dd2 --- /dev/null +++ b/pkg/gui/views.go @@ -0,0 +1,128 @@ +package gui + +import ( + "github.com/fatih/color" + "github.com/jesseduffield/gocui" + "github.com/samber/lo" +) + +type Views struct { + Project *gocui.View + Services *gocui.View + Containers *gocui.View + Images *gocui.View + Volumes *gocui.View + + Main *gocui.View + + Options *gocui.View + Confirmation *gocui.View + Menu *gocui.View + Information *gocui.View + AppStatus *gocui.View + Limit *gocui.View +} + +type viewNameMapping struct { + viewPtr **gocui.View + name string +} + +func (gui *Gui) orderedViews() []*gocui.View { + return lo.Map(gui.orderedViewNameMappings(), func(v viewNameMapping, _ int) *gocui.View { + return *v.viewPtr + }) +} + +func (gui *Gui) orderedViewNameMappings() []viewNameMapping { + return []viewNameMapping{ + // first layer. Ordering within this layer does not matter because there are + // no overlapping views + {viewPtr: &gui.Views.Project, name: "project"}, + {viewPtr: &gui.Views.Services, name: "services"}, + {viewPtr: &gui.Views.Containers, name: "containers"}, + {viewPtr: &gui.Views.Images, name: "images"}, + {viewPtr: &gui.Views.Volumes, name: "volumes"}, + + {viewPtr: &gui.Views.Main, name: "main"}, + + // bottom line + {viewPtr: &gui.Views.Options, name: "options"}, + {viewPtr: &gui.Views.AppStatus, name: "appStatus"}, + {viewPtr: &gui.Views.Information, name: "information"}, + + // popups. + {viewPtr: &gui.Views.Menu, name: "menu"}, + {viewPtr: &gui.Views.Confirmation, name: "confirmation"}, + + // this guy will cover everything else when it appears + {viewPtr: &gui.Views.Limit, name: "limit"}, + } +} + +func (gui *Gui) createAllViews() error { + var err error + for _, mapping := range gui.orderedViewNameMappings() { + *mapping.viewPtr, err = gui.prepareView(mapping.name) + if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG { + return err + } + (*mapping.viewPtr).FgColor = gocui.ColorDefault + } + + gui.Views.Main.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel + // when you run a docker container with the -it flags (interactive mode) it adds carriage returns for some reason. This is not docker's fault, it's an os-level default. + gui.Views.Main.IgnoreCarriageReturns = true + + gui.Views.Project.Title = gui.Tr.ProjectTitle + + gui.Views.Services.Highlight = true + gui.Views.Services.Title = gui.Tr.ServicesTitle + + gui.Views.Containers.Highlight = true + if gui.Config.UserConfig.Gui.ShowAllContainers || !gui.DockerCommand.InDockerComposeProject { + gui.Views.Containers.Title = gui.Tr.ContainersTitle + } else { + gui.Views.Containers.Title = gui.Tr.StandaloneContainersTitle + } + + gui.Views.Images.Highlight = true + gui.Views.Images.Title = gui.Tr.ImagesTitle + + gui.Views.Volumes.Highlight = true + gui.Views.Volumes.Title = gui.Tr.VolumesTitle + + gui.Views.Options.Frame = false + gui.Views.Options.FgColor = gui.GetOptionsPanelTextColor() + + gui.Views.AppStatus.FgColor = gocui.ColorCyan + gui.Views.AppStatus.Frame = false + + gui.Views.Information.Frame = false + gui.Views.Information.FgColor = gocui.ColorGreen + + if err := gui.renderString(gui.g, "information", gui.getInformationContent()); err != nil { + return err + } + + gui.Views.Confirmation.Visible = false + gui.Views.Menu.Visible = false + + gui.Views.Limit.Visible = false + gui.Views.Limit.Title = gui.Tr.NotEnoughSpace + gui.Views.Limit.Wrap = true + + gui.waitForIntro.Done() + + return nil +} + +func (gui *Gui) getInformationContent() string { + informationStr := gui.Config.Version + if !gui.g.Mouse { + return informationStr + } + + donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.Donate) + return donate + " " + informationStr +} diff --git a/pkg/gui/window.go b/pkg/gui/window.go new file mode 100644 index 0000000..42c12dd --- /dev/null +++ b/pkg/gui/window.go @@ -0,0 +1,7 @@ +package gui + +func (gui *Gui) currentWindow() string { + // at the moment, we only have one view per window in lazydocker, so we + // are using the view name as the window name + return gui.currentViewName() +} diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index cec206e..62d9a1c 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -105,6 +105,9 @@ type TranslationSet struct { No string Yes string + + LcNextScreenMode string + LcPrevScreenMode string } func englishSet() TranslationSet { @@ -217,5 +220,8 @@ func englishSet() TranslationSet { No: "no", Yes: "yes", + + LcNextScreenMode: "next screen mode (normal/half/fullscreen)", + LcPrevScreenMode: "prev screen mode", } } diff --git a/scripts/bump_gocui.sh b/scripts/bump_gocui.sh new file mode 100755 index 0000000..3e0adeb --- /dev/null +++ b/scripts/bump_gocui.sh @@ -0,0 +1,5 @@ +# Go's proxy servers are not very up-to-date so that's why we use `GOPROXY=direct` +# We specify the `awesome` branch to avoid the default behaviour of looking for a semver tag. +GOPROXY=direct go get -u github.com/jesseduffield/gocui@awesome && go mod vendor && go mod tidy + +# Note to self if you ever want to fork a repo be sure to use this same approach: it's important to use the branch name (e.g. master) diff --git a/scripts/bump_lazycore.sh b/scripts/bump_lazycore.sh new file mode 100755 index 0000000..810e5f0 --- /dev/null +++ b/scripts/bump_lazycore.sh @@ -0,0 +1,5 @@ +# Go's proxy servers are not very up-to-date so that's why we use `GOPROXY=direct` +# We specify the `awesome` branch to avoid the default behaviour of looking for a semver tag. +GOPROXY=direct go get -u github.com/jesseduffield/lazycore@master && go mod vendor && go mod tidy + +# Note to self if you ever want to fork a repo be sure to use this same approach: it's important to use the branch name (e.g. master) diff --git a/vendor/github.com/jesseduffield/lazycore/LICENSE b/vendor/github.com/jesseduffield/lazycore/LICENSE new file mode 100644 index 0000000..2a7175d --- /dev/null +++ b/vendor/github.com/jesseduffield/lazycore/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Jesse Duffield + +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/jesseduffield/lazycore/pkg/boxlayout/boxlayout.go b/vendor/github.com/jesseduffield/lazycore/pkg/boxlayout/boxlayout.go new file mode 100644 index 0000000..c516cbb --- /dev/null +++ b/vendor/github.com/jesseduffield/lazycore/pkg/boxlayout/boxlayout.go @@ -0,0 +1,211 @@ +package boxlayout + +import ( + "github.com/jesseduffield/lazycore/pkg/utils" + "github.com/samber/lo" +) + +type Dimensions struct { + X0 int + X1 int + Y0 int + Y1 int +} + +type Direction int + +const ( + ROW Direction = iota + COLUMN +) + +// to give a high-level explanation of what's going on here. We layout our windows by arranging a bunch of boxes in the available space. +// If a box has children, it needs to specify how it wants to arrange those children: ROW or COLUMN. +// If a box represents a window, you can put the window name in the Window field. +// When determining how to divvy-up the available height (for row children) or width (for column children), we first +// give the boxes with a static `size` the space that they want. Then we apportion +// the remaining space based on the weights of the dynamic boxes (you can't define +// both size and weight at the same time: you gotta pick one). If there are two +// boxes, one with weight 1 and the other with weight 2, the first one gets 33% +// of the available space and the second one gets the remaining 66% + +type Box struct { + // Direction decides how the children boxes are laid out. ROW means the children will each form a row i.e. that they will be stacked on top of eachother. + Direction Direction + + // function which takes the width and height assigned to the box and decides which orientation it will have + ConditionalDirection func(width int, height int) Direction + + Children []*Box + + // function which takes the width and height assigned to the box and decides the layout of the children. + ConditionalChildren func(width int, height int) []*Box + + // Window refers to the name of the window this box represents, if there is one + Window string + + // static Size. If parent box's direction is ROW this refers to height, otherwise width + Size int + + // dynamic size. Once all statically sized children have been considered, Weight decides how much of the remaining space will be taken up by the box + // TODO: consider making there be one int and a type enum so we can't have size and Weight simultaneously defined + Weight int +} + +func ArrangeWindows(root *Box, x0, y0, width, height int) map[string]Dimensions { + children := root.getChildren(width, height) + if len(children) == 0 { + // leaf node + if root.Window != "" { + dimensionsForWindow := Dimensions{X0: x0, Y0: y0, X1: x0 + width - 1, Y1: y0 + height - 1} + return map[string]Dimensions{root.Window: dimensionsForWindow} + } + return map[string]Dimensions{} + } + + direction := root.getDirection(width, height) + + var availableSize int + if direction == COLUMN { + availableSize = width + } else { + availableSize = height + } + + sizes := calcSizes(children, availableSize) + + result := map[string]Dimensions{} + offset := 0 + for i, child := range children { + boxSize := sizes[i] + + var resultForChild map[string]Dimensions + if direction == COLUMN { + resultForChild = ArrangeWindows(child, x0+offset, y0, boxSize, height) + } else { + resultForChild = ArrangeWindows(child, x0, y0+offset, width, boxSize) + } + + result = mergeDimensionMaps(result, resultForChild) + offset += boxSize + } + + return result +} + +func calcSizes(boxes []*Box, availableSpace int) []int { + normalizedWeights := normalizeWeights(lo.Map(boxes, func(box *Box, _ int) int { return box.Weight })) + + totalWeight := 0 + reservedSpace := 0 + for i, box := range boxes { + if box.isStatic() { + reservedSpace += box.Size + } else { + totalWeight += normalizedWeights[i] + } + } + + dynamicSpace := utils.Max(0, availableSpace-reservedSpace) + + unitSize := 0 + extraSpace := 0 + if totalWeight > 0 { + unitSize = dynamicSpace / totalWeight + extraSpace = dynamicSpace % totalWeight + } + + result := make([]int, len(boxes)) + for i, box := range boxes { + if box.isStatic() { + // assuming that only one static child can have a size greater than the + // available space. In that case we just crop the size to what's available + result[i] = utils.Min(availableSpace, box.Size) + } else { + result[i] = unitSize * normalizedWeights[i] + } + } + + // distribute the remainder across dynamic boxes. + for extraSpace > 0 { + for i, weight := range normalizedWeights { + if weight > 0 { + result[i]++ + extraSpace-- + normalizedWeights[i]-- + + if extraSpace == 0 { + break + } + } + } + } + + return result +} + +// removes common multiple from weights e.g. if we get 2, 4, 4 we return 1, 2, 2. +func normalizeWeights(weights []int) []int { + if len(weights) == 0 { + return []int{} + } + + // to spare us some computation we'll exit early if any of our weights is 1 + if lo.SomeBy(weights, func(weight int) bool { return weight == 1 }) { + return weights + } + + // map weights to factorSlices and find the lowest common factor + positiveWeights := lo.Filter(weights, func(weight int, _ int) bool { return weight > 0 }) + factorSlices := lo.Map(positiveWeights, func(weight int, _ int) []int { return calcFactors(weight) }) + commonFactors := factorSlices[0] + for _, factors := range factorSlices { + commonFactors = lo.Intersect(commonFactors, factors) + } + + if len(commonFactors) == 0 { + return weights + } + + newWeights := lo.Map(weights, func(weight int, _ int) int { return weight / commonFactors[0] }) + + return normalizeWeights(newWeights) +} + +func calcFactors(n int) []int { + factors := []int{} + for i := 2; i <= n; i++ { + if n%i == 0 { + factors = append(factors, i) + } + } + return factors +} + +func (b *Box) isStatic() bool { + return b.Size > 0 +} + +func (b *Box) getDirection(width int, height int) Direction { + if b.ConditionalDirection != nil { + return b.ConditionalDirection(width, height) + } + return b.Direction +} + +func (b *Box) getChildren(width int, height int) []*Box { + if b.ConditionalChildren != nil { + return b.ConditionalChildren(width, height) + } + return b.Children +} + +func mergeDimensionMaps(a map[string]Dimensions, b map[string]Dimensions) map[string]Dimensions { + result := map[string]Dimensions{} + for _, dimensionMap := range []map[string]Dimensions{a, b} { + for k, v := range dimensionMap { + result[k] = v + } + } + return result +} diff --git a/vendor/github.com/jesseduffield/lazycore/pkg/utils/utils.go b/vendor/github.com/jesseduffield/lazycore/pkg/utils/utils.go new file mode 100644 index 0000000..9fa9337 --- /dev/null +++ b/vendor/github.com/jesseduffield/lazycore/pkg/utils/utils.go @@ -0,0 +1,16 @@ +package utils + +// Min returns the minimum of two integers +func Min(x, y int) int { + if x < y { + return x + } + return y +} + +func Max(x, y int) int { + if x > y { + return x + } + return y +} diff --git a/vendor/github.com/samber/lo/CHANGELOG.md b/vendor/github.com/samber/lo/CHANGELOG.md index 2eaa45c..9ba32f1 100644 --- a/vendor/github.com/samber/lo/CHANGELOG.md +++ b/vendor/github.com/samber/lo/CHANGELOG.md @@ -2,6 +2,128 @@ @samber: I sometimes forget to update this file. Ping me on [Twitter](https://twitter.com/samuelberthe) or open an issue in case of error. We need to keep a clear changelog for easier lib upgrade. +## 1.31.0 (2022-10-06) + +Adding: + +- lo.SliceToChannel +- lo.Generator +- lo.Batch +- lo.BatchWithTimeout + +## 1.30.1 (2022-10-06) + +Fix: + +- lo.Try1: remove generic type +- lo.Validate: format error properly + +## 1.30.0 (2022-10-04) + +Adding: + +- lo.TernaryF +- lo.Validate + +## 1.29.0 (2022-10-02) + +Adding: + +- lo.ErrorAs +- lo.TryOr +- lo.TryOrX + +## 1.28.0 (2022-09-05) + +Adding: + +- lo.ChannelDispatcher with 6 dispatching strategies: + - lo.DispatchingStrategyRoundRobin + - lo.DispatchingStrategyRandom + - lo.DispatchingStrategyWeightedRandom + - lo.DispatchingStrategyFirst + - lo.DispatchingStrategyLeast + - lo.DispatchingStrategyMost + +## 1.27.1 (2022-08-15) + +Bugfix: + +- Removed comparable constraint for lo.FindKeyBy + +## 1.27.0 (2022-07-29) + +Breaking: + +- Change of MapToSlice prototype: `MapToSlice[K comparable, V any, R any](in map[K]V, iteratee func(V, K) R) []R` -> `MapToSlice[K comparable, V any, R any](in map[K]V, iteratee func(K, V) R) []R` + +Added: + +- lo.ChunkString +- lo.SliceToMap (alias to lo.Associate) + +## 1.26.0 (2022-07-24) + +Adding: + +- lo.Associate +- lo.ReduceRight +- lo.FromPtrOr +- lo.MapToSlice +- lo.IsSorted +- lo.IsSortedByKey + +## 1.25.0 (2022-07-04) + +Adding: + +- lo.FindUniques +- lo.FindUniquesBy +- lo.FindDuplicates +- lo.FindDuplicatesBy +- lo.IsNotEmpty + +## 1.24.0 (2022-07-04) + +Adding: + +- lo.Without +- lo.WithoutEmpty + +## 1.23.0 (2022-07-04) + +Adding: + +- lo.FindKey +- lo.FindKeyBy + +## 1.22.0 (2022-07-04) + +Adding: + +- lo.Slice +- lo.FromPtr +- lo.IsEmpty +- lo.Compact +- lo.ToPairs: alias to lo.Entries +- lo.FromPairs: alias to lo.FromEntries +- lo.Partial + +Change: + +- lo.Must + lo.MustX: add context to panic message + +Fix: + +- lo.Nth: out of bound exception (#137) + +## 1.21.0 (2022-05-10) + +Adding: + +- lo.ToAnySlice +- lo.FromAnySlice + ## 1.20.0 (2022-05-02) Adding: @@ -55,7 +177,7 @@ Improvement: Adding: -- lo.Colaesce +- lo.Coalesce ## 1.13.0 (2022-04-14) diff --git a/vendor/github.com/samber/lo/Makefile b/vendor/github.com/samber/lo/Makefile index 9141618..57bb491 100644 --- a/vendor/github.com/samber/lo/Makefile +++ b/vendor/github.com/samber/lo/Makefile @@ -1,10 +1,5 @@ BIN=go -# BIN=go1.18beta1 - -go1.18beta1: - go install golang.org/dl/go1.18beta1@latest - go1.18beta1 download build: ${BIN} build -v ./... @@ -20,7 +15,7 @@ watch-bench: reflex -t 50ms -s -- sh -c 'go test -benchmem -count 3 -bench ./...' coverage: - ${BIN} test -v -coverprofile cover.out . + ${BIN} test -v -coverprofile=cover.out -covermode=atomic . ${BIN} tool cover -html=cover.out -o cover.html # tools @@ -31,7 +26,7 @@ tools: ${BIN} install github.com/jondot/goweight@latest ${BIN} install github.com/golangci/golangci-lint/cmd/golangci-lint@latest ${BIN} get -t -u golang.org/x/tools/cmd/cover - ${BIN} get -t -u github.com/sonatype-nexus-community/nancy@latest + ${BIN} install github.com/sonatype-nexus-community/nancy@latest go mod tidy lint: @@ -40,11 +35,9 @@ lint-fix: golangci-lint run --timeout 60s --max-same-issues 50 --fix ./... audit: tools - ${BIN} mod tidy ${BIN} list -json -m all | nancy sleuth outdated: tools - ${BIN} mod tidy ${BIN} list -u -m -json all | go-mod-outdated -update -direct weight: tools diff --git a/vendor/github.com/samber/lo/README.md b/vendor/github.com/samber/lo/README.md index 10107ce..49cbe49 100644 --- a/vendor/github.com/samber/lo/README.md +++ b/vendor/github.com/samber/lo/README.md @@ -1,10 +1,12 @@ # lo -![Build Status](https://github.com/samber/lo/actions/workflows/go.yml/badge.svg) +[![tag](https://img.shields.io/github/tag/samber/lo.svg)](https://github.com/samber/lo/releases) [![GoDoc](https://godoc.org/github.com/samber/lo?status.svg)](https://pkg.go.dev/github.com/samber/lo) +![Build Status](https://github.com/samber/lo/actions/workflows/go.yml/badge.svg) [![Go report](https://goreportcard.com/badge/github.com/samber/lo)](https://goreportcard.com/report/github.com/samber/lo) +[![codecov](https://codecov.io/gh/samber/lo/branch/master/graph/badge.svg)](https://codecov.io/gh/samber/lo) -✨ **`lo` is a Lodash-style Go library based on Go 1.18+ Generics.** +✨ **`samber/lo` is a Lodash-style Go library based on Go 1.18+ Generics.** This project started as an experiment with the new generics implementation. It may look like [Lodash](https://github.com/lodash/lodash) in some aspects. I used to code with the fantastic ["go-funk"](https://github.com/thoas/go-funk) package, but "go-funk" uses reflection and therefore is not typesafe. @@ -12,7 +14,12 @@ As expected, benchmarks demonstrate that generics will be much faster than imple In the future, 5 to 10 helpers will overlap with those coming into the Go standard library (under package names `slices` and `maps`). I feel this library is legitimate and offers many more valuable abstractions. -### Why this name? +**See also:** + +- [samber/do](https://github.com/samber/do): A dependency injection toolkit based on Go 1.18+ Generics +- [samber/mo](https://github.com/samber/mo): Monads based on Go 1.18+ Generics (Option, Result, Either...) + +**Why this name?** I wanted a **short name**, similar to "Lodash" and no Go package currently uses this name. @@ -52,126 +59,187 @@ GoDoc: [https://godoc.org/github.com/samber/lo](https://godoc.org/github.com/sam Supported helpers for slices: -- Filter -- Map -- FilterMap -- FlatMap -- Reduce -- ForEach -- Times -- Uniq -- UniqBy -- GroupBy -- Chunk -- PartitionBy -- Flatten -- Shuffle -- Reverse -- Fill -- Repeat -- KeyBy -- Drop -- DropRight -- DropWhile -- DropRightWhile -- Reject -- Count -- CountBy +- [Filter](#filter) +- [Map](#map) +- [FilterMap](#filtermap) +- [FlatMap](#flatmap) +- [Reduce](#reduce) +- [ReduceRight](#reduceright) +- [ForEach](#foreach) +- [Times](#times) +- [Uniq](#uniq) +- [UniqBy](#uniqby) +- [GroupBy](#groupby) +- [Chunk](#chunk) +- [PartitionBy](#partitionby) +- [Flatten](#flatten) +- [Shuffle](#shuffle) +- [Reverse](#reverse) +- [Fill](#fill) +- [Repeat](#repeat) +- [RepeatBy](#repeatby) +- [KeyBy](#keyby) +- [Associate / SliceToMap](#associate-alias-slicetomap) +- [Drop](#drop) +- [DropRight](#dropright) +- [DropWhile](#dropwhile) +- [DropRightWhile](#droprightwhile) +- [Reject](#reject) +- [Count](#count) +- [CountBy](#countby) +- [Subset](#subset) +- [Slice](#slice) +- [Replace](#replace) +- [ReplaceAll](#replaceall) +- [Compact](#compact) +- [IsSorted](#issorted) +- [IsSortedByKey](#issortedbykey) Supported helpers for maps: -- Keys -- Values -- PickBy -- PickByKeys -- PickByValues -- OmitBy -- OmitByKeys -- OmitByValues -- Entries -- FromEntries -- Invert -- Assign (merge of maps) -- MapKeys -- MapValues +- [Keys](#keys) +- [Values](#values) +- [PickBy](#pickby) +- [PickByKeys](#pickbykeys) +- [PickByValues](#pickbyvalues) +- [OmitBy](#omitby) +- [OmitByKeys](#omitbykeys) +- [OmitByValues](#omitbyvalues) +- [Entries / ToPairs](#entries-alias-topairs) +- [FromEntries / FromPairs](#fromentries-alias-frompairs) +- [Invert](#invert) +- [Assign (merge of maps)](#assign) +- [MapKeys](#mapkeys) +- [MapValues](#mapvalues) +- [MapToSlice](#maptoslice) Supported math helpers: -- Range / RangeFrom / RangeWithSteps -- Clamp -- SumBy +- [Range / RangeFrom / RangeWithSteps](#range--rangefrom--rangewithsteps) +- [Clamp](#clamp) +- [SumBy](#sumby) Supported helpers for strings: -- Substring -- RuneLength +- [Substring](#substring) +- [ChunkString](#chunkstring) +- [RuneLength](#runelength) Supported helpers for tuples: -- T2 -> T9 -- Unpack2 -> Unpack9 -- Zip2 -> Zip9 -- Unzip2 -> Unzip9 +- [T2 -> T9](#t2---t9) +- [Unpack2 -> Unpack9](#unpack2---unpack9) +- [Zip2 -> Zip9](#zip2---zip9) +- [Unzip2 -> Unzip9](#unzip2---unzip9) + +Supported helpers for channels: + +- [ChannelDispatcher](#channeldispatcher) +- [SliceToChannel](#slicetochannel) +- [Generator](#generator) +- [Batch](#batch) +- [BatchWithTimeout](#batchwithtimeout) Supported intersection helpers: -- Contains -- ContainsBy -- Every -- EveryBy -- Some -- SomeBy -- None -- NoneBy -- Intersect -- Difference -- Union +- [Contains](#contains) +- [ContainsBy](#containsby) +- [Every](#every) +- [EveryBy](#everyby) +- [Some](#some) +- [SomeBy](#someby) +- [None](#none) +- [NoneBy](#noneby) +- [Intersect](#intersect) +- [Difference](#difference) +- [Union](#union) +- [Without](#without) +- [WithoutEmpty](#withoutempty) Supported search helpers: -- IndexOf -- LastIndexOf -- Find -- FindIndexOf -- FindLastIndexOf -- Min -- MinBy -- Max -- MaxBy -- Last -- Nth -- Sample -- Samples - -Other functional programming helpers: - -- Ternary (1 line if/else statement) -- If / ElseIf / Else -- Switch / Case / Default -- ToPtr -- ToSlicePtr -- Empty -- Coalesce +- [IndexOf](#indexof) +- [LastIndexOf](#lastindexof) +- [Find](#find) +- [FindIndexOf](#findindexof) +- [FindLastIndexOf](#findlastindexof) +- [FindKey](#findkey) +- [FindKeyBy](#findkeyby) +- [FindUniques](#finduniques) +- [FindUniquesBy](#finduniquesby) +- [FindDuplicates](#findduplicates) +- [FindDuplicatesBy](#findduplicatesby) +- [Min](#min) +- [MinBy](#minby) +- [Max](#max) +- [MaxBy](#maxby) +- [Last](#last) +- [Nth](#nth) +- [Sample](#sample) +- [Samples](#samples) + +Conditional helpers: + +- [Ternary](#ternary) +- [TernaryF](#ternaryf) +- [If / ElseIf / Else](#if--elseif--else) +- [Switch / Case / Default](#switch--case--default) + +Type manipulation helpers: + +- [ToPtr](#toptr) +- [FromPtr](#fromptr) +- [FromPtrOr](#fromptror) +- [ToSlicePtr](#tosliceptr) +- [ToAnySlice](#toanyslice) +- [FromAnySlice](#fromanyslice) +- [Empty](#empty) +- [IsEmpty](#isempty) +- [IsNotEmpty](#isnotempty) +- [Coalesce](#coalesce) + +Function helpers: + +- [Partial](#partial) Concurrency helpers: -- Attempt -- AttemptWithDelay -- Debounce -- Async +- [Attempt](#attempt) +- [AttemptWithDelay](#attemptwithdelay) +- [Debounce](#debounce) +- [Synchronize](#synchronize) +- [Async](#async) Error handling: -- Must -- Try -- TryCatch -- TryWithErrorValue -- TryCatchWithErrorValue +- [Validate](#validate) +- [Must](#must) +- [Try](#try) +- [Try1 -> Try6](#try0-6) +- [TryOr](#tryor) +- [TryOr1 -> TryOr6](#tryor0-6) +- [TryCatch](#trycatch) +- [TryWithErrorValue](#trywitherrorvalue) +- [TryCatchWithErrorValue](#trycatchwitherrorvalue) +- [ErrorsAs](#errorsas) Constraints: - Clonable +### Filter + +Iterates over a collection and returns an array of all the elements the predicate function returns `true` for. + +```go +even := lo.Filter[int]([]int{1, 2, 3, 4}, func(x int, index int) bool { + return x%2 == 0 +}) +// []int{2, 4} +``` + +[[play](https://go.dev/play/p/Apjg3WeSi7K)] + ### Map Manipulates a slice of one type and transforms it into a slice of another type: @@ -179,12 +247,14 @@ Manipulates a slice of one type and transforms it into a slice of another type: ```go import "github.com/samber/lo" -lo.Map[int64, string]([]int64{1, 2, 3, 4}, func(x int64, _ int) string { +lo.Map[int64, string]([]int64{1, 2, 3, 4}, func(x int64, index int) string { return strconv.FormatInt(x, 10) }) // []string{"1", "2", "3", "4"} ``` +[[play](https://go.dev/play/p/OkPcYAhBo0D)] + Parallel processing: like `lo.Map()`, but the mapper function is called in a goroutine. Results are returned in the same order. ```go @@ -196,20 +266,6 @@ lop.Map[int64, string]([]int64{1, 2, 3, 4}, func(x int64, _ int) string { // []string{"1", "2", "3", "4"} ``` -### FlatMap - -Manipulates a slice and transforms and flattens it to a slice of another type. - -```go -lo.FlatMap[int, string]([]int{0, 1, 2}, func(x int, _ int) []string { - return []string{ - strconv.FormatInt(x, 10), - strconv.FormatInt(x, 10), - } -}) -// []string{"0", "0", "1", "1", "2", "2"} -``` - ### FilterMap Returns a slice which obtained after both filtering and mapping using the given callback function. @@ -226,37 +282,24 @@ matching := lo.FilterMap[string, string]([]string{"cpu", "gpu", "mouse", "keyboa // []string{"xpu", "xpu"} ``` -### Filter - -Iterates over a collection and returns an array of all the elements the predicate function returns `true` for. - -```go -even := lo.Filter[int]([]int{1, 2, 3, 4}, func(x int, _ int) bool { - return x%2 == 0 -}) -// []int{2, 4} -``` - -### Contains - -Returns true if an element is present in a collection. +[[play](https://go.dev/play/p/-AuYXfy7opz)] -```go -present := lo.Contains[int]([]int{0, 1, 2, 3, 4, 5}, 5) -// true -``` - -### ContainsBy +### FlatMap -Returns true if the predicate function returns `true`. +Manipulates a slice and transforms and flattens it to a slice of another type. ```go -present := lo.ContainsBy[int]([]int{0, 1, 2, 3, 4, 5}, func(x int) bool { - return x == 3 +lo.FlatMap[int, string]([]int{0, 1, 2}, func(x int, _ int) []string { + return []string{ + strconv.FormatInt(x, 10), + strconv.FormatInt(x, 10), + } }) -// true +// []string{"0", "0", "1", "1", "2", "2"} ``` +[[play](https://go.dev/play/p/YSoYmQTA8-U)] + ### Reduce Reduces a collection to a single value. The value is calculated by accumulating the result of running each element in the collection through an accumulator function. Each successive invocation is supplied with the return value returned by the previous call. @@ -268,6 +311,21 @@ sum := lo.Reduce[int, int]([]int{1, 2, 3, 4}, func(agg int, item int, _ int) int // 10 ``` +[[play](https://go.dev/play/p/R4UHXZNaaUG)] + +### ReduceRight + +Like `lo.Reduce` except that it iterates over elements of collection from right to left. + +```go +result := lo.ReduceRight[[]int, []int]([][]int{{0, 1}, {2, 3}, {4, 5}}, func(agg []int, item []int, _ int) []int { + return append(agg, item...) +}, []int{}) +// []int{4, 5, 2, 3, 0, 1} +``` + +[[play](https://go.dev/play/p/Fq3W70l7wXF)] + ### ForEach Iterates over elements of a collection and invokes the function over each element. @@ -281,6 +339,8 @@ lo.ForEach[string]([]string{"hello", "world"}, func(x string, _ int) { // prints "hello\nworld\n" ``` +[[play](https://go.dev/play/p/oofyiUPRf8t)] + Parallel processing: like `lo.ForEach()`, but the callback is called as a goroutine. ```go @@ -305,6 +365,8 @@ lo.Times[string](3, func(i int) string { // []string{"0", "1", "2"} ``` +[[play](https://go.dev/play/p/vgQj3Glr6lT)] + Parallel processing: like `lo.Times()`, but callback is called in goroutine. ```go @@ -325,6 +387,8 @@ uniqValues := lo.Uniq[int]([]int{1, 2, 2, 1}) // []int{1, 2} ``` +[[play](https://go.dev/play/p/DTzbeXZ6iEN)] + ### UniqBy Returns a duplicate-free version of an array, in which only the first occurrence of each element is kept. The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is invoked for each element in array to generate the criterion by which uniqueness is computed. @@ -336,6 +400,8 @@ uniqValues := lo.UniqBy[int, int]([]int{0, 1, 2, 3, 4, 5}, func(i int) int { // []int{0, 1, 2} ``` +[[play](https://go.dev/play/p/g42Z3QSb53u)] + ### GroupBy Returns an object composed of keys generated from the results of running each element of collection through iteratee. @@ -349,6 +415,8 @@ groups := lo.GroupBy[int, int]([]int{0, 1, 2, 3, 4, 5}, func(i int) int { // map[int][]int{0: []int{0, 3}, 1: []int{1, 4}, 2: []int{2, 5}} ``` +[[play](https://go.dev/play/p/XnQBd_v6brd)] + Parallel processing: like `lo.GroupBy()`, but callback is called in goroutine. ```go @@ -378,6 +446,8 @@ lo.Chunk[int]([]int{0}, 2) // [][]int{{0}} ``` +[[play](https://go.dev/play/p/EeKl0AuTehH)] + ### PartitionBy Returns an array of elements split into groups. The order of grouped values is determined by the order they occur in collection. The grouping is generated from the results of running each element of collection through iteratee. @@ -396,12 +466,14 @@ partitions := lo.PartitionBy[int, string]([]int{-2, -1, 0, 1, 2, 3, 4, 5}, func( // [][]int{{-2, -1}, {0, 2, 4}, {1, 3, 5}} ``` +[[play](https://go.dev/play/p/NfQ_nGjkgXW)] + Parallel processing: like `lo.PartitionBy()`, but callback is called in goroutine. Results are returned in the same order. ```go import lop "github.com/samber/lo/parallel" -partitions := lo.PartitionBy[int, string]([]int{-2, -1, 0, 1, 2, 3, 4, 5}, func(x int) string { +partitions := lop.PartitionBy[int, string]([]int{-2, -1, 0, 1, 2, 3, 4, 5}, func(x int) string { if x < 0 { return "negative" } else if x%2 == 0 { @@ -421,24 +493,30 @@ flat := lo.Flatten[int]([][]int{{0, 1}, {2, 3, 4, 5}}) // []int{0, 1, 2, 3, 4, 5} ``` +[[play](https://go.dev/play/p/rbp9ORaMpjw)] + ### Shuffle Returns an array of shuffled values. Uses the Fisher-Yates shuffle algorithm. ```go randomOrder := lo.Shuffle[int]([]int{0, 1, 2, 3, 4, 5}) -// []int{0, 1, 2, 3, 4, 5} +// []int{1, 4, 0, 3, 5, 2} ``` +[[play](https://go.dev/play/p/Qp73bnTDnc7)] + ### Reverse Reverses array so that the first element becomes the last, the second element becomes the second to last, and so on. ```go -reverseOder := lo.Reverse[int]([]int{0, 1, 2, 3, 4, 5}) +reverseOrder := lo.Reverse[int]([]int{0, 1, 2, 3, 4, 5}) // []int{5, 4, 3, 2, 1, 0} ``` +[[play](https://go.dev/play/p/fhUMLvZ7vS6)] + ### Fill Fills elements of array with `initial` value. @@ -456,6 +534,8 @@ initializedSlice := lo.Fill[foo]([]foo{foo{"a"}, foo{"a"}}, foo{"b"}) // []foo{foo{"b"}, foo{"b"}} ``` +[[play](https://go.dev/play/p/VwR34GzqEub)] + ### Repeat Builds a slice with N copies of initial value. @@ -473,22 +553,26 @@ slice := lo.Repeat[foo](2, foo{"a"}) // []foo{foo{"a"}, foo{"a"}} ``` +[[play](https://go.dev/play/p/g3uHXbmc3b6)] + ### RepeatBy Builds a slice with values returned by N calls of callback. ```go -slice := lo.RepeatBy[int](0, func (i int) int { - return math.Pow(i, 2) +slice := lo.RepeatBy[string](0, func (i int) string { + return strconv.FormatInt(int64(math.Pow(float64(i), 2)), 10) }) // []int{} -slice := lo.RepeatBy[int](5, func (i int) int { - return math.Pow(i, 2) +slice := lo.RepeatBy[string](5, func(i int) string { + return strconv.FormatInt(int64(math.Pow(float64(i), 2)), 10) }) // []int{0, 1, 4, 9, 16} ``` +[[play](https://go.dev/play/p/ozZLCtX_hNU)] + ### KeyBy Transforms a slice or an array of structs to a map based on a pivot callback. @@ -513,6 +597,26 @@ result := lo.KeyBy[string, Character](characters, func(char Character) string { //map[a:{dir:left code:97} d:{dir:right code:100}] ``` +[[play](https://go.dev/play/p/mdaClUAT-zZ)] + +### Associate (alias: SliceToMap) + +Returns a map containing key-value pairs provided by transform function applied to elements of the given slice. +If any of two pairs would have the same key the last one gets added to the map. + +The order of keys in returned map is not specified and is not guaranteed to be the same from the original array. + +```go +in := []*foo{{baz: "apple", bar: 1}, {baz: "banana", bar: 2}} + +aMap := lo.Associate[*foo, string, int](in, func (f *foo) (string, int) { + return f.baz, f.bar +}) +// map[string][int]{ "apple":1, "banana":2 } +``` + +[[play](https://go.dev/play/p/WHa2CfMO3Lr)] + ### Drop Drops n elements from the beginning of a slice or array. @@ -522,6 +626,8 @@ l := lo.Drop[int]([]int{0, 1, 2, 3, 4, 5}, 2) // []int{2, 3, 4, 5} ``` +[[play](https://go.dev/play/p/JswS7vXRJP2)] + ### DropRight Drops n elements from the end of a slice or array. @@ -531,6 +637,8 @@ l := lo.DropRight[int]([]int{0, 1, 2, 3, 4, 5}, 2) // []int{0, 1, 2, 3} ``` +[[play](https://go.dev/play/p/GG0nXkSJJa3)] + ### DropWhile Drop elements from the beginning of a slice or array while the predicate returns true. @@ -542,6 +650,8 @@ l := lo.DropWhile[string]([]string{"a", "aa", "aaa", "aa", "aa"}, func(val strin // []string{"aaa", "aa", "aa"} ``` +[[play](https://go.dev/play/p/7gBPYw2IK16)] + ### DropRightWhile Drop elements from the end of a slice or array while the predicate returns true. @@ -553,6 +663,8 @@ l := lo.DropRightWhile[string]([]string{"a", "aa", "aaa", "aa", "aa"}, func(val // []string{"a", "aa", "aaa"} ``` +[[play](https://go.dev/play/p/3-n71oEC0Hz)] + ### Reject The opposite of Filter, this method returns the elements of collection that predicate does not return truthy for. @@ -564,6 +676,8 @@ odd := lo.Reject[int]([]int{1, 2, 3, 4}, func(x int, _ int) bool { // []int{1, 3} ``` +[[play](https://go.dev/play/p/YkLMODy1WEL)] + ### Count Counts the number of elements in the collection that compare equal to value. @@ -573,6 +687,8 @@ count := lo.Count[int]([]int{1, 5, 1}, 1) // 2 ``` +[[play](https://go.dev/play/p/Y3FlK54yveC)] + ### CountBy Counts the number of elements in the collection for which predicate is true. @@ -584,9 +700,11 @@ count := lo.CountBy[int]([]int{1, 5, 1}, func(i int) bool { // 2 ``` +[[play](https://go.dev/play/p/ByQbNYQQi4X)] + ### Subset -Return part of a slice. +Returns a copy of a slice from `offset` up to `length` elements. Like `slice[start:start+length]`, but does not panic on overflow. ```go in := []int{0, 1, 2, 3, 4} @@ -601,6 +719,30 @@ sub := lo.Subset(in, -2, math.MaxUint) // []int{3, 4} ``` +[[play](https://go.dev/play/p/tOQu1GhFcog)] + +### Slice + +Returns a copy of a slice from `start` up to, but not including `end`. Like `slice[start:end]`, but does not panic on overflow. + +```go +in := []int{0, 1, 2, 3, 4} + +slice := lo.Slice(in, 0, 5) +// []int{0, 1, 2, 3, 4} + +slice := lo.Slice(in, 2, 3) +// []int{2} + +slice := lo.Slice(in, 2, 6) +// []int{2, 3, 4} + +slice := lo.Slice(in, 4, 3) +// []int{} +``` + +[[play](https://go.dev/play/p/8XWYhfMMA1h)] + ### Replace Returns a copy of the slice with the first n non-overlapping instances of old replaced by new. @@ -621,6 +763,8 @@ slice := lo.Replace(in, 0, 42, -1) // []int{42, 1, 42, 1, 2, 3, 42} ``` +[[play](https://go.dev/play/p/XfPzmf9gql6)] + ### ReplaceAll Returns a copy of the slice with all non-overlapping instances of old replaced by new. @@ -635,15 +779,56 @@ slice := lo.ReplaceAll(in, -1, 42) // []int{0, 1, 0, 1, 2, 3, 0} ``` +[[play](https://go.dev/play/p/a9xZFUHfYcV)] + +### Compact + +Returns a slice of all non-zero elements. + +```go +in := []string{"", "foo", "", "bar", ""} + +slice := lo.Compact[string](in) +// []string{"foo", "bar"} +``` + +[[play](https://go.dev/play/p/tXiy-iK6PAc)] + +### IsSorted + +Checks if a slice is sorted. + +```go +slice := lo.IsSorted([]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) +// true +``` + +[[play](https://go.dev/play/p/mc3qR-t4mcx)] + +### IsSortedByKey + +Checks if a slice is sorted by iteratee. + +```go +slice := lo.IsSortedByKey([]string{"a", "bb", "ccc"}, func(s string) int { + return len(s) +}) +// true +``` + +[[play](https://go.dev/play/p/wiG6XyBBu49)] + ### Keys Creates an array of the map keys. ```go keys := lo.Keys[string, int](map[string]int{"foo": 1, "bar": 2}) -// []string{"bar", "foo"} +// []string{"foo", "bar"} ``` +[[play](https://go.dev/play/p/Uu11fHASqrU)] + ### Values Creates an array of the map values. @@ -653,6 +838,8 @@ values := lo.Values[string, int](map[string]int{"foo": 1, "bar": 2}) // []int{1, 2} ``` +[[play](https://go.dev/play/p/nnRTQkzQfF6)] + ### PickBy Returns same map type filtered by given predicate. @@ -664,6 +851,8 @@ m := lo.PickBy[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, func(k // map[string]int{"foo": 1, "baz": 3} ``` +[[play](https://go.dev/play/p/kdg8GR_QMmf)] + ### PickByKeys Returns same map type filtered by given keys. @@ -673,6 +862,8 @@ m := lo.PickByKeys[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, [] // map[string]int{"foo": 1, "baz": 3} ``` +[[play](https://go.dev/play/p/R1imbuci9qU)] + ### PickByValues Returns same map type filtered by given values. @@ -682,6 +873,8 @@ m := lo.PickByValues[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, // map[string]int{"foo": 1, "baz": 3} ``` +[[play](https://go.dev/play/p/1zdzSvbfsJc)] + ### OmitBy Returns same map type filtered by given predicate. @@ -693,6 +886,8 @@ m := lo.OmitBy[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, func(k // map[string]int{"bar": 2} ``` +[[play](https://go.dev/play/p/EtBsR43bdsd)] + ### OmitByKeys Returns same map type filtered by given keys. @@ -702,6 +897,8 @@ m := lo.OmitByKeys[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, [] // map[string]int{"bar": 2} ``` +[[play](https://go.dev/play/p/t1QjCrs-ysk)] + ### OmitByValues Returns same map type filtered by given values. @@ -711,7 +908,9 @@ m := lo.OmitByValues[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, // map[string]int{"bar": 2} ``` -### Entries +[[play](https://go.dev/play/p/9UYZi-hrs8j)] + +### Entries (alias: ToPairs) Transforms a map into array of key/value pairs. @@ -729,7 +928,9 @@ entries := lo.Entries[string, int](map[string]int{"foo": 1, "bar": 2}) // } ``` -### FromEntries +[[play](https://go.dev/play/p/3Dhgx46gawJ)] + +### FromEntries (alias: FromPairs) Transforms an array of key/value pairs into a map. @@ -747,18 +948,22 @@ m := lo.FromEntries[string, int]([]lo.Entry[string, int]{ // map[string]int{"foo": 1, "bar": 2} ``` +[[play](https://go.dev/play/p/oIr5KHFGCEN)] + ### Invert Creates a map composed of the inverted keys and values. If map contains duplicate values, subsequent values overwrite property assignments of previous values. ```go -m1 := lo.Invert[string, int]([map[string]int{"a": 1, "b": 2}) +m1 := lo.Invert[string, int](map[string]int{"a": 1, "b": 2}) // map[int]string{1: "a", 2: "b"} -m2 := lo.Invert[string, int]([map[string]int{"a": 1, "b": 2, "c": 1}) +m2 := lo.Invert[string, int](map[string]int{"a": 1, "b": 2, "c": 1}) // map[int]string{1: "c", 2: "b"} ``` +[[play](https://go.dev/play/p/rFQ4rak6iA1)] + ### Assign Merges multiple maps from left to right. @@ -771,6 +976,8 @@ mergedMaps := lo.Assign[string, int]( // map[string]int{"a": 1, "b": 3, "c": 4} ``` +[[play](https://go.dev/play/p/VhwfJOyxf5o)] + ### MapKeys Manipulates a map keys and transforms it to a map of another type. @@ -782,6 +989,8 @@ m2 := lo.MapKeys[int, int, string](map[int]int{1: 1, 2: 2, 3: 3, 4: 4}, func(_ i // map[string]int{"1": 1, "2": 2, "3": 3, "4": 4} ``` +[[play](https://go.dev/play/p/9_4WPIqOetJ)] + ### MapValues Manipulates a map values and transforms it to a map of another type. @@ -795,6 +1004,23 @@ m2 := lo.MapValues[int, int64, string](m1, func(x int64, _ int) string { // map[int]string{1: "1", 2: "2", 3: "3"} ``` +[[play](https://go.dev/play/p/T_8xAfvcf0W)] + +### MapToSlice + +Transforms a map into a slice based on specific iteratee. + +```go +m := map[int]int64{1: 4, 2: 5, 3: 6} + +s := lo.MapToSlice(m, func(k int, v int64) string { + return fmt.Sprintf("%d_%d", k, v) +}) +// []string{"1_4", "2_5", "3_6"} +``` + +[[play](https://go.dev/play/p/ZuiCZpDt6LD)] + ### Range / RangeFrom / RangeWithSteps Creates an array of numbers (positive and/or negative) progressing from start up to, but not including end. @@ -803,28 +1029,30 @@ Creates an array of numbers (positive and/or negative) progressing from start up result := Range(4) // [0, 1, 2, 3] -result := Range(-4); +result := Range(-4) // [0, -1, -2, -3] -result := RangeFrom(1, 5); -// [1, 2, 3, 4] +result := RangeFrom(1, 5) +// [1, 2, 3, 4, 5] -result := RangeFrom[float64](1.0, 5); -// [1.0, 2.0, 3.0, 4.0] +result := RangeFrom[float64](1.0, 5) +// [1.0, 2.0, 3.0, 4.0, 5.0] -result := RangeWithSteps(0, 20, 5); +result := RangeWithSteps(0, 20, 5) // [0, 5, 10, 15] -result := RangeWithSteps[float32](-1.0, -4.0, -1.0); +result := RangeWithSteps[float32](-1.0, -4.0, -1.0) // [-1.0, -2.0, -3.0] -result := RangeWithSteps(1, 4, -1); +result := RangeWithSteps(1, 4, -1) // [] -result := Range(0); +result := Range(0) // [] ``` +[[play](https://go.dev/play/p/0r6VimXAi9H)] + ### Clamp Clamps number within the inclusive lower and upper bounds. @@ -840,7 +1068,9 @@ r3 := lo.Clamp(42, -10, 10) // 10 ``` -### SumBy +[[play](https://go.dev/play/p/RU4lJNC2hlI)] + +### SumBy Summarizes the values in a collection using the given return value from the iteration function. If collection is empty 0 is returned. @@ -853,6 +1083,8 @@ sum := lo.SumBy(strings, func(item string) int { // 6 ``` +[[play](https://go.dev/play/p/Dz_a_7jN_ca)] + ### Substring Return part of a string. @@ -868,6 +1100,28 @@ sub := lo.Substring("hello", -2, math.MaxUint) // "lo" ``` +[[play](https://go.dev/play/p/TQlxQi82Lu1)] + +### ChunkString + +Returns an array of strings split into groups the length of size. If array can't be split evenly, the final chunk will be the remaining elements. + +```go +lo.ChunkString("123456", 2) +// []string{"12", "34", "56"} + +lo.ChunkString("1234567", 2) +// []string{"12", "34", "56", "7"} + +lo.ChunkString("", 2) +// []string{""} + +lo.ChunkString("1", 2) +// []string{"1"} +``` + +[[play](https://go.dev/play/p/__FLTuJVz54)] + ### RuneLength An alias to utf8.RuneCountInString which returns the number of runes in string. @@ -880,19 +1134,23 @@ sub := len("hellô") // 6 ``` +[[play](https://go.dev/play/p/tuhgW_lWY8l)] + ### T2 -> T9 Creates a tuple from a list of values. ```go -tuple1 := lo.T2[string, int]("x", 1) +tuple1 := lo.T2("x", 1) // Tuple2[string, int]{A: "x", B: 1} func example() (string, int) { return "y", 2 } -tuple2 := lo.T2[string, int](example()) +tuple2 := lo.T2(example()) // Tuple2[string, int]{A: "y", B: 2} ``` +[[play](https://go.dev/play/p/IllL3ZO4BQm)] + ### Unpack2 -> Unpack9 Returns values contained in tuple. @@ -902,6 +1160,8 @@ r1, r2 := lo.Unpack2[string, int](lo.Tuple2[string, int]{"a", 1}) // "a", 1 ``` +[[play](https://go.dev/play/p/xVP_k0kJ96W)] + ### Zip2 -> Zip9 Zip creates a slice of grouped elements, the first of which contains the first elements of the given arrays, the second of which contains the second elements of the given arrays, and so on. @@ -913,6 +1173,8 @@ tuples := lo.Zip2[string, int]([]string{"a", "b"}, []int{1, 2}) // []Tuple2[string, int]{{A: "a", B: 1}, {A: "b", B: 2}} ``` +[[play](https://go.dev/play/p/jujaA6GaJTp)] + ### Unzip2 -> Unzip9 Unzip accepts an array of grouped elements and creates an array regrouping the elements to their pre-zip configuration. @@ -923,6 +1185,235 @@ a, b := lo.Unzip2[string, int]([]Tuple2[string, int]{{A: "a", B: 1}, {A: "b", B: // []int{1, 2} ``` +[[play](https://go.dev/play/p/ciHugugvaAW)] + +### ChannelDispatcher + +Distributes messages from input channels into N child channels. Close events are propagated to children. + +Underlying channels can have a fixed buffer capacity or be unbuffered when cap is 0. + +```go +ch := make(chan int, 42) +for i := 0; i <= 10; i++ { + ch <- i +} + +children := lo.ChannelDispatcher(ch, 5, 10, DispatchingStrategyRoundRobin[int]) +// []<-chan int{...} + +consumer := func(c <-chan int) { + for { + msg, ok := <-c + if !ok { + println("closed") + break + } + + println(msg) + } +} + +for i := range children { + go consumer(children[i]) +} +``` + +Many distributions strategies are available: + +- [lo.DispatchingStrategyRoundRobin](./channel.go): Distributes messages in a rotating sequential manner. +- [lo.DispatchingStrategyRandom](./channel.go): Distributes messages in a random manner. +- [lo.DispatchingStrategyWeightedRandom](./channel.go): Distributes messages in a weighted manner. +- [lo.DispatchingStrategyFirst](./channel.go): Distributes messages in the first non-full channel. +- [lo.DispatchingStrategyLeast](./channel.go): Distributes messages in the emptiest channel. +- [lo.DispatchingStrategyMost](./channel.go): Distributes to the fulliest channel. + +Some strategies bring fallback, in order to favor non-blocking behaviors. See implementations. + +For custom strategies, just implement the `lo.DispatchingStrategy` prototype: + +```go +type DispatchingStrategy[T any] func(message T, messageIndex uint64, channels []<-chan T) int +``` + +Eg: + +```go +type Message struct { + TenantID uuid.UUID +} + +func hash(id uuid.UUID) int { + h := fnv.New32a() + h.Write([]byte(id.String())) + return int(h.Sum32()) +} + +// Routes messages per TenantID. +customStrategy := func(message pubsub.AMQPSubMessage, messageIndex uint64, channels []<-chan pubsub.AMQPSubMessage) int { + destination := hash(message.TenantID) % len(channels) + + // check if channel is full + if len(channels[destination]) < cap(channels[destination]) { + return destination + } + + // fallback when child channel is full + return utils.DispatchingStrategyRoundRobin(message, uint64(destination), channels) +} + +children := lo.ChannelDispatcher(ch, 5, 10, customStrategy) +... +``` + +### SliceToChannel + +Returns a read-only channels of collection elements. Channel is closed after last element. Channel capacity can be customized. + +```go +list := []int{1, 2, 3, 4, 5} + +for v := range lo.SliceToChannel(2, list) { + println(v) +} +// prints 1, then 2, then 3, then 4, then 5 +``` + +### Generator + +Implements the generator design pattern. Channel is closed after last element. Channel capacity can be customized. + +```go +generator := func(yield func(int)) { + yield(1) + yield(2) + yield(3) +} + +for v := range lo.Generator(2, generator) { + println(v) +} +// prints 1, then 2, then 3 +``` + +### Batch + +Creates a slice of n elements from a channel. Returns the slice, the slice length, the read time and the channel status (opened/closed). + +```go +ch := lo.SliceToChannel(2, []int{1, 2, 3, 4, 5}) + +items1, length1, duration1, ok1 := lo.Batch(ch, 3) +// []int{1, 2, 3}, 3, 0s, true +items2, length2, duration2, ok2 := lo.Batch(ch, 3) +// []int{4, 5}, 2, 0s, false +``` + +Example: RabbitMQ consumer 👇 + +```go +ch := readFromQueue() + +for { + // read 1k items + items, length, _, ok := lo.Batch(ch, 1000) + + // do batching stuff + + if !ok { + break + } +} +``` + +### BatchWithTimeout + +Creates a slice of n elements from a channel, with timeout. Returns the slice, the slice length, the read time and the channel status (opened/closed). + +```go +generator := func(yield func(int)) { + for i := 0; i < 5; i++ { + yield(i) + time.Sleep(35*time.Millisecond) + } +} + +ch := lo.Generator(0, generator) + +items1, length1, duration1, ok1 := lo.BatchWithTimeout(ch, 3, 100*time.Millisecond) +// []int{1, 2}, 2, 100ms, true +items2, length2, duration2, ok2 := lo.BatchWithTimeout(ch, 3, 100*time.Millisecond) +// []int{3, 4, 5}, 3, 75ms, true +items3, length3, duration2, ok3 := lo.BatchWithTimeout(ch, 3, 100*time.Millisecond) +// []int{}, 0, 10ms, false +``` + +Example: RabbitMQ consumer 👇 + +```go +ch := readFromQueue() + +for { + // read 1k items + // wait up to 1 second + items, length, _, ok := lo.BatchWithTimeout(ch, 1000, 1*time.Second) + + // do batching stuff + + if !ok { + break + } +} +``` + +Example: Multithreaded RabbitMQ consumer 👇 + +```go +ch := readFromQueue() + +// 5 workers +// prefetch 1k messages per worker +children := lo.ChannelDispatcher(ch, 5, 1000, DispatchingStrategyFirst[int]) + +consumer := func(c <-chan int) { + for { + // read 1k items + // wait up to 1 second + items, length, _, ok := lo.BatchWithTimeout(ch, 1000, 1*time.Second) + + // do batching stuff + + if !ok { + break + } + } +} + +for i := range children { + go consumer(children[i]) +} +``` + +### Contains + +Returns true if an element is present in a collection. + +```go +present := lo.Contains[int]([]int{0, 1, 2, 3, 4, 5}, 5) +// true +``` + +### ContainsBy + +Returns true if the predicate function returns `true`. + +```go +present := lo.ContainsBy[int]([]int{0, 1, 2, 3, 4, 5}, func(x int) bool { + return x == 3 +}) +// true +``` + ### Every Returns true if all elements of a subset are contained into a collection or if the subset is empty. @@ -961,7 +1452,7 @@ ok := lo.Some[int]([]int{0, 1, 2, 3, 4, 5}, []int{-1, 6}) ### SomeBy -Returns true if the predicate returns true for any of the elements in the collection. +Returns true if the predicate returns true for any of the elements in the collection. If the collection is empty SomeBy returns false. ```go @@ -1032,6 +1523,27 @@ union := lo.Union[int]([]int{0, 1, 2, 3, 4, 5}, []int{0, 2, 10}) // []int{0, 1, 2, 3, 4, 5, 10} ``` +### Without + +Returns slice excluding all given values. + +```go +subset := lo.Without[int]([]int{0, 2, 10}, 2) +// []int{0, 10} + +subset := lo.Without[int]([]int{0, 2, 10}, 0, 1, 2, 3, 4, 5) +// []int{10} +``` + +### WithoutEmpty + +Returns slice excluding empty values. + +```go +subset := lo.WithoutEmpty[int]([]int{0, 2, 10}) +// []int{2, 10} +``` + ### IndexOf Returns the index at which the first occurrence of a value is found in an array or return -1 if the value cannot be found. @@ -1044,7 +1556,7 @@ notFound := lo.IndexOf[int]([]int{0, 1, 2, 1, 2, 3}, 6) // -1 ``` -### LastIndex +### LastIndexOf Returns the index at which the last occurrence of a value is found in an array or return -1 if the value cannot be found. @@ -1104,6 +1616,80 @@ str, index, ok := lo.FindLastIndexOf[string]([]string{"foobar"}, func(i string) // "", -1, false ``` +### FindKey + +Returns the key of the first value matching. + +```go +result1, ok1 := lo.FindKey(map[string]int{"foo": 1, "bar": 2, "baz": 3}, 2) +// "bar", true + +result2, ok2 := lo.FindKey(map[string]int{"foo": 1, "bar": 2, "baz": 3}, 42) +// "", false + +type test struct { + foobar string +} +result3, ok3 := lo.FindKey(map[string]test{"foo": test{"foo"}, "bar": test{"bar"}, "baz": test{"baz"}}, test{"foo"}) +// "foo", true +``` + +### FindKeyBy + +Returns the key of the first element predicate returns truthy for. + +```go +result1, ok1 := lo.FindKeyBy(map[string]int{"foo": 1, "bar": 2, "baz": 3}, func(k string, v int) bool { + return k == "foo" +}) +// "foo", true + +result2, ok2 := lo.FindKeyBy(map[string]int{"foo": 1, "bar": 2, "baz": 3}, func(k string, v int) bool { + return false +}) +// "", false +``` + +### FindUniques + +Returns a slice with all the unique elements of the collection. The order of result values is determined by the order they occur in the array. + +```go +uniqueValues := lo.FindUniques[int]([]int{1, 2, 2, 1, 2, 3}) +// []int{3} +``` + +### FindUniquesBy + +Returns a slice with all the unique elements of the collection. The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is invoked for each element in array to generate the criterion by which uniqueness is computed. + +```go +uniqueValues := lo.FindUniquesBy[int, int]([]int{3, 4, 5, 6, 7}, func(i int) int { + return i%3 +}) +// []int{5} +``` + +### FindDuplicates + +Returns a slice with the first occurence of each duplicated elements of the collection. The order of result values is determined by the order they occur in the array. + +```go +duplicatedValues := lo.FindDuplicates[int]([]int{1, 2, 2, 1, 2, 3}) +// []int{1, 2} +``` + +### FindDuplicatesBy + +Returns a slice with the first occurence of each duplicated elements of the collection. The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is invoked for each element in array to generate the criterion by which uniqueness is computed. + +```go +duplicatedValues := lo.FindDuplicatesBy[int, int]([]int{3, 4, 5, 6, 7}, func(i int) int { + return i%3 +}) +// []int{3, 4} +``` + ### Min Search the minimum value of a collection. @@ -1216,6 +1802,31 @@ result := lo.Ternary[string](false, "a", "b") // "b" ``` +[[play](https://go.dev/play/p/t-D7WBL44h2)] + +### TernaryF + +A 1 line if/else statement whose options are functions. + +```go +result := lo.TernaryF[string](true, func() string { return "a" }, func() string { return "b" }) +// "a" + +result := lo.TernaryF[string](false, func() string { return "a" }, func() string { return "b" }) +// "b" +``` + +Useful to avoid nil-pointer dereferencing in intializations, or avoid running unnecessary code + +```go +var s *string + +someStr := TernaryF[string](s == nil, func() string { return uuid.New().String() }, func() string { return *s }) +// ef782193-c30c-4e2e-a7ae-f8ab5e125e02 +``` + +[[play](https://go.dev/play/p/AO4VW20JoqM)] + ### If / ElseIf / Else ```go @@ -1260,6 +1871,8 @@ result := lo.IfF[int](true, func () int { // 1 ``` +[[play](https://go.dev/play/p/WSw3ApMxhyW)] + ### Switch / Case / Default ```go @@ -1309,6 +1922,8 @@ result := lo.Switch[int, string](1). // "1" ``` +[[play](https://go.dev/play/p/TGbKUMAeRUd)] + ### ToPtr Returns a pointer copy of value. @@ -1318,6 +1933,32 @@ ptr := lo.ToPtr[string]("hello world") // *string{"hello world"} ``` +### FromPtr + +Returns the pointer value or empty. + +```go +str := "hello world" +value := lo.FromPtr[string](&str) +// "hello world" + +value := lo.FromPtr[string](nil) +// "" +``` + +### FromPtrOr + +Returns the pointer value or the fallback value. + +```go +str := "hello world" +value := lo.FromPtrOr[string](&str, "empty") +// "hello world" + +value := lo.FromPtrOr[string](nil, "empty") +// "empty" +``` + ### ToSlicePtr Returns a slice of pointer copy of value. @@ -1327,6 +1968,27 @@ ptr := lo.ToSlicePtr[string]([]string{"hello", "world"}) // []*string{"hello", "world"} ``` +### ToAnySlice + +Returns a slice with all elements mapped to `any` type. + +```go +elements := lo.ToAnySlice[int]([]int{1, 5, 1}) +// []any{1, 5, 1} +``` + +### FromAnySlice + +Returns an `any` slice with all elements mapped to a type. Returns false in case of type conversion failure. + +```go +elements, ok := lo.FromAnySlice[string]([]any{"foobar", 42}) +// []string{}, false + +elements, ok := lo.FromAnySlice[string]([]any{"foobar", "42"}) +// []string{"foobar", "42"}, true +``` + ### Empty Returns an empty value. @@ -1340,6 +2002,56 @@ lo.Empty[bool]() // false ``` +### IsEmpty + +Returns true if argument is a zero value. + +```go +lo.IsEmpty[int](0) +// true +lo.IsEmpty[int](42) +// false + +lo.IsEmpty[string]("") +// true +lo.IsEmpty[bool]("foobar") +// false + +type test struct { + foobar string +} + +lo.IsEmpty[test](test{foobar: ""}) +// true +lo.IsEmpty[test](test{foobar: "foobar"}) +// false +``` + +### IsNotEmpty + +Returns true if argument is a zero value. + +```go +lo.IsNotEmpty[int](0) +// false +lo.IsNotEmpty[int](42) +// true + +lo.IsNotEmpty[string]("") +// false +lo.IsNotEmpty[bool]("foobar") +// true + +type test struct { + foobar string +} + +lo.IsNotEmpty[test](test{foobar: ""}) +// false +lo.IsNotEmpty[test](test{foobar: "foobar"}) +// true +``` + ### Coalesce Returns the first non-empty arguments. Arguments must be comparable. @@ -1357,9 +2069,24 @@ result, ok := lo.Coalesce[*string](nil, nilStr, &str) // &"foobar" true ``` +### Partial + +Returns new function that, when called, has its first argument set to the provided value. + +```go +add := func(x, y int) int { return x + y } +f := lo.Partial(add, 5) + +f(10) +// 15 + +f(42) +// 47 +``` + ### Attempt -Invokes a function N times until it returns valid output. Returning either the caught error or nil. When first argument is less than `1`, the function runs until a sucessfull response is returned. +Invokes a function N times until it returns valid output. Returning either the caught error or nil. When first argument is less than `1`, the function runs until a successful response is returned. ```go iter, err := lo.Attempt(42, func(i int) error { @@ -1395,11 +2122,13 @@ iter, err := lo.Attempt(0, func(i int) error { For more advanced retry strategies (delay, exponential backoff...), please take a look on [cenkalti/backoff](https://github.com/cenkalti/backoff). +[[play](https://go.dev/play/p/3ggJZ2ZKcMj)] + ### AttemptWithDelay -Invokes a function N times until it returns valid output, with a pause betwwen each call. Returning either the caught error or nil. +Invokes a function N times until it returns valid output, with a pause between each call. Returning either the caught error or nil. -When first argument is less than `1`, the function runs until a sucessfull response is returned. +When first argument is less than `1`, the function runs until a successful response is returned. ```go iter, duration, err := lo.AttemptWithDelay(5, 2*time.Second, func(i int, duration time.Duration) error { @@ -1416,6 +2145,8 @@ iter, duration, err := lo.AttemptWithDelay(5, 2*time.Second, func(i int, duratio For more advanced retry strategies (delay, exponential backoff...), please take a look on [cenkalti/backoff](https://github.com/cenkalti/backoff). +[[play](https://go.dev/play/p/tVs6CygC7m1)] + ### Debounce `NewDebounce` creates a debounced instance that delays invoking functions given until after wait milliseconds have elapsed, until `cancel` is called. @@ -1434,6 +2165,8 @@ time.Sleep(1 * time.Second) cancel() ``` +[[play](https://go.dev/play/p/mz32VMK2nqe)] + ### Synchronize Wraps the underlying callback in a mutex. It receives an optional mutex. @@ -1493,6 +2226,22 @@ ch := lo.Async2(func() (int, string) { // chan lo.Tuple2[int, string] ({42, "Hello"}) ``` +### Validate + +Helper function that creates an error when a condition is not met. + +```go +slice := []string{"a"} +val := lo.Validate(len(slice) == 0, "Slice should be empty but contains %v", slice) +// error("Slice should be empty but contains [a]") + +slice := []string{} +val := lo.Validate(len(slice) == 0, "Slice should be empty but contains %v", slice) +// nil +``` + +[[play](https://go.dev/play/p/vPyh51XpCBt)] + ### Must Wraps a function call to panics if second argument is `error` or `false`, returns the value otherwise. @@ -1505,8 +2254,12 @@ val := lo.Must(time.Parse("2006-01-02", "bad-value")) // panics ``` +[[play](https://go.dev/play/p/TMoWrRp3DyC)] + ### Must{0->6} -Must* has the same behavior than Must, but returns multiple values. + +Must\* has the same behavior than Must, but returns multiple values. + ```go func example0() (error) func example1() (int, error) @@ -1525,7 +2278,8 @@ val1, val2, val3, val4, val5 := lo.Must5(example5()) val1, val2, val3, val4, val5, val6 := lo.Must6(example6()) ``` -You can wrap functions like `func (...) (..., ok bool)`. +You can wrap functions like `func (...) (..., ok bool)`. + ```go // math.Signbit(float64) bool lo.Must0(math.Signbit(v)) @@ -1534,7 +2288,23 @@ lo.Must0(math.Signbit(v)) before, after := lo.Must2(bytes.Cut(s, sep)) ``` -## Try +You can give context to the panic message by adding some printf-like arguments. + +```go +val, ok := lo.Find(myString, func(i string) bool { + return i == requiredChar +}) +lo.Must0(ok, "'%s' must always contain '%s'", myString, requiredChar) + +list := []int{0, 1, 2} +item := 5 +lo.Must0(lo.Contains[int](list, item), "'%s' must always contain '%s'", list, item) +... +``` + +[[play](https://go.dev/play/p/TMoWrRp3DyC)] + +### Try Calls the function and return false in case of error and on panic. @@ -1556,7 +2326,9 @@ ok := lo.Try(func() error { // false ``` -## Try{0->6} +[[play](https://go.dev/play/p/mTyyWUvn9u4)] + +### Try{0->6} The same behavior than `Try`, but callback returns 2 variables. @@ -1568,7 +2340,52 @@ ok := lo.Try2(func() (string, error) { // false ``` -## TryWithErrorValue +[[play](https://go.dev/play/p/mTyyWUvn9u4)] + +### TryOr + +Calls the function and return a default value in case of error and on panic. + +```go +str, ok := lo.TryOr(func() (string, error) { + panic("error") + return "hello", nil +}, "world") +// world +// false + +ok := lo.TryOr(func() error { + return "hello", nil +}, "world") +// hello +// true + +ok := lo.TryOr(func() error { + return "hello", fmt.Errorf("error") +}, "world") +// world +// false +``` + +[[play](https://go.dev/play/p/B4F7Wg2Zh9X)] + +### TryOr{0->6} + +The same behavior than `TryOr`, but callback returns 2 variables. + +```go +str, nbr, ok := lo.TryOr2(func() (string, int, error) { + panic("error") + return "hello", 42, nil +}, "world", 21) +// world +// 21 +// false +``` + +[[play](https://go.dev/play/p/B4F7Wg2Zh9X)] + +### TryWithErrorValue The same behavior than `Try`, but also returns value passed to panic. @@ -1580,7 +2397,9 @@ err, ok := lo.TryWithErrorValue(func() error { // "error", false ``` -## TryCatch +[[play](https://go.dev/play/p/Kc7afQIT2Fs)] + +### TryCatch The same behavior than `Try`, but calls the catch function in case of error. @@ -1597,7 +2416,9 @@ ok := lo.TryCatch(func() error { // caught == true ``` -## TryCatchWithErrorValue +[[play](https://go.dev/play/p/PnOON-EqBiU)] + +### TryCatchWithErrorValue The same behavior than `TryWithErrorValue`, but calls the catch function in case of error. @@ -1614,6 +2435,33 @@ ok := lo.TryCatchWithErrorValue(func() error { // caught == true ``` +[[play](https://go.dev/play/p/8Pc9gwX_GZO)] + +### ErrorsAs + +A shortcut for: + +```go +err := doSomething() + +var rateLimitErr *RateLimitError +if ok := errors.As(err, &rateLimitErr); ok { + // retry later +} +``` + +1 line `lo` helper: + +```go +err := doSomething() + +if rateLimitErr, ok := lo.ErrorsAs[*RateLimitError](err); ok { + // retry later +} +``` + +[[play](https://go.dev/play/p/8wk5rH8UfrE)] + ## 🛩 Benchmark We executed a simple benchmark with the a dead-simple `lo.Map` loop: @@ -1645,10 +2493,10 @@ PASS ok github.com/samber/lo 6.657s ``` -- `lo.Map` is way faster (x7) than `go-funk`, a relection-based Map implementation. +- `lo.Map` is way faster (x7) than `go-funk`, a reflection-based Map implementation. - `lo.Map` have the same allocation profile than `for`. - `lo.Map` is 4% slower than `for`. -- `lop.Map` is slower than `lo.Map` because it implies more memory allocation and locks. `lop.Map` will be usefull for long-running callbacks, such as i/o bound processing. +- `lop.Map` is slower than `lo.Map` because it implies more memory allocation and locks. `lop.Map` will be useful for long-running callbacks, such as i/o bound processing. - `for` beats other implementations for memory and CPU. ## 🤝 Contributing @@ -1659,14 +2507,6 @@ ok github.com/samber/lo 6.657s Don't hesitate ;) -### Install go 1.18 - -```bash -make go1.18beta1 -``` - -If your OS currently not default to Go 1.18, replace `BIN=go` by `BIN=go1.18beta1` in the Makefile. - ### With Docker ```bash diff --git a/vendor/github.com/samber/lo/channel.go b/vendor/github.com/samber/lo/channel.go new file mode 100644 index 0000000..ccbd360 --- /dev/null +++ b/vendor/github.com/samber/lo/channel.go @@ -0,0 +1,228 @@ +package lo + +import ( + "math/rand" + "time" +) + +type DispatchingStrategy[T any] func(msg T, index uint64, channels []<-chan T) int + +// ChannelDispatcher distributes messages from input channels into N child channels. +// Close events are propagated to children. +// Underlying channels can have a fixed buffer capacity or be unbuffered when cap is 0. +func ChannelDispatcher[T any](stream <-chan T, count int, channelBufferCap int, strategy DispatchingStrategy[T]) []<-chan T { + children := createChannels[T](count, channelBufferCap) + + roChildren := channelsToReadOnly(children) + + go func() { + // propagate channel closing to children + defer closeChannels(children) + + var i uint64 = 0 + + for { + msg, ok := <-stream + if !ok { + return + } + + destination := strategy(msg, i, roChildren) % count + children[destination] <- msg + + i++ + } + }() + + return roChildren +} + +func createChannels[T any](count int, channelBufferCap int) []chan T { + children := make([]chan T, 0, count) + + for i := 0; i < count; i++ { + children = append(children, make(chan T, channelBufferCap)) + } + + return children +} + +func channelsToReadOnly[T any](children []chan T) []<-chan T { + roChildren := make([]<-chan T, 0, len(children)) + + for i := range children { + roChildren = append(roChildren, children[i]) + } + + return roChildren +} + +func closeChannels[T any](children []chan T) { + for i := 0; i < len(children); i++ { + close(children[i]) + } +} + +func channelIsNotFull[T any](ch <-chan T) bool { + return cap(ch) == 0 || len(ch) < cap(ch) +} + +// DispatchingStrategyRoundRobin distributes messages in a rotating sequential manner. +// If the channel capacity is exceeded, the next channel will be selected and so on. +func DispatchingStrategyRoundRobin[T any](msg T, index uint64, channels []<-chan T) int { + for { + i := int(index % uint64(len(channels))) + if channelIsNotFull(channels[i]) { + return i + } + + index++ + time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥 + } +} + +// DispatchingStrategyRandom distributes messages in a random manner. +// If the channel capacity is exceeded, another random channel will be selected and so on. +func DispatchingStrategyRandom[T any](msg T, index uint64, channels []<-chan T) int { + for { + i := rand.Intn(len(channels)) + if channelIsNotFull(channels[i]) { + return i + } + + time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥 + } +} + +// DispatchingStrategyRandom distributes messages in a weighted manner. +// If the channel capacity is exceeded, another random channel will be selected and so on. +func DispatchingStrategyWeightedRandom[T any](weights []int) DispatchingStrategy[T] { + seq := []int{} + + for i := 0; i < len(weights); i++ { + for j := 0; j < weights[i]; j++ { + seq = append(seq, i) + } + } + + return func(msg T, index uint64, channels []<-chan T) int { + for { + i := seq[rand.Intn(len(seq))] + if channelIsNotFull(channels[i]) { + return i + } + + time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥 + } + } +} + +// DispatchingStrategyFirst distributes messages in the first non-full channel. +// If the capacity of the first channel is exceeded, the second channel will be selected and so on. +func DispatchingStrategyFirst[T any](msg T, index uint64, channels []<-chan T) int { + for { + for i := range channels { + if channelIsNotFull(channels[i]) { + return i + } + } + + time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥 + } +} + +// DispatchingStrategyLeast distributes messages in the emptiest channel. +func DispatchingStrategyLeast[T any](msg T, index uint64, channels []<-chan T) int { + seq := Range(len(channels)) + + return MinBy(seq, func(item int, min int) bool { + return len(channels[item]) < len(channels[min]) + }) +} + +// DispatchingStrategyMost distributes messages in the fulliest channel. +// If the channel capacity is exceeded, the next channel will be selected and so on. +func DispatchingStrategyMost[T any](msg T, index uint64, channels []<-chan T) int { + seq := Range(len(channels)) + + return MaxBy(seq, func(item int, max int) bool { + return len(channels[item]) > len(channels[max]) && channelIsNotFull(channels[item]) + }) +} + +// SliceToChannel returns a read-only channels of collection elements. +func SliceToChannel[T any](bufferSize int, collection []T) <-chan T { + ch := make(chan T, bufferSize) + + go func() { + for _, item := range collection { + ch <- item + } + + close(ch) + }() + + return ch +} + +// Generator implements the generator design pattern. +func Generator[T any](bufferSize int, generator func(yield func(T))) <-chan T { + ch := make(chan T, bufferSize) + + go func() { + // WARNING: infinite loop + generator(func(t T) { + ch <- t + }) + + close(ch) + }() + + return ch +} + +// Batch creates a slice of n elements from a channel. Returns the slice and the slice length. +// @TODO: we should probaby provide an helper that reuse the same buffer. +func Batch[T any](ch <-chan T, size int) (collection []T, length int, readTime time.Duration, ok bool) { + buffer := make([]T, 0, size) + index := 0 + now := time.Now() + + for ; index < size; index++ { + item, ok := <-ch + if !ok { + return buffer, index, time.Since(now), false + } + + buffer = append(buffer, item) + } + + return buffer, index, time.Since(now), true +} + +// BatchWithTimeout creates a slice of n elements from a channel, with timeout. Returns the slice and the slice length. +// @TODO: we should probaby provide an helper that reuse the same buffer. +func BatchWithTimeout[T any](ch <-chan T, size int, timeout time.Duration) (collection []T, length int, readTime time.Duration, ok bool) { + expire := time.NewTimer(timeout) + defer expire.Stop() + + buffer := make([]T, 0, size) + index := 0 + now := time.Now() + + for ; index < size; index++ { + select { + case item, ok := <-ch: + if !ok { + return buffer, index, time.Since(now), false + } + + buffer = append(buffer, item) + + case <-expire.C: + return buffer, index, time.Since(now), true + } + } + + return buffer, index, time.Since(now), true +} diff --git a/vendor/github.com/samber/lo/condition.go b/vendor/github.com/samber/lo/condition.go index 6fd31f1..1d4e75d 100644 --- a/vendor/github.com/samber/lo/condition.go +++ b/vendor/github.com/samber/lo/condition.go @@ -1,6 +1,7 @@ package lo // Ternary is a 1 line if/else statement. +// Play: https://go.dev/play/p/t-D7WBL44h2 func Ternary[T any](condition bool, ifOutput T, elseOutput T) T { if condition { return ifOutput @@ -9,12 +10,23 @@ func Ternary[T any](condition bool, ifOutput T, elseOutput T) T { return elseOutput } +// TernaryF is a 1 line if/else statement whose options are functions +// Play: https://go.dev/play/p/AO4VW20JoqM +func TernaryF[T any](condition bool, ifFunc func() T, elseFunc func() T) T { + if condition { + return ifFunc() + } + + return elseFunc() +} + type ifElse[T any] struct { result T done bool } // If. +// Play: https://go.dev/play/p/WSw3ApMxhyW func If[T any](condition bool, result T) *ifElse[T] { if condition { return &ifElse[T]{result, true} @@ -25,6 +37,7 @@ func If[T any](condition bool, result T) *ifElse[T] { } // IfF. +// Play: https://go.dev/play/p/WSw3ApMxhyW func IfF[T any](condition bool, resultF func() T) *ifElse[T] { if condition { return &ifElse[T]{resultF(), true} @@ -35,6 +48,7 @@ func IfF[T any](condition bool, resultF func() T) *ifElse[T] { } // ElseIf. +// Play: https://go.dev/play/p/WSw3ApMxhyW func (i *ifElse[T]) ElseIf(condition bool, result T) *ifElse[T] { if !i.done && condition { i.result = result @@ -45,6 +59,7 @@ func (i *ifElse[T]) ElseIf(condition bool, result T) *ifElse[T] { } // ElseIfF. +// Play: https://go.dev/play/p/WSw3ApMxhyW func (i *ifElse[T]) ElseIfF(condition bool, resultF func() T) *ifElse[T] { if !i.done && condition { i.result = resultF() @@ -55,6 +70,7 @@ func (i *ifElse[T]) ElseIfF(condition bool, resultF func() T) *ifElse[T] { } // Else. +// Play: https://go.dev/play/p/WSw3ApMxhyW func (i *ifElse[T]) Else(result T) T { if i.done { return i.result @@ -64,6 +80,7 @@ func (i *ifElse[T]) Else(result T) T { } // ElseF. +// Play: https://go.dev/play/p/WSw3ApMxhyW func (i *ifElse[T]) ElseF(resultF func() T) T { if i.done { return i.result @@ -79,6 +96,7 @@ type switchCase[T comparable, R any] struct { } // Switch is a pure functional switch/case/default statement. +// Play: https://go.dev/play/p/TGbKUMAeRUd func Switch[T comparable, R any](predicate T) *switchCase[T, R] { var result R @@ -90,6 +108,7 @@ func Switch[T comparable, R any](predicate T) *switchCase[T, R] { } // Case. +// Play: https://go.dev/play/p/TGbKUMAeRUd func (s *switchCase[T, R]) Case(val T, result R) *switchCase[T, R] { if !s.done && s.predicate == val { s.result = result @@ -100,6 +119,7 @@ func (s *switchCase[T, R]) Case(val T, result R) *switchCase[T, R] { } // CaseF. +// Play: https://go.dev/play/p/TGbKUMAeRUd func (s *switchCase[T, R]) CaseF(val T, cb func() R) *switchCase[T, R] { if !s.done && s.predicate == val { s.result = cb() @@ -110,6 +130,7 @@ func (s *switchCase[T, R]) CaseF(val T, cb func() R) *switchCase[T, R] { } // Default. +// Play: https://go.dev/play/p/TGbKUMAeRUd func (s *switchCase[T, R]) Default(result R) R { if !s.done { s.result = result @@ -119,6 +140,7 @@ func (s *switchCase[T, R]) Default(result R) R { } // DefaultF. +// Play: https://go.dev/play/p/TGbKUMAeRUd func (s *switchCase[T, R]) DefaultF(cb func() R) R { if !s.done { s.result = cb() diff --git a/vendor/github.com/samber/lo/docker-compose.yml b/vendor/github.com/samber/lo/docker-compose.yml index c6f3f65..511e85f 100644 --- a/vendor/github.com/samber/lo/docker-compose.yml +++ b/vendor/github.com/samber/lo/docker-compose.yml @@ -2,8 +2,8 @@ version: '3' services: dev: - build: . + image: golang:1.18-bullseye volumes: - ./:/go/src/github.com/samber/lo working_dir: /go/src/github.com/samber/lo - command: bash -c 'make tools ; make watch-test' + command: make watch-test diff --git a/vendor/github.com/samber/lo/errors.go b/vendor/github.com/samber/lo/errors.go index 0b50ad4..0a4cb5b 100644 --- a/vendor/github.com/samber/lo/errors.go +++ b/vendor/github.com/samber/lo/errors.go @@ -1,62 +1,115 @@ package lo +import ( + "errors" + "fmt" + "reflect" +) + +// Validate is a helper that creates an error when a condition is not met. +// Play: https://go.dev/play/p/vPyh51XpCBt +func Validate(ok bool, format string, args ...any) error { + if !ok { + return fmt.Errorf(fmt.Sprintf(format, args...)) + } + return nil +} + +func messageFromMsgAndArgs(msgAndArgs ...interface{}) string { + if len(msgAndArgs) == 1 { + if msgAsStr, ok := msgAndArgs[0].(string); ok { + return msgAsStr + } + return fmt.Sprintf("%+v", msgAndArgs[0]) + } + if len(msgAndArgs) > 1 { + return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) + } + return "" +} + // must panics if err is error or false. -func must(err any) { - b, isBool := err.(bool) - if isBool && !b { - panic("not ok") +func must(err any, messageArgs ...interface{}) { + if err == nil { + return } - e, isError := err.(error) - if isError { - panic(e) + switch e := err.(type) { + case bool: + if !e { + message := messageFromMsgAndArgs(messageArgs...) + if message == "" { + message = "not ok" + } + + panic(message) + } + + case error: + message := messageFromMsgAndArgs(messageArgs...) + if message != "" { + panic(message + ": " + e.Error()) + } else { + panic(e.Error()) + } + + default: + panic("must: invalid err type '" + reflect.TypeOf(err).Name() + "', should either be a bool or an error") } } // Must is a helper that wraps a call to a function returning a value and an error // and panics if err is error or false. -func Must[T any](val T, err any) T { - must(err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must[T any](val T, err any, messageArgs ...interface{}) T { + must(err, messageArgs...) return val } // Must0 has the same behavior than Must, but callback returns no variable. -func Must0(err any) { - must(err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must0(err any, messageArgs ...interface{}) { + must(err, messageArgs...) } // Must1 is an alias to Must -func Must1[T any](val T, err any) T { - return Must(val, err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must1[T any](val T, err any, messageArgs ...interface{}) T { + return Must(val, err, messageArgs...) } // Must2 has the same behavior than Must, but callback returns 2 variables. -func Must2[T1 any, T2 any](val1 T1, val2 T2, err any) (T1, T2) { - must(err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must2[T1 any, T2 any](val1 T1, val2 T2, err any, messageArgs ...interface{}) (T1, T2) { + must(err, messageArgs...) return val1, val2 } // Must3 has the same behavior than Must, but callback returns 3 variables. -func Must3[T1 any, T2 any, T3 any](val1 T1, val2 T2, val3 T3, err any) (T1, T2, T3) { - must(err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must3[T1 any, T2 any, T3 any](val1 T1, val2 T2, val3 T3, err any, messageArgs ...interface{}) (T1, T2, T3) { + must(err, messageArgs...) return val1, val2, val3 } // Must4 has the same behavior than Must, but callback returns 4 variables. -func Must4[T1 any, T2 any, T3 any, T4 any](val1 T1, val2 T2, val3 T3, val4 T4, err any) (T1, T2, T3, T4) { - must(err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must4[T1 any, T2 any, T3 any, T4 any](val1 T1, val2 T2, val3 T3, val4 T4, err any, messageArgs ...interface{}) (T1, T2, T3, T4) { + must(err, messageArgs...) return val1, val2, val3, val4 } // Must5 has the same behavior than Must, but callback returns 5 variables. -func Must5[T1 any, T2 any, T3 any, T4 any, T5 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, err any) (T1, T2, T3, T4, T5) { - must(err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must5[T1 any, T2 any, T3 any, T4 any, T5 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, err any, messageArgs ...interface{}) (T1, T2, T3, T4, T5) { + must(err, messageArgs...) return val1, val2, val3, val4, val5 } // Must6 has the same behavior than Must, but callback returns 6 variables. -func Must6[T1 any, T2 any, T3 any, T4 any, T5 any, T6 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, val6 T6, err any) (T1, T2, T3, T4, T5, T6) { - must(err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must6[T1 any, T2 any, T3 any, T4 any, T5 any, T6 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, val6 T6, err any, messageArgs ...interface{}) (T1, T2, T3, T4, T5, T6) { + must(err, messageArgs...) return val1, val2, val3, val4, val5, val6 } @@ -79,6 +132,7 @@ func Try(callback func() error) (ok bool) { } // Try0 has the same behavior than Try, but callback returns no variable. +// Play: https://go.dev/play/p/mTyyWUvn9u4 func Try0(callback func()) bool { return Try(func() error { callback() @@ -87,11 +141,13 @@ func Try0(callback func()) bool { } // Try1 is an alias to Try. -func Try1[T any](callback func() error) bool { +// Play: https://go.dev/play/p/mTyyWUvn9u4 +func Try1(callback func() error) bool { return Try(callback) } // Try2 has the same behavior than Try, but callback returns 2 variables. +// Play: https://go.dev/play/p/mTyyWUvn9u4 func Try2[T any](callback func() (T, error)) bool { return Try(func() error { _, err := callback() @@ -100,6 +156,7 @@ func Try2[T any](callback func() (T, error)) bool { } // Try3 has the same behavior than Try, but callback returns 3 variables. +// Play: https://go.dev/play/p/mTyyWUvn9u4 func Try3[T, R any](callback func() (T, R, error)) bool { return Try(func() error { _, _, err := callback() @@ -108,6 +165,7 @@ func Try3[T, R any](callback func() (T, R, error)) bool { } // Try4 has the same behavior than Try, but callback returns 4 variables. +// Play: https://go.dev/play/p/mTyyWUvn9u4 func Try4[T, R, S any](callback func() (T, R, S, error)) bool { return Try(func() error { _, _, _, err := callback() @@ -116,6 +174,7 @@ func Try4[T, R, S any](callback func() (T, R, S, error)) bool { } // Try5 has the same behavior than Try, but callback returns 5 variables. +// Play: https://go.dev/play/p/mTyyWUvn9u4 func Try5[T, R, S, Q any](callback func() (T, R, S, Q, error)) bool { return Try(func() error { _, _, _, _, err := callback() @@ -124,6 +183,7 @@ func Try5[T, R, S, Q any](callback func() (T, R, S, Q, error)) bool { } // Try6 has the same behavior than Try, but callback returns 6 variables. +// Play: https://go.dev/play/p/mTyyWUvn9u4 func Try6[T, R, S, Q, U any](callback func() (T, R, S, Q, U, error)) bool { return Try(func() error { _, _, _, _, _, err := callback() @@ -131,7 +191,125 @@ func Try6[T, R, S, Q, U any](callback func() (T, R, S, Q, U, error)) bool { }) } +// TryOr has the same behavior than Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr[A any](callback func() (A, error), fallbackA A) (A, bool) { + return TryOr1(callback, fallbackA) +} + +// TryOr1 has the same behavior than Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr1[A any](callback func() (A, error), fallbackA A) (A, bool) { + ok := false + + Try0(func() { + a, err := callback() + if err == nil { + fallbackA = a + ok = true + } + }) + + return fallbackA, ok +} + +// TryOr2 has the same behavior than Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr2[A any, B any](callback func() (A, B, error), fallbackA A, fallbackB B) (A, B, bool) { + ok := false + + Try0(func() { + a, b, err := callback() + if err == nil { + fallbackA = a + fallbackB = b + ok = true + } + }) + + return fallbackA, fallbackB, ok +} + +// TryOr3 has the same behavior than Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr3[A any, B any, C any](callback func() (A, B, C, error), fallbackA A, fallbackB B, fallbackC C) (A, B, C, bool) { + ok := false + + Try0(func() { + a, b, c, err := callback() + if err == nil { + fallbackA = a + fallbackB = b + fallbackC = c + ok = true + } + }) + + return fallbackA, fallbackB, fallbackC, ok +} + +// TryOr4 has the same behavior than Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr4[A any, B any, C any, D any](callback func() (A, B, C, D, error), fallbackA A, fallbackB B, fallbackC C, fallbackD D) (A, B, C, D, bool) { + ok := false + + Try0(func() { + a, b, c, d, err := callback() + if err == nil { + fallbackA = a + fallbackB = b + fallbackC = c + fallbackD = d + ok = true + } + }) + + return fallbackA, fallbackB, fallbackC, fallbackD, ok +} + +// TryOr5 has the same behavior than Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr5[A any, B any, C any, D any, E any](callback func() (A, B, C, D, E, error), fallbackA A, fallbackB B, fallbackC C, fallbackD D, fallbackE E) (A, B, C, D, E, bool) { + ok := false + + Try0(func() { + a, b, c, d, e, err := callback() + if err == nil { + fallbackA = a + fallbackB = b + fallbackC = c + fallbackD = d + fallbackE = e + ok = true + } + }) + + return fallbackA, fallbackB, fallbackC, fallbackD, fallbackE, ok +} + +// TryOr6 has the same behavior than Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr6[A any, B any, C any, D any, E any, F any](callback func() (A, B, C, D, E, F, error), fallbackA A, fallbackB B, fallbackC C, fallbackD D, fallbackE E, fallbackF F) (A, B, C, D, E, F, bool) { + ok := false + + Try0(func() { + a, b, c, d, e, f, err := callback() + if err == nil { + fallbackA = a + fallbackB = b + fallbackC = c + fallbackD = d + fallbackE = e + fallbackF = f + ok = true + } + }) + + return fallbackA, fallbackB, fallbackC, fallbackD, fallbackE, fallbackF, ok +} + // TryWithErrorValue has the same behavior than Try, but also returns value passed to panic. +// Play: https://go.dev/play/p/Kc7afQIT2Fs func TryWithErrorValue(callback func() error) (errorValue any, ok bool) { ok = true @@ -152,6 +330,7 @@ func TryWithErrorValue(callback func() error) (errorValue any, ok bool) { } // TryCatch has the same behavior than Try, but calls the catch function in case of error. +// Play: https://go.dev/play/p/PnOON-EqBiU func TryCatch(callback func() error, catch func()) { if !Try(callback) { catch() @@ -159,8 +338,17 @@ func TryCatch(callback func() error, catch func()) { } // TryCatchWithErrorValue has the same behavior than TryWithErrorValue, but calls the catch function in case of error. +// Play: https://go.dev/play/p/8Pc9gwX_GZO func TryCatchWithErrorValue(callback func() error, catch func(any)) { if err, ok := TryWithErrorValue(callback); !ok { catch(err) } } + +// ErrorsAs is a shortcut for errors.As(err, &&T). +// Play: https://go.dev/play/p/8wk5rH8UfrE +func ErrorsAs[T error](err error) (T, bool) { + var t T + ok := errors.As(err, &t) + return t, ok +} diff --git a/vendor/github.com/samber/lo/find.go b/vendor/github.com/samber/lo/find.go index 8f24e23..e40bfcc 100644 --- a/vendor/github.com/samber/lo/find.go +++ b/vendor/github.com/samber/lo/find.go @@ -2,7 +2,6 @@ package lo import ( "fmt" - "math" "math/rand" "golang.org/x/exp/constraints" @@ -87,6 +86,140 @@ func FindOrElse[T any](collection []T, fallback T, predicate func(T) bool) T { return fallback } +// FindKey returns the key of the first value matching. +func FindKey[K comparable, V comparable](object map[K]V, value V) (K, bool) { + for k, v := range object { + if v == value { + return k, true + } + } + + return Empty[K](), false +} + +// FindKeyBy returns the key of the first element predicate returns truthy for. +func FindKeyBy[K comparable, V any](object map[K]V, predicate func(K, V) bool) (K, bool) { + for k, v := range object { + if predicate(k, v) { + return k, true + } + } + + return Empty[K](), false +} + +// FindUniques returns a slice with all the unique elements of the collection. +// The order of result values is determined by the order they occur in the collection. +func FindUniques[T comparable](collection []T) []T { + isDupl := make(map[T]bool, len(collection)) + + for _, item := range collection { + duplicated, ok := isDupl[item] + if !ok { + isDupl[item] = false + } else if !duplicated { + isDupl[item] = true + } + } + + result := make([]T, 0, len(collection)-len(isDupl)) + + for _, item := range collection { + if duplicated := isDupl[item]; !duplicated { + result = append(result, item) + } + } + + return result +} + +// FindUniquesBy returns a slice with all the unique elements of the collection. +// The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is +// invoked for each element in array to generate the criterion by which uniqueness is computed. +func FindUniquesBy[T any, U comparable](collection []T, iteratee func(T) U) []T { + isDupl := make(map[U]bool, len(collection)) + + for _, item := range collection { + key := iteratee(item) + + duplicated, ok := isDupl[key] + if !ok { + isDupl[key] = false + } else if !duplicated { + isDupl[key] = true + } + } + + result := make([]T, 0, len(collection)-len(isDupl)) + + for _, item := range collection { + key := iteratee(item) + + if duplicated := isDupl[key]; !duplicated { + result = append(result, item) + } + } + + return result +} + +// FindDuplicates returns a slice with the first occurence of each duplicated elements of the collection. +// The order of result values is determined by the order they occur in the collection. +func FindDuplicates[T comparable](collection []T) []T { + isDupl := make(map[T]bool, len(collection)) + + for _, item := range collection { + duplicated, ok := isDupl[item] + if !ok { + isDupl[item] = false + } else if !duplicated { + isDupl[item] = true + } + } + + result := make([]T, 0, len(collection)-len(isDupl)) + + for _, item := range collection { + if duplicated := isDupl[item]; duplicated { + result = append(result, item) + isDupl[item] = false + } + } + + return result +} + +// FindDuplicatesBy returns a slice with the first occurence of each duplicated elements of the collection. +// The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is +// invoked for each element in array to generate the criterion by which uniqueness is computed. +func FindDuplicatesBy[T any, U comparable](collection []T, iteratee func(T) U) []T { + isDupl := make(map[U]bool, len(collection)) + + for _, item := range collection { + key := iteratee(item) + + duplicated, ok := isDupl[key] + if !ok { + isDupl[key] = false + } else if !duplicated { + isDupl[key] = true + } + } + + result := make([]T, 0, len(collection)-len(isDupl)) + + for _, item := range collection { + key := iteratee(item) + + if duplicated := isDupl[key]; duplicated { + result = append(result, item) + isDupl[key] = false + } + } + + return result +} + // Min search the minimum value of a collection. func Min[T constraints.Ordered](collection []T) T { var min T @@ -187,19 +320,18 @@ func Last[T any](collection []T) (T, error) { // Nth returns the element at index `nth` of collection. If `nth` is negative, the nth element // from the end is returned. An error is returned when nth is out of slice bounds. -func Nth[T any](collection []T, nth int) (T, error) { - if int(math.Abs(float64(nth))) >= len(collection) { +func Nth[T any, N constraints.Integer](collection []T, nth N) (T, error) { + n := int(nth) + l := len(collection) + if n >= l || -n > l { var t T - return t, fmt.Errorf("nth: %d out of slice bounds", nth) + return t, fmt.Errorf("nth: %d out of slice bounds", n) } - length := len(collection) - - if nth >= 0 { - return collection[nth], nil + if n >= 0 { + return collection[n], nil } - - return collection[length+nth], nil + return collection[l+n], nil } // Sample returns a random item from collection. @@ -216,11 +348,7 @@ func Sample[T any](collection []T) T { func Samples[T any](collection []T, count int) []T { size := len(collection) - // put values into a map, for faster deletion - cOpy := make([]T, 0, size) - for _, v := range collection { - cOpy = append(cOpy, v) - } + cOpy := append([]T{}, collection...) results := []T{} diff --git a/vendor/github.com/samber/lo/func.go b/vendor/github.com/samber/lo/func.go new file mode 100644 index 0000000..90afc9a --- /dev/null +++ b/vendor/github.com/samber/lo/func.go @@ -0,0 +1,8 @@ +package lo + +// Partial returns new function that, when called, has its first argument set to the provided value. +func Partial[T1, T2, R any](f func(T1, T2) R, arg1 T1) func(T2) R { + return func(t2 T2) R { + return f(arg1, t2) + } +} diff --git a/vendor/github.com/samber/lo/intersect.go b/vendor/github.com/samber/lo/intersect.go index eac1639..169cc2c 100644 --- a/vendor/github.com/samber/lo/intersect.go +++ b/vendor/github.com/samber/lo/intersect.go @@ -175,3 +175,28 @@ func Union[T comparable](list1 []T, list2 []T) []T { return result } + +// Without returns slice excluding all given values. +func Without[T comparable](collection []T, exclude ...T) []T { + result := make([]T, 0, len(collection)) + for _, e := range collection { + if !Contains(exclude, e) { + result = append(result, e) + } + } + return result +} + +// WithoutEmpty returns slice excluding empty values. +func WithoutEmpty[T comparable](collection []T) []T { + var empty T + + result := make([]T, 0, len(collection)) + for _, e := range collection { + if e != empty { + result = append(result, e) + } + } + + return result +} diff --git a/vendor/github.com/samber/lo/map.go b/vendor/github.com/samber/lo/map.go index 294421c..7915e37 100644 --- a/vendor/github.com/samber/lo/map.go +++ b/vendor/github.com/samber/lo/map.go @@ -1,6 +1,7 @@ package lo // Keys creates an array of the map keys. +// Play: https://go.dev/play/p/Uu11fHASqrU func Keys[K comparable, V any](in map[K]V) []K { result := make([]K, 0, len(in)) @@ -12,6 +13,7 @@ func Keys[K comparable, V any](in map[K]V) []K { } // Values creates an array of the map values. +// Play: https://go.dev/play/p/nnRTQkzQfF6 func Values[K comparable, V any](in map[K]V) []V { result := make([]V, 0, len(in)) @@ -23,6 +25,7 @@ func Values[K comparable, V any](in map[K]V) []V { } // PickBy returns same map type filtered by given predicate. +// Play: https://go.dev/play/p/kdg8GR_QMmf func PickBy[K comparable, V any](in map[K]V, predicate func(K, V) bool) map[K]V { r := map[K]V{} for k, v := range in { @@ -34,6 +37,7 @@ func PickBy[K comparable, V any](in map[K]V, predicate func(K, V) bool) map[K]V } // PickByKeys returns same map type filtered by given keys. +// Play: https://go.dev/play/p/R1imbuci9qU func PickByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V { r := map[K]V{} for k, v := range in { @@ -45,6 +49,7 @@ func PickByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V { } // PickByValues returns same map type filtered by given values. +// Play: https://go.dev/play/p/1zdzSvbfsJc func PickByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V { r := map[K]V{} for k, v := range in { @@ -55,7 +60,8 @@ func PickByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V { return r } -// PickBy returns same map type filtered by given predicate. +// OmitBy returns same map type filtered by given predicate. +// Play: https://go.dev/play/p/EtBsR43bdsd func OmitBy[K comparable, V any](in map[K]V, predicate func(K, V) bool) map[K]V { r := map[K]V{} for k, v := range in { @@ -67,6 +73,7 @@ func OmitBy[K comparable, V any](in map[K]V, predicate func(K, V) bool) map[K]V } // OmitByKeys returns same map type filtered by given keys. +// Play: https://go.dev/play/p/t1QjCrs-ysk func OmitByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V { r := map[K]V{} for k, v := range in { @@ -78,6 +85,7 @@ func OmitByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V { } // OmitByValues returns same map type filtered by given values. +// Play: https://go.dev/play/p/9UYZi-hrs8j func OmitByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V { r := map[K]V{} for k, v := range in { @@ -89,6 +97,7 @@ func OmitByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V { } // Entries transforms a map into array of key/value pairs. +// Play: func Entries[K comparable, V any](in map[K]V) []Entry[K, V] { entries := make([]Entry[K, V], 0, len(in)) @@ -102,7 +111,15 @@ func Entries[K comparable, V any](in map[K]V) []Entry[K, V] { return entries } +// ToPairs transforms a map into array of key/value pairs. +// Alias of Entries(). +// Play: https://go.dev/play/p/3Dhgx46gawJ +func ToPairs[K comparable, V any](in map[K]V) []Entry[K, V] { + return Entries(in) +} + // FromEntries transforms an array of key/value pairs into a map. +// Play: https://go.dev/play/p/oIr5KHFGCEN func FromEntries[K comparable, V any](entries []Entry[K, V]) map[K]V { out := map[K]V{} @@ -113,9 +130,17 @@ func FromEntries[K comparable, V any](entries []Entry[K, V]) map[K]V { return out } +// FromPairs transforms an array of key/value pairs into a map. +// Alias of FromEntries(). +// Play: https://go.dev/play/p/oIr5KHFGCEN +func FromPairs[K comparable, V any](entries []Entry[K, V]) map[K]V { + return FromEntries(entries) +} + // Invert creates a map composed of the inverted keys and values. If map // contains duplicate values, subsequent values overwrite property assignments // of previous values. +// Play: https://go.dev/play/p/rFQ4rak6iA1 func Invert[K comparable, V comparable](in map[K]V) map[V]K { out := map[V]K{} @@ -127,6 +152,7 @@ func Invert[K comparable, V comparable](in map[K]V) map[V]K { } // Assign merges multiple maps from left to right. +// Play: https://go.dev/play/p/VhwfJOyxf5o func Assign[K comparable, V any](maps ...map[K]V) map[K]V { out := map[K]V{} @@ -140,6 +166,7 @@ func Assign[K comparable, V any](maps ...map[K]V) map[K]V { } // MapKeys manipulates a map keys and transforms it to a map of another type. +// Play: https://go.dev/play/p/9_4WPIqOetJ func MapKeys[K comparable, V any, R comparable](in map[K]V, iteratee func(V, K) R) map[R]V { result := map[R]V{} @@ -151,6 +178,7 @@ func MapKeys[K comparable, V any, R comparable](in map[K]V, iteratee func(V, K) } // MapValues manipulates a map values and transforms it to a map of another type. +// Play: https://go.dev/play/p/T_8xAfvcf0W func MapValues[K comparable, V any, R any](in map[K]V, iteratee func(V, K) R) map[K]R { result := map[K]R{} @@ -160,3 +188,15 @@ func MapValues[K comparable, V any, R any](in map[K]V, iteratee func(V, K) R) ma return result } + +// MapToSlice transforms a map into a slice based on specific iteratee +// Play: https://go.dev/play/p/ZuiCZpDt6LD +func MapToSlice[K comparable, V any, R any](in map[K]V, iteratee func(K, V) R) []R { + result := make([]R, 0, len(in)) + + for k, v := range in { + result = append(result, iteratee(k, v)) + } + + return result +} diff --git a/vendor/github.com/samber/lo/math.go b/vendor/github.com/samber/lo/math.go index 1ce1ebe..30e7e9e 100644 --- a/vendor/github.com/samber/lo/math.go +++ b/vendor/github.com/samber/lo/math.go @@ -3,6 +3,7 @@ package lo import "golang.org/x/exp/constraints" // Range creates an array of numbers (positive and/or negative) with given length. +// Play: https://go.dev/play/p/0r6VimXAi9H func Range(elementNum int) []int { length := If(elementNum < 0, -elementNum).Else(elementNum) result := make([]int, length) @@ -14,6 +15,7 @@ func Range(elementNum int) []int { } // RangeFrom creates an array of numbers from start with specified length. +// Play: https://go.dev/play/p/0r6VimXAi9H func RangeFrom[T constraints.Integer | constraints.Float](start T, elementNum int) []T { length := If(elementNum < 0, -elementNum).Else(elementNum) result := make([]T, length) @@ -26,6 +28,7 @@ func RangeFrom[T constraints.Integer | constraints.Float](start T, elementNum in // RangeWithSteps creates an array of numbers (positive and/or negative) progressing from start up to, but not including end. // step set to zero will return empty array. +// Play: https://go.dev/play/p/0r6VimXAi9H func RangeWithSteps[T constraints.Integer | constraints.Float](start, end, step T) []T { result := []T{} if start == end || step == 0 { @@ -50,6 +53,7 @@ func RangeWithSteps[T constraints.Integer | constraints.Float](start, end, step } // Clamp clamps number within the inclusive lower and upper bounds. +// Play: https://go.dev/play/p/RU4lJNC2hlI func Clamp[T constraints.Ordered](value T, min T, max T) T { if value < min { return min @@ -59,7 +63,8 @@ func Clamp[T constraints.Ordered](value T, min T, max T) T { return value } -// Summarizes the values in a collection. +// SumBy summarizes the values in a collection using the given return value from the iteration function. If collection is empty 0 is returned. +// Play: https://go.dev/play/p/Dz_a_7jN_ca func SumBy[T any, R constraints.Float | constraints.Integer](collection []T, iteratee func(T) R) R { var sum R = 0 for _, item := range collection { diff --git a/vendor/github.com/samber/lo/pointers.go b/vendor/github.com/samber/lo/pointers.go deleted file mode 100644 index 623d547..0000000 --- a/vendor/github.com/samber/lo/pointers.go +++ /dev/null @@ -1,32 +0,0 @@ -package lo - -// ToPtr returns a pointer copy of value. -func ToPtr[T any](x T) *T { - return &x -} - -// ToSlicePtr returns a slice of pointer copy of value. -func ToSlicePtr[T any](collection []T) []*T { - return Map(collection, func(x T, _ int) *T { - return &x - }) -} - -// Empty returns an empty value. -func Empty[T any]() T { - var t T - return t -} - -// Coalesce returns the first non-empty arguments. Arguments must be comparable. -func Coalesce[T comparable](v ...T) (result T, ok bool) { - for _, e := range v { - if e != result { - result = e - ok = true - return - } - } - - return -} diff --git a/vendor/github.com/samber/lo/retry.go b/vendor/github.com/samber/lo/retry.go index 7d86795..b4a61ef 100644 --- a/vendor/github.com/samber/lo/retry.go +++ b/vendor/github.com/samber/lo/retry.go @@ -46,6 +46,7 @@ func (d *debounce) cancel() { } // NewDebounce creates a debounced instance that delays invoking functions given until after wait milliseconds have elapsed. +// Play: https://go.dev/play/p/mz32VMK2nqe func NewDebounce(duration time.Duration, f ...func()) (func(), func()) { d := &debounce{ after: duration, @@ -60,7 +61,8 @@ func NewDebounce(duration time.Duration, f ...func()) (func(), func()) { }, d.cancel } -// Attempt invokes a function N times until it returns valid output. Returning either the caught error or nil. When first argument is less than `1`, the function runs until a sucessfull response is returned. +// Attempt invokes a function N times until it returns valid output. Returning either the caught error or nil. When first argument is less than `1`, the function runs until a successful response is returned. +// Play: https://go.dev/play/p/3ggJZ2ZKcMj func Attempt(maxIteration int, f func(int) error) (int, error) { var err error @@ -76,9 +78,10 @@ func Attempt(maxIteration int, f func(int) error) (int, error) { } // AttemptWithDelay invokes a function N times until it returns valid output, -// with a pause betwwen each call. Returning either the caught error or nil. -// When first argument is less than `1`, the function runs until a sucessfull +// with a pause between each call. Returning either the caught error or nil. +// When first argument is less than `1`, the function runs until a successful // response is returned. +// Play: https://go.dev/play/p/tVs6CygC7m1 func AttemptWithDelay(maxIteration int, delay time.Duration, f func(int, time.Duration) error) (int, time.Duration, error) { var err error diff --git a/vendor/github.com/samber/lo/slice.go b/vendor/github.com/samber/lo/slice.go index 04d048b..516fc8b 100644 --- a/vendor/github.com/samber/lo/slice.go +++ b/vendor/github.com/samber/lo/slice.go @@ -2,9 +2,12 @@ package lo import ( "math/rand" + + "golang.org/x/exp/constraints" ) // Filter iterates over elements of collection, returning an array of all elements predicate returns truthy for. +// Play: https://go.dev/play/p/Apjg3WeSi7K func Filter[V any](collection []V, predicate func(V, int) bool) []V { result := []V{} @@ -18,6 +21,7 @@ func Filter[V any](collection []V, predicate func(V, int) bool) []V { } // Map manipulates a slice and transforms it to a slice of another type. +// Play: https://go.dev/play/p/OkPcYAhBo0D func Map[T any, R any](collection []T, iteratee func(T, int) R) []R { result := make([]R, len(collection)) @@ -32,6 +36,8 @@ func Map[T any, R any](collection []T, iteratee func(T, int) R) []R { // The callback function should return two values: // - the result of the mapping operation and // - whether the result element should be included or not. +// +// Play: https://go.dev/play/p/-AuYXfy7opz func FilterMap[T any, R any](collection []T, callback func(T, int) (R, bool)) []R { result := []R{} @@ -45,6 +51,7 @@ func FilterMap[T any, R any](collection []T, callback func(T, int) (R, bool)) [] } // FlatMap manipulates a slice and transforms and flattens it to a slice of another type. +// Play: https://go.dev/play/p/YSoYmQTA8-U func FlatMap[T any, R any](collection []T, iteratee func(T, int) []R) []R { result := []R{} @@ -57,6 +64,7 @@ func FlatMap[T any, R any](collection []T, iteratee func(T, int) []R) []R { // Reduce reduces collection to a value which is the accumulated result of running each element in collection // through accumulator, where each successive invocation is supplied the return value of the previous. +// Play: https://go.dev/play/p/R4UHXZNaaUG func Reduce[T any, R any](collection []T, accumulator func(R, T, int) R, initial R) R { for i, item := range collection { initial = accumulator(initial, item, i) @@ -65,7 +73,18 @@ func Reduce[T any, R any](collection []T, accumulator func(R, T, int) R, initial return initial } +// ReduceRight helper is like Reduce except that it iterates over elements of collection from right to left. +// Play: https://go.dev/play/p/Fq3W70l7wXF +func ReduceRight[T any, R any](collection []T, accumulator func(R, T, int) R, initial R) R { + for i := len(collection) - 1; i >= 0; i-- { + initial = accumulator(initial, collection[i], i) + } + + return initial +} + // ForEach iterates over elements of collection and invokes iteratee for each element. +// Play: https://go.dev/play/p/oofyiUPRf8t func ForEach[T any](collection []T, iteratee func(T, int)) { for i, item := range collection { iteratee(item, i) @@ -74,6 +93,7 @@ func ForEach[T any](collection []T, iteratee func(T, int)) { // Times invokes the iteratee n times, returning an array of the results of each invocation. // The iteratee is invoked with index as argument. +// Play: https://go.dev/play/p/vgQj3Glr6lT func Times[T any](count int, iteratee func(int) T) []T { result := make([]T, count) @@ -86,6 +106,7 @@ func Times[T any](count int, iteratee func(int) T) []T { // Uniq returns a duplicate-free version of an array, in which only the first occurrence of each element is kept. // The order of result values is determined by the order they occur in the array. +// Play: https://go.dev/play/p/DTzbeXZ6iEN func Uniq[T comparable](collection []T) []T { result := make([]T, 0, len(collection)) seen := make(map[T]struct{}, len(collection)) @@ -105,6 +126,7 @@ func Uniq[T comparable](collection []T) []T { // UniqBy returns a duplicate-free version of an array, in which only the first occurrence of each element is kept. // The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is // invoked for each element in array to generate the criterion by which uniqueness is computed. +// Play: https://go.dev/play/p/g42Z3QSb53u func UniqBy[T any, U comparable](collection []T, iteratee func(T) U) []T { result := make([]T, 0, len(collection)) seen := make(map[U]struct{}, len(collection)) @@ -124,6 +146,7 @@ func UniqBy[T any, U comparable](collection []T, iteratee func(T) U) []T { } // GroupBy returns an object composed of keys generated from the results of running each element of collection through iteratee. +// Play: https://go.dev/play/p/XnQBd_v6brd func GroupBy[T any, U comparable](collection []T, iteratee func(T) U) map[U][]T { result := map[U][]T{} @@ -138,22 +161,25 @@ func GroupBy[T any, U comparable](collection []T, iteratee func(T) U) map[U][]T // Chunk returns an array of elements split into groups the length of size. If array can't be split evenly, // the final chunk will be the remaining elements. +// Play: https://go.dev/play/p/EeKl0AuTehH func Chunk[T any](collection []T, size int) [][]T { if size <= 0 { panic("Second parameter must be greater than 0") } - result := make([][]T, 0, len(collection)/2+1) - length := len(collection) + chunksNum := len(collection) / size + if len(collection)%size != 0 { + chunksNum += 1 + } - for i := 0; i < length; i++ { - chunk := i / size + result := make([][]T, 0, chunksNum) - if i%size == 0 { - result = append(result, make([]T, 0, size)) + for i := 0; i < chunksNum; i++ { + last := (i + 1) * size + if last > len(collection) { + last = len(collection) } - - result[chunk] = append(result[chunk], collection[i]) + result = append(result, collection[i*size:last]) } return result @@ -162,6 +188,7 @@ func Chunk[T any](collection []T, size int) [][]T { // PartitionBy returns an array of elements split into groups. The order of grouped values is // determined by the order they occur in collection. The grouping is generated from the results // of running each element of collection through iteratee. +// Play: https://go.dev/play/p/NfQ_nGjkgXW func PartitionBy[T any, K comparable](collection []T, iteratee func(x T) K) [][]T { result := [][]T{} seen := map[K]int{} @@ -187,17 +214,23 @@ func PartitionBy[T any, K comparable](collection []T, iteratee func(x T) K) [][] } // Flatten returns an array a single level deep. +// Play: https://go.dev/play/p/rbp9ORaMpjw func Flatten[T any](collection [][]T) []T { - result := []T{} + totalLen := 0 + for i := range collection { + totalLen += len(collection[i]) + } - for _, item := range collection { - result = append(result, item...) + result := make([]T, 0, totalLen) + for i := range collection { + result = append(result, collection[i]...) } return result } // Shuffle returns an array of shuffled values. Uses the Fisher-Yates shuffle algorithm. +// Play: https://go.dev/play/p/Qp73bnTDnc7 func Shuffle[T any](collection []T) []T { rand.Shuffle(len(collection), func(i, j int) { collection[i], collection[j] = collection[j], collection[i] @@ -207,6 +240,7 @@ func Shuffle[T any](collection []T) []T { } // Reverse reverses array so that the first element becomes the last, the second element becomes the second to last, and so on. +// Play: https://go.dev/play/p/fhUMLvZ7vS6 func Reverse[T any](collection []T) []T { length := len(collection) half := length / 2 @@ -220,6 +254,7 @@ func Reverse[T any](collection []T) []T { } // Fill fills elements of array with `initial` value. +// Play: https://go.dev/play/p/VwR34GzqEub func Fill[T Clonable[T]](collection []T, initial T) []T { result := make([]T, 0, len(collection)) @@ -231,6 +266,7 @@ func Fill[T Clonable[T]](collection []T, initial T) []T { } // Repeat builds a slice with N copies of initial value. +// Play: https://go.dev/play/p/g3uHXbmc3b6 func Repeat[T Clonable[T]](count int, initial T) []T { result := make([]T, 0, count) @@ -242,6 +278,7 @@ func Repeat[T Clonable[T]](count int, initial T) []T { } // RepeatBy builds a slice with values returned by N calls of callback. +// Play: https://go.dev/play/p/ozZLCtX_hNU func RepeatBy[T any](count int, predicate func(int) T) []T { result := make([]T, 0, count) @@ -253,6 +290,7 @@ func RepeatBy[T any](count int, predicate func(int) T) []T { } // KeyBy transforms a slice or an array of structs to a map based on a pivot callback. +// Play: https://go.dev/play/p/mdaClUAT-zZ func KeyBy[K comparable, V any](collection []V, iteratee func(V) K) map[K]V { result := make(map[K]V, len(collection)) @@ -264,21 +302,55 @@ func KeyBy[K comparable, V any](collection []V, iteratee func(V) K) map[K]V { return result } +// Associate returns a map containing key-value pairs provided by transform function applied to elements of the given slice. +// If any of two pairs would have the same key the last one gets added to the map. +// The order of keys in returned map is not specified and is not guaranteed to be the same from the original array. +// Play: https://go.dev/play/p/WHa2CfMO3Lr +func Associate[T any, K comparable, V any](collection []T, transform func(T) (K, V)) map[K]V { + result := make(map[K]V) + + for _, t := range collection { + k, v := transform(t) + result[k] = v + } + + return result +} + +// SliceToMap returns a map containing key-value pairs provided by transform function applied to elements of the given slice. +// If any of two pairs would have the same key the last one gets added to the map. +// The order of keys in returned map is not specified and is not guaranteed to be the same from the original array. +// Alias of Associate(). +// Play: https://go.dev/play/p/WHa2CfMO3Lr +func SliceToMap[T any, K comparable, V any](collection []T, transform func(T) (K, V)) map[K]V { + return Associate(collection, transform) +} + // Drop drops n elements from the beginning of a slice or array. +// Play: https://go.dev/play/p/JswS7vXRJP2 func Drop[T any](collection []T, n int) []T { if len(collection) <= n { return make([]T, 0) } - result := make([]T, len(collection)-n) - for i := n; i < len(collection); i++ { - result[i-n] = collection[i] + result := make([]T, 0, len(collection)-n) + + return append(result, collection[n:]...) +} + +// DropRight drops n elements from the end of a slice or array. +// Play: https://go.dev/play/p/GG0nXkSJJa3 +func DropRight[T any](collection []T, n int) []T { + if len(collection) <= n { + return []T{} } - return result + result := make([]T, 0, len(collection)-n) + return append(result, collection[:len(collection)-n]...) } // DropWhile drops elements from the beginning of a slice or array while the predicate returns true. +// Play: https://go.dev/play/p/7gBPYw2IK16 func DropWhile[T any](collection []T, predicate func(T) bool) []T { i := 0 for ; i < len(collection); i++ { @@ -287,30 +359,12 @@ func DropWhile[T any](collection []T, predicate func(T) bool) []T { } } - result := make([]T, len(collection)-i) - - for j := 0; i < len(collection); i, j = i+1, j+1 { - result[j] = collection[i] - } - - return result -} - -// DropRight drops n elements from the end of a slice or array. -func DropRight[T any](collection []T, n int) []T { - if len(collection) <= n { - return make([]T, 0) - } - - result := make([]T, len(collection)-n) - for i := len(collection) - 1 - n; i >= 0; i-- { - result[i] = collection[i] - } - - return result + result := make([]T, 0, len(collection)-i) + return append(result, collection[i:]...) } // DropRightWhile drops elements from the end of a slice or array while the predicate returns true. +// Play: https://go.dev/play/p/3-n71oEC0Hz func DropRightWhile[T any](collection []T, predicate func(T) bool) []T { i := len(collection) - 1 for ; i >= 0; i-- { @@ -319,16 +373,12 @@ func DropRightWhile[T any](collection []T, predicate func(T) bool) []T { } } - result := make([]T, i+1) - - for ; i >= 0; i-- { - result[i] = collection[i] - } - - return result + result := make([]T, 0, i+1) + return append(result, collection[:i+1]...) } // Reject is the opposite of Filter, this method returns the elements of collection that predicate does not return truthy for. +// Play: https://go.dev/play/p/YkLMODy1WEL func Reject[V any](collection []V, predicate func(V, int) bool) []V { result := []V{} @@ -342,6 +392,7 @@ func Reject[V any](collection []V, predicate func(V, int) bool) []V { } // Count counts the number of elements in the collection that compare equal to value. +// Play: https://go.dev/play/p/Y3FlK54yveC func Count[T comparable](collection []T, value T) (count int) { for _, item := range collection { if item == value { @@ -353,6 +404,7 @@ func Count[T comparable](collection []T, value T) (count int) { } // CountBy counts the number of elements in the collection for which predicate is true. +// Play: https://go.dev/play/p/ByQbNYQQi4X func CountBy[T any](collection []T, predicate func(T) bool) (count int) { for _, item := range collection { if predicate(item) { @@ -363,7 +415,8 @@ func CountBy[T any](collection []T, predicate func(T) bool) (count int) { return count } -// Subset return part of a slice. +// Subset returns a copy of a slice from `offset` up to `length` elements. Like `slice[start:start+length]`, but does not panic on overflow. +// Play: https://go.dev/play/p/tOQu1GhFcog func Subset[T any](collection []T, offset int, length uint) []T { size := len(collection) @@ -385,17 +438,36 @@ func Subset[T any](collection []T, offset int, length uint) []T { return collection[offset : offset+int(length)] } +// Slice returns a copy of a slice from `start` up to, but not including `end`. Like `slice[start:end]`, but does not panic on overflow. +// Play: https://go.dev/play/p/8XWYhfMMA1h +func Slice[T any](collection []T, start int, end int) []T { + size := len(collection) + + if start >= end { + return []T{} + } + + if start > size { + start = size + } + + if end > size { + end = size + } + + return collection[start:end] +} + // Replace returns a copy of the slice with the first n non-overlapping instances of old replaced by new. +// Play: https://go.dev/play/p/XfPzmf9gql6 func Replace[T comparable](collection []T, old T, new T, n int) []T { - size := len(collection) - result := make([]T, 0, size) + result := make([]T, len(collection)) + copy(result, collection) - for _, item := range collection { - if item == old && n != 0 { - result = append(result, new) + for i := range result { + if result[i] == old && n != 0 { + result[i] = new n-- - } else { - result = append(result, item) } } @@ -403,6 +475,49 @@ func Replace[T comparable](collection []T, old T, new T, n int) []T { } // ReplaceAll returns a copy of the slice with all non-overlapping instances of old replaced by new. +// Play: https://go.dev/play/p/a9xZFUHfYcV func ReplaceAll[T comparable](collection []T, old T, new T) []T { - return Replace[T](collection, old, new, -1) + return Replace(collection, old, new, -1) +} + +// Compact returns a slice of all non-zero elements. +// Play: https://go.dev/play/p/tXiy-iK6PAc +func Compact[T comparable](collection []T) []T { + var zero T + + result := []T{} + + for _, item := range collection { + if item != zero { + result = append(result, item) + } + } + + return result +} + +// IsSorted checks if a slice is sorted. +// Play: https://go.dev/play/p/mc3qR-t4mcx +func IsSorted[T constraints.Ordered](collection []T) bool { + for i := 1; i < len(collection); i++ { + if collection[i-1] > collection[i] { + return false + } + } + + return true +} + +// IsSortedByKey checks if a slice is sorted by iteratee. +// Play: https://go.dev/play/p/wiG6XyBBu49 +func IsSortedByKey[T any, K constraints.Ordered](collection []T, iteratee func(T) K) bool { + size := len(collection) + + for i := 0; i < size-1; i++ { + if iteratee(collection[i]) > iteratee(collection[i+1]) { + return false + } + } + + return true } diff --git a/vendor/github.com/samber/lo/string.go b/vendor/github.com/samber/lo/string.go index f60f764..a63167c 100644 --- a/vendor/github.com/samber/lo/string.go +++ b/vendor/github.com/samber/lo/string.go @@ -1,8 +1,11 @@ package lo -import "unicode/utf8" +import ( + "unicode/utf8" +) // Substring return part of a string. +// Play: https://go.dev/play/p/TQlxQi82Lu1 func Substring[T ~string](str T, offset int, length uint) T { size := len(str) @@ -24,7 +27,39 @@ func Substring[T ~string](str T, offset int, length uint) T { return str[offset : offset+int(length)] } +// ChunkString returns an array of strings split into groups the length of size. If array can't be split evenly, +// the final chunk will be the remaining elements. +// Play: https://go.dev/play/p/__FLTuJVz54 +func ChunkString[T ~string](str T, size int) []T { + if size <= 0 { + panic("lo.ChunkString: Size parameter must be greater than 0") + } + + if len(str) == 0 { + return []T{""} + } + + if size >= len(str) { + return []T{str} + } + + var chunks []T = make([]T, 0, ((len(str)-1)/size)+1) + currentLen := 0 + currentStart := 0 + for i := range str { + if currentLen == size { + chunks = append(chunks, str[currentStart:i]) + currentLen = 0 + currentStart = i + } + currentLen++ + } + chunks = append(chunks, str[currentStart:]) + return chunks +} + // RuneLength is an alias to utf8.RuneCountInString which returns the number of runes in string. +// Play: https://go.dev/play/p/tuhgW_lWY8l func RuneLength(str string) int { return utf8.RuneCountInString(str) } diff --git a/vendor/github.com/samber/lo/test.go b/vendor/github.com/samber/lo/test.go new file mode 100644 index 0000000..26db4c2 --- /dev/null +++ b/vendor/github.com/samber/lo/test.go @@ -0,0 +1,32 @@ +package lo + +import ( + "os" + "testing" + "time" +) + +// https://github.com/stretchr/testify/issues/1101 +func testWithTimeout(t *testing.T, timeout time.Duration) { + t.Helper() + + testFinished := make(chan struct{}) + t.Cleanup(func() { close(testFinished) }) + + go func() { + select { + case <-testFinished: + case <-time.After(timeout): + t.Errorf("test timed out after %s", timeout) + os.Exit(1) + } + }() +} + +type foo struct { + bar string +} + +func (f foo) Clone() foo { + return foo{f.bar} +} diff --git a/vendor/github.com/samber/lo/tuples.go b/vendor/github.com/samber/lo/tuples.go index d5f7a7b..f8ea2f1 100644 --- a/vendor/github.com/samber/lo/tuples.go +++ b/vendor/github.com/samber/lo/tuples.go @@ -1,81 +1,97 @@ package lo // T2 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T2[A any, B any](a A, b B) Tuple2[A, B] { return Tuple2[A, B]{A: a, B: b} } // T3 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T3[A any, B any, C any](a A, b B, c C) Tuple3[A, B, C] { return Tuple3[A, B, C]{A: a, B: b, C: c} } // T4 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T4[A any, B any, C any, D any](a A, b B, c C, d D) Tuple4[A, B, C, D] { return Tuple4[A, B, C, D]{A: a, B: b, C: c, D: d} } // T5 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T5[A any, B any, C any, D any, E any](a A, b B, c C, d D, e E) Tuple5[A, B, C, D, E] { return Tuple5[A, B, C, D, E]{A: a, B: b, C: c, D: d, E: e} } // T6 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T6[A any, B any, C any, D any, E any, F any](a A, b B, c C, d D, e E, f F) Tuple6[A, B, C, D, E, F] { return Tuple6[A, B, C, D, E, F]{A: a, B: b, C: c, D: d, E: e, F: f} } // T7 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T7[A any, B any, C any, D any, E any, F any, G any](a A, b B, c C, d D, e E, f F, g G) Tuple7[A, B, C, D, E, F, G] { return Tuple7[A, B, C, D, E, F, G]{A: a, B: b, C: c, D: d, E: e, F: f, G: g} } // T8 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T8[A any, B any, C any, D any, E any, F any, G any, H any](a A, b B, c C, d D, e E, f F, g G, h H) Tuple8[A, B, C, D, E, F, G, H] { return Tuple8[A, B, C, D, E, F, G, H]{A: a, B: b, C: c, D: d, E: e, F: f, G: g, H: h} } // T8 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T9[A any, B any, C any, D any, E any, F any, G any, H any, I any](a A, b B, c C, d D, e E, f F, g G, h H, i I) Tuple9[A, B, C, D, E, F, G, H, I] { return Tuple9[A, B, C, D, E, F, G, H, I]{A: a, B: b, C: c, D: d, E: e, F: f, G: g, H: h, I: i} } // Unpack2 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack2[A any, B any](tuple Tuple2[A, B]) (A, B) { return tuple.A, tuple.B } // Unpack3 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack3[A any, B any, C any](tuple Tuple3[A, B, C]) (A, B, C) { return tuple.A, tuple.B, tuple.C } // Unpack4 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack4[A any, B any, C any, D any](tuple Tuple4[A, B, C, D]) (A, B, C, D) { return tuple.A, tuple.B, tuple.C, tuple.D } // Unpack5 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack5[A any, B any, C any, D any, E any](tuple Tuple5[A, B, C, D, E]) (A, B, C, D, E) { return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E } // Unpack6 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack6[A any, B any, C any, D any, E any, F any](tuple Tuple6[A, B, C, D, E, F]) (A, B, C, D, E, F) { return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F } // Unpack7 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack7[A any, B any, C any, D any, E any, F any, G any](tuple Tuple7[A, B, C, D, E, F, G]) (A, B, C, D, E, F, G) { return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F, tuple.G } // Unpack8 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack8[A any, B any, C any, D any, E any, F any, G any, H any](tuple Tuple8[A, B, C, D, E, F, G, H]) (A, B, C, D, E, F, G, H) { return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F, tuple.G, tuple.H } // Unpack9 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack9[A any, B any, C any, D any, E any, F any, G any, H any, I any](tuple Tuple9[A, B, C, D, E, F, G, H, I]) (A, B, C, D, E, F, G, H, I) { return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F, tuple.G, tuple.H, tuple.I } @@ -83,14 +99,15 @@ func Unpack9[A any, B any, C any, D any, E any, F any, G any, H any, I any](tupl // Zip2 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip2[A any, B any](a []A, b []B) []Tuple2[A, B] { - size := Max[int]([]int{len(a), len(b)}) + size := Max([]int{len(a), len(b)}) result := make([]Tuple2[A, B], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) result = append(result, Tuple2[A, B]{ A: _a, @@ -104,15 +121,16 @@ func Zip2[A any, B any](a []A, b []B) []Tuple2[A, B] { // Zip3 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip3[A any, B any, C any](a []A, b []B, c []C) []Tuple3[A, B, C] { - size := Max[int]([]int{len(a), len(b), len(c)}) + size := Max([]int{len(a), len(b), len(c)}) result := make([]Tuple3[A, B, C], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) - _c, _ := Nth[C](c, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) result = append(result, Tuple3[A, B, C]{ A: _a, @@ -127,16 +145,17 @@ func Zip3[A any, B any, C any](a []A, b []B, c []C) []Tuple3[A, B, C] { // Zip4 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip4[A any, B any, C any, D any](a []A, b []B, c []C, d []D) []Tuple4[A, B, C, D] { - size := Max[int]([]int{len(a), len(b), len(c), len(d)}) + size := Max([]int{len(a), len(b), len(c), len(d)}) result := make([]Tuple4[A, B, C, D], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) - _c, _ := Nth[C](c, index) - _d, _ := Nth[D](d, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) result = append(result, Tuple4[A, B, C, D]{ A: _a, @@ -152,17 +171,18 @@ func Zip4[A any, B any, C any, D any](a []A, b []B, c []C, d []D) []Tuple4[A, B, // Zip5 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip5[A any, B any, C any, D any, E any](a []A, b []B, c []C, d []D, e []E) []Tuple5[A, B, C, D, E] { - size := Max[int]([]int{len(a), len(b), len(c), len(d), len(e)}) + size := Max([]int{len(a), len(b), len(c), len(d), len(e)}) result := make([]Tuple5[A, B, C, D, E], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) - _c, _ := Nth[C](c, index) - _d, _ := Nth[D](d, index) - _e, _ := Nth[E](e, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) + _e, _ := Nth(e, index) result = append(result, Tuple5[A, B, C, D, E]{ A: _a, @@ -179,18 +199,19 @@ func Zip5[A any, B any, C any, D any, E any](a []A, b []B, c []C, d []D, e []E) // Zip6 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip6[A any, B any, C any, D any, E any, F any](a []A, b []B, c []C, d []D, e []E, f []F) []Tuple6[A, B, C, D, E, F] { - size := Max[int]([]int{len(a), len(b), len(c), len(d), len(e), len(f)}) + size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f)}) result := make([]Tuple6[A, B, C, D, E, F], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) - _c, _ := Nth[C](c, index) - _d, _ := Nth[D](d, index) - _e, _ := Nth[E](e, index) - _f, _ := Nth[F](f, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) + _e, _ := Nth(e, index) + _f, _ := Nth(f, index) result = append(result, Tuple6[A, B, C, D, E, F]{ A: _a, @@ -208,19 +229,20 @@ func Zip6[A any, B any, C any, D any, E any, F any](a []A, b []B, c []C, d []D, // Zip7 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip7[A any, B any, C any, D any, E any, F any, G any](a []A, b []B, c []C, d []D, e []E, f []F, g []G) []Tuple7[A, B, C, D, E, F, G] { - size := Max[int]([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g)}) + size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g)}) result := make([]Tuple7[A, B, C, D, E, F, G], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) - _c, _ := Nth[C](c, index) - _d, _ := Nth[D](d, index) - _e, _ := Nth[E](e, index) - _f, _ := Nth[F](f, index) - _g, _ := Nth[G](g, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) + _e, _ := Nth(e, index) + _f, _ := Nth(f, index) + _g, _ := Nth(g, index) result = append(result, Tuple7[A, B, C, D, E, F, G]{ A: _a, @@ -239,20 +261,21 @@ func Zip7[A any, B any, C any, D any, E any, F any, G any](a []A, b []B, c []C, // Zip8 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip8[A any, B any, C any, D any, E any, F any, G any, H any](a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H) []Tuple8[A, B, C, D, E, F, G, H] { - size := Max[int]([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g), len(h)}) + size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g), len(h)}) result := make([]Tuple8[A, B, C, D, E, F, G, H], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) - _c, _ := Nth[C](c, index) - _d, _ := Nth[D](d, index) - _e, _ := Nth[E](e, index) - _f, _ := Nth[F](f, index) - _g, _ := Nth[G](g, index) - _h, _ := Nth[H](h, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) + _e, _ := Nth(e, index) + _f, _ := Nth(f, index) + _g, _ := Nth(g, index) + _h, _ := Nth(h, index) result = append(result, Tuple8[A, B, C, D, E, F, G, H]{ A: _a, @@ -272,21 +295,22 @@ func Zip8[A any, B any, C any, D any, E any, F any, G any, H any](a []A, b []B, // Zip9 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip9[A any, B any, C any, D any, E any, F any, G any, H any, I any](a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, i []I) []Tuple9[A, B, C, D, E, F, G, H, I] { - size := Max[int]([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g), len(h), len(i)}) + size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g), len(h), len(i)}) result := make([]Tuple9[A, B, C, D, E, F, G, H, I], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) - _c, _ := Nth[C](c, index) - _d, _ := Nth[D](d, index) - _e, _ := Nth[E](e, index) - _f, _ := Nth[F](f, index) - _g, _ := Nth[G](g, index) - _h, _ := Nth[H](h, index) - _i, _ := Nth[I](i, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) + _e, _ := Nth(e, index) + _f, _ := Nth(f, index) + _g, _ := Nth(g, index) + _h, _ := Nth(h, index) + _i, _ := Nth(i, index) result = append(result, Tuple9[A, B, C, D, E, F, G, H, I]{ A: _a, @@ -306,6 +330,7 @@ func Zip9[A any, B any, C any, D any, E any, F any, G any, H any, I any](a []A, // Unzip2 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip2[A any, B any](tuples []Tuple2[A, B]) ([]A, []B) { size := len(tuples) r1 := make([]A, 0, size) @@ -321,6 +346,7 @@ func Unzip2[A any, B any](tuples []Tuple2[A, B]) ([]A, []B) { // Unzip3 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip3[A any, B any, C any](tuples []Tuple3[A, B, C]) ([]A, []B, []C) { size := len(tuples) r1 := make([]A, 0, size) @@ -338,6 +364,7 @@ func Unzip3[A any, B any, C any](tuples []Tuple3[A, B, C]) ([]A, []B, []C) { // Unzip4 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip4[A any, B any, C any, D any](tuples []Tuple4[A, B, C, D]) ([]A, []B, []C, []D) { size := len(tuples) r1 := make([]A, 0, size) @@ -357,6 +384,7 @@ func Unzip4[A any, B any, C any, D any](tuples []Tuple4[A, B, C, D]) ([]A, []B, // Unzip5 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip5[A any, B any, C any, D any, E any](tuples []Tuple5[A, B, C, D, E]) ([]A, []B, []C, []D, []E) { size := len(tuples) r1 := make([]A, 0, size) @@ -378,6 +406,7 @@ func Unzip5[A any, B any, C any, D any, E any](tuples []Tuple5[A, B, C, D, E]) ( // Unzip6 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip6[A any, B any, C any, D any, E any, F any](tuples []Tuple6[A, B, C, D, E, F]) ([]A, []B, []C, []D, []E, []F) { size := len(tuples) r1 := make([]A, 0, size) @@ -401,6 +430,7 @@ func Unzip6[A any, B any, C any, D any, E any, F any](tuples []Tuple6[A, B, C, D // Unzip7 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip7[A any, B any, C any, D any, E any, F any, G any](tuples []Tuple7[A, B, C, D, E, F, G]) ([]A, []B, []C, []D, []E, []F, []G) { size := len(tuples) r1 := make([]A, 0, size) @@ -426,6 +456,7 @@ func Unzip7[A any, B any, C any, D any, E any, F any, G any](tuples []Tuple7[A, // Unzip8 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip8[A any, B any, C any, D any, E any, F any, G any, H any](tuples []Tuple8[A, B, C, D, E, F, G, H]) ([]A, []B, []C, []D, []E, []F, []G, []H) { size := len(tuples) r1 := make([]A, 0, size) @@ -453,6 +484,7 @@ func Unzip8[A any, B any, C any, D any, E any, F any, G any, H any](tuples []Tup // Unzip9 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip9[A any, B any, C any, D any, E any, F any, G any, H any, I any](tuples []Tuple9[A, B, C, D, E, F, G, H, I]) ([]A, []B, []C, []D, []E, []F, []G, []H, []I) { size := len(tuples) r1 := make([]A, 0, size) diff --git a/vendor/github.com/samber/lo/type_manipulation.go b/vendor/github.com/samber/lo/type_manipulation.go new file mode 100644 index 0000000..fe99ee1 --- /dev/null +++ b/vendor/github.com/samber/lo/type_manipulation.go @@ -0,0 +1,88 @@ +package lo + +// ToPtr returns a pointer copy of value. +func ToPtr[T any](x T) *T { + return &x +} + +// FromPtr returns the pointer value or empty. +func FromPtr[T any](x *T) T { + if x == nil { + return Empty[T]() + } + + return *x +} + +// FromPtrOr returns the pointer value or the fallback value. +func FromPtrOr[T any](x *T, fallback T) T { + if x == nil { + return fallback + } + + return *x +} + +// ToSlicePtr returns a slice of pointer copy of value. +func ToSlicePtr[T any](collection []T) []*T { + return Map(collection, func(x T, _ int) *T { + return &x + }) +} + +// ToAnySlice returns a slice with all elements mapped to `any` type +func ToAnySlice[T any](collection []T) []any { + result := make([]any, len(collection)) + for i, item := range collection { + result[i] = item + } + return result +} + +// FromAnySlice returns an `any` slice with all elements mapped to a type. +// Returns false in case of type conversion failure. +func FromAnySlice[T any](in []any) (out []T, ok bool) { + defer func() { + if r := recover(); r != nil { + out = []T{} + ok = false + } + }() + + result := make([]T, len(in)) + for i, item := range in { + result[i] = item.(T) + } + return result, true +} + +// Empty returns an empty value. +func Empty[T any]() T { + var zero T + return zero +} + +// IsEmpty returns true if argument is a zero value. +func IsEmpty[T comparable](v T) bool { + var zero T + return zero == v +} + +// IsNotEmpty returns true if argument is not a zero value. +func IsNotEmpty[T comparable](v T) bool { + var zero T + return zero != v +} + +// Coalesce returns the first non-empty arguments. Arguments must be comparable. +func Coalesce[T comparable](v ...T) (result T, ok bool) { + for _, e := range v { + if e != result { + result = e + ok = true + return + } + } + + return +} diff --git a/vendor/github.com/stretchr/testify/assert/assertion_compare.go b/vendor/github.com/stretchr/testify/assert/assertion_compare.go index 41649d2..95d8e59 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_compare.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_compare.go @@ -1,8 +1,10 @@ package assert import ( + "bytes" "fmt" "reflect" + "time" ) type CompareType int @@ -30,6 +32,9 @@ var ( float64Type = reflect.TypeOf(float64(1)) stringType = reflect.TypeOf("") + + timeType = reflect.TypeOf(time.Time{}) + bytesType = reflect.TypeOf([]byte{}) ) func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { @@ -299,6 +304,47 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { return compareLess, true } } + // Check for known struct types we can check for compare results. + case reflect.Struct: + { + // All structs enter here. We're not interested in most types. + if !canConvert(obj1Value, timeType) { + break + } + + // time.Time can compared! + timeObj1, ok := obj1.(time.Time) + if !ok { + timeObj1 = obj1Value.Convert(timeType).Interface().(time.Time) + } + + timeObj2, ok := obj2.(time.Time) + if !ok { + timeObj2 = obj2Value.Convert(timeType).Interface().(time.Time) + } + + return compare(timeObj1.UnixNano(), timeObj2.UnixNano(), reflect.Int64) + } + case reflect.Slice: + { + // We only care about the []byte type. + if !canConvert(obj1Value, bytesType) { + break + } + + // []byte can be compared! + bytesObj1, ok := obj1.([]byte) + if !ok { + bytesObj1 = obj1Value.Convert(bytesType).Interface().([]byte) + + } + bytesObj2, ok := obj2.([]byte) + if !ok { + bytesObj2 = obj2Value.Convert(bytesType).Interface().([]byte) + } + + return CompareType(bytes.Compare(bytesObj1, bytesObj2)), true + } } return compareEqual, false @@ -310,7 +356,10 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { // assert.Greater(t, float64(2), float64(1)) // assert.Greater(t, "b", "a") func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { - return compareTwoValues(t, e1, e2, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs) + if h, ok := t.(tHelper); ok { + h.Helper() + } + return compareTwoValues(t, e1, e2, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs...) } // GreaterOrEqual asserts that the first element is greater than or equal to the second @@ -320,7 +369,10 @@ func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface // assert.GreaterOrEqual(t, "b", "a") // assert.GreaterOrEqual(t, "b", "b") func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { - return compareTwoValues(t, e1, e2, []CompareType{compareGreater, compareEqual}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs) + if h, ok := t.(tHelper); ok { + h.Helper() + } + return compareTwoValues(t, e1, e2, []CompareType{compareGreater, compareEqual}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs...) } // Less asserts that the first element is less than the second @@ -329,7 +381,10 @@ func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...in // assert.Less(t, float64(1), float64(2)) // assert.Less(t, "a", "b") func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { - return compareTwoValues(t, e1, e2, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs) + if h, ok := t.(tHelper); ok { + h.Helper() + } + return compareTwoValues(t, e1, e2, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs...) } // LessOrEqual asserts that the first element is less than or equal to the second @@ -339,7 +394,10 @@ func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) // assert.LessOrEqual(t, "a", "b") // assert.LessOrEqual(t, "b", "b") func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { - return compareTwoValues(t, e1, e2, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs) + if h, ok := t.(tHelper); ok { + h.Helper() + } + return compareTwoValues(t, e1, e2, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...) } // Positive asserts that the specified element is positive @@ -347,8 +405,11 @@ func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...inter // assert.Positive(t, 1) // assert.Positive(t, 1.23) func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } zero := reflect.Zero(reflect.TypeOf(e)) - return compareTwoValues(t, e, zero.Interface(), []CompareType{compareGreater}, "\"%v\" is not positive", msgAndArgs) + return compareTwoValues(t, e, zero.Interface(), []CompareType{compareGreater}, "\"%v\" is not positive", msgAndArgs...) } // Negative asserts that the specified element is negative @@ -356,8 +417,11 @@ func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { // assert.Negative(t, -1) // assert.Negative(t, -1.23) func Negative(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } zero := reflect.Zero(reflect.TypeOf(e)) - return compareTwoValues(t, e, zero.Interface(), []CompareType{compareLess}, "\"%v\" is not negative", msgAndArgs) + return compareTwoValues(t, e, zero.Interface(), []CompareType{compareLess}, "\"%v\" is not negative", msgAndArgs...) } func compareTwoValues(t TestingT, e1 interface{}, e2 interface{}, allowedComparesResults []CompareType, failMessage string, msgAndArgs ...interface{}) bool { diff --git a/vendor/github.com/stretchr/testify/assert/assertion_compare_can_convert.go b/vendor/github.com/stretchr/testify/assert/assertion_compare_can_convert.go new file mode 100644 index 0000000..da86790 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertion_compare_can_convert.go @@ -0,0 +1,16 @@ +//go:build go1.17 +// +build go1.17 + +// TODO: once support for Go 1.16 is dropped, this file can be +// merged/removed with assertion_compare_go1.17_test.go and +// assertion_compare_legacy.go + +package assert + +import "reflect" + +// Wrapper around reflect.Value.CanConvert, for compatibility +// reasons. +func canConvert(value reflect.Value, to reflect.Type) bool { + return value.CanConvert(to) +} diff --git a/vendor/github.com/stretchr/testify/assert/assertion_compare_legacy.go b/vendor/github.com/stretchr/testify/assert/assertion_compare_legacy.go new file mode 100644 index 0000000..1701af2 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertion_compare_legacy.go @@ -0,0 +1,16 @@ +//go:build !go1.17 +// +build !go1.17 + +// TODO: once support for Go 1.16 is dropped, this file can be +// merged/removed with assertion_compare_go1.17_test.go and +// assertion_compare_can_convert.go + +package assert + +import "reflect" + +// Older versions of Go does not have the reflect.Value.CanConvert +// method. +func canConvert(value reflect.Value, to reflect.Type) bool { + return false +} diff --git a/vendor/github.com/stretchr/testify/assert/assertion_format.go b/vendor/github.com/stretchr/testify/assert/assertion_format.go index 4dfd122..7880b8f 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_format.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_format.go @@ -123,6 +123,18 @@ func ErrorAsf(t TestingT, err error, target interface{}, msg string, args ...int return ErrorAs(t, err, target, append([]interface{}{msg}, args...)...) } +// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// assert.ErrorContainsf(t, err, expectedErrorSubString, "error message %s", "formatted") +func ErrorContainsf(t TestingT, theError error, contains string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return ErrorContains(t, theError, contains, append([]interface{}{msg}, args...)...) +} + // ErrorIsf asserts that at least one of the errors in err's chain matches target. // This is a wrapper for errors.Is. func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) bool { @@ -724,6 +736,16 @@ func WithinDurationf(t TestingT, expected time.Time, actual time.Time, delta tim return WithinDuration(t, expected, actual, delta, append([]interface{}{msg}, args...)...) } +// WithinRangef asserts that a time is within a time range (inclusive). +// +// assert.WithinRangef(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second), "error message %s", "formatted") +func WithinRangef(t TestingT, actual time.Time, start time.Time, end time.Time, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return WithinRange(t, actual, start, end, append([]interface{}{msg}, args...)...) +} + // YAMLEqf asserts that two YAML strings are equivalent. func YAMLEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { diff --git a/vendor/github.com/stretchr/testify/assert/assertion_forward.go b/vendor/github.com/stretchr/testify/assert/assertion_forward.go index 25337a6..339515b 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_forward.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_forward.go @@ -222,6 +222,30 @@ func (a *Assertions) ErrorAsf(err error, target interface{}, msg string, args .. return ErrorAsf(a.t, err, target, msg, args...) } +// ErrorContains asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// a.ErrorContains(err, expectedErrorSubString) +func (a *Assertions) ErrorContains(theError error, contains string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return ErrorContains(a.t, theError, contains, msgAndArgs...) +} + +// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// a.ErrorContainsf(err, expectedErrorSubString, "error message %s", "formatted") +func (a *Assertions) ErrorContainsf(theError error, contains string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return ErrorContainsf(a.t, theError, contains, msg, args...) +} + // ErrorIs asserts that at least one of the errors in err's chain matches target. // This is a wrapper for errors.Is. func (a *Assertions) ErrorIs(err error, target error, msgAndArgs ...interface{}) bool { @@ -1437,6 +1461,26 @@ func (a *Assertions) WithinDurationf(expected time.Time, actual time.Time, delta return WithinDurationf(a.t, expected, actual, delta, msg, args...) } +// WithinRange asserts that a time is within a time range (inclusive). +// +// a.WithinRange(time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second)) +func (a *Assertions) WithinRange(actual time.Time, start time.Time, end time.Time, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return WithinRange(a.t, actual, start, end, msgAndArgs...) +} + +// WithinRangef asserts that a time is within a time range (inclusive). +// +// a.WithinRangef(time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second), "error message %s", "formatted") +func (a *Assertions) WithinRangef(actual time.Time, start time.Time, end time.Time, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return WithinRangef(a.t, actual, start, end, msg, args...) +} + // YAMLEq asserts that two YAML strings are equivalent. func (a *Assertions) YAMLEq(expected string, actual string, msgAndArgs ...interface{}) bool { if h, ok := a.t.(tHelper); ok { diff --git a/vendor/github.com/stretchr/testify/assert/assertion_order.go b/vendor/github.com/stretchr/testify/assert/assertion_order.go index 1c3b471..7594487 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_order.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_order.go @@ -50,7 +50,7 @@ func isOrdered(t TestingT, object interface{}, allowedComparesResults []CompareT // assert.IsIncreasing(t, []float{1, 2}) // assert.IsIncreasing(t, []string{"a", "b"}) func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs) + return isOrdered(t, object, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs...) } // IsNonIncreasing asserts that the collection is not increasing @@ -59,7 +59,7 @@ func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) boo // assert.IsNonIncreasing(t, []float{2, 1}) // assert.IsNonIncreasing(t, []string{"b", "a"}) func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareEqual, compareGreater}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs) + return isOrdered(t, object, []CompareType{compareEqual, compareGreater}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs...) } // IsDecreasing asserts that the collection is decreasing @@ -68,7 +68,7 @@ func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) // assert.IsDecreasing(t, []float{2, 1}) // assert.IsDecreasing(t, []string{"b", "a"}) func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs) + return isOrdered(t, object, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs...) } // IsNonDecreasing asserts that the collection is not decreasing @@ -77,5 +77,5 @@ func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) boo // assert.IsNonDecreasing(t, []float{1, 2}) // assert.IsNonDecreasing(t, []string{"a", "b"}) func IsNonDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs) + return isOrdered(t, object, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...) } diff --git a/vendor/github.com/stretchr/testify/assert/assertions.go b/vendor/github.com/stretchr/testify/assert/assertions.go index bcac440..fa1245b 100644 --- a/vendor/github.com/stretchr/testify/assert/assertions.go +++ b/vendor/github.com/stretchr/testify/assert/assertions.go @@ -8,6 +8,7 @@ import ( "fmt" "math" "os" + "path/filepath" "reflect" "regexp" "runtime" @@ -144,7 +145,8 @@ func CallerInfo() []string { if len(parts) > 1 { dir := parts[len(parts)-2] if (dir != "assert" && dir != "mock" && dir != "require") || file == "mock_test.go" { - callers = append(callers, fmt.Sprintf("%s:%d", file, line)) + path, _ := filepath.Abs(file) + callers = append(callers, fmt.Sprintf("%s:%d", path, line)) } } @@ -563,16 +565,17 @@ func isEmpty(object interface{}) bool { switch objValue.Kind() { // collection types are empty when they have no element - case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: + case reflect.Chan, reflect.Map, reflect.Slice: return objValue.Len() == 0 - // pointers are empty if nil or if the value they point to is empty + // pointers are empty if nil or if the value they point to is empty case reflect.Ptr: if objValue.IsNil() { return true } deref := objValue.Elem().Interface() return isEmpty(deref) - // for all other types, compare against the zero value + // for all other types, compare against the zero value + // array types are empty when they match their zero-initialized state default: zero := reflect.Zero(objValue.Type()) return reflect.DeepEqual(object, zero.Interface()) @@ -718,10 +721,14 @@ func NotEqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...inte // return (false, false) if impossible. // return (true, false) if element was not found. // return (true, true) if element was found. -func includeElement(list interface{}, element interface{}) (ok, found bool) { +func containsElement(list interface{}, element interface{}) (ok, found bool) { listValue := reflect.ValueOf(list) - listKind := reflect.TypeOf(list).Kind() + listType := reflect.TypeOf(list) + if listType == nil { + return false, false + } + listKind := listType.Kind() defer func() { if e := recover(); e != nil { ok = false @@ -764,7 +771,7 @@ func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bo h.Helper() } - ok, found := includeElement(s, contains) + ok, found := containsElement(s, contains) if !ok { return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", s), msgAndArgs...) } @@ -787,7 +794,7 @@ func NotContains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) h.Helper() } - ok, found := includeElement(s, contains) + ok, found := containsElement(s, contains) if !ok { return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", s), msgAndArgs...) } @@ -811,7 +818,6 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok return true // we consider nil to be equal to the nil set } - subsetValue := reflect.ValueOf(subset) defer func() { if e := recover(); e != nil { ok = false @@ -821,17 +827,35 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok listKind := reflect.TypeOf(list).Kind() subsetKind := reflect.TypeOf(subset).Kind() - if listKind != reflect.Array && listKind != reflect.Slice { + if listKind != reflect.Array && listKind != reflect.Slice && listKind != reflect.Map { return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...) } - if subsetKind != reflect.Array && subsetKind != reflect.Slice { + if subsetKind != reflect.Array && subsetKind != reflect.Slice && listKind != reflect.Map { return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...) } + subsetValue := reflect.ValueOf(subset) + if subsetKind == reflect.Map && listKind == reflect.Map { + listValue := reflect.ValueOf(list) + subsetKeys := subsetValue.MapKeys() + + for i := 0; i < len(subsetKeys); i++ { + subsetKey := subsetKeys[i] + subsetElement := subsetValue.MapIndex(subsetKey).Interface() + listElement := listValue.MapIndex(subsetKey).Interface() + + if !ObjectsAreEqual(subsetElement, listElement) { + return Fail(t, fmt.Sprintf("\"%s\" does not contain \"%s\"", list, subsetElement), msgAndArgs...) + } + } + + return true + } + for i := 0; i < subsetValue.Len(); i++ { element := subsetValue.Index(i).Interface() - ok, found := includeElement(list, element) + ok, found := containsElement(list, element) if !ok { return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...) } @@ -852,10 +876,9 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) h.Helper() } if subset == nil { - return Fail(t, fmt.Sprintf("nil is the empty set which is a subset of every set"), msgAndArgs...) + return Fail(t, "nil is the empty set which is a subset of every set", msgAndArgs...) } - subsetValue := reflect.ValueOf(subset) defer func() { if e := recover(); e != nil { ok = false @@ -865,17 +888,35 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) listKind := reflect.TypeOf(list).Kind() subsetKind := reflect.TypeOf(subset).Kind() - if listKind != reflect.Array && listKind != reflect.Slice { + if listKind != reflect.Array && listKind != reflect.Slice && listKind != reflect.Map { return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...) } - if subsetKind != reflect.Array && subsetKind != reflect.Slice { + if subsetKind != reflect.Array && subsetKind != reflect.Slice && listKind != reflect.Map { return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...) } + subsetValue := reflect.ValueOf(subset) + if subsetKind == reflect.Map && listKind == reflect.Map { + listValue := reflect.ValueOf(list) + subsetKeys := subsetValue.MapKeys() + + for i := 0; i < len(subsetKeys); i++ { + subsetKey := subsetKeys[i] + subsetElement := subsetValue.MapIndex(subsetKey).Interface() + listElement := listValue.MapIndex(subsetKey).Interface() + + if !ObjectsAreEqual(subsetElement, listElement) { + return true + } + } + + return Fail(t, fmt.Sprintf("%q is a subset of %q", subset, list), msgAndArgs...) + } + for i := 0; i < subsetValue.Len(); i++ { element := subsetValue.Index(i).Interface() - ok, found := includeElement(list, element) + ok, found := containsElement(list, element) if !ok { return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...) } @@ -1000,27 +1041,21 @@ func Condition(t TestingT, comp Comparison, msgAndArgs ...interface{}) bool { type PanicTestFunc func() // didPanic returns true if the function passed to it panics. Otherwise, it returns false. -func didPanic(f PanicTestFunc) (bool, interface{}, string) { - - didPanic := false - var message interface{} - var stack string - func() { - - defer func() { - if message = recover(); message != nil { - didPanic = true - stack = string(debug.Stack()) - } - }() - - // call the target function - f() +func didPanic(f PanicTestFunc) (didPanic bool, message interface{}, stack string) { + didPanic = true + defer func() { + message = recover() + if didPanic { + stack = string(debug.Stack()) + } }() - return didPanic, message, stack + // call the target function + f() + didPanic = false + return } // Panics asserts that the code inside the specified PanicTestFunc panics. @@ -1111,6 +1146,27 @@ func WithinDuration(t TestingT, expected, actual time.Time, delta time.Duration, return true } +// WithinRange asserts that a time is within a time range (inclusive). +// +// assert.WithinRange(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second)) +func WithinRange(t TestingT, actual, start, end time.Time, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + if end.Before(start) { + return Fail(t, "Start should be before end", msgAndArgs...) + } + + if actual.Before(start) { + return Fail(t, fmt.Sprintf("Time %v expected to be in time range %v to %v, but is before the range", actual, start, end), msgAndArgs...) + } else if actual.After(end) { + return Fail(t, fmt.Sprintf("Time %v expected to be in time range %v to %v, but is after the range", actual, start, end), msgAndArgs...) + } + + return true +} + func toFloat(x interface{}) (float64, bool) { var xf float64 xok := true @@ -1161,11 +1217,15 @@ func InDelta(t TestingT, expected, actual interface{}, delta float64, msgAndArgs bf, bok := toFloat(actual) if !aok || !bok { - return Fail(t, fmt.Sprintf("Parameters must be numerical"), msgAndArgs...) + return Fail(t, "Parameters must be numerical", msgAndArgs...) + } + + if math.IsNaN(af) && math.IsNaN(bf) { + return true } if math.IsNaN(af) { - return Fail(t, fmt.Sprintf("Expected must not be NaN"), msgAndArgs...) + return Fail(t, "Expected must not be NaN", msgAndArgs...) } if math.IsNaN(bf) { @@ -1188,7 +1248,7 @@ func InDeltaSlice(t TestingT, expected, actual interface{}, delta float64, msgAn if expected == nil || actual == nil || reflect.TypeOf(actual).Kind() != reflect.Slice || reflect.TypeOf(expected).Kind() != reflect.Slice { - return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...) + return Fail(t, "Parameters must be slice", msgAndArgs...) } actualSlice := reflect.ValueOf(actual) @@ -1250,8 +1310,12 @@ func InDeltaMapValues(t TestingT, expected, actual interface{}, delta float64, m func calcRelativeError(expected, actual interface{}) (float64, error) { af, aok := toFloat(expected) - if !aok { - return 0, fmt.Errorf("expected value %q cannot be converted to float", expected) + bf, bok := toFloat(actual) + if !aok || !bok { + return 0, fmt.Errorf("Parameters must be numerical") + } + if math.IsNaN(af) && math.IsNaN(bf) { + return 0, nil } if math.IsNaN(af) { return 0, errors.New("expected value must not be NaN") @@ -1259,10 +1323,6 @@ func calcRelativeError(expected, actual interface{}) (float64, error) { if af == 0 { return 0, fmt.Errorf("expected value must have a value other than zero to calculate the relative error") } - bf, bok := toFloat(actual) - if !bok { - return 0, fmt.Errorf("actual value %q cannot be converted to float", actual) - } if math.IsNaN(bf) { return 0, errors.New("actual value must not be NaN") } @@ -1298,7 +1358,7 @@ func InEpsilonSlice(t TestingT, expected, actual interface{}, epsilon float64, m if expected == nil || actual == nil || reflect.TypeOf(actual).Kind() != reflect.Slice || reflect.TypeOf(expected).Kind() != reflect.Slice { - return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...) + return Fail(t, "Parameters must be slice", msgAndArgs...) } actualSlice := reflect.ValueOf(actual) @@ -1375,6 +1435,27 @@ func EqualError(t TestingT, theError error, errString string, msgAndArgs ...inte return true } +// ErrorContains asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// assert.ErrorContains(t, err, expectedErrorSubString) +func ErrorContains(t TestingT, theError error, contains string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if !Error(t, theError, msgAndArgs...) { + return false + } + + actual := theError.Error() + if !strings.Contains(actual, contains) { + return Fail(t, fmt.Sprintf("Error %#v does not contain %#v", actual, contains), msgAndArgs...) + } + + return true +} + // matchRegexp return true if a specified regexp matches a string. func matchRegexp(rx interface{}, str interface{}) bool { @@ -1588,12 +1669,17 @@ func diff(expected interface{}, actual interface{}) string { } var e, a string - if et != reflect.TypeOf("") { - e = spewConfig.Sdump(expected) - a = spewConfig.Sdump(actual) - } else { + + switch et { + case reflect.TypeOf(""): e = reflect.ValueOf(expected).String() a = reflect.ValueOf(actual).String() + case reflect.TypeOf(time.Time{}): + e = spewConfigStringerEnabled.Sdump(expected) + a = spewConfigStringerEnabled.Sdump(actual) + default: + e = spewConfig.Sdump(expected) + a = spewConfig.Sdump(actual) } diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ @@ -1625,6 +1711,14 @@ var spewConfig = spew.ConfigState{ MaxDepth: 10, } +var spewConfigStringerEnabled = spew.ConfigState{ + Indent: " ", + DisablePointerAddresses: true, + DisableCapacities: true, + SortKeys: true, + MaxDepth: 10, +} + type tHelper interface { Helper() } diff --git a/vendor/gopkg.in/yaml.v3/decode.go b/vendor/gopkg.in/yaml.v3/decode.go index df36e3a..0173b69 100644 --- a/vendor/gopkg.in/yaml.v3/decode.go +++ b/vendor/gopkg.in/yaml.v3/decode.go @@ -100,7 +100,10 @@ func (p *parser) peek() yaml_event_type_t { if p.event.typ != yaml_NO_EVENT { return p.event.typ } - if !yaml_parser_parse(&p.parser, &p.event) { + // It's curious choice from the underlying API to generally return a + // positive result on success, but on this case return true in an error + // scenario. This was the source of bugs in the past (issue #666). + if !yaml_parser_parse(&p.parser, &p.event) || p.parser.error != yaml_NO_ERROR { p.fail() } return p.event.typ @@ -320,6 +323,8 @@ type decoder struct { decodeCount int aliasCount int aliasDepth int + + mergedFields map[interface{}]bool } var ( @@ -808,6 +813,11 @@ func (d *decoder) mapping(n *Node, out reflect.Value) (good bool) { } } + mergedFields := d.mergedFields + d.mergedFields = nil + + var mergeNode *Node + mapIsNew := false if out.IsNil() { out.Set(reflect.MakeMap(outt)) @@ -815,11 +825,18 @@ func (d *decoder) mapping(n *Node, out reflect.Value) (good bool) { } for i := 0; i < l; i += 2 { if isMerge(n.Content[i]) { - d.merge(n.Content[i+1], out) + mergeNode = n.Content[i+1] continue } k := reflect.New(kt).Elem() if d.unmarshal(n.Content[i], k) { + if mergedFields != nil { + ki := k.Interface() + if mergedFields[ki] { + continue + } + mergedFields[ki] = true + } kkind := k.Kind() if kkind == reflect.Interface { kkind = k.Elem().Kind() @@ -833,6 +850,12 @@ func (d *decoder) mapping(n *Node, out reflect.Value) (good bool) { } } } + + d.mergedFields = mergedFields + if mergeNode != nil { + d.merge(n, mergeNode, out) + } + d.stringMapType = stringMapType d.generalMapType = generalMapType return true @@ -844,7 +867,8 @@ func isStringMap(n *Node) bool { } l := len(n.Content) for i := 0; i < l; i += 2 { - if n.Content[i].ShortTag() != strTag { + shortTag := n.Content[i].ShortTag() + if shortTag != strTag && shortTag != mergeTag { return false } } @@ -861,7 +885,6 @@ func (d *decoder) mappingStruct(n *Node, out reflect.Value) (good bool) { var elemType reflect.Type if sinfo.InlineMap != -1 { inlineMap = out.Field(sinfo.InlineMap) - inlineMap.Set(reflect.New(inlineMap.Type()).Elem()) elemType = inlineMap.Type().Elem() } @@ -870,6 +893,9 @@ func (d *decoder) mappingStruct(n *Node, out reflect.Value) (good bool) { d.prepare(n, field) } + mergedFields := d.mergedFields + d.mergedFields = nil + var mergeNode *Node var doneFields []bool if d.uniqueKeys { doneFields = make([]bool, len(sinfo.FieldsList)) @@ -879,13 +905,20 @@ func (d *decoder) mappingStruct(n *Node, out reflect.Value) (good bool) { for i := 0; i < l; i += 2 { ni := n.Content[i] if isMerge(ni) { - d.merge(n.Content[i+1], out) + mergeNode = n.Content[i+1] continue } if !d.unmarshal(ni, name) { continue } - if info, ok := sinfo.FieldsMap[name.String()]; ok { + sname := name.String() + if mergedFields != nil { + if mergedFields[sname] { + continue + } + mergedFields[sname] = true + } + if info, ok := sinfo.FieldsMap[sname]; ok { if d.uniqueKeys { if doneFields[info.Id] { d.terrors = append(d.terrors, fmt.Sprintf("line %d: field %s already set in type %s", ni.Line, name.String(), out.Type())) @@ -911,6 +944,11 @@ func (d *decoder) mappingStruct(n *Node, out reflect.Value) (good bool) { d.terrors = append(d.terrors, fmt.Sprintf("line %d: field %s not found in type %s", ni.Line, name.String(), out.Type())) } } + + d.mergedFields = mergedFields + if mergeNode != nil { + d.merge(n, mergeNode, out) + } return true } @@ -918,19 +956,29 @@ func failWantMap() { failf("map merge requires map or sequence of maps as the value") } -func (d *decoder) merge(n *Node, out reflect.Value) { - switch n.Kind { +func (d *decoder) merge(parent *Node, merge *Node, out reflect.Value) { + mergedFields := d.mergedFields + if mergedFields == nil { + d.mergedFields = make(map[interface{}]bool) + for i := 0; i < len(parent.Content); i += 2 { + k := reflect.New(ifaceType).Elem() + if d.unmarshal(parent.Content[i], k) { + d.mergedFields[k.Interface()] = true + } + } + } + + switch merge.Kind { case MappingNode: - d.unmarshal(n, out) + d.unmarshal(merge, out) case AliasNode: - if n.Alias != nil && n.Alias.Kind != MappingNode { + if merge.Alias != nil && merge.Alias.Kind != MappingNode { failWantMap() } - d.unmarshal(n, out) + d.unmarshal(merge, out) case SequenceNode: - // Step backwards as earlier nodes take precedence. - for i := len(n.Content) - 1; i >= 0; i-- { - ni := n.Content[i] + for i := 0; i < len(merge.Content); i++ { + ni := merge.Content[i] if ni.Kind == AliasNode { if ni.Alias != nil && ni.Alias.Kind != MappingNode { failWantMap() @@ -943,6 +991,8 @@ func (d *decoder) merge(n *Node, out reflect.Value) { default: failWantMap() } + + d.mergedFields = mergedFields } func isMerge(n *Node) bool { diff --git a/vendor/gopkg.in/yaml.v3/parserc.go b/vendor/gopkg.in/yaml.v3/parserc.go index ac66fcc..268558a 100644 --- a/vendor/gopkg.in/yaml.v3/parserc.go +++ b/vendor/gopkg.in/yaml.v3/parserc.go @@ -687,6 +687,9 @@ func yaml_parser_parse_node(parser *yaml_parser_t, event *yaml_event_t, block, i func yaml_parser_parse_block_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { if first { token := peek_token(parser) + if token == nil { + return false + } parser.marks = append(parser.marks, token.start_mark) skip_token(parser) } @@ -786,7 +789,7 @@ func yaml_parser_split_stem_comment(parser *yaml_parser_t, stem_len int) { } token := peek_token(parser) - if token.typ != yaml_BLOCK_SEQUENCE_START_TOKEN && token.typ != yaml_BLOCK_MAPPING_START_TOKEN { + if token == nil || token.typ != yaml_BLOCK_SEQUENCE_START_TOKEN && token.typ != yaml_BLOCK_MAPPING_START_TOKEN { return } @@ -813,6 +816,9 @@ func yaml_parser_split_stem_comment(parser *yaml_parser_t, stem_len int) { func yaml_parser_parse_block_mapping_key(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { if first { token := peek_token(parser) + if token == nil { + return false + } parser.marks = append(parser.marks, token.start_mark) skip_token(parser) } @@ -922,6 +928,9 @@ func yaml_parser_parse_block_mapping_value(parser *yaml_parser_t, event *yaml_ev func yaml_parser_parse_flow_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { if first { token := peek_token(parser) + if token == nil { + return false + } parser.marks = append(parser.marks, token.start_mark) skip_token(parser) } diff --git a/vendor/modules.txt b/vendor/modules.txt index 3c9757d..a881851 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -121,6 +121,10 @@ github.com/jesseduffield/gocui # github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 ## explicit; go 1.18 github.com/jesseduffield/kill +# github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316 +## explicit; go 1.18 +github.com/jesseduffield/lazycore/pkg/boxlayout +github.com/jesseduffield/lazycore/pkg/utils # github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56 ## explicit github.com/jesseduffield/yaml @@ -169,7 +173,7 @@ github.com/pmezard/go-difflib/difflib # github.com/rivo/uniseg v0.2.0 ## explicit; go 1.12 github.com/rivo/uniseg -# github.com/samber/lo v1.20.0 +# github.com/samber/lo v1.31.0 ## explicit; go 1.18 github.com/samber/lo # github.com/sirupsen/logrus v1.4.2 @@ -178,7 +182,7 @@ github.com/sirupsen/logrus # github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad ## explicit github.com/spkg/bom -# github.com/stretchr/testify v1.7.0 +# github.com/stretchr/testify v1.8.0 ## explicit; go 1.13 github.com/stretchr/testify/assert # github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 @@ -213,7 +217,7 @@ golang.org/x/xerrors golang.org/x/xerrors/internal # gopkg.in/yaml.v2 v2.2.2 ## explicit -# gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b +# gopkg.in/yaml.v3 v3.0.1 ## explicit gopkg.in/yaml.v3 # gotest.tools/v3 v3.2.0