github.com/greenpau/go-authcrunch@v1.1.4/internal/tag/tag.go (about)

     1  // Copyright 2022 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  	AllowFieldMismatch bool
    32  	AllowedFields      map[string]interface{}
    33  }
    34  
    35  // GetTagCompliance performs struct tag compliance checks.
    36  func GetTagCompliance(resource interface{}, opts *Options) ([]string, error) {
    37  	var output []string
    38  	if opts == nil {
    39  		opts = &Options{}
    40  	}
    41  
    42  	if opts.Disabled {
    43  		return output, nil
    44  	}
    45  
    46  	rr := reflect.TypeOf(resource).Elem()
    47  	//resourceType := fmt.Sprintf("%s", rr.Name())
    48  	rk := fmt.Sprintf("%s", rr.Kind())
    49  
    50  	if rk != "struct" {
    51  		return nil, fmt.Errorf("resource kind %q is unsupported", rk)
    52  	}
    53  
    54  	suggestedStructChanges := []string{}
    55  
    56  	requiredTags := []string{"json", "xml", "yaml"}
    57  	for i := 0; i < rr.NumField(); i++ {
    58  		resourceField := rr.Field(i)
    59  		if !unicode.IsUpper(rune(resourceField.Name[0])) {
    60  			// Skip internal fields.
    61  			continue
    62  		}
    63  
    64  		expTagValue := convertFieldToTag(resourceField.Name)
    65  		if !opts.DisableTagOnEmpty {
    66  			expTagValue = expTagValue + ",omitempty"
    67  		}
    68  		var lastTag bool
    69  		for j, tagName := range requiredTags {
    70  			if len(requiredTags)-1 == j {
    71  				lastTag = true
    72  			}
    73  
    74  			tagValue := resourceField.Tag.Get(tagName)
    75  
    76  			if tagValue == "-" {
    77  				break
    78  			}
    79  			if tagValue == "" && !opts.DisableTagPresent {
    80  				output = append(output, fmt.Sprintf(
    81  					"tag %q not found in %s.%s (%v)",
    82  					tagName,
    83  					//resourceType,
    84  					rr.Name(),
    85  					resourceField.Name,
    86  					resourceField.Type,
    87  				))
    88  				if lastTag {
    89  					tags := makeTags(requiredTags, expTagValue)
    90  					resType := fmt.Sprintf("%v", resourceField.Type)
    91  					resType = strings.Replace(resType, "identity.", "", -1)
    92  					suggestedStructChanges = append(suggestedStructChanges, fmt.Sprintf(
    93  						"%s %s %s", resourceField.Name, resType, tags,
    94  					))
    95  				}
    96  				continue
    97  			}
    98  			//if strings.Contains(tagValue, ",omitempty") {
    99  			//	tagValue = strings.Replace(tagValue, ",omitempty", "", -1)
   100  			//}
   101  			if (tagValue != expTagValue) && !opts.DisableTagMismatch {
   102  				if opts.AllowFieldMismatch && opts.AllowedFields != nil {
   103  					fieldName := strings.Split(tagValue, ",")[0]
   104  					if _, exists := opts.AllowedFields[fieldName]; exists {
   105  						continue
   106  					}
   107  				}
   108  				output = append(output, fmt.Sprintf(
   109  					"tag %q mismatch found in %s.%s (%v): %s (actual) vs. %s (expected)",
   110  					tagName,
   111  					//resourceType,
   112  					rr.Name(),
   113  					resourceField.Name,
   114  					resourceField.Type,
   115  					tagValue,
   116  					expTagValue,
   117  				))
   118  				continue
   119  			}
   120  		}
   121  	}
   122  
   123  	if len(suggestedStructChanges) > 0 {
   124  		output = append(output, fmt.Sprintf(
   125  			"suggested struct changes to %s:\n%s",
   126  			rr.Name(),
   127  			strings.Join(suggestedStructChanges, "\n"),
   128  		))
   129  	}
   130  
   131  	if len(output) > 0 {
   132  		return output, fmt.Errorf("struct %q is not compliant", rr.Name())
   133  	}
   134  
   135  	return output, nil
   136  }
   137  
   138  func convertFieldToTag(s string) string {
   139  	s = strcase.ToSnake(s)
   140  	s = strings.ReplaceAll(s, "_md_5", "_md5")
   141  	s = strings.ReplaceAll(s, "open_ssh", "openssh")
   142  	s = strings.ReplaceAll(s, "o_auth_2", "oauth2")
   143  	s = strings.ReplaceAll(s, "_ur_ls", "_urls")
   144  	return s
   145  }
   146  
   147  func makeTags(tags []string, s string) string {
   148  	var b strings.Builder
   149  	b.WriteRune('`')
   150  	tagOutput := []string{}
   151  	for _, tag := range tags {
   152  		tagOutput = append(tagOutput, tag+":\""+s+"\"")
   153  	}
   154  	b.WriteString(strings.Join(tagOutput, " "))
   155  	b.WriteRune('`')
   156  	return b.String()
   157  }