diff --git a/index/server/pkg/server/endpoint.go b/index/server/pkg/server/endpoint.go index 107c49ba..9e763dea 100644 --- a/index/server/pkg/server/endpoint.go +++ b/index/server/pkg/server/endpoint.go @@ -481,16 +481,6 @@ func ServeUI(c *gin.Context) { func buildIndexAPIResponse(c *gin.Context, indexType string, wantV1Index bool, params IndexParams) { iconType := "" - archs := []string{} - var result util.FilterResult - - if params.Icon != nil { - iconType = *params.Icon - } - - if params.Arch != nil { - archs = append(archs, *params.Arch...) - } var bytes []byte var responseIndexPath, responseBase64IndexPath string @@ -571,39 +561,24 @@ func buildIndexAPIResponse(c *gin.Context, indexType string, wantV1Index bool, p } } - result = util.FilterDevfileSchemaVersion(index, minSchemaVersion, maxSchemaVersion) - result.Eval() - if !result.IsEval { - c.JSON(http.StatusInternalServerError, gin.H{ - "status": fmt.Sprintf("failed to apply schema version filter: %v", "unevaluated filter"), - }) - return - } else if result.Error != nil { + index, err = util.FilterDevfileSchemaVersion(index, minSchemaVersion, maxSchemaVersion) + if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ - "status": fmt.Sprintf("failed to apply schema version filter: %v", result.Error), + "status": fmt.Sprintf("failed to apply schema version filter: %v", err), }) return } - index = result.Index } } - // Filter the index if archs has been requested - if len(archs) > 0 { - result = util.FilterDevfileStrArrayField(index, util.ARRAY_PARAM_ARCHITECTURES, archs, wantV1Index) - result.Eval() - if !result.IsEval { - c.JSON(http.StatusInternalServerError, gin.H{ - "status": fmt.Sprintf("failed to apply schema version filter: %v", "unevaluated filter"), - }) - return - } else if result.Error != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "status": fmt.Sprintf("failed to apply archs filter: %v", result.Error), - }) - return - } - index = result.Index + + // Filter the fields of the index + index, err = filterFieldsByParams(index, wantV1Index, params) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "status": fmt.Sprintf("failed to perform field filtering: %v", err), + }) } + bytes, err = json.MarshalIndent(&index, "", " ") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ @@ -701,20 +676,13 @@ func fetchDevfile(c *gin.Context, name string, version string) ([]byte, indexSch } } - result := util.FilterDevfileSchemaVersion(index, minSchemaVersion, maxSchemaVersion) - result.Eval() - if !result.IsEval { - c.JSON(http.StatusInternalServerError, gin.H{ - "status": fmt.Sprintf("failed to apply schema version filter: %v", "unevaluated filter"), - }) - return []byte{}, indexSchema.Schema{} - } else if result.Error != nil { + index, err = util.FilterDevfileSchemaVersion(index, minSchemaVersion, maxSchemaVersion) + if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ - "status": fmt.Sprintf("failed to apply schema version filter: %v", result.Error), + "status": fmt.Sprintf("failed to apply schema version filter: %v", err), }) return []byte{}, indexSchema.Schema{} } - index = result.Index } for _, devfileIndex := range index { diff --git a/index/server/pkg/server/filter.go b/index/server/pkg/server/filter.go new file mode 100644 index 00000000..a27beeb8 --- /dev/null +++ b/index/server/pkg/server/filter.go @@ -0,0 +1,81 @@ +// +// Copyright Red Hat +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "fmt" + + indexSchema "github.com/devfile/registry-support/index/generator/schema" + "github.com/devfile/registry-support/index/server/pkg/util" +) + +const checkEvalRecursively = true + +func checkChildrenEval(result *util.FilterResult) error { + if !result.IsChildrenEval(checkEvalRecursively) { + errMsg := "there was a problem evaluating and logically filters" + + if result.Error != nil { + errMsg += ", see error for details: %v" + return fmt.Errorf(errMsg, result.Error) + } + + return fmt.Errorf(errMsg) + } + + return nil +} + +func filterFieldbyParam(index []indexSchema.Schema, wantV1Index bool, paramName string, paramValue any) util.FilterResult { + switch typedValue := paramValue.(type) { + case string: + return util.FilterDevfileStrField(index, paramName, typedValue, wantV1Index) + default: + return util.FilterDevfileStrField(index, paramName, fmt.Sprintf("%v", typedValue), wantV1Index) + } +} + +func filterFieldsByParams(index []indexSchema.Schema, wantV1Index bool, params IndexParams) ([]indexSchema.Schema, error) { + paramsMap := util.StructToMap(params) + results := []*util.FilterResult{} + var andResult util.FilterResult + + if len(paramsMap) == 0 { + return index, nil + } + + for paramName, paramValue := range paramsMap { + var result util.FilterResult + + if util.IsFieldParameter(paramName) { + result = filterFieldbyParam(index, wantV1Index, paramName, paramValue) + } else if util.IsArrayParameter(paramName) { + typedValues := paramValue.([]string) + result = util.FilterDevfileStrArrayField(index, paramName, typedValues, wantV1Index) + } + + results = append(results, &result) + } + + andResult = util.AndFilter(results...) + andResult.Eval() + + if err := checkChildrenEval(&andResult); err != nil { + return []indexSchema.Schema{}, err + } + + return andResult.Index, andResult.Error +} diff --git a/index/server/pkg/server/filter_test.go b/index/server/pkg/server/filter_test.go new file mode 100644 index 00000000..c0300bf5 --- /dev/null +++ b/index/server/pkg/server/filter_test.go @@ -0,0 +1,997 @@ +// +// Copyright Red Hat +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "fmt" + "reflect" + "sort" + "strings" + "testing" + + indexSchema "github.com/devfile/registry-support/index/generator/schema" + "github.com/devfile/registry-support/index/server/pkg/util" + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" +) + +var testIndexSchema = []indexSchema.Schema{ + { + Name: "devfileA", + DisplayName: "Python", + Description: "A python stack.", + Attributes: map[string]apiext.JSON{ + "attributeA": {}, + "attributeB": {}, + "attributeC": {}, + "attributeD": {}, + }, + Architectures: []string{"amd64", "arm64"}, + Tags: []string{"Python", "Django", "Flask"}, + Versions: []indexSchema.Version{ + { + Version: "v1.0.0", + Description: "A python stack.", + Architectures: []string{"amd64"}, + Tags: []string{"Python"}, + Resources: []string{"devfile.yaml"}, + StarterProjects: []string{"starterA"}, + }, + { + Version: "v1.1.0", + Description: "A python stack.", + Architectures: []string{"amd64", "arm64"}, + Tags: []string{"Python", "Django"}, + Resources: []string{"devfile.yaml"}, + StarterProjects: []string{"starterA", "starterB"}, + Links: map[string]string{ + "linkA": "git.test.com", + "linkB": "https://http.test.com", + "linkC": "https://another.testlink.ca", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.BuildCommandGroupKind: true, + indexSchema.RunCommandGroupKind: true, + }, + }, + { + Version: "v2.0.0", + Description: "A python stack.", + Icon: "devfileA.png", + Default: true, + Tags: []string{"Python", "Flask"}, + Resources: []string{"devfile.yaml", "archive.tar"}, + StarterProjects: []string{"starterA", "starterB"}, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DebugCommandGroupKind: false, + indexSchema.DeployCommandGroupKind: false, + indexSchema.BuildCommandGroupKind: true, + indexSchema.RunCommandGroupKind: true, + indexSchema.TestCommandGroupKind: false, + }, + }, + }, + }, + { + Name: "devfileB", + DisplayName: "Python", + Description: "A python sample.", + Icon: "devfileB.ico", + Attributes: map[string]apiext.JSON{ + "attributeA": {}, + "attributeC": {}, + "attributeD": {}, + "attributeE": {}, + }, + Architectures: []string{"amd64"}, + Tags: []string{"Python"}, + Resources: []string{"devfile.yaml"}, + StarterProjects: []string{"starterB"}, + Links: map[string]string{ + "linkA": "git.test.com", + "linkC": "https://another.testlink.ca", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.BuildCommandGroupKind: true, + indexSchema.RunCommandGroupKind: true, + }, + }, + { + Name: "devfileC", + DisplayName: "Flask", + Icon: "devfileC.jpg", + Attributes: map[string]apiext.JSON{ + "attributeB": {}, + "attributeC": {}, + "attributeD": {}, + "attributeE": {}, + }, + Resources: []string{"devfile.yaml", "archive.tar"}, + Links: map[string]string{ + "linkA": "git.test.com", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DeployCommandGroupKind: false, + indexSchema.RunCommandGroupKind: true, + }, + Versions: []indexSchema.Version{ + { + Version: "v1.0.0", + Icon: "devfileC.png", + Resources: []string{"devfile.yaml", "archive.tar"}, + StarterProjects: []string{"starterA", "starterC"}, + Default: true, + Links: map[string]string{ + "linkA": "git.test.com", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DeployCommandGroupKind: false, + indexSchema.RunCommandGroupKind: true, + }, + }, + { + Version: "v2.0.0", + Icon: "devfileC.ico", + StarterProjects: []string{"starterA", "starterB"}, + Links: map[string]string{ + "linkA": "git.test.com", + "linkC": "https://another.testlink.ca", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DebugCommandGroupKind: false, + indexSchema.DeployCommandGroupKind: false, + indexSchema.BuildCommandGroupKind: true, + indexSchema.RunCommandGroupKind: true, + indexSchema.TestCommandGroupKind: false, + }, + }, + }, + }, + { + Name: "devfileD", + }, +} + +func TestCheckChildrenEval(t *testing.T) { + tests := []struct { + name string + result *util.FilterResult + wantErr bool + }{ + { + name: "Case 1: Test non-recursive check", + result: util.MakeMockFilterResult( + nil, + true, + &util.FilterResult{ + IsEval: true, + }, + &util.FilterResult{ + IsEval: true, + }, + ), + }, + { + name: "Case 2: Test recursive check", + result: util.MakeMockFilterResult( + nil, + true, + &util.FilterResult{ + IsEval: true, + }, + &util.FilterResult{ + IsEval: true, + }, + util.MakeMockFilterResult( + nil, + true, + &util.FilterResult{ + IsEval: true, + }, + ), + ), + }, + { + name: "Case 3: Test non-recursive fail", + result: util.MakeMockFilterResult( + nil, + true, + &util.FilterResult{ + IsEval: true, + }, + &util.FilterResult{ + IsEval: false, + }, + ), + wantErr: true, + }, + { + name: "Case 4: Test non-recursive fail with a result error", + result: util.MakeMockFilterResult( + nil, + true, + &util.FilterResult{ + IsEval: true, + }, + &util.FilterResult{ + IsEval: false, + Error: fmt.Errorf("failed to filter"), + }, + ), + wantErr: true, + }, + { + name: "Case 5: Test recursive fail", + result: util.MakeMockFilterResult( + nil, + true, + &util.FilterResult{ + IsEval: true, + }, + &util.FilterResult{ + IsEval: true, + }, + util.MakeMockFilterResult( + nil, + true, + &util.FilterResult{ + IsEval: false, + }, + ), + ), + wantErr: true, + }, + { + name: "Case 6: Test parent fail", + result: util.MakeMockFilterResult( + nil, + false, + &util.FilterResult{ + IsEval: true, + }, + &util.FilterResult{ + IsEval: true, + }, + util.MakeMockFilterResult( + nil, + true, + &util.FilterResult{ + IsEval: true, + }, + ), + ), + wantErr: true, + }, + { + name: "Case 7: Test empty children", + result: util.MakeMockFilterResult( + nil, + true, + ), + }, + { + name: "Case 8: Test empty children fail", + result: util.MakeMockFilterResult( + nil, + false, + ), + wantErr: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := checkChildrenEval(test.result) + if !test.wantErr && err != nil { + t.Errorf("unexpected error: %v", err) + } + }) + } +} + +func TestFilterFieldbyParam(t *testing.T) { + tests := []struct { + name string + index []indexSchema.Schema + v1Index bool + paramName string + paramValue any + wantIndex []indexSchema.Schema + }{ + { + name: "Case 1: string parameter", + index: testIndexSchema, + v1Index: true, + paramName: util.PARAM_ICON, + paramValue: ".jpg", + wantIndex: []indexSchema.Schema{ + { + Name: "devfileC", + DisplayName: "Flask", + Icon: "devfileC.jpg", + Attributes: map[string]apiext.JSON{ + "attributeB": {}, + "attributeC": {}, + "attributeD": {}, + "attributeE": {}, + }, + Resources: []string{"devfile.yaml", "archive.tar"}, + Links: map[string]string{ + "linkA": "git.test.com", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DeployCommandGroupKind: false, + indexSchema.RunCommandGroupKind: true, + }, + Versions: []indexSchema.Version{ + { + Version: "v1.0.0", + Icon: "devfileC.png", + Resources: []string{"devfile.yaml", "archive.tar"}, + StarterProjects: []string{"starterA", "starterC"}, + Default: true, + Links: map[string]string{ + "linkA": "git.test.com", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DeployCommandGroupKind: false, + indexSchema.RunCommandGroupKind: true, + }, + }, + { + Version: "v2.0.0", + Icon: "devfileC.ico", + StarterProjects: []string{"starterA", "starterB"}, + Links: map[string]string{ + "linkA": "git.test.com", + "linkC": "https://another.testlink.ca", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DebugCommandGroupKind: false, + indexSchema.DeployCommandGroupKind: false, + indexSchema.BuildCommandGroupKind: true, + indexSchema.RunCommandGroupKind: true, + indexSchema.TestCommandGroupKind: false, + }, + }, + }, + }, + }, + }, + { + name: "Case 2: string parameter v2", + index: testIndexSchema, + paramName: util.PARAM_ICON, + paramValue: ".png", + wantIndex: []indexSchema.Schema{ + { + Name: "devfileA", + DisplayName: "Python", + Description: "A python stack.", + Attributes: map[string]apiext.JSON{ + "attributeA": {}, + "attributeB": {}, + "attributeC": {}, + "attributeD": {}, + }, + Architectures: []string{"amd64", "arm64"}, + Tags: []string{"Python", "Django", "Flask"}, + Versions: []indexSchema.Version{ + { + Version: "v2.0.0", + Description: "A python stack.", + Icon: "devfileA.png", + Default: true, + Tags: []string{"Python", "Flask"}, + Resources: []string{"devfile.yaml", "archive.tar"}, + StarterProjects: []string{"starterA", "starterB"}, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DebugCommandGroupKind: false, + indexSchema.DeployCommandGroupKind: false, + indexSchema.BuildCommandGroupKind: true, + indexSchema.RunCommandGroupKind: true, + indexSchema.TestCommandGroupKind: false, + }, + }, + }, + }, + { + Name: "devfileC", + DisplayName: "Flask", + Icon: "devfileC.jpg", + Attributes: map[string]apiext.JSON{ + "attributeB": {}, + "attributeC": {}, + "attributeD": {}, + "attributeE": {}, + }, + Resources: []string{"devfile.yaml", "archive.tar"}, + Links: map[string]string{ + "linkA": "git.test.com", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DeployCommandGroupKind: false, + indexSchema.RunCommandGroupKind: true, + }, + Versions: []indexSchema.Version{ + { + Version: "v1.0.0", + Icon: "devfileC.png", + Resources: []string{"devfile.yaml", "archive.tar"}, + StarterProjects: []string{"starterA", "starterC"}, + Default: true, + Links: map[string]string{ + "linkA": "git.test.com", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DeployCommandGroupKind: false, + indexSchema.RunCommandGroupKind: true, + }, + }, + }, + }, + }, + }, + { + name: "Case 3: Non-string parameter", + index: testIndexSchema, + paramName: util.PARAM_DEFAULT, + paramValue: true, + wantIndex: []indexSchema.Schema{ + { + Name: "devfileA", + DisplayName: "Python", + Description: "A python stack.", + Attributes: map[string]apiext.JSON{ + "attributeA": {}, + "attributeB": {}, + "attributeC": {}, + "attributeD": {}, + }, + Architectures: []string{"amd64", "arm64"}, + Tags: []string{"Python", "Django", "Flask"}, + Versions: []indexSchema.Version{ + { + Version: "v2.0.0", + Description: "A python stack.", + Icon: "devfileA.png", + Default: true, + Tags: []string{"Python", "Flask"}, + Resources: []string{"devfile.yaml", "archive.tar"}, + StarterProjects: []string{"starterA", "starterB"}, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DebugCommandGroupKind: false, + indexSchema.DeployCommandGroupKind: false, + indexSchema.BuildCommandGroupKind: true, + indexSchema.RunCommandGroupKind: true, + indexSchema.TestCommandGroupKind: false, + }, + }, + }, + }, + { + Name: "devfileC", + DisplayName: "Flask", + Icon: "devfileC.jpg", + Attributes: map[string]apiext.JSON{ + "attributeB": {}, + "attributeC": {}, + "attributeD": {}, + "attributeE": {}, + }, + Resources: []string{"devfile.yaml", "archive.tar"}, + Links: map[string]string{ + "linkA": "git.test.com", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DeployCommandGroupKind: false, + indexSchema.RunCommandGroupKind: true, + }, + Versions: []indexSchema.Version{ + { + Version: "v1.0.0", + Icon: "devfileC.png", + Resources: []string{"devfile.yaml", "archive.tar"}, + StarterProjects: []string{"starterA", "starterC"}, + Default: true, + Links: map[string]string{ + "linkA": "git.test.com", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DeployCommandGroupKind: false, + indexSchema.RunCommandGroupKind: true, + }, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + gotResult := filterFieldbyParam(test.index, test.v1Index, test.paramName, test.paramValue) + gotResult.Eval() + + if gotResult.Error != nil { + t.Errorf("unexpected error: %v", gotResult.Error) + } + + sort.Slice(gotResult.Index, func(i, j int) bool { + return gotResult.Index[i].Name < gotResult.Index[j].Name + }) + sort.Slice(test.wantIndex, func(i, j int) bool { + return test.wantIndex[i].Name < test.wantIndex[j].Name + }) + + if !reflect.DeepEqual(gotResult.Index, test.wantIndex) { + t.Errorf("expected: %v, got: %v", test.wantIndex, gotResult.Index) + } + }) + } +} + +func TestFilterFieldsByParams(t *testing.T) { + tests := []struct { + name string + index []indexSchema.Schema + v1Index bool + params IndexParams + wantIndex []indexSchema.Schema + wantErr bool + wantErrStr string + }{ + { + name: "Case 1: Single filter", + index: testIndexSchema, + params: IndexParams{ + Arch: &[]string{"arm64"}, + }, + v1Index: true, + wantIndex: []indexSchema.Schema{ + { + Name: "devfileA", + DisplayName: "Python", + Description: "A python stack.", + Attributes: map[string]apiext.JSON{ + "attributeA": {}, + "attributeB": {}, + "attributeC": {}, + "attributeD": {}, + }, + Architectures: []string{"amd64", "arm64"}, + Tags: []string{"Python", "Django", "Flask"}, + Versions: []indexSchema.Version{ + { + Version: "v1.0.0", + Description: "A python stack.", + Architectures: []string{"amd64"}, + Tags: []string{"Python"}, + Resources: []string{"devfile.yaml"}, + StarterProjects: []string{"starterA"}, + }, + { + Version: "v1.1.0", + Description: "A python stack.", + Architectures: []string{"amd64", "arm64"}, + Tags: []string{"Python", "Django"}, + Resources: []string{"devfile.yaml"}, + StarterProjects: []string{"starterA", "starterB"}, + Links: map[string]string{ + "linkA": "git.test.com", + "linkB": "https://http.test.com", + "linkC": "https://another.testlink.ca", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.BuildCommandGroupKind: true, + indexSchema.RunCommandGroupKind: true, + }, + }, + { + Version: "v2.0.0", + Description: "A python stack.", + Icon: "devfileA.png", + Default: true, + Tags: []string{"Python", "Flask"}, + Resources: []string{"devfile.yaml", "archive.tar"}, + StarterProjects: []string{"starterA", "starterB"}, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DebugCommandGroupKind: false, + indexSchema.DeployCommandGroupKind: false, + indexSchema.BuildCommandGroupKind: true, + indexSchema.RunCommandGroupKind: true, + indexSchema.TestCommandGroupKind: false, + }, + }, + }, + }, + { + Name: "devfileC", + DisplayName: "Flask", + Icon: "devfileC.jpg", + Attributes: map[string]apiext.JSON{ + "attributeB": {}, + "attributeC": {}, + "attributeD": {}, + "attributeE": {}, + }, + Resources: []string{"devfile.yaml", "archive.tar"}, + Links: map[string]string{ + "linkA": "git.test.com", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DeployCommandGroupKind: false, + indexSchema.RunCommandGroupKind: true, + }, + Versions: []indexSchema.Version{ + { + Version: "v1.0.0", + Icon: "devfileC.png", + Resources: []string{"devfile.yaml", "archive.tar"}, + StarterProjects: []string{"starterA", "starterC"}, + Default: true, + Links: map[string]string{ + "linkA": "git.test.com", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DeployCommandGroupKind: false, + indexSchema.RunCommandGroupKind: true, + }, + }, + { + Version: "v2.0.0", + Icon: "devfileC.ico", + StarterProjects: []string{"starterA", "starterB"}, + Links: map[string]string{ + "linkA": "git.test.com", + "linkC": "https://another.testlink.ca", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DebugCommandGroupKind: false, + indexSchema.DeployCommandGroupKind: false, + indexSchema.BuildCommandGroupKind: true, + indexSchema.RunCommandGroupKind: true, + indexSchema.TestCommandGroupKind: false, + }, + }, + }, + }, + { + Name: "devfileD", + }, + }, + }, + { + name: "Case 2: Single filter v2", + index: testIndexSchema, + params: IndexParams{ + Arch: &[]string{"arm64"}, + }, + wantIndex: []indexSchema.Schema{ + { + Name: "devfileA", + DisplayName: "Python", + Description: "A python stack.", + Attributes: map[string]apiext.JSON{ + "attributeA": {}, + "attributeB": {}, + "attributeC": {}, + "attributeD": {}, + }, + Architectures: []string{"amd64", "arm64"}, + Tags: []string{"Python", "Django", "Flask"}, + Versions: []indexSchema.Version{ + { + Version: "v1.1.0", + Description: "A python stack.", + Architectures: []string{"amd64", "arm64"}, + Tags: []string{"Python", "Django"}, + Resources: []string{"devfile.yaml"}, + StarterProjects: []string{"starterA", "starterB"}, + Links: map[string]string{ + "linkA": "git.test.com", + "linkB": "https://http.test.com", + "linkC": "https://another.testlink.ca", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.BuildCommandGroupKind: true, + indexSchema.RunCommandGroupKind: true, + }, + }, + { + Version: "v2.0.0", + Description: "A python stack.", + Icon: "devfileA.png", + Default: true, + Tags: []string{"Python", "Flask"}, + Resources: []string{"devfile.yaml", "archive.tar"}, + StarterProjects: []string{"starterA", "starterB"}, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DebugCommandGroupKind: false, + indexSchema.DeployCommandGroupKind: false, + indexSchema.BuildCommandGroupKind: true, + indexSchema.RunCommandGroupKind: true, + indexSchema.TestCommandGroupKind: false, + }, + }, + }, + }, + { + Name: "devfileC", + DisplayName: "Flask", + Icon: "devfileC.jpg", + Attributes: map[string]apiext.JSON{ + "attributeB": {}, + "attributeC": {}, + "attributeD": {}, + "attributeE": {}, + }, + Resources: []string{"devfile.yaml", "archive.tar"}, + Links: map[string]string{ + "linkA": "git.test.com", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DeployCommandGroupKind: false, + indexSchema.RunCommandGroupKind: true, + }, + Versions: []indexSchema.Version{ + { + Version: "v1.0.0", + Icon: "devfileC.png", + Resources: []string{"devfile.yaml", "archive.tar"}, + StarterProjects: []string{"starterA", "starterC"}, + Default: true, + Links: map[string]string{ + "linkA": "git.test.com", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DeployCommandGroupKind: false, + indexSchema.RunCommandGroupKind: true, + }, + }, + { + Version: "v2.0.0", + Icon: "devfileC.ico", + StarterProjects: []string{"starterA", "starterB"}, + Links: map[string]string{ + "linkA": "git.test.com", + "linkC": "https://another.testlink.ca", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DebugCommandGroupKind: false, + indexSchema.DeployCommandGroupKind: false, + indexSchema.BuildCommandGroupKind: true, + indexSchema.RunCommandGroupKind: true, + indexSchema.TestCommandGroupKind: false, + }, + }, + }, + }, + { + Name: "devfileD", + }, + }, + }, + { + name: "Case 3: Multi filter", + index: testIndexSchema, + params: IndexParams{ + Arch: &[]string{"arm64"}, + CommandGroups: &[]string{ + string(indexSchema.RunCommandGroupKind), + }, + }, + v1Index: true, + wantIndex: []indexSchema.Schema{ + { + Name: "devfileC", + DisplayName: "Flask", + Icon: "devfileC.jpg", + Attributes: map[string]apiext.JSON{ + "attributeB": {}, + "attributeC": {}, + "attributeD": {}, + "attributeE": {}, + }, + Resources: []string{"devfile.yaml", "archive.tar"}, + Links: map[string]string{ + "linkA": "git.test.com", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DeployCommandGroupKind: false, + indexSchema.RunCommandGroupKind: true, + }, + Versions: []indexSchema.Version{ + { + Version: "v1.0.0", + Icon: "devfileC.png", + Resources: []string{"devfile.yaml", "archive.tar"}, + StarterProjects: []string{"starterA", "starterC"}, + Default: true, + Links: map[string]string{ + "linkA": "git.test.com", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DeployCommandGroupKind: false, + indexSchema.RunCommandGroupKind: true, + }, + }, + { + Version: "v2.0.0", + Icon: "devfileC.ico", + StarterProjects: []string{"starterA", "starterB"}, + Links: map[string]string{ + "linkA": "git.test.com", + "linkC": "https://another.testlink.ca", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DebugCommandGroupKind: false, + indexSchema.DeployCommandGroupKind: false, + indexSchema.BuildCommandGroupKind: true, + indexSchema.RunCommandGroupKind: true, + indexSchema.TestCommandGroupKind: false, + }, + }, + }, + }, + }, + }, + { + name: "Case 4: Multi filter v2", + index: testIndexSchema, + params: IndexParams{ + Arch: &[]string{"arm64"}, + CommandGroups: &[]string{ + string(indexSchema.BuildCommandGroupKind), + string(indexSchema.RunCommandGroupKind), + }, + }, + wantIndex: []indexSchema.Schema{ + { + Name: "devfileA", + DisplayName: "Python", + Description: "A python stack.", + Attributes: map[string]apiext.JSON{ + "attributeA": {}, + "attributeB": {}, + "attributeC": {}, + "attributeD": {}, + }, + Architectures: []string{"amd64", "arm64"}, + Tags: []string{"Python", "Django", "Flask"}, + Versions: []indexSchema.Version{ + { + Version: "v1.1.0", + Description: "A python stack.", + Architectures: []string{"amd64", "arm64"}, + Tags: []string{"Python", "Django"}, + Resources: []string{"devfile.yaml"}, + StarterProjects: []string{"starterA", "starterB"}, + Links: map[string]string{ + "linkA": "git.test.com", + "linkB": "https://http.test.com", + "linkC": "https://another.testlink.ca", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.BuildCommandGroupKind: true, + indexSchema.RunCommandGroupKind: true, + }, + }, + { + Version: "v2.0.0", + Description: "A python stack.", + Icon: "devfileA.png", + Default: true, + Tags: []string{"Python", "Flask"}, + Resources: []string{"devfile.yaml", "archive.tar"}, + StarterProjects: []string{"starterA", "starterB"}, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DebugCommandGroupKind: false, + indexSchema.DeployCommandGroupKind: false, + indexSchema.BuildCommandGroupKind: true, + indexSchema.RunCommandGroupKind: true, + indexSchema.TestCommandGroupKind: false, + }, + }, + }, + }, + { + Name: "devfileC", + DisplayName: "Flask", + Icon: "devfileC.jpg", + Attributes: map[string]apiext.JSON{ + "attributeB": {}, + "attributeC": {}, + "attributeD": {}, + "attributeE": {}, + }, + Resources: []string{"devfile.yaml", "archive.tar"}, + Links: map[string]string{ + "linkA": "git.test.com", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DeployCommandGroupKind: false, + indexSchema.RunCommandGroupKind: true, + }, + Versions: []indexSchema.Version{ + { + Version: "v2.0.0", + Icon: "devfileC.ico", + StarterProjects: []string{"starterA", "starterB"}, + Links: map[string]string{ + "linkA": "git.test.com", + "linkC": "https://another.testlink.ca", + }, + CommandGroups: map[indexSchema.CommandGroupKind]bool{ + indexSchema.DebugCommandGroupKind: false, + indexSchema.DeployCommandGroupKind: false, + indexSchema.BuildCommandGroupKind: true, + indexSchema.RunCommandGroupKind: true, + indexSchema.TestCommandGroupKind: false, + }, + }, + }, + }, + }, + }, + { + name: "Case 5: Blank result", + index: testIndexSchema, + params: IndexParams{ + Arch: &[]string{"arm64"}, + AttributeNames: &[]string{"attributeE"}, + CommandGroups: &[]string{ + string(indexSchema.DeployCommandGroupKind), + string(indexSchema.RunCommandGroupKind), + }, + }, + wantIndex: []indexSchema.Schema{}, + }, + { + name: "Case 6: Blank filter", + index: testIndexSchema, + params: IndexParams{}, + wantIndex: testIndexSchema, + }, + { + name: "Case 7: Error", + index: testIndexSchema, + params: IndexParams{}, + wantErr: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + gotIndex, gotErr := filterFieldsByParams(test.index, test.v1Index, test.params) + + // sorting is not consistent in output + sort.Slice(gotIndex, func(i, j int) bool { + return gotIndex[i].Name < gotIndex[j].Name + }) + sort.Slice(test.wantIndex, func(i, j int) bool { + return test.wantIndex[i].Name < test.wantIndex[j].Name + }) + + if test.wantErr && gotErr != nil && !strings.Contains(gotErr.Error(), test.wantErrStr) { + t.Errorf("unexpected error %v", gotErr) + } else if !test.wantErr && gotErr != nil { + t.Errorf("unexpected error %v", gotErr) + } else if !test.wantErr && !reflect.DeepEqual(gotIndex, test.wantIndex) { + t.Errorf("expected: %v, got: %v", test.wantIndex, gotIndex) + } + }) + } +} diff --git a/index/server/pkg/util/filter.go b/index/server/pkg/util/filter.go index e484b63b..83bb52e3 100644 --- a/index/server/pkg/util/filter.go +++ b/index/server/pkg/util/filter.go @@ -55,13 +55,13 @@ const ( // Parameter 'default' PARAM_DEFAULT = "default" // Parameter 'git.url' - PARAM_GIT_URL = "git.url" + PARAM_GIT_URL = "gitUrl" // Parameter 'git.remoteName' - PARAM_GIT_REMOTE_NAME = "git.remoteName" + PARAM_GIT_REMOTE_NAME = "gitRemoteName" // Parameter 'git.subDir' - PARAM_GIT_SUBDIR = "git.subDir" + PARAM_GIT_SUBDIR = "gitSubDir" // Parameter 'git.revision' - PARAM_GIT_REVISION = "git.revision" + PARAM_GIT_REVISION = "gitRevision" // Parameter 'provider' PARAM_PROVIDER = "provider" // Parameter 'supportUrl' @@ -69,12 +69,12 @@ const ( /* Array Parameter Names */ - // Parameter 'attributes' - ARRAY_PARAM_ATTRIBUTE_NAMES = "attributes" + // Parameter 'attributeNames' + ARRAY_PARAM_ATTRIBUTE_NAMES = "attributeNames" // Parameter 'tags' ARRAY_PARAM_TAGS = "tags" // Parameter 'architectures' - ARRAY_PARAM_ARCHITECTURES = "architectures" + ARRAY_PARAM_ARCHITECTURES = "arch" // Parameter 'resources' ARRAY_PARAM_RESOURCES = "resources" // Parameter 'starterProjects' @@ -83,14 +83,16 @@ const ( ARRAY_PARAM_LINKS = "links" // Parameter 'commandGroups' ARRAY_PARAM_COMMAND_GROUPS = "commandGroups" - // Parameter 'git.remotes' - ARRAY_PARAM_GIT_REMOTES = "git.remotes" + // Parameter 'gitRemoteNames' + ARRAY_PARAM_GIT_REMOTE_NAMES = "gitRemoteNames" + // Parameter 'gitRemotes' + ARRAY_PARAM_GIT_REMOTES = "gitRemotes" ) // FilterResult result entity of filtering the index schema type FilterResult struct { filterFn func(*FilterResult) - children []FilterResult + children []*FilterResult // Index schema result Index []indexSchema.Schema @@ -106,6 +108,31 @@ func (fr *FilterResult) Eval() { fr.IsEval = true } +// IsChildrenEval checks children results if they are evaluated yet, if parent caller is unevaluated returns +// false, if parent has no children results then returns true. If recurse is true check children of children +// until any root conditions are hit otherwise only direct children will be checked. +func (fr *FilterResult) IsChildrenEval(recurse bool) bool { + if !fr.IsEval { + return false + } else if len(fr.children) == 0 { + return true + } + isEval := true + + for _, child := range fr.children { + if !child.IsEval { + isEval = false + break + } + + if recurse && len(child.children) > 0 { + isEval = child.IsChildrenEval(true) + } + } + + return isEval +} + // FilterOptions provides filtering options to filters operations type FilterOptions[T any] struct { GetFromIndexField func(*indexSchema.Schema) T @@ -115,24 +142,24 @@ type FilterOptions[T any] struct { } // indexFieldEmptyHandler handles what to do with empty index array fields -func indexFieldEmptyHandler(schema *indexSchema.Schema, requestedValues []string, options FilterOptions[[]string]) bool { +func indexFieldEmptyHandler(fieldValues []string, requestedValues []string, options FilterOptions[[]string]) bool { // If filtering out empty, assume that if a stack has no field values mentioned and field value are request // Else assume that if a stack has no array field values mentioned, then assume all possible are valid if options.FilterOutEmpty { - return len(requestedValues) != 0 && len(options.GetFromIndexField(schema)) == 0 + return len(requestedValues) != 0 && len(fieldValues) == 0 } else { - return len(options.GetFromIndexField(schema)) == 0 + return len(fieldValues) == 0 } } // versionFieldEmptyHandler handles what to do with empty version array fields -func versionFieldEmptyHandler(version *indexSchema.Version, requestedValues []string, options FilterOptions[[]string]) bool { +func versionFieldEmptyHandler(fieldValues []string, requestedValues []string, options FilterOptions[[]string]) bool { // If filtering out empty, assume that if a stack has no field values mentioned and field value are request // Else assume that if a stack has no array field values mentioned, then assume all possible are valid if options.FilterOutEmpty { - return len(requestedValues) != 0 && len(options.GetFromVersionField(version)) == 0 + return len(requestedValues) != 0 && len(fieldValues) == 0 } else { - return len(options.GetFromVersionField(version)) == 0 + return len(fieldValues) == 0 } } @@ -175,12 +202,26 @@ func preProcessString(s string) string { return trimPunc(trimExtraSpace(sLower)) } +// preProcessStringTokens gives array of string tokens +func preProcessStringTokens(s string) []string { + if len(s) > 0 { + re := regexp.MustCompile(`\s+`) + return re.Split(s, -1) + } + + return []string{} +} + // getFuzzySetFromArray gets a fuzzy pre-processed set from given array func getFuzzySetFromArray(arr []string) *sets.Set[string] { preProcessedArray := []string{} for i := 0; i < len(arr); i++ { - preProcessedArray = append(preProcessedArray, preProcessString(arr[i])) + preProcessedString := preProcessString(arr[i]) + tokens := preProcessStringTokens(preProcessedString) + + preProcessedArray = append(preProcessedArray, tokens...) + preProcessedArray = append(preProcessedArray, preProcessedString) } return sets.From(preProcessedArray) @@ -252,11 +293,13 @@ func filterDevfileArrayFuzzy(index []indexSchema.Schema, requestedValues []strin toFilterOutIndex := false if options.GetFromIndexField != nil { + fieldValues := options.GetFromIndexField(&filteredIndex[i]) + // If index schema field is not empty perform fuzzy filtering // else if filtering out based on empty fields is set, set index schema to be filtered out // (after version filtering if applicable) - if !indexFieldEmptyHandler(&filteredIndex[i], requestedValues, options) { - valuesInIndex := getFuzzySetFromArray(options.GetFromIndexField(&filteredIndex[i])) + if !indexFieldEmptyHandler(fieldValues, requestedValues, options) { + valuesInIndex := getFuzzySetFromArray(fieldValues) for _, requestedValue := range requestedValues { if !fuzzyMatchInSet(valuesInIndex, requestedValue) { @@ -273,10 +316,12 @@ func filterDevfileArrayFuzzy(index []indexSchema.Schema, requestedValues []strin if !options.V1Index && options.GetFromVersionField != nil { filteredVersions := deepcopy.Copy(filteredIndex[i].Versions).([]indexSchema.Version) for versionIndex := 0; versionIndex < len(filteredVersions); versionIndex++ { + fieldValues := options.GetFromVersionField(&filteredVersions[versionIndex]) + // If version schema field is not empty perform fuzzy filtering // else if filtering out based on empty fields is set, filter out version schema - if !versionFieldEmptyHandler(&filteredVersions[versionIndex], requestedValues, options) { - valuesInVersion := getFuzzySetFromArray(options.GetFromVersionField(&filteredVersions[versionIndex])) + if !versionFieldEmptyHandler(fieldValues, requestedValues, options) { + valuesInVersion := getFuzzySetFromArray(fieldValues) for _, requestedValue := range requestedValues { if !fuzzyMatchInSet(valuesInVersion, requestedValue) { @@ -341,6 +386,7 @@ func IsArrayParameter(name string) bool { ARRAY_PARAM_STARTER_PROJECTS, ARRAY_PARAM_LINKS, ARRAY_PARAM_COMMAND_GROUPS, + ARRAY_PARAM_GIT_REMOTE_NAMES, ARRAY_PARAM_GIT_REMOTES, }) @@ -348,60 +394,53 @@ func IsArrayParameter(name string) bool { } // FilterDevfileSchemaVersion filters devfiles based on schema version -func FilterDevfileSchemaVersion(index []indexSchema.Schema, minSchemaVersion, maxSchemaVersion string) FilterResult { - return FilterResult{ - filterFn: func(fr *FilterResult) { - for i := 0; i < len(index); i++ { - for versionIndex := 0; versionIndex < len(index[i].Versions); versionIndex++ { - currectSchemaVersion := index[i].Versions[versionIndex].SchemaVersion - schemaVersionWithoutServiceVersion := currectSchemaVersion[:strings.LastIndex(currectSchemaVersion, ".")] - curVersion, err := versionpkg.NewVersion(schemaVersionWithoutServiceVersion) - if err != nil { - fr.Error = fmt.Errorf("failed to parse schemaVersion %s for stack: %s, version %s. Error: %v", currectSchemaVersion, index[i].Name, index[i].Versions[versionIndex].Version, err) - return - } - - versionInRange := true - if minSchemaVersion != "" { - minVersion, err := versionpkg.NewVersion(minSchemaVersion) - if err != nil { - fr.Error = fmt.Errorf("failed to parse minSchemaVersion %s. Error: %v", minSchemaVersion, err) - return - } - if minVersion.GreaterThan(curVersion) { - versionInRange = false - } - } - if versionInRange && maxSchemaVersion != "" { - maxVersion, err := versionpkg.NewVersion(maxSchemaVersion) - if err != nil { - fr.Error = fmt.Errorf("failed to parse maxSchemaVersion %s. Error: %v", maxSchemaVersion, err) - return - } - if maxVersion.LessThan(curVersion) { - versionInRange = false - } - } - if !versionInRange { - // if schemaVersion is not in requested range, filter it out - index[i].Versions = append(index[i].Versions[:versionIndex], index[i].Versions[versionIndex+1:]...) +func FilterDevfileSchemaVersion(index []indexSchema.Schema, minSchemaVersion, maxSchemaVersion string) ([]indexSchema.Schema, error) { + for i := 0; i < len(index); i++ { + for versionIndex := 0; versionIndex < len(index[i].Versions); versionIndex++ { + currectSchemaVersion := index[i].Versions[versionIndex].SchemaVersion + schemaVersionWithoutServiceVersion := currectSchemaVersion[:strings.LastIndex(currectSchemaVersion, ".")] + curVersion, err := versionpkg.NewVersion(schemaVersionWithoutServiceVersion) + if err != nil { + return []indexSchema.Schema{}, fmt.Errorf("failed to parse schemaVersion %s for stack: %s, version %s. Error: %v", currectSchemaVersion, index[i].Name, index[i].Versions[versionIndex].Version, err) + } - // decrement counter, since we shifted the array - versionIndex-- - } + versionInRange := true + if minSchemaVersion != "" { + minVersion, err := versionpkg.NewVersion(minSchemaVersion) + if err != nil { + return []indexSchema.Schema{}, fmt.Errorf("failed to parse minSchemaVersion %s. Error: %v", minSchemaVersion, err) } - if len(index[i].Versions) == 0 { - // if versions list is empty after filter, remove this index - index = append(index[:i], index[i+1:]...) - - // decrement counter, since we shifted the array - i-- + if minVersion.GreaterThan(curVersion) { + versionInRange = false } } + if versionInRange && maxSchemaVersion != "" { + maxVersion, err := versionpkg.NewVersion(maxSchemaVersion) + if err != nil { + return []indexSchema.Schema{}, fmt.Errorf("failed to parse maxSchemaVersion %s. Error: %v", maxSchemaVersion, err) + } + if maxVersion.LessThan(curVersion) { + versionInRange = false + } + } + if !versionInRange { + // if schemaVersion is not in requested range, filter it out + index[i].Versions = append(index[i].Versions[:versionIndex], index[i].Versions[versionIndex+1:]...) - fr.Index = index - }, + // decrement counter, since we shifted the array + versionIndex-- + } + } + if len(index[i].Versions) == 0 { + // if versions list is empty after filter, remove this index + index = append(index[:i], index[i+1:]...) + + // decrement counter, since we shifted the array + i-- + } } + + return index, nil } // FilterDevfileStrField filters by given string field, returns unchanged index if given parameter name is unrecognized @@ -451,6 +490,11 @@ func FilterDevfileStrField(index []indexSchema.Schema, paramName, requestedValue options.GetFromVersionField = func(v *indexSchema.Version) string { return v.SchemaVersion } + case PARAM_DEFAULT: + options.GetFromIndexField = nil + options.GetFromVersionField = func(v *indexSchema.Version) string { + return fmt.Sprintf("%v", v.Default) + } case PARAM_GIT_URL: options.GetFromIndexField = func(s *indexSchema.Schema) string { return s.Git.Url @@ -499,7 +543,7 @@ func FilterDevfileStrField(index []indexSchema.Schema, paramName, requestedValue } // AndFilter filters results of given filters to only overlapping results -func AndFilter(results ...FilterResult) FilterResult { +func AndFilter(results ...*FilterResult) FilterResult { return FilterResult{ children: results, filterFn: func(fr *FilterResult) { @@ -509,20 +553,20 @@ func AndFilter(results ...FilterResult) FilterResult { }{} andResults := []indexSchema.Schema{} - for _, result := range fr.children { + for i := 0; i < len(fr.children); i++ { // Evaluates filter if not already evaluated - if !result.IsEval { - result.Eval() + if !fr.children[i].IsEval { + fr.children[i].Eval() } // If a filter returns an error, return as overall result - if result.Error != nil { - fr.Error = result.Error + if fr.children[i].Error != nil { + fr.Error = fr.children[i].Error return } - for _, schema := range result.Index { + for _, schema := range fr.children[i].Index { andResult, found := andResultsMap[schema.Name] // if not found, initize is a seen counter of one and the current seen schema // else increment seen counter and re-assign current seen schema if versions have been filtered @@ -536,7 +580,7 @@ func AndFilter(results ...FilterResult) FilterResult { } } else { andResultsMap[schema.Name].count += 1 - if len(schema.Versions) < len(andResult.schema.Version) { + if len(schema.Versions) < len(andResult.schema.Versions) { andResultsMap[schema.Name].schema = schema } } @@ -645,13 +689,36 @@ func FilterDevfileStrArrayField(index []indexSchema.Schema, paramName string, re return commandGroups } + case ARRAY_PARAM_GIT_REMOTE_NAMES: + options.GetFromIndexField = func(s *indexSchema.Schema) []string { + gitRemoteNames := []string{} + + if s.Git != nil { + for remoteName := range s.Git.Remotes { + gitRemoteNames = append(gitRemoteNames, remoteName) + } + } + + return gitRemoteNames + } + options.GetFromVersionField = func(v *indexSchema.Version) []string { + gitRemoteNames := []string{} + + if v.Git != nil { + for remoteName := range v.Git.Remotes { + gitRemoteNames = append(gitRemoteNames, remoteName) + } + } + + return gitRemoteNames + } case ARRAY_PARAM_GIT_REMOTES: options.GetFromIndexField = func(s *indexSchema.Schema) []string { gitRemotes := []string{} if s.Git != nil { - for remoteName := range s.Git.Remotes { - gitRemotes = append(gitRemotes, remoteName) + for _, remoteUrl := range s.Git.Remotes { + gitRemotes = append(gitRemotes, remoteUrl) } } @@ -661,8 +728,8 @@ func FilterDevfileStrArrayField(index []indexSchema.Schema, paramName string, re gitRemotes := []string{} if v.Git != nil { - for remoteName := range v.Git.Remotes { - gitRemotes = append(gitRemotes, remoteName) + for _, remoteUrl := range v.Git.Remotes { + gitRemotes = append(gitRemotes, remoteUrl) } } diff --git a/index/server/pkg/util/filter_test.go b/index/server/pkg/util/filter_test.go index 587cfd08..d62c93b7 100644 --- a/index/server/pkg/util/filter_test.go +++ b/index/server/pkg/util/filter_test.go @@ -850,6 +850,189 @@ var ( }, }, } + filterGitRemoteNamesTestCases = []filterDevfileStrArrayFieldTestCase{ + { + Name: "two git remote name filters", + FieldName: ARRAY_PARAM_GIT_REMOTE_NAMES, + Index: []indexSchema.Schema{ + { + Name: "devfileA", + Git: &indexSchema.Git{ + Remotes: map[string]string{ + "linkA": "git.test.com", + "linkB": "https://http.test.com", + "linkC": "https://another.testlink.ca", + }, + }, + }, + { + Name: "devfileB", + Git: &indexSchema.Git{ + Remotes: map[string]string{ + "linkA": "git.test.com", + "linkC": "https://another.testlink.ca", + }, + }, + }, + { + Name: "devfileC", + Git: &indexSchema.Git{ + Remotes: map[string]string{ + "linkA": "git.test.com", + }, + }, + }, + { + Name: "devfileD", + Git: &indexSchema.Git{ + RemoteName: "remoteA", + }, + }, + { + Name: "devfileE", + }, + }, + V1Index: true, + Values: []string{"linkA", "linkC"}, + WantIndex: []indexSchema.Schema{ + { + Name: "devfileA", + Git: &indexSchema.Git{ + Remotes: map[string]string{ + "linkA": "git.test.com", + "linkB": "https://http.test.com", + "linkC": "https://another.testlink.ca", + }, + }, + }, + { + Name: "devfileB", + Git: &indexSchema.Git{ + Remotes: map[string]string{ + "linkA": "git.test.com", + "linkC": "https://another.testlink.ca", + }, + }, + }, + }, + }, + { + Name: "two git remote name filters with v2 index", + FieldName: ARRAY_PARAM_GIT_REMOTE_NAMES, + Index: []indexSchema.Schema{ + { + Name: "devfileA", + Versions: []indexSchema.Version{ + { + Version: "1.0.0", + }, + { + Version: "1.1.0", + Git: &indexSchema.Git{ + Remotes: map[string]string{ + "linkA": "git.test.com", + "linkB": "https://http.test.com", + "linkC": "https://another.testlink.ca", + }, + }, + }, + }, + }, + { + Name: "devfileB", + Git: &indexSchema.Git{ + Remotes: map[string]string{ + "linkA": "git.test.com", + "linkC": "https://another.testlink.ca", + }, + }, + }, + { + Name: "devfileC", + Git: &indexSchema.Git{ + Remotes: map[string]string{ + "linkA": "git.test.com", + }, + }, + Versions: []indexSchema.Version{ + { + Version: "1.0.0", + Git: &indexSchema.Git{ + Remotes: map[string]string{ + "linkA": "git.test.com", + }, + }, + }, + { + Version: "1.1.0", + Git: &indexSchema.Git{ + Remotes: map[string]string{ + "linkA": "git.test.com", + "linkC": "https://another.testlink.ca", + }, + }, + }, + }, + }, + { + Name: "devfileD", + Git: &indexSchema.Git{ + RemoteName: "remoteA", + }, + }, + { + Name: "devfileE", + }, + }, + V1Index: false, + Values: []string{"linkA", "linkC"}, + WantIndex: []indexSchema.Schema{ + { + Name: "devfileA", + Versions: []indexSchema.Version{ + { + Version: "1.1.0", + Git: &indexSchema.Git{ + Remotes: map[string]string{ + "linkA": "git.test.com", + "linkB": "https://http.test.com", + "linkC": "https://another.testlink.ca", + }, + }, + }, + }, + }, + { + Name: "devfileB", + Git: &indexSchema.Git{ + Remotes: map[string]string{ + "linkA": "git.test.com", + "linkC": "https://another.testlink.ca", + }, + }, + }, + { + Name: "devfileC", + Git: &indexSchema.Git{ + Remotes: map[string]string{ + "linkA": "git.test.com", + }, + }, + Versions: []indexSchema.Version{ + { + Version: "1.1.0", + Git: &indexSchema.Git{ + Remotes: map[string]string{ + "linkA": "git.test.com", + "linkC": "https://another.testlink.ca", + }, + }, + }, + }, + }, + }, + }, + } filterGitRemotesTestCases = []filterDevfileStrArrayFieldTestCase{ { Name: "two git remote filters", @@ -893,7 +1076,7 @@ var ( }, }, V1Index: true, - Values: []string{"linkA", "linkC"}, + Values: []string{"git", ".com"}, WantIndex: []indexSchema.Schema{ { Name: "devfileA", @@ -914,6 +1097,14 @@ var ( }, }, }, + { + Name: "devfileC", + Git: &indexSchema.Git{ + Remotes: map[string]string{ + "linkA": "git.test.com", + }, + }, + }, }, }, { @@ -985,7 +1176,7 @@ var ( }, }, V1Index: false, - Values: []string{"linkA", "linkC"}, + Values: []string{"git", ".com"}, WantIndex: []indexSchema.Schema{ { Name: "devfileA", @@ -1019,6 +1210,14 @@ var ( }, }, Versions: []indexSchema.Version{ + { + Version: "1.0.0", + Git: &indexSchema.Git{ + Remotes: map[string]string{ + "linkA": "git.test.com", + }, + }, + }, { Version: "1.1.0", Git: &indexSchema.Git{ @@ -1748,6 +1947,44 @@ func TestFuzzyMatch(t *testing.T) { } } +func TestPreProcessStringTokens(t *testing.T) { + tests := []struct { + name string + value string + want []string + }{ + { + name: "Case 1: One Token", + value: "Test", + want: []string{"Test"}, + }, + { + name: "Case 2: Two Tokens", + value: "Test token", + want: []string{"Test", "token"}, + }, + { + name: "Case 2: Three Tokens", + value: "Test 3 tokens", + want: []string{"Test", "3", "tokens"}, + }, + { + name: "Case 3: No tokens (Empty string)", + value: "", + want: []string{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := preProcessStringTokens(test.value) + if !reflect.DeepEqual(got, test.want) { + t.Errorf("Got: %v, Expected: %v", got, test.want) + } + }) + } +} + func TestFilterDevfileSchemaVersion(t *testing.T) { tests := []struct { @@ -1917,14 +2154,11 @@ func TestFilterDevfileSchemaVersion(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - gotResult := FilterDevfileSchemaVersion(test.index, test.minSchemaVersion, test.maxSchemaVersion) - gotResult.Eval() - if !gotResult.IsEval { - t.Errorf("Got unexpected unevaluated result: %v", gotResult) - } else if gotResult.Error != nil { - t.Errorf("Unexpected error: %v", gotResult.Error) - } else if !reflect.DeepEqual(gotResult.Index, test.wantIndex) { - t.Errorf("Got: %v, Expected: %v", gotResult.Index, test.wantIndex) + gotIndex, gotErr := FilterDevfileSchemaVersion(test.index, test.minSchemaVersion, test.maxSchemaVersion) + if gotErr != nil { + t.Errorf("Unexpected error: %v", gotErr) + } else if !reflect.DeepEqual(gotIndex, test.wantIndex) { + t.Errorf("Got: %v, Expected: %v", gotIndex, test.wantIndex) } }) } @@ -1939,6 +2173,7 @@ func TestFilterDevfileStrArrayField(t *testing.T) { tests = append(tests, filterStarterProjectsTestCases...) tests = append(tests, filterLinksTestCases...) tests = append(tests, filterCommandGroupsTestCases...) + tests = append(tests, filterGitRemoteNamesTestCases...) tests = append(tests, filterGitRemotesTestCases...) for _, test := range tests { @@ -1994,7 +2229,7 @@ func TestFilterDevfileStrField(t *testing.T) { func TestAndFilter(t *testing.T) { tests := []struct { name string - filters []FilterResult + filters []*FilterResult wantIndex []indexSchema.Schema wantNotEval bool wantErr bool @@ -2002,7 +2237,7 @@ func TestAndFilter(t *testing.T) { }{ { name: "Test Valid And with same results", - filters: []FilterResult{ + filters: []*FilterResult{ { Index: []indexSchema.Schema{ { @@ -2049,7 +2284,7 @@ func TestAndFilter(t *testing.T) { }, { name: "Test Valid And with same results v2", - filters: []FilterResult{ + filters: []*FilterResult{ { Index: []indexSchema.Schema{ { @@ -2189,7 +2424,7 @@ func TestAndFilter(t *testing.T) { }, { name: "Test Valid And with overlapping results", - filters: []FilterResult{ + filters: []*FilterResult{ { Index: []indexSchema.Schema{ { @@ -2226,7 +2461,7 @@ func TestAndFilter(t *testing.T) { }, { name: "Test Valid And with overlapping results v2", - filters: []FilterResult{ + filters: []*FilterResult{ { Index: []indexSchema.Schema{ { @@ -2316,7 +2551,7 @@ func TestAndFilter(t *testing.T) { }, { name: "Test Valid And with overlapping results and versions v2", - filters: []FilterResult{ + filters: []*FilterResult{ { Index: []indexSchema.Schema{ { @@ -2416,12 +2651,12 @@ func TestAndFilter(t *testing.T) { }, { name: "Test Valid And with no results", - filters: []FilterResult{}, + filters: []*FilterResult{}, wantIndex: []indexSchema.Schema{}, }, { name: "Test Invalid And with single error", - filters: []FilterResult{ + filters: []*FilterResult{ { Error: fmt.Errorf("A test error"), IsEval: true, @@ -2433,7 +2668,7 @@ func TestAndFilter(t *testing.T) { }, { name: "Test Invalid And with multiple errors", - filters: []FilterResult{ + filters: []*FilterResult{ { Error: fmt.Errorf("First test error"), IsEval: true, @@ -2453,7 +2688,7 @@ func TestAndFilter(t *testing.T) { }, { name: "Test Invalid And with valid filters and errors", - filters: []FilterResult{ + filters: []*FilterResult{ { Index: []indexSchema.Schema{ { @@ -2498,7 +2733,7 @@ func TestAndFilter(t *testing.T) { }, { name: "Test Invalid And with valid filters and errors v2", - filters: []FilterResult{ + filters: []*FilterResult{ { Index: []indexSchema.Schema{ { @@ -2595,7 +2830,7 @@ func TestAndFilter(t *testing.T) { }, { name: "Test Unevaluated FilterResult entities with And filter", - filters: []FilterResult{ + filters: []*FilterResult{ { filterFn: func(fr *FilterResult) { newIndex := []indexSchema.Schema{} diff --git a/index/server/pkg/util/testing.go b/index/server/pkg/util/testing.go new file mode 100644 index 00000000..c5bbca12 --- /dev/null +++ b/index/server/pkg/util/testing.go @@ -0,0 +1,25 @@ +// +// Copyright Red Hat +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +// MakeMockFilterResult produces a mock filter result entity +func MakeMockFilterResult(filterFn func(*FilterResult), isEval bool, children ...*FilterResult) *FilterResult { + return &FilterResult{ + filterFn: filterFn, + children: children, + IsEval: isEval, + } +} diff --git a/index/server/pkg/util/util.go b/index/server/pkg/util/util.go index 573076e4..f2bbf0b5 100644 --- a/index/server/pkg/util/util.go +++ b/index/server/pkg/util/util.go @@ -251,7 +251,7 @@ func StructToMap[T any](s T) map[string]any { val := reflect.ValueOf(s) - if val.Kind() == reflect.Ptr { + if val.Kind() == reflect.Pointer { if val.IsNil() { return nil } @@ -265,7 +265,7 @@ func StructToMap[T any](s T) map[string]any { var fieldValueKind reflect.Kind = field.Kind() var fieldValue interface{} - if fieldValueKind == reflect.Ptr { + if fieldValueKind == reflect.Pointer { if field.IsNil() { continue } diff --git a/index/server/pkg/util/util_test.go b/index/server/pkg/util/util_test.go index c0631908..7a373b99 100644 --- a/index/server/pkg/util/util_test.go +++ b/index/server/pkg/util/util_test.go @@ -23,6 +23,7 @@ import ( "testing" indexSchema "github.com/devfile/registry-support/index/generator/schema" + "k8s.io/utils/pointer" ) func TestIsHtmlRequested(t *testing.T) { @@ -404,7 +405,7 @@ func TestStructToMap(t *testing.T) { wantMap map[string]any }{ { - name: "Test struct type with map conversion", + name: "Case 1: Test struct type with map conversion", inputStruct: struct { Language string Stack string @@ -421,7 +422,7 @@ func TestStructToMap(t *testing.T) { }, }, { - name: "Test schema type with map conversion", + name: "Case 2: Test schema type with map conversion", inputStruct: indexSchema.Schema{ Name: "go", DisplayName: "Go", @@ -437,6 +438,23 @@ func TestStructToMap(t *testing.T) { "tags": []string{"Go", "Gin"}, }, }, + { + name: "Case 3: Test pointer fields struct type with map conversion", + inputStruct: struct { + P1 *string + P2 *string + P3 *[]string + P4 *bool + }{ + P1: pointer.String("test"), + P3: &[]string{"test1", "test2"}, + P4: nil, + }, + wantMap: map[string]any{ + "p1": "test", + "p3": []string{"test1", "test2"}, + }, + }, } for _, test := range tests {