github.com/greenpau/go-identity@v1.1.6/internal/tag/tag.go (about)

     1  // Copyright 2020 Paul Greenberg greenpau@outlook.com
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package tag
    16  
    17  import (
    18  	"fmt"
    19  	"github.com/iancoleman/strcase"
    20  	"reflect"
    21  	"strings"
    22  	"unicode"
    23  )
    24  
    25  // Options stores compliance check options.
    26  type Options struct {
    27  	Disabled           bool
    28  	DisableTagPresent  bool
    29  	DisableTagMismatch bool
    30  	DisableTagOnEmpty  bool
    31  }
    32  
    33  // GetTagCompliance performs struct tag compliance checks.
    34  func GetTagCompliance(resource interface{}, opts *Options) ([]string, error) {
    35  	var output []string
    36  	if opts == nil {
    37  		opts = &Options{}
    38  	}
    39  
    40  	if opts.Disabled {
    41  		return output, nil
    42  	}
    43  
    44  	rr := reflect.TypeOf(resource).Elem()
    45  	//resourceType := fmt.Sprintf("%s", rr.Name())
    46  	rk := fmt.Sprintf("%s", rr.Kind())
    47  
    48  	if rk != "struct" {
    49  		return nil, fmt.Errorf("resource kind %q is unsupported", rk)
    50  	}
    51  
    52  	suggestedStructChanges := []string{}
    53  
    54  	requiredTags := []string{"json", "xml", "yaml"}
    55  	for i := 0; i < rr.NumField(); i++ {
    56  		resourceField := rr.Field(i)
    57  		if !unicode.IsUpper(rune(resourceField.Name[0])) {
    58  			// Skip internal fields.
    59  			continue
    60  		}
    61  
    62  		expTagValue := convertFieldToTag(resourceField.Name)
    63  		if !opts.DisableTagOnEmpty {
    64  			expTagValue = expTagValue + ",omitempty"
    65  		}
    66  		var lastTag bool
    67  		for j, tagName := range requiredTags {
    68  			if len(requiredTags)-1 == j {
    69  				lastTag = true
    70  			}
    71  
    72  			tagValue := resourceField.Tag.Get(tagName)
    73  
    74  			if tagValue == "-" {
    75  				break
    76  			}
    77  			if tagValue == "" && !opts.DisableTagPresent {
    78  				output = append(output, fmt.Sprintf(
    79  					"tag %q not found in %s.%s (%v)",
    80  					tagName,
    81  					//resourceType,
    82  					rr.Name(),
    83  					resourceField.Name,
    84  					resourceField.Type,
    85  				))
    86  				if lastTag {
    87  					tags := makeTags(requiredTags, expTagValue)
    88  					resType := fmt.Sprintf("%v", resourceField.Type)
    89  					resType = strings.Replace(resType, "identity.", "", -1)
    90  					suggestedStructChanges = append(suggestedStructChanges, fmt.Sprintf(
    91  						"%s %s %s", resourceField.Name, resType, tags,
    92  					))
    93  				}
    94  				continue
    95  			}
    96  			//if strings.Contains(tagValue, ",omitempty") {
    97  			//	tagValue = strings.Replace(tagValue, ",omitempty", "", -1)
    98  			//}
    99  			if (tagValue != expTagValue) && !opts.DisableTagMismatch {
   100  				output = append(output, fmt.Sprintf(
   101  					"tag %q mismatch found in %s.%s (%v): %s (actual) vs. %s (expected)",
   102  					tagName,
   103  					//resourceType,
   104  					rr.Name(),
   105  					resourceField.Name,
   106  					resourceField.Type,
   107  					tagValue,
   108  					expTagValue,
   109  				))
   110  				continue
   111  
   112  			}
   113  		}
   114  	}
   115  
   116  	if len(suggestedStructChanges) > 0 {
   117  		output = append(output, fmt.Sprintf(
   118  			"suggested struct changes to %s:\n%s",
   119  			rr.Name(),
   120  			strings.Join(suggestedStructChanges, "\n"),
   121  		))
   122  	}
   123  
   124  	if len(output) > 0 {
   125  		return output, fmt.Errorf("struct %q is not compliant", rr.Name())
   126  	}
   127  
   128  	return output, nil
   129  }
   130  
   131  func convertFieldToTag(s string) string {
   132  	s = strcase.ToSnake(s)
   133  	s = strings.ReplaceAll(s, "_md_5", "_md5")
   134  	s = strings.ReplaceAll(s, "open_ssh", "openssh")
   135  	return s
   136  }
   137  
   138  func makeTags(tags []string, s string) string {
   139  	var b strings.Builder
   140  	b.WriteRune('`')
   141  	tagOutput := []string{}
   142  	for _, tag := range tags {
   143  		tagOutput = append(tagOutput, tag+":\""+s+"\"")
   144  	}
   145  	b.WriteString(strings.Join(tagOutput, " "))
   146  	b.WriteRune('`')
   147  	return b.String()
   148  }