Separate comparison side selectors
This commit is contained in:
@@ -10,57 +10,6 @@ import (
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestComparisonSelectionInheritsFieldsIndependently(t *testing.T) {
|
||||
leftConfig := commandkit.Supplied[string]{Value: "before.yml", Set: true}
|
||||
leftOverlay := commandkit.Supplied[string]{Value: "base", Set: true}
|
||||
tests := []struct {
|
||||
name string
|
||||
left Selection
|
||||
right Selection
|
||||
want Selection
|
||||
}{
|
||||
{
|
||||
name: "omitted fields stay omitted",
|
||||
},
|
||||
{
|
||||
name: "both fields inherit supplied state",
|
||||
left: Selection{Config: leftConfig, Overlay: leftOverlay},
|
||||
want: Selection{Config: leftConfig, Overlay: leftOverlay},
|
||||
},
|
||||
{
|
||||
name: "explicit empty config overrides while overlay inherits",
|
||||
left: Selection{Config: leftConfig, Overlay: leftOverlay},
|
||||
right: Selection{Config: commandkit.Supplied[string]{Set: true}},
|
||||
want: Selection{Config: commandkit.Supplied[string]{Set: true}, Overlay: leftOverlay},
|
||||
},
|
||||
{
|
||||
name: "explicit overlay overrides while config inherits",
|
||||
left: Selection{Config: leftConfig, Overlay: leftOverlay},
|
||||
right: Selection{Overlay: commandkit.Supplied[string]{Value: "candidate", Set: true}},
|
||||
want: Selection{Config: leftConfig, Overlay: commandkit.Supplied[string]{Value: "candidate", Set: true}},
|
||||
},
|
||||
{
|
||||
name: "explicit empty overlay remains supplied",
|
||||
left: Selection{Config: leftConfig, Overlay: leftOverlay},
|
||||
right: Selection{Overlay: commandkit.Supplied[string]{Set: true}},
|
||||
want: Selection{Config: leftConfig, Overlay: commandkit.Supplied[string]{Set: true}},
|
||||
},
|
||||
{
|
||||
name: "both explicit right fields replace left",
|
||||
left: Selection{Config: leftConfig, Overlay: leftOverlay},
|
||||
right: Selection{Config: commandkit.Supplied[string]{Value: "after.yml", Set: true}, Overlay: commandkit.Supplied[string]{Value: "new", Set: true}},
|
||||
want: Selection{Config: commandkit.Supplied[string]{Value: "after.yml", Set: true}, Overlay: commandkit.Supplied[string]{Value: "new", Set: true}},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := inheritComparisonSelection(test.left, test.right); got != test.want {
|
||||
t.Fatalf("inherited selection = %#v, want %#v", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestComparisonHelpLimitsFlagsAndDefaultsFormatToYAML(t *testing.T) {
|
||||
scope := Scope[struct{}, struct{}]{
|
||||
Name: "app", Operations: []Operation{Diff, Show, Explain, Validate, Sources},
|
||||
@@ -125,7 +74,6 @@ func TestComparisonPreparation(t *testing.T) {
|
||||
t.Fatalf("comparison default scope = %q, want app", input.plan.scopeName)
|
||||
}
|
||||
if want := (Selection{
|
||||
Config: commandkit.Supplied[string]{Value: "before.yml", Set: true},
|
||||
Overlay: commandkit.Supplied[string]{Value: "candidate", Set: true},
|
||||
}); input.against != want {
|
||||
t.Fatalf("resolved right selection = %#v, want %#v", input.against, want)
|
||||
|
||||
@@ -62,7 +62,7 @@ func TestComparisonPreparesEqualSelectionsWithSeparateTypedInputs(t *testing.T)
|
||||
}
|
||||
return struct{}{}
|
||||
},
|
||||
Selectors: []Selector[*input]{{Binding: enabled}, {Binding: limit}, {Binding: tags}},
|
||||
Selectors: []Selector[*input]{{Binding: enabled, AgainstName: "against-enabled"}, {Binding: limit, AgainstName: "against-limit"}, {Binding: tags, AgainstName: "against-tag"}},
|
||||
CheckInput: func(scope string, selection Selection, value *input) error {
|
||||
checkCalls++
|
||||
preparedInputs = append(preparedInputs, value)
|
||||
@@ -91,7 +91,7 @@ func TestComparisonPreparesEqualSelectionsWithSeparateTypedInputs(t *testing.T)
|
||||
app := newConfigTestApp(t, Command(suite))
|
||||
outcome := app.Execute(context.Background(), []string{
|
||||
"config", "diff", "--config", "same.yml", "--against-config", "same.yml", "--overlay=",
|
||||
"--enabled=false", "--limit=0", "--tag", "alpha", "--tag", "beta",
|
||||
"--enabled=false", "--limit=0", "--tag", "alpha", "--tag", "beta", "--against-enabled=false", "--against-limit=0", "--against-tag", "alpha", "--against-tag", "beta", "--against-overlay=",
|
||||
}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess {
|
||||
t.Fatalf("valid comparison outcome = %#v", outcome)
|
||||
@@ -99,7 +99,7 @@ func TestComparisonPreparesEqualSelectionsWithSeparateTypedInputs(t *testing.T)
|
||||
if inputCalls != 2 || checkCalls != 2 || stateCalls != 1 {
|
||||
t.Fatalf("preparation calls input=%d check=%d state=%d", inputCalls, checkCalls, stateCalls)
|
||||
}
|
||||
if parseCalls != 3 {
|
||||
if parseCalls != 6 {
|
||||
t.Fatalf("typed values were converted %d times, want once per supplied occurrence", parseCalls)
|
||||
}
|
||||
wantSelection := Selection{
|
||||
@@ -161,7 +161,7 @@ func TestComparisonProviderReceivesCheckedPairAndSharedState(t *testing.T) {
|
||||
t.Fatalf("comparison outcome = %#v", outcome)
|
||||
}
|
||||
left := Selection{Config: commandkit.Supplied[string]{Value: "before.yml", Set: true}}
|
||||
right := Selection{Config: left.Config, Overlay: commandkit.Supplied[string]{Set: true}}
|
||||
right := Selection{Overlay: commandkit.Supplied[string]{Set: true}}
|
||||
if request.Left.Selection != left || request.Right.Selection != right {
|
||||
t.Fatalf("provider selections = %#v", request)
|
||||
}
|
||||
|
||||
@@ -141,6 +141,9 @@ type Selector[I any] struct {
|
||||
Binding commandkit.Flag[I]
|
||||
// Scopes lists the scope names that receive this flag's value.
|
||||
Scopes []string
|
||||
// AgainstName optionally declares the long name of an independent
|
||||
// right-side comparison flag using the same parser and setter.
|
||||
AgainstName string
|
||||
}
|
||||
|
||||
// Suite declares the typed input, shared state, selectors, and configuration
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
func TestConfigDiffExecutesWithDefaultAndExplicitYAMLAndInheritedSelections(t *testing.T) {
|
||||
func TestConfigDiffExecutesWithDefaultAndExplicitYAMLAndIndependentSelections(t *testing.T) {
|
||||
type settings struct {
|
||||
Greeting string `yaml:"greeting"`
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func TestConfigDiffExecutesWithDefaultAndExplicitYAMLAndInheritedSelections(t *t
|
||||
if outcome.Kind != commandkit.OutcomeSuccess {
|
||||
t.Fatalf("default YAML comparison outcome = %#v", outcome)
|
||||
}
|
||||
if len(selections) != 2 || !selections[0].Config.Set || selections[0].Config.Value != "before.yml" || !selections[1].Config.Set || selections[1].Config.Value != "after.yml" || selections[1].Overlay != selections[0].Overlay {
|
||||
if len(selections) != 2 || !selections[0].Config.Set || selections[0].Config.Value != "before.yml" || !selections[1].Config.Set || selections[1].Config.Value != "after.yml" || selections[1].Overlay.Set {
|
||||
t.Fatalf("comparison selections = %#v", selections)
|
||||
}
|
||||
var document yaml.Node
|
||||
@@ -256,7 +256,7 @@ func TestConfigDiffRepeatedInvocationsPrepareFreshInputsAndState(t *testing.T) {
|
||||
if len(observed) != 2 || observed[0].left.ID == observed[0].right.ID || observed[1].left.ID == observed[1].right.ID || observed[0].left.ID == observed[1].left.ID || observed[0].right.ID == observed[1].right.ID {
|
||||
t.Fatalf("input instances were reused: %#v", observed)
|
||||
}
|
||||
if observed[0].left.Label != "current" || observed[0].right.Label != "current" || observed[1].left.Label != "current" || observed[1].right.Label != "current" {
|
||||
if observed[0].left.Label != "current" || observed[0].right.Label != "" || observed[1].left.Label != "current" || observed[1].right.Label != "" {
|
||||
t.Fatalf("comparison inputs missed captured flag values: %#v", observed)
|
||||
}
|
||||
if observed[0].state == nil || observed[1].state == nil || observed[0].state == observed[1].state || observed[0].state.ID != 1 || observed[1].state.ID != 2 {
|
||||
|
||||
@@ -23,8 +23,9 @@ type scopeInventory struct {
|
||||
}
|
||||
|
||||
type preparedScope[I, S any] struct {
|
||||
scope Scope[I, S]
|
||||
bindings []commandkit.Flag[I]
|
||||
scope Scope[I, S]
|
||||
bindings []commandkit.Flag[I]
|
||||
againstBindings []commandkit.Flag[I]
|
||||
}
|
||||
|
||||
type preparedSuite[I, S any] struct {
|
||||
@@ -34,6 +35,7 @@ type preparedSuite[I, S any] struct {
|
||||
scopes []*preparedScope[I, S]
|
||||
byName map[string]*preparedScope[I, S]
|
||||
bindingsByOperation map[Operation][]commandkit.Flag[I]
|
||||
againstBindings []commandkit.Flag[I]
|
||||
scopeChoices string
|
||||
operations []Operation
|
||||
inventory scopeInventory
|
||||
@@ -90,9 +92,13 @@ func captureSuite[I, S any](suite Suite[I, S]) *preparedSuite[I, S] {
|
||||
bindingsByOperation: make(map[Operation][]commandkit.Flag[I]),
|
||||
}
|
||||
selectors := make([]Selector[I], len(suite.Selectors))
|
||||
against := make([]commandkit.Flag[I], len(suite.Selectors))
|
||||
for index, selector := range suite.Selectors {
|
||||
selector.Scopes = append([]string(nil), selector.Scopes...)
|
||||
selectors[index] = selector
|
||||
if selector.AgainstName != "" {
|
||||
against[index] = commandkit.RenameFlag(selector.Binding, selector.AgainstName)
|
||||
}
|
||||
}
|
||||
|
||||
seenNames := make(map[string]struct{}, len(suite.Scopes))
|
||||
@@ -158,12 +164,18 @@ func captureSuite[I, S any](suite Suite[I, S]) *preparedSuite[I, S] {
|
||||
for scopeIndex := range prepared.scopes {
|
||||
if len(prepared.scopes[scopeIndex].scope.Operations) > 0 {
|
||||
prepared.scopes[scopeIndex].bindings = append(prepared.scopes[scopeIndex].bindings, selector.Binding)
|
||||
if selector.AgainstName != "" && containsOperation(prepared.scopes[scopeIndex].scope.Operations, Diff) {
|
||||
prepared.scopes[scopeIndex].againstBindings = append(prepared.scopes[scopeIndex].againstBindings, against[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, name := range uniqueScopes {
|
||||
if scope, exists := prepared.byName[name]; exists && len(scope.scope.Operations) > 0 {
|
||||
scope.bindings = append(scope.bindings, selector.Binding)
|
||||
if selector.AgainstName != "" && containsOperation(scope.scope.Operations, Diff) {
|
||||
scope.againstBindings = append(scope.againstBindings, against[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,10 +188,13 @@ func captureSuite[I, S any](suite Suite[I, S]) *preparedSuite[I, S] {
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, selector := range selectors {
|
||||
for index, selector := range selectors {
|
||||
for _, scope := range prepared.scopes {
|
||||
if containsOperation(scope.scope.Operations, operation) && selectorApplies(selector.Scopes, scope.scope.Name) {
|
||||
prepared.bindingsByOperation[operation] = append(prepared.bindingsByOperation[operation], selector.Binding)
|
||||
if operation == Diff && selector.AgainstName != "" {
|
||||
prepared.againstBindings = append(prepared.againstBindings, against[index])
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
79
integrations/ckconfig/independent_comparison_test.go
Normal file
79
integrations/ckconfig/independent_comparison_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package ckconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
commandkit "go.maximumdirect.net/commandkit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestComparisonSidesKeepIndependentSelectionsAndDefaults(t *testing.T) {
|
||||
type input struct {
|
||||
Label commandkit.Supplied[string]
|
||||
Tags []string
|
||||
}
|
||||
label := commandkit.ValueFlag(commandkit.FlagMeta{Name: "label"}, (*string)(nil), parseConfigString, func(i *input, value commandkit.Supplied[string]) { i.Label = value })
|
||||
tags := commandkit.RepeatFlag(commandkit.FlagMeta{Name: "tag"}, []string(nil), parseConfigString, func(i *input, value commandkit.Supplied[[]string]) { i.Tags = value.Value })
|
||||
var pair ComparisonRequest[input]
|
||||
calls := 0
|
||||
suite := Suite[input, struct{}]{
|
||||
NewInput: func() input {
|
||||
return input{Label: commandkit.Supplied[string]{Value: "factory"}, Tags: []string{"factory"}}
|
||||
},
|
||||
Selectors: []Selector[input]{{Binding: label, AgainstName: "against-label"}, {Binding: tags, AgainstName: "against-tag"}},
|
||||
Scopes: []Scope[input, struct{}]{{Name: "app", Operations: []Operation{Diff}, Compare: func(_ context.Context, request ComparisonRequest[input], _ struct{}) (commandkit.Acquired[[]configkit.Difference], error) {
|
||||
calls++
|
||||
pair = request
|
||||
return commandkit.Acquired[[]configkit.Difference]{}, nil
|
||||
}}},
|
||||
}
|
||||
app := newConfigTestApp(t, Command(suite))
|
||||
tests := []struct {
|
||||
args []string
|
||||
left, right Selection
|
||||
leftLabel, rightLabel commandkit.Supplied[string]
|
||||
leftTags, rightTags []string
|
||||
}{
|
||||
{leftLabel: commandkit.Supplied[string]{Value: "factory"}, rightLabel: commandkit.Supplied[string]{Value: "factory"}, leftTags: []string{"factory"}, rightTags: []string{"factory"}},
|
||||
{args: []string{"--config=left", "--overlay=base", "--label=", "--tag=a", "--tag=b"}, left: Selection{Config: commandkit.Supplied[string]{Value: "left", Set: true}, Overlay: commandkit.Supplied[string]{Value: "base", Set: true}}, leftLabel: commandkit.Supplied[string]{Set: true}, rightLabel: commandkit.Supplied[string]{Value: "factory"}, leftTags: []string{"a", "b"}, rightTags: []string{"factory"}},
|
||||
{args: []string{"--against-config=right", "--against-label=r", "--against-tag=x"}, right: Selection{Config: commandkit.Supplied[string]{Value: "right", Set: true}}, leftLabel: commandkit.Supplied[string]{Value: "factory"}, rightLabel: commandkit.Supplied[string]{Value: "r", Set: true}, leftTags: []string{"factory"}, rightTags: []string{"x"}},
|
||||
{args: []string{"--config=left", "--against-overlay=", "--label=l", "--against-label=r", "--tag=a", "--against-tag=b"}, left: Selection{Config: commandkit.Supplied[string]{Value: "left", Set: true}}, right: Selection{Overlay: commandkit.Supplied[string]{Set: true}}, leftLabel: commandkit.Supplied[string]{Value: "l", Set: true}, rightLabel: commandkit.Supplied[string]{Value: "r", Set: true}, leftTags: []string{"a"}, rightTags: []string{"b"}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
result := app.Execute(context.Background(), append([]string{"config", "diff"}, test.args...), commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if result.Kind != commandkit.OutcomeSuccess || pair.Left.Selection != test.left || pair.Right.Selection != test.right || pair.Left.Input.Label != test.leftLabel || pair.Right.Input.Label != test.rightLabel || !reflect.DeepEqual(pair.Left.Input.Tags, test.leftTags) || !reflect.DeepEqual(pair.Right.Input.Tags, test.rightTags) {
|
||||
t.Fatalf("args=%v outcome=%#v pair=%#v", test.args, result, pair)
|
||||
}
|
||||
}
|
||||
if calls != len(tests) {
|
||||
t.Fatalf("provider calls = %d", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComparisonRequiredCounterpartsAndCollisions(t *testing.T) {
|
||||
flag := commandkit.ValueFlag(commandkit.FlagMeta{Name: "choice", Required: true}, (*string)(nil), parseConfigString, func(i *string, value commandkit.Supplied[string]) { *i = value.Value })
|
||||
calls := 0
|
||||
scope := Scope[string, struct{}]{Name: "app", Operations: []Operation{Diff}, Compare: func(context.Context, ComparisonRequest[string], struct{}) (commandkit.Acquired[[]configkit.Difference], error) {
|
||||
calls++
|
||||
return commandkit.Acquired[[]configkit.Difference]{}, nil
|
||||
}}
|
||||
suite := Suite[string, struct{}]{Selectors: []Selector[string]{{Binding: flag, AgainstName: "against-choice"}}, Scopes: []Scope[string, struct{}]{scope}}
|
||||
app := newConfigTestApp(t, Command(suite))
|
||||
for _, args := range [][]string{{"--choice=x"}, {"--against-choice=x"}} {
|
||||
result := app.Execute(context.Background(), append([]string{"config", "diff"}, args...), commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if result.Kind != commandkit.OutcomeUsage || calls != 0 {
|
||||
t.Fatalf("missing side %v: %#v", args, result)
|
||||
}
|
||||
}
|
||||
result := app.Execute(context.Background(), []string{"config", "diff", "--choice=x", "--against-choice=y"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if result.Kind != commandkit.OutcomeSuccess || calls != 1 {
|
||||
t.Fatalf("complete sides: %#v", result)
|
||||
}
|
||||
suite.Selectors[0].AgainstName = "against-config"
|
||||
if _, err := commandkit.New(commandkit.Identity{Name: "app"}, Command(suite)); err == nil {
|
||||
t.Fatal("conflicting counterpart accepted")
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,13 @@ import (
|
||||
)
|
||||
|
||||
type configCommandInput[I, S any] struct {
|
||||
selection Selection
|
||||
against Selection
|
||||
scope commandkit.Supplied[string]
|
||||
path commandkit.Supplied[string]
|
||||
values commandkit.FlagValues[I]
|
||||
plan *configOperationPlan[I, S]
|
||||
selection Selection
|
||||
against Selection
|
||||
scope commandkit.Supplied[string]
|
||||
path commandkit.Supplied[string]
|
||||
values commandkit.FlagValues[I]
|
||||
againstValues commandkit.FlagValues[I]
|
||||
plan *configOperationPlan[I, S]
|
||||
}
|
||||
|
||||
type selectedScopeInput[I, S any] struct {
|
||||
@@ -85,6 +86,14 @@ func newConfigOperationCommand[I, S any](prepared *preparedSuite[I, S], operatio
|
||||
}
|
||||
return &(*input).values
|
||||
})...)
|
||||
if operation == Diff {
|
||||
flags = append(flags, commandkit.CaptureFlags(prepared.againstBindings, func(input **configCommandInput[I, S]) *commandkit.FlagValues[I] {
|
||||
if input == nil || *input == nil {
|
||||
return nil
|
||||
}
|
||||
return &(*input).againstValues
|
||||
})...)
|
||||
}
|
||||
|
||||
name, summary := operationDeclaration(operation)
|
||||
return commandkit.NewCommand(commandkit.CommandSpec[*configCommandInput[I, S]]{
|
||||
@@ -169,13 +178,12 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O
|
||||
scopes: make([]selectedScopeInput[I, S], 0, len(selected)),
|
||||
}
|
||||
if operation == Diff {
|
||||
input.against = inheritComparisonSelection(input.selection, input.against)
|
||||
scope := selected[0]
|
||||
left, err := prepareScopeInput(prepared, input.values, scope, input.selection)
|
||||
left, err := prepareScopeInput(prepared, input.values, scope, scope.bindings, input.selection, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("configuration scope %q left input: %w", scope.scope.Name, err)
|
||||
}
|
||||
right, err := prepareScopeInput(prepared, input.values, scope, input.against)
|
||||
right, err := prepareScopeInput(prepared, input.againstValues, scope, scope.againstBindings, input.against, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("configuration scope %q right input: %w", scope.scope.Name, err)
|
||||
}
|
||||
@@ -190,7 +198,7 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O
|
||||
return nil
|
||||
}
|
||||
for _, scope := range selected {
|
||||
value, err := prepareScopeInput(prepared, input.values, scope, input.selection)
|
||||
value, err := prepareScopeInput(prepared, input.values, scope, scope.bindings, input.selection, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("configuration scope %q: %w", scope.scope.Name, err)
|
||||
}
|
||||
@@ -200,14 +208,18 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareScopeInput[I, S any](prepared *preparedSuite[I, S], values commandkit.FlagValues[I], scope *preparedScope[I, S], selection Selection) (I, error) {
|
||||
func prepareScopeInput[I, S any](prepared *preparedSuite[I, S], values commandkit.FlagValues[I], scope *preparedScope[I, S], bindings []commandkit.Flag[I], selection Selection, suppliedOnly bool) (I, error) {
|
||||
var value I
|
||||
if prepared.newInput != nil {
|
||||
value = prepared.newInput()
|
||||
}
|
||||
// Availability was checked before this helper, so every binding for the
|
||||
// selected scope belongs to the operation's captured union.
|
||||
if err := values.ApplyTo(&value, scope.bindings...); err != nil {
|
||||
apply := values.ApplyTo
|
||||
if suppliedOnly {
|
||||
apply = values.ApplySuppliedTo
|
||||
}
|
||||
if err := apply(&value, bindings...); err != nil {
|
||||
return value, err
|
||||
}
|
||||
if prepared.checkInput != nil {
|
||||
@@ -218,16 +230,6 @@ func prepareScopeInput[I, S any](prepared *preparedSuite[I, S], values commandki
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func inheritComparisonSelection(left, right Selection) Selection {
|
||||
if !right.Config.Set {
|
||||
right.Config = left.Config
|
||||
}
|
||||
if !right.Overlay.Set {
|
||||
right.Overlay = left.Overlay
|
||||
}
|
||||
return right
|
||||
}
|
||||
|
||||
func joinOperations(operations []Operation) string {
|
||||
values := make([]string, len(operations))
|
||||
for index, operation := range operations {
|
||||
|
||||
Reference in New Issue
Block a user