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 }