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