github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/internal/jsonschema/main.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  	"reflect"
    11  	"sort"
    12  	"strings"
    13  
    14  	"github.com/iancoleman/strcase"
    15  	"github.com/invopop/jsonschema"
    16  
    17  	"github.com/anchore/syft/internal"
    18  	syftJsonModel "github.com/anchore/syft/syft/format/syftjson/model"
    19  	"github.com/anchore/syft/syft/internal/packagemetadata"
    20  )
    21  
    22  /*
    23  This method of creating the JSON schema only captures strongly typed fields for the purpose of integrations between syft
    24  JSON output and integrations. The downside to this approach is that any values and types used on weakly typed fields
    25  are not captured (empty interfaces). This means that pkg.Package.Metadata is not validated at this time. This approach
    26  can be extended to include specific package metadata struct shapes in the future.
    27  */
    28  
    29  func main() {
    30  	write(encode(build()))
    31  }
    32  
    33  func schemaID() jsonschema.ID {
    34  	// Today we do not host the schemas at this address, but per the JSON schema spec we should be referencing
    35  	// the schema by a URL in a domain we control. This is a placeholder for now.
    36  	return jsonschema.ID(fmt.Sprintf("anchore.io/schema/syft/json/%s", internal.JSONSchemaVersion))
    37  }
    38  
    39  func assembleTypeContainer(items []any) (any, map[string]string) {
    40  	structFields := make([]reflect.StructField, len(items))
    41  	mapping := make(map[string]string, len(items))
    42  	typesMissingNames := make([]reflect.Type, 0)
    43  	for i, item := range items {
    44  		itemType := reflect.TypeOf(item)
    45  
    46  		jsonName := packagemetadata.JSONName(item)
    47  		fieldName := strcase.ToCamel(jsonName)
    48  
    49  		if jsonName == "" {
    50  			typesMissingNames = append(typesMissingNames, itemType)
    51  			continue
    52  		}
    53  
    54  		mapping[itemType.Name()] = fieldName
    55  
    56  		structFields[i] = reflect.StructField{
    57  			Name: fieldName,
    58  			Type: itemType,
    59  		}
    60  	}
    61  
    62  	if len(typesMissingNames) > 0 {
    63  		fmt.Println("the following types are missing JSON names (manually curated in ./syft/internal/packagemetadata/names.go):")
    64  		for _, t := range typesMissingNames {
    65  			fmt.Println("  - ", t.Name())
    66  		}
    67  		os.Exit(1)
    68  	}
    69  
    70  	structType := reflect.StructOf(structFields)
    71  	return reflect.New(structType).Elem().Interface(), mapping
    72  }
    73  
    74  func build() *jsonschema.Schema {
    75  	reflector := &jsonschema.Reflector{
    76  		BaseSchemaID:              schemaID(),
    77  		AllowAdditionalProperties: true,
    78  		Namer: func(r reflect.Type) string {
    79  			return strings.TrimPrefix(r.Name(), "JSON")
    80  		},
    81  	}
    82  
    83  	pkgMetadataContainer, pkgMetadataMapping := assembleTypeContainer(packagemetadata.AllTypes())
    84  	pkgMetadataContainerType := reflect.TypeOf(pkgMetadataContainer)
    85  
    86  	// srcMetadataContainer := assembleTypeContainer(sourcemetadata.AllTypes())
    87  	// srcMetadataContainerType := reflect.TypeOf(srcMetadataContainer)
    88  
    89  	documentSchema := reflector.ReflectFromType(reflect.TypeOf(&syftJsonModel.Document{}))
    90  	pkgMetadataSchema := reflector.ReflectFromType(reflect.TypeOf(pkgMetadataContainer))
    91  	// srcMetadataSchema := reflector.ReflectFromType(reflect.TypeOf(srcMetadataContainer))
    92  
    93  	// TODO: add source metadata types
    94  
    95  	// inject the definitions of all packages metadata into the schema definitions
    96  
    97  	var metadataNames []string
    98  	for typeName, definition := range pkgMetadataSchema.Definitions {
    99  		if typeName == pkgMetadataContainerType.Name() {
   100  			// ignore the definition for the fake container
   101  			continue
   102  		}
   103  
   104  		displayName, ok := pkgMetadataMapping[typeName]
   105  		if ok {
   106  			// this is a package metadata type...
   107  			documentSchema.Definitions[displayName] = definition
   108  			metadataNames = append(metadataNames, displayName)
   109  		} else {
   110  			// this is a type that the metadata type uses (e.g. DpkgFileRecord)
   111  			documentSchema.Definitions[typeName] = definition
   112  		}
   113  	}
   114  
   115  	// ensure the generated list of names is stable between runs
   116  	sort.Strings(metadataNames)
   117  
   118  	metadataTypes := []map[string]string{
   119  		// allow for no metadata to be provided
   120  		{"type": "null"},
   121  	}
   122  	for _, name := range metadataNames {
   123  		metadataTypes = append(metadataTypes, map[string]string{
   124  			"$ref": fmt.Sprintf("#/$defs/%s", name),
   125  		})
   126  	}
   127  
   128  	// set the "anyOf" field for Package.Metadata to be a conjunction of several types
   129  	documentSchema.Definitions["Package"].Properties.Set("metadata", map[string][]map[string]string{
   130  		"anyOf": metadataTypes,
   131  	})
   132  
   133  	return documentSchema
   134  }
   135  
   136  func encode(schema *jsonschema.Schema) []byte {
   137  	newSchemaBuffer := new(bytes.Buffer)
   138  	enc := json.NewEncoder(newSchemaBuffer)
   139  	// prevent > and < from being escaped in the payload
   140  	enc.SetEscapeHTML(false)
   141  	enc.SetIndent("", "  ")
   142  	err := enc.Encode(&schema)
   143  	if err != nil {
   144  		panic(err)
   145  	}
   146  
   147  	return newSchemaBuffer.Bytes()
   148  }
   149  
   150  func write(schema []byte) {
   151  	repoRoot, err := packagemetadata.RepoRoot()
   152  	if err != nil {
   153  		fmt.Println("unable to determine repo root")
   154  		os.Exit(1)
   155  	}
   156  	schemaPath := filepath.Join(repoRoot, "schema", "json", fmt.Sprintf("schema-%s.json", internal.JSONSchemaVersion))
   157  	latestSchemaPath := filepath.Join(repoRoot, "schema", "json", "schema-latest.json")
   158  
   159  	if _, err := os.Stat(schemaPath); !os.IsNotExist(err) {
   160  		// check if the schema is the same...
   161  		existingFh, err := os.Open(schemaPath)
   162  		if err != nil {
   163  			panic(err)
   164  		}
   165  
   166  		existingSchemaBytes, err := io.ReadAll(existingFh)
   167  		if err != nil {
   168  			panic(err)
   169  		}
   170  
   171  		if bytes.Equal(existingSchemaBytes, schema) {
   172  			// the generated schema is the same, bail with no error :)
   173  			fmt.Println("No change to the existing schema!")
   174  			os.Exit(0)
   175  		}
   176  
   177  		// the generated schema is different, bail with error :(
   178  		fmt.Printf("Cowardly refusing to overwrite existing schema (%s)!\nSee the schema/json/README.md for how to increment\n", schemaPath)
   179  		os.Exit(1)
   180  	}
   181  
   182  	fh, err := os.Create(schemaPath)
   183  	if err != nil {
   184  		panic(err)
   185  	}
   186  	defer fh.Close()
   187  
   188  	_, err = fh.Write(schema)
   189  	if err != nil {
   190  		panic(err)
   191  	}
   192  
   193  	latestFile, err := os.Create(latestSchemaPath)
   194  	if err != nil {
   195  		panic(err)
   196  	}
   197  	defer latestFile.Close()
   198  
   199  	_, err = latestFile.Write(schema)
   200  	if err != nil {
   201  		panic(err)
   202  	}
   203  
   204  	fmt.Printf("Wrote new schema to %q\n", schemaPath)
   205  }