github.com/nextlinux/gosbom@v0.81.1-0.20230627115839-1ff50c281391/schema/json/main.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"reflect"
    10  	"sort"
    11  	"strings"
    12  
    13  	"github.com/invopop/jsonschema"
    14  	gosbomjsonModel "github.com/nextlinux/gosbom/gosbom/formats/gosbomjson/model"
    15  	"github.com/nextlinux/gosbom/internal"
    16  	genInt "github.com/nextlinux/gosbom/schema/json/internal"
    17  )
    18  
    19  /*
    20  This method of creating the JSON schema only captures strongly typed fields for the purpose of integrations between gosbom
    21  JSON output and integrations. The downside to this approach is that any values and types used on weakly typed fields
    22  are not captured (empty interfaces). This means that pkg.Package.Metadata is not validated at this time. This approach
    23  can be extended to include specific package metadata struct shapes in the future.
    24  */
    25  
    26  //go:generate go run ./generate/main.go
    27  
    28  const schemaVersion = internal.JSONSchemaVersion
    29  
    30  func main() {
    31  	write(encode(build()))
    32  }
    33  
    34  func build() *jsonschema.Schema {
    35  	reflector := &jsonschema.Reflector{
    36  		AllowAdditionalProperties: true,
    37  		Namer: func(r reflect.Type) string {
    38  			return strings.TrimPrefix(r.Name(), "JSON")
    39  		},
    40  	}
    41  	documentSchema := reflector.ReflectFromType(reflect.TypeOf(&gosbomjsonModel.Document{}))
    42  	metadataSchema := reflector.ReflectFromType(reflect.TypeOf(&genInt.ArtifactMetadataContainer{}))
    43  	// TODO: inject source definitions
    44  
    45  	// inject the definitions of all metadatas into the schema definitions
    46  
    47  	var metadataNames []string
    48  	for name, definition := range metadataSchema.Definitions {
    49  		if name == reflect.TypeOf(genInt.ArtifactMetadataContainer{}).Name() {
    50  			// ignore the definition for the fake container
    51  			continue
    52  		}
    53  		documentSchema.Definitions[name] = definition
    54  		if strings.HasSuffix(name, "Metadata") {
    55  			metadataNames = append(metadataNames, name)
    56  		}
    57  	}
    58  
    59  	// ensure the generated list of names is stable between runs
    60  	sort.Strings(metadataNames)
    61  
    62  	var metadataTypes = []map[string]string{
    63  		// allow for no metadata to be provided
    64  		{"type": "null"},
    65  	}
    66  	for _, name := range metadataNames {
    67  		metadataTypes = append(metadataTypes, map[string]string{
    68  			"$ref": fmt.Sprintf("#/$defs/%s", name),
    69  		})
    70  	}
    71  
    72  	// set the "anyOf" field for Package.Metadata to be a conjunction of several types
    73  	documentSchema.Definitions["Package"].Properties.Set("metadata", map[string][]map[string]string{
    74  		"anyOf": metadataTypes,
    75  	})
    76  
    77  	return documentSchema
    78  }
    79  
    80  func encode(schema *jsonschema.Schema) []byte {
    81  	var newSchemaBuffer = new(bytes.Buffer)
    82  	enc := json.NewEncoder(newSchemaBuffer)
    83  	// prevent > and < from being escaped in the payload
    84  	enc.SetEscapeHTML(false)
    85  	enc.SetIndent("", "  ")
    86  	err := enc.Encode(&schema)
    87  	if err != nil {
    88  		panic(err)
    89  	}
    90  
    91  	return newSchemaBuffer.Bytes()
    92  }
    93  
    94  func write(schema []byte) {
    95  	filename := fmt.Sprintf("schema-%s.json", schemaVersion)
    96  
    97  	if _, err := os.Stat(filename); !os.IsNotExist(err) {
    98  		// check if the schema is the same...
    99  		existingFh, err := os.Open(filename)
   100  		if err != nil {
   101  			panic(err)
   102  		}
   103  
   104  		existingSchemaBytes, err := io.ReadAll(existingFh)
   105  		if err != nil {
   106  			panic(err)
   107  		}
   108  
   109  		if bytes.Equal(existingSchemaBytes, schema) {
   110  			// the generated schema is the same, bail with no error :)
   111  			fmt.Println("No change to the existing schema!")
   112  			os.Exit(0)
   113  		}
   114  
   115  		// the generated schema is different, bail with error :(
   116  		fmt.Printf("Cowardly refusing to overwrite existing schema (%s)!\nSee the schema/json/README.md for how to increment\n", filename)
   117  		os.Exit(1)
   118  	}
   119  
   120  	fh, err := os.Create(filename)
   121  	if err != nil {
   122  		panic(err)
   123  	}
   124  
   125  	_, err = fh.Write(schema)
   126  	if err != nil {
   127  		panic(err)
   128  	}
   129  
   130  	defer fh.Close()
   131  
   132  	fmt.Printf("Wrote new schema to %q\n", filename)
   133  }