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 }