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 }