blob: 688d01b3c4534d6790ea66a2a20f949ab6fbfe8d [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You 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 internal
import (
"fmt"
"sort"
"strings"
)
import (
"github.com/apache/dubbo-kubernetes/pkg/bufman/pkg/normalpath"
"github.com/apache/dubbo-kubernetes/pkg/bufman/pkg/stringutil"
)
const (
defaultEnumZeroValueSuffix = "_UNSPECIFIED"
defaultServiceSuffix = "Service"
)
// Config is the check config.
type Config struct {
// Rules are the rules to run.
//
// Rules will be sorted by first categories, then id when Configs are
// created from this package, i.e. created wth ConfigBuilder.NewConfig.
Rules []*Rule
IgnoreRootPaths map[string]struct{}
IgnoreIDToRootPaths map[string]map[string]struct{}
AllowCommentIgnores bool
IgnoreUnstablePackages bool
}
// ConfigBuilder is a config builder.
type ConfigBuilder struct {
Use []string
Except []string
IgnoreRootPaths []string
IgnoreIDOrCategoryToRootPaths map[string][]string
AllowCommentIgnores bool
IgnoreUnstablePackages bool
EnumZeroValueSuffix string
RPCAllowSameRequestResponse bool
RPCAllowGoogleProtobufEmptyRequests bool
RPCAllowGoogleProtobufEmptyResponses bool
ServiceSuffix string
}
// NewConfig returns a new Config.
func (b ConfigBuilder) NewConfig(versionSpec *VersionSpec) (*Config, error) {
return newConfig(b, versionSpec)
}
func newConfig(configBuilder ConfigBuilder, versionSpec *VersionSpec) (*Config, error) {
configBuilder.Use = stringutil.SliceToUniqueSortedSliceFilterEmptyStrings(configBuilder.Use)
configBuilder.Except = stringutil.SliceToUniqueSortedSliceFilterEmptyStrings(configBuilder.Except)
if len(configBuilder.Use) == 0 {
// default behavior
configBuilder.Use = versionSpec.DefaultCategories
}
if configBuilder.EnumZeroValueSuffix == "" {
configBuilder.EnumZeroValueSuffix = defaultEnumZeroValueSuffix
}
if configBuilder.ServiceSuffix == "" {
configBuilder.ServiceSuffix = defaultServiceSuffix
}
return newConfigForRuleBuilders(
configBuilder,
versionSpec.RuleBuilders,
versionSpec.IDToCategories,
)
}
func newConfigForRuleBuilders(
configBuilder ConfigBuilder,
ruleBuilders []*RuleBuilder,
idToCategories map[string][]string,
) (*Config, error) {
// this checks that there are not duplicate IDs for a given revision
// which would be a system error
idToRuleBuilder, err := getIDToRuleBuilder(ruleBuilders)
if err != nil {
return nil, err
}
categoryToIDs := getCategoryToIDs(idToCategories)
useIDMap, err := transformToIDMap(configBuilder.Use, idToCategories, categoryToIDs)
if err != nil {
return nil, err
}
exceptIDMap, err := transformToIDMap(configBuilder.Except, idToCategories, categoryToIDs)
if err != nil {
return nil, err
}
// this removes duplicates
// we already know that a given rule with the same ID is equivalent
resultIDToRuleBuilder := make(map[string]*RuleBuilder)
for id := range useIDMap {
ruleBuilder, ok := idToRuleBuilder[id]
if !ok {
return nil, fmt.Errorf("%q is not a known id after verification", id)
}
resultIDToRuleBuilder[ruleBuilder.id] = ruleBuilder
}
for id := range exceptIDMap {
if _, ok := idToRuleBuilder[id]; !ok {
return nil, fmt.Errorf("%q is not a known id after verification", id)
}
delete(resultIDToRuleBuilder, id)
}
resultRuleBuilders := make([]*RuleBuilder, 0, len(resultIDToRuleBuilder))
for _, ruleBuilder := range resultIDToRuleBuilder {
resultRuleBuilders = append(resultRuleBuilders, ruleBuilder)
}
resultRules := make([]*Rule, 0, len(resultRuleBuilders))
for _, ruleBuilder := range resultRuleBuilders {
categories, err := getRuleBuilderCategories(ruleBuilder, idToCategories)
if err != nil {
return nil, err
}
rule, err := ruleBuilder.NewRule(configBuilder, categories)
if err != nil {
return nil, err
}
resultRules = append(resultRules, rule)
}
sortRules(resultRules)
ignoreIDToRootPathsUnnormalized, err := transformToIDToListMap(configBuilder.IgnoreIDOrCategoryToRootPaths, idToCategories, categoryToIDs)
if err != nil {
return nil, err
}
ignoreIDToRootPaths := make(map[string]map[string]struct{})
for id, rootPaths := range ignoreIDToRootPathsUnnormalized {
for rootPath := range rootPaths {
if rootPath == "" {
continue
}
rootPath, err := normalpath.NormalizeAndValidate(rootPath)
if err != nil {
return nil, err
}
if rootPath == "." {
return nil, fmt.Errorf("cannot specify %q as an ignore path", rootPath)
}
resultRootPathMap, ok := ignoreIDToRootPaths[id]
if !ok {
resultRootPathMap = make(map[string]struct{})
ignoreIDToRootPaths[id] = resultRootPathMap
}
resultRootPathMap[rootPath] = struct{}{}
}
}
ignoreRootPaths := make(map[string]struct{}, len(configBuilder.IgnoreRootPaths))
for _, rootPath := range configBuilder.IgnoreRootPaths {
if rootPath == "" {
continue
}
rootPath, err := normalpath.NormalizeAndValidate(rootPath)
if err != nil {
return nil, err
}
if rootPath == "." {
return nil, fmt.Errorf("cannot specify %q as an ignore path", rootPath)
}
ignoreRootPaths[rootPath] = struct{}{}
}
return &Config{
Rules: resultRules,
IgnoreIDToRootPaths: ignoreIDToRootPaths,
IgnoreRootPaths: ignoreRootPaths,
AllowCommentIgnores: configBuilder.AllowCommentIgnores,
IgnoreUnstablePackages: configBuilder.IgnoreUnstablePackages,
}, nil
}
func transformToIDMap(idsOrCategories []string, idToCategories map[string][]string, categoryToIDs map[string][]string) (map[string]struct{}, error) {
if len(idsOrCategories) == 0 {
return nil, nil
}
idMap := make(map[string]struct{}, len(idsOrCategories))
for _, idOrCategory := range idsOrCategories {
if idOrCategory == "" {
continue
}
if _, ok := idToCategories[idOrCategory]; ok {
id := idOrCategory
idMap[id] = struct{}{}
} else if ids, ok := categoryToIDs[idOrCategory]; ok {
for _, id := range ids {
idMap[id] = struct{}{}
}
} else {
return nil, fmt.Errorf("%q is not a known id or category", idOrCategory)
}
}
return idMap, nil
}
func transformToIDToListMap(idOrCategoryToList map[string][]string, idToCategories map[string][]string, categoryToIDs map[string][]string) (map[string]map[string]struct{}, error) {
if len(idOrCategoryToList) == 0 {
return nil, nil
}
idToListMap := make(map[string]map[string]struct{}, len(idOrCategoryToList))
for idOrCategory, list := range idOrCategoryToList {
if idOrCategory == "" {
continue
}
if _, ok := idToCategories[idOrCategory]; ok {
id := idOrCategory
if _, ok := idToListMap[id]; !ok {
idToListMap[id] = make(map[string]struct{})
}
for _, elem := range list {
idToListMap[id][elem] = struct{}{}
}
} else if ids, ok := categoryToIDs[idOrCategory]; ok {
for _, id := range ids {
if _, ok := idToListMap[id]; !ok {
idToListMap[id] = make(map[string]struct{})
}
for _, elem := range list {
idToListMap[id][elem] = struct{}{}
}
}
} else {
return nil, fmt.Errorf("%q is not a known id or category", idOrCategory)
}
}
return idToListMap, nil
}
func getCategoryToIDs(idToCategories map[string][]string) map[string][]string {
categoryToIDs := make(map[string][]string)
for id, categories := range idToCategories {
for _, category := range categories {
// handles empty category as well
categoryToIDs[category] = append(categoryToIDs[category], id)
}
}
return categoryToIDs
}
func getIDToRuleBuilder(ruleBuilders []*RuleBuilder) (map[string]*RuleBuilder, error) {
m := make(map[string]*RuleBuilder)
for _, ruleBuilder := range ruleBuilders {
if _, ok := m[ruleBuilder.id]; ok {
return nil, fmt.Errorf("duplicate rule ID: %q", ruleBuilder.id)
}
m[ruleBuilder.id] = ruleBuilder
}
return m, nil
}
func getRuleBuilderCategories(
ruleBuilder *RuleBuilder,
idToCategories map[string][]string,
) ([]string, error) {
categories, ok := idToCategories[ruleBuilder.id]
if !ok {
return nil, fmt.Errorf("%q is not configured for categories", ruleBuilder.id)
}
// it is ok for categories to be empty, however the map must contain an entry
// or otherwise this is a system error
return categories, nil
}
func sortRules(rules []*Rule) {
sort.Slice(
rules,
func(i int, j int) bool {
// categories are sorted at this point
// so we know the first category is a top-level category if present
one := rules[i]
two := rules[j]
oneCategories := one.Categories()
twoCategories := two.Categories()
if len(oneCategories) == 0 && len(twoCategories) > 0 {
return false
}
if len(oneCategories) > 0 && len(twoCategories) == 0 {
return true
}
if len(oneCategories) > 0 && len(twoCategories) > 0 {
compare := categoryCompare(oneCategories[0], twoCategories[0])
if compare < 0 {
return true
}
if compare > 0 {
return false
}
}
oneCategoriesString := strings.Join(oneCategories, ",")
twoCategoriesString := strings.Join(twoCategories, ",")
if oneCategoriesString < twoCategoriesString {
return true
}
if oneCategoriesString > twoCategoriesString {
return false
}
return one.ID() < two.ID()
},
)
}