github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/format/syftjson/schema_test.go (about) 1 package syftjson 2 3 import ( 4 "encoding/json" 5 "os" 6 "path/filepath" 7 "testing" 8 9 "github.com/iancoleman/strcase" 10 "github.com/stretchr/testify/require" 11 12 "github.com/anchore/syft/syft/internal/packagemetadata" 13 ) 14 15 type schema struct { 16 Schema string `json:"$schema"` 17 ID string `json:"$id"` 18 Ref string `json:"$ref"` 19 Defs map[string]properties `json:"$defs"` 20 } 21 22 type properties struct { 23 Properties map[string]any `json:"properties"` 24 Type string `json:"type"` 25 Required []string `json:"required"` 26 } 27 28 func (p properties) fields() []string { 29 var result []string 30 31 for k := range p.Properties { 32 result = append(result, k) 33 } 34 return result 35 } 36 37 func Test_JSONSchemaConventions(t *testing.T) { 38 // read schema/json/schema-latest.json 39 // look at all attributes and ensure that all fields are camelCase 40 // we want to strictly follow https://google.github.io/styleguide/javaguide.html#s5.3-camel-case 41 // 42 // > Convert the phrase to plain ASCII and remove any apostrophes. For example, "Müller's algorithm" might become "Muellers algorithm". 43 // > Divide this result into words, splitting on spaces and any remaining punctuation (typically hyphens). 44 // > Recommended: if any word already has a conventional camel-case appearance in common usage, split this into its constituent parts (e.g., "AdWords" becomes "ad words"). Note that a word such as "iOS" is not really in camel case per se; it defies any convention, so this recommendation does not apply. 45 // > Now lowercase everything (including acronyms), then uppercase only the first character of: 46 // > ... each word, to yield upper camel case, or 47 // > ... each word except the first, to yield lower camel case 48 // > Finally, join all the words into a single identifier. 49 // 50 // This means that acronyms should be treated as words (e.g. "HttpServer" not "HTTPServer") 51 52 root, err := packagemetadata.RepoRoot() 53 require.NoError(t, err) 54 55 contents, err := os.ReadFile(filepath.Join(root, "schema", "json", "schema-latest.json")) 56 require.NoError(t, err) 57 58 var s schema 59 require.NoError(t, json.Unmarshal(contents, &s)) 60 61 require.NotEmpty(t, s.Defs) 62 63 for name, def := range s.Defs { 64 checkAndConvertFields(t, name, def.fields()) 65 } 66 } 67 68 func checkAndConvertFields(t *testing.T, path string, properties []string) { 69 for _, fieldName := range properties { 70 if pass, exp := isFollowingConvention(path, fieldName); !pass { 71 t.Logf("%s: has non camel case field: %q (expected %q)", path, fieldName, exp) 72 } 73 74 } 75 } 76 77 func isFollowingConvention(path, fieldName string) (bool, string) { 78 exp := strcase.ToLowerCamel(fieldName) 79 result := exp == fieldName 80 81 exception := func(exceptions ...string) (bool, string) { 82 for _, e := range exceptions { 83 if e == fieldName { 84 return true, fieldName 85 } 86 } 87 return result, exp 88 } 89 90 // add exceptions as needed... these are grandfathered in and will be addressed in a future breaking schema change 91 // ideally in the future there will be no exceptions to the camel case convention for fields 92 switch path { 93 case "Coordinates", "Location": 94 return exception("layerID") 95 case "MicrosoftKbPatch": 96 return exception("product_id") 97 case "HaskellHackageStackLockEntry": 98 return exception("snapshotURL") 99 case "LinuxRelease": 100 return exception("imageID", "supportURL", "privacyPolicyURL", "versionID", "variantID", "homeURL", "buildID", "bugReportURL") 101 case "CConanLockV2Entry": 102 return exception("packageID") 103 case "CConanInfoEntry": 104 return exception("package_id") 105 case "PhpComposerInstalledEntry", "PhpComposerLockEntry": 106 return exception("notification-url", "require-dev") 107 case "LinuxKernelArchive": 108 return exception("rwRootFS") 109 case "CConanLockEntry": 110 return exception("build_requires", "py_requires", "package_id") 111 case "FileMetadataEntry": 112 return exception("userID", "groupID") 113 case "DartPubspecLockEntry": 114 return exception("hosted_url", "vcs_url") 115 case "ELFSecurityFeatures": 116 return exception("relRO") 117 } 118 return result, exp 119 }