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  }