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 }