diff --git a/.gitignore b/.gitignore index 5274014..9dbe56c 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,5 @@ coverage ginkgo.report src/assistant/internal/l10n/out/translate.en-US.json + +.DS_Store diff --git a/.vscode/settings.json b/.vscode/settings.json index 4aa0ace..dad81f5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,7 @@ "beezledub", "bindnative", "bodyclose", + "clif", "cogen", "colors", "Comparables", @@ -36,6 +37,7 @@ "ipmask", "ipnet", "linters", + "magick", "memfs", "nakedret", "nolint", diff --git a/main b/main deleted file mode 100755 index c1b02b4..0000000 Binary files a/main and /dev/null differ diff --git a/public-cobrass-clif-api.go b/public-cobrass-clif-api.go new file mode 100644 index 0000000..2e737db --- /dev/null +++ b/public-cobrass-clif-api.go @@ -0,0 +1,68 @@ +package cobrass + +import ( + "github.com/snivilised/cobrass/src/clif" +) + +type ( + // ThirdPartyFlagName raw name of a flag, ie without the leading --/- + ThirdPartyFlagName = clif.ThirdPartyFlagName + + // ThirdPartyOptionValue the string value of an option. Since this option + // is being delegated to a third party command, it does not have to be + // of a particular native go type and can be composed from a go type + // using the value's String() method. + ThirdPartyOptionValue = clif.ThirdPartyOptionValue + + // PresentFlagsCollection represents the set of third party flags + // presented by the user on the command line. + // (NB: Cobra does not currently have a mechanism to collect third + // party flags, by convention, anything that follows " -- "), therefore + // we need to collect and handle these flags/options explicitly, + // which is less than ideal. + // A difference between PresentFlagsCollection and ThirdPartyCommandLine + // is that switch flags have a true/false option value in PresentFlagsCollection + // but not in ThirdPartyCommandLine. + PresentFlagsCollection = clif.PresentFlagsCollection + + // ThirdPartyPresentFlags (see PresentFlagsCollection) + ThirdPartyPresentFlags = clif.ThirdPartyPresentFlags + + // KnownByCollection collection maps a full flag name to the + // short name it is also known by. If a flag does not + // have a short name, it should be mapped to the empty + // string. + KnownByCollection = clif.KnownByCollection + + // ThirdPartyFlagKnownBy (see KnownByCollection). + ThirdPartyFlagKnownBy = clif.ThirdPartyFlagKnownBy + + // ThirdPartyCommandLine represents the collection of flags + // used to invoke a third party command. This collection + // represents the raw flags used for the invocation in + // the order required by the third party command. It also means + // that this collection contains the leading --/- not just + // the names of the flags and options. + // For example, to invoke the magick command we may want to + // compose this collection with: + // magick --strip --interlace plane --gaussian-blur 0.05 + // and in this case, the list would be defined as a string slice: + // []string{"--strip", "--interlace", "plane", "--gaussian-blur", "0.05"} + ThirdPartyCommandLine = clif.ThirdPartyCommandLine + + // ExternalThirdParty base struct for cli applications using the + // entry paradigm that need to delegate an invocation to an + // external third party command. + ExternalThirdParty = clif.ExternalThirdParty +) + +var ( + // Evaluate merges the secondary command line with the present flags. + // The flags that occur in present take precedence over those in + // secondary. There is a slight complication caused by the fact that + // a flag in the present set may be in the secondary set but in the opposite + // form; eg a flag may be in its short from in present but in long form + // in secondary. This is resolved by the knownBy set. The present set + // contains flags in their bare long form (bare as in without dash prefix). + Evaluate = clif.Evaluate +) diff --git a/src/clif/clif-suite_test.go b/src/clif/clif-suite_test.go new file mode 100644 index 0000000..7a5d0ca --- /dev/null +++ b/src/clif/clif-suite_test.go @@ -0,0 +1,13 @@ +package clif_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestClif(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Clif Suite") +} diff --git a/src/clif/evaluate.go b/src/clif/evaluate.go new file mode 100644 index 0000000..14c1ee3 --- /dev/null +++ b/src/clif/evaluate.go @@ -0,0 +1,201 @@ +package clif + +import ( + "strings" + + "github.com/samber/lo" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" +) + +var booleans = []string{"true", "false"} + +type ( + tokenInput struct { + token string + lead string + bare string + optionValue string + existingCL ThirdPartyCommandLine + presentFlags PresentFlagsCollection + knownBy KnownByCollection + } + + handleTokenResult struct { + doConcatenate bool + } + + concatIfFunc func(input *tokenInput) *handleTokenResult + concatenateResult struct { + commandLine ThirdPartyCommandLine + handleResult handleTokenResult + } +) + +func (i *tokenInput) yoke(nextIndex int, secondaryCL ThirdPartyCommandLine) int { + const ( + unaryIncrement = 1 + pairIncrement = 2 + ) + + handleAsPair := false + + if nextIndex < len(secondaryCL) { + next := secondaryCL[nextIndex] + nextLead, nextBare := split(next) + + if strings.HasPrefix(i.lead, "-") && !strings.HasPrefix(nextLead, "-") { + i.optionValue = nextBare + handleAsPair = true + } + } + + return lo.Ternary(handleAsPair, pairIncrement, unaryIncrement) +} + +func (i *tokenInput) concatIf(concatFunc concatIfFunc) *concatenateResult { + handleResult := concatFunc(i) + + if handleResult.doConcatenate { + i.existingCL = append(i.existingCL, i.token) + + if i.optionValue != "" { + i.existingCL = append(i.existingCL, i.optionValue) + } + } + + return &concatenateResult{ + commandLine: i.existingCL, + handleResult: *handleResult, + } +} + +func concatenate(input *tokenInput) *handleTokenResult { + var ( + result = &handleTokenResult{} + ) + + if input.lead == "" { + result.doConcatenate = true + + return result + } + + if _, found := input.presentFlags[input.bare]; found { + return result + } + + aka := input.knownBy[input.bare] + _, found := input.presentFlags[aka] + result.doConcatenate = !found + + return result +} + +// Evaluate merges the secondary command line with the present flags. +// The flags that occur in present take precedence over those in +// secondary. There is a slight complication caused by the fact that +// a flag in the present set may be in the secondary set but in the opposite +// form; eg a flag may be in its short from in present but in long form +// in secondary. This is resolved by the knownBy set. The present set +// contains flags in their bare long form. +func Evaluate(presentFlags PresentFlagsCollection, + knownBy KnownByCollection, + secondaryCL ThirdPartyCommandLine, +) ThirdPartyCommandLine { + result := &concatenateResult{} + bilateralKnownBy := composeBilateral(knownBy) + + result.commandLine = spreadFlags(presentFlags) + + if len(secondaryCL) == 0 { + return result.commandLine + } + + if len(secondaryCL) == 1 { + token := secondaryCL[0] + lead, bare := split(token) + + input := &tokenInput{ + token: token, + lead: lead, + bare: bare, + existingCL: result.commandLine, + presentFlags: presentFlags, + knownBy: bilateralKnownBy, + } + result = input.concatIf(concatenate) + + return result.commandLine + } + + for t, n := 0, 1; t < len(secondaryCL); { + token := secondaryCL[t] + lead, bare := split(token) + + input := &tokenInput{ + token: token, + lead: lead, + bare: bare, + existingCL: result.commandLine, + presentFlags: presentFlags, + knownBy: bilateralKnownBy, + } + increment := input.yoke(n, secondaryCL) + result = input.concatIf(concatenate) + + t += increment + n += increment + } + + return result.commandLine +} + +func split(token string) (string, string) { //nolint:gocritic // pedant + var ( + lead string + bare = token + ) + + if strings.HasPrefix(token, "--") { + lead = "--" + bare = token[2:] + } else if strings.HasPrefix(token, "-") { + lead = "-" + bare = token[1:] + } + + return lead, bare +} + +func spreadFlags(presentFlags PresentFlagsCollection) ThirdPartyCommandLine { + commandLine := ThirdPartyCommandLine{} + + for _, flag := range presentFlags.Keys() { + option := presentFlags[flag] + dash := lo.Ternary(len(flag) == 1, "-", "--") + prefixed := dash + flag + withOption := !slices.Contains(booleans, option) + + commandLine = append(commandLine, prefixed) + + if withOption { + commandLine = append(commandLine, option) + } + } + + return commandLine +} + +func composeBilateral(knownBy KnownByCollection) KnownByCollection { + const twice = 2 + bilateral := make(KnownByCollection, len(knownBy)*twice) + + maps.Copy(bilateral, knownBy) + + for long, short := range knownBy { + bilateral[short] = long + } + + return bilateral +} diff --git a/src/clif/evaluate_test.go b/src/clif/evaluate_test.go new file mode 100644 index 0000000..407efc2 --- /dev/null +++ b/src/clif/evaluate_test.go @@ -0,0 +1,277 @@ +package clif_test + +import ( + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/snivilised/cobrass/src/clif" +) + +type evaluateTE struct { + baseTE + present clif.PresentFlagsCollection + secondary clif.ThirdPartyCommandLine +} + +var _ = Describe("Evaluate", Ordered, func() { + var knownBy clif.KnownByCollection + + BeforeAll(func() { + knownBy = clif.KnownByCollection{ + "dry-run": "D", + "gaussian-blur": "b", + "sampling-factor": "f", + "interlace": "i", + "strip": "s", + "quality": "q", + } + }) + + DescribeTable("ThirdPartyCommandLine", + func(entry *evaluateTE) { + actual := clif.Evaluate(entry.present, knownBy, entry.secondary) + Expect(actual).To(HaveExactElements(entry.expected)) + }, + func(entry *evaluateTE) string { + return fmt.Sprintf("🧪 ===> given: '%v', should: return '%v'", + entry.given, entry.shouldReturn, + ) + }, + + // secondary empty + // + Entry(nil, &evaluateTE{ + baseTE: baseTE{ + given: "present contains single switch; secondary is empty", + shouldReturn: "present", + expected: []string{"--dry-run"}, + }, + present: clif.PresentFlagsCollection{ + "dry-run": "true", + }, + secondary: clif.ThirdPartyCommandLine{}, + }), + + Entry(nil, &evaluateTE{ + baseTE: baseTE{ + given: "present contains single flag; secondary is empty", + shouldReturn: "present", + expected: []string{"--sampling-factor", "4:2:0"}, + }, + present: clif.PresentFlagsCollection{ + "sampling-factor": "4:2:0", + }, + secondary: clif.ThirdPartyCommandLine{}, + }), + + Entry(nil, &evaluateTE{ + baseTE: baseTE{ + given: "present contains single flag; secondary is empty", + shouldReturn: "present", + expected: []string{"--sampling-factor", "4:2:0"}, + }, + present: clif.PresentFlagsCollection{ + "sampling-factor": "4:2:0", + }, + secondary: clif.ThirdPartyCommandLine{}, + }), + + Entry(nil, &evaluateTE{ + baseTE: baseTE{ + given: "present contains flag and a switch; secondary is empty", + shouldReturn: "all present", + expected: []string{"--dry-run", "--sampling-factor", "4:2:0"}, + }, + present: clif.PresentFlagsCollection{ + "dry-run": "true", + "sampling-factor": "4:2:0", + }, + secondary: clif.ThirdPartyCommandLine{}, + }), + // + // end: secondary empty + + // single secondary token + // + // ---> secondary switch in present + Entry(nil, &evaluateTE{ + baseTE: baseTE{ + given: "present contains single switch; single long secondary switch in present", + shouldReturn: "present, ignore secondary", + expected: []string{"--dry-run"}, + }, + present: clif.PresentFlagsCollection{ + "dry-run": "true", + }, + secondary: clif.ThirdPartyCommandLine{"--dry-run"}, + }), + + Entry(nil, &evaluateTE{ + baseTE: baseTE{ + given: "present contains single switch; single short secondary switch in present", + shouldReturn: "present, ignore secondary", + expected: []string{"--dry-run"}, + }, + present: clif.PresentFlagsCollection{ + "dry-run": "true", + }, + secondary: clif.ThirdPartyCommandLine{"-D"}, + }), + // ---> secondary switch NOT in present + Entry(nil, &evaluateTE{ + baseTE: baseTE{ + given: "present contains single switch; single long secondary switch NOT in present", + shouldReturn: "present with secondary", + expected: []string{"--dry-run", "--strip"}, + }, + present: clif.PresentFlagsCollection{ + "dry-run": "true", + }, + secondary: clif.ThirdPartyCommandLine{"--strip"}, + }), + + Entry(nil, &evaluateTE{ + baseTE: baseTE{ + given: "present contains single switch; single short secondary switch NOT in present", + shouldReturn: "present with secondary", + expected: []string{"--dry-run", "-s"}, + }, + present: clif.PresentFlagsCollection{ + "dry-run": "true", + }, + secondary: clif.ThirdPartyCommandLine{"-s"}, + }), + // + // end: single secondary token + + // single flag/option secondary tokens + // + // ---> secondary flag in present + + Entry(nil, &evaluateTE{ + baseTE: baseTE{ + given: "present contains single flag; long secondary flag/option in present", + shouldReturn: "present, ignore secondary", + expected: []string{"--sampling-factor", "4:2:0"}, + }, + present: clif.PresentFlagsCollection{ + "sampling-factor": "4:2:0", + }, + secondary: clif.ThirdPartyCommandLine{"--sampling-factor", "2x1"}, + }), + + Entry(nil, &evaluateTE{ + baseTE: baseTE{ + given: "present contains single flag; short secondary flag/option in present", + shouldReturn: "present, ignore secondary", + expected: []string{"--sampling-factor", "4:2:0"}, + }, + present: clif.PresentFlagsCollection{ + "sampling-factor": "4:2:0", + }, + secondary: clif.ThirdPartyCommandLine{"-f", "2x1"}, + }), + // ---> secondary flag NOT in present + Entry(nil, &evaluateTE{ + baseTE: baseTE{ + given: "present contains single flag; long secondary flag/option NOT in present", + shouldReturn: "present with secondary", + expected: []string{"--sampling-factor", "4:2:0", "--gaussian-blur", "0.05"}, + }, + present: clif.PresentFlagsCollection{ + "sampling-factor": "4:2:0", + }, + secondary: clif.ThirdPartyCommandLine{"--gaussian-blur", "0.05"}, + }), + + Entry(nil, &evaluateTE{ + baseTE: baseTE{ + given: "present contains single flag; short secondary flag/option NOT in present", + shouldReturn: "present with secondary", + expected: []string{"--sampling-factor", "4:2:0", "-b", "0.05"}, + }, + present: clif.PresentFlagsCollection{ + "sampling-factor": "4:2:0", + }, + secondary: clif.ThirdPartyCommandLine{"-b", "0.05"}, + }), + // + // end: single flag/option secondary tokens + + // secondary switch followed by a flag + // + Entry(nil, &evaluateTE{ + baseTE: baseTE{ + given: "secondary switch followed by a flag; both in present", + shouldReturn: "present, ignore secondary", + expected: []string{"--dry-run", "--sampling-factor", "4:2:0"}, + }, + present: clif.PresentFlagsCollection{ + "dry-run": "true", + "sampling-factor": "4:2:0", + }, + secondary: clif.ThirdPartyCommandLine{"--dry-run", "--sampling-factor", "2x1"}, + }), + + Entry(nil, &evaluateTE{ + baseTE: baseTE{ + given: "secondary switch followed by a flag; switch in present", + shouldReturn: "present, with secondary flag", + expected: []string{"--dry-run", "--sampling-factor", "2x1"}, + }, + present: clif.PresentFlagsCollection{ + "dry-run": "true", + }, + secondary: clif.ThirdPartyCommandLine{"--dry-run", "--sampling-factor", "2x1"}, + }), + + Entry(nil, &evaluateTE{ + baseTE: baseTE{ + given: "secondary switch followed by a flag; flag in present", + shouldReturn: "present, with secondary switch", + expected: []string{"--sampling-factor", "4:2:0", "--dry-run"}, + }, + present: clif.PresentFlagsCollection{ + "sampling-factor": "4:2:0", + }, + secondary: clif.ThirdPartyCommandLine{"--dry-run", "--sampling-factor", "2x1"}, + }), + + Entry(nil, &evaluateTE{ + baseTE: baseTE{ + given: "secondary switch followed by a flag; neither in present", + shouldReturn: "present, secondary switch and flag", + expected: []string{"--gaussian-blur", "0.05", "--dry-run", "--sampling-factor", "2x1"}, + }, + present: clif.PresentFlagsCollection{ + "gaussian-blur": "0.05", + }, + secondary: clif.ThirdPartyCommandLine{"--dry-run", "--sampling-factor", "2x1"}, + }), + // + // end: secondary switch followed by a flag + + Entry(nil, &evaluateTE{ + baseTE: baseTE{ + given: "many in present; many in secondary", + shouldReturn: "present flags/options overriding secondary flags/options", + expected: []string{ + "--gaussian-blur", "0.05", + "-i", "plane", + "-D", + "-f", "2x1", + "--strip", + }, + }, + present: clif.PresentFlagsCollection{ + "gaussian-blur": "0.05", + "i": "plane", + }, + secondary: clif.ThirdPartyCommandLine{ + "-D", "-f", "2x1", "--strip", "--gaussian-blur", "0.15", "--interlace", "line", + }, + }), + ) +}) diff --git a/src/clif/expand.go b/src/clif/expand.go new file mode 100644 index 0000000..89aee9c --- /dev/null +++ b/src/clif/expand.go @@ -0,0 +1,15 @@ +package clif + +func Expand(before, flags ThirdPartyCommandLine, after ...ThirdPartyFlagName) []string { + // approxTokensPerFlag: this is an approximate value because switches + // do not need an option value + const approxTokensPerFlag = 2 + + capacity := len(before) + (len(flags) * approxTokensPerFlag) + len(after) + allFlags := make([]string, 0, capacity) + allFlags = append(allFlags, before...) + allFlags = append(allFlags, flags...) + allFlags = append(allFlags, after...) + + return allFlags +} diff --git a/src/clif/expand_test.go b/src/clif/expand_test.go new file mode 100644 index 0000000..5894a42 --- /dev/null +++ b/src/clif/expand_test.go @@ -0,0 +1,95 @@ +package clif_test + +import ( + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/snivilised/cobrass/src/clif" +) + +type baseTE struct { + given string + shouldReturn string + expected []string +} + +type expandTE struct { + baseTE + before clif.ThirdPartyCommandLine + flags clif.ThirdPartyCommandLine + after []clif.ThirdPartyFlagName +} + +var _ = Describe("Expand", func() { + DescribeTable("ThirdPartyFlags", + func(entry *expandTE) { + actual := clif.Expand(entry.before, entry.flags, entry.after...) + Expect(actual).To(HaveExactElements(entry.expected)) + }, + func(entry *expandTE) string { + return fmt.Sprintf("🧪 ===> given: '%v', should: return '%v'", + entry.given, entry.shouldReturn, + ) + }, + + Entry(nil, &expandTE{ + baseTE: baseTE{ + given: "none before/none after", + shouldReturn: "the single before", + expected: []string{"--dry-run"}, + }, + before: clif.ThirdPartyCommandLine{}, + flags: clif.ThirdPartyCommandLine{"--dry-run"}, + }), + + Entry(nil, &expandTE{ + baseTE: baseTE{ + given: "1 before, none after", + shouldReturn: "the single before with flags", + expected: []string{"file.jpg", "--dry-run"}, + }, + before: clif.ThirdPartyCommandLine{"file.jpg"}, + flags: clif.ThirdPartyCommandLine{"--dry-run"}, + }), + + Entry(nil, &expandTE{ + baseTE: baseTE{ + given: "none before/none after", + shouldReturn: "after and single after", + expected: []string{"--dry-run", "result.jpg"}, + }, + before: clif.ThirdPartyCommandLine{}, + flags: clif.ThirdPartyCommandLine{"--dry-run"}, + after: []clif.ThirdPartyFlagName{"result.jpg"}, + }), + + Entry(nil, &expandTE{ + baseTE: baseTE{ + given: "multi before, some flags and single after", + shouldReturn: "before, flags and after", + expected: []string{"first.jpg", "second.jpg", + "--dry-run", "--interlace", "plane", + "result-1.jpg", "result-2.jpg", + }, + }, + before: clif.ThirdPartyCommandLine{"first.jpg", "second.jpg"}, + flags: clif.ThirdPartyCommandLine{"--dry-run", "--interlace", "plane"}, + after: []clif.ThirdPartyFlagName{"result-1.jpg", "result-2.jpg"}, + }), + + Entry(nil, &expandTE{ + baseTE: baseTE{ + given: "multi before, no flags and single after", + shouldReturn: "before and after", + expected: []string{"first.jpg", "second.jpg", + "result-1.jpg", "result-2.jpg", + }, + }, + before: clif.ThirdPartyCommandLine{"first.jpg", "second.jpg"}, + flags: clif.ThirdPartyCommandLine{}, + after: []clif.ThirdPartyFlagName{"result-1.jpg", "result-2.jpg"}, + }), + ) +}) diff --git a/src/clif/public-clif-api.go b/src/clif/public-clif-api.go new file mode 100644 index 0000000..a54bf1c --- /dev/null +++ b/src/clif/public-clif-api.go @@ -0,0 +1,65 @@ +package clif + +import ( + "github.com/snivilised/cobrass/src/collections" +) + +type ( + // ThirdPartyFlagName raw name of a flag, ie without the leading --/- + ThirdPartyFlagName = string + + // ThirdPartyOptionValue the string value of an option. Since this option + // is being delegated to a third party command, it does not have to be + // of a particular native go type and can be composed from a go type + // using the value's String() method. + ThirdPartyOptionValue = string + + // PresentFlagsCollection represents the set of third party flags + // presented by the user on the command line. + // (NB: Cobra does not currently have a mechanism to collect third + // party flags, by convention, anything that follows " -- "), therefore + // we need to collect and handle these flags/options explicitly, + // which is less than ideal. + // A difference between PresentFlagsCollection and ThirdPartyCommandLine + // is that switch flags have a true/false option value in PresentFlagsCollection + // but not in ThirdPartyCommandLine. + PresentFlagsCollection = collections.OrderedKeysMap[ThirdPartyFlagName, ThirdPartyOptionValue] + + // ThirdPartyPresentFlags (see PresentFlagsCollection). + ThirdPartyPresentFlags PresentFlagsCollection + + // KnownByCollection collection maps a full flag name to the + // short name it is also known by. If a flag does not + // have a short name, it should be mapped to the empty + // string. + KnownByCollection map[ThirdPartyFlagName]ThirdPartyFlagName + + // ThirdPartyFlagKnownBy (see KnownByCollection). + ThirdPartyFlagKnownBy KnownByCollection + + // ThirdPartyCommandLine represents the collection of flags + // used to invoke a third party command. This collection + // represents the raw flags used for the invocation in + // the order required by the third party command. It also means + // that this collection contains the leading --/- not just + // the names of the flags and options. + // For example, to invoke the magick command we may want to + // compose this collection with: + // magick --strip --interlace plane --gaussian-blur 0.05 + // and in this case, the list would be defined as a string slice: + // []string{"--strip", "--interlace", "plane", "--gaussian-blur", "0.05"} + ThirdPartyCommandLine []string + + // ExternalThirdParty base struct for cli applications using the + // entry paradigm that need to delegate an invocation to an + // external third party command. + ExternalThirdParty struct { + // KnownBy represents the collection of all possible flags that + // can be specified in a particular invocation (see ThirdPartyFlagKnownBy) + KnownBy ThirdPartyFlagKnownBy + + // Known represents a particular invocation of a third party + // command (see ThirdPartyFlagsInvocation). + Known ThirdPartyCommandLine + } +) diff --git a/src/collections/sorted-keys-map.go b/src/collections/sorted-keys-map.go new file mode 100644 index 0000000..2fee73d --- /dev/null +++ b/src/collections/sorted-keys-map.go @@ -0,0 +1,27 @@ +package collections + +import ( + "cmp" + + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" +) + +// https://go.dev/blog/comparable + +// OrderedKeysMap provides a deterministic way to iterate the contents +// of a map. The problem with a regular iteration is that the order +// the elements are presented is not guaranteed. This can be +// detrimental when consistency is paramount. Any map that needs to +// be iterated deterministically, should use the Keys() function for +// the iteration loop and obtain the value using this key to index +// into the map. +type OrderedKeysMap[K cmp.Ordered, V any] map[K]V + +func (m *OrderedKeysMap[K, V]) Keys() []K { + keys := maps.Keys(*m) + + slices.Sort(keys) + + return keys +}