go.ligato.io/vpp-agent/v3@v3.5.0/plugins/restapi/jsonschema/converter/types.go (about) 1 package converter 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "math" 7 "strconv" 8 "strings" 9 10 "github.com/alecthomas/jsonschema" 11 "github.com/iancoleman/orderedmap" 12 "github.com/xeipuuv/gojsonschema" 13 "google.golang.org/protobuf/encoding/prototext" 14 "google.golang.org/protobuf/proto" 15 "google.golang.org/protobuf/types/descriptorpb" 16 17 "go.ligato.io/vpp-agent/v3/proto/ligato" 18 ) 19 20 const ( 21 PatternIpv6WithMask = "^(::|(([a-fA-F0-9]{1,4}):){7}(([a-fA-F0-9]{1,4}))|(:(:([a-fA-F0-9]{1,4})){1,6})|((([a-fA-F0-9]{1,4}):){1,6}:)|((([a-fA-F0-9]{1,4}):)(:([a-fA-F0-9]{1,4})){1,6})|((([a-fA-F0-9]{1,4}):){2}(:([a-fA-F0-9]{1,4})){1,5})|((([a-fA-F0-9]{1,4}):){3}(:([a-fA-F0-9]{1,4})){1,4})|((([a-fA-F0-9]{1,4}):){4}(:([a-fA-F0-9]{1,4})){1,3})|((([a-fA-F0-9]{1,4}):){5}(:([a-fA-F0-9]{1,4})){1,2}))(\\\\/(12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))$" 22 PatternIpv4WithMask = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(/(3[0-2]|[1-2][0-9]|[0-9]))$" 23 ) 24 25 var ( 26 globalPkg = newProtoPackage(nil, "") 27 28 wellKnownTypes = map[string]bool{ 29 "DoubleValue": true, 30 "FloatValue": true, 31 "Int64Value": true, 32 "UInt64Value": true, 33 "Int32Value": true, 34 "UInt32Value": true, 35 "BoolValue": true, 36 "StringValue": true, 37 "BytesValue": true, 38 "Value": true, 39 } 40 ) 41 42 // min/max constants that are safe to assign to int on 32-bit systems 43 // The "github.com/alecthomas/jsonschema".Type has manimum and maximum defined as int, but that is insufficient 44 // for some types. Therefore the ranges for these types must be artificially cut to be usable with int. 45 var ( 46 intSafeMaxUint32 int = math.MaxInt32 // int32 can't hold values up to math.MaxUint32 47 intSafeMinInt64 int = math.MinInt32 48 intSafeMaxInt64 int = math.MaxInt32 49 intSafeMaxUint64 int = math.MaxInt32 50 ) 51 52 func init() { 53 if strconv.IntSize == 64 { // override of min/max constants for 64-bit systems 54 intSafeMaxUint32 = math.MaxUint32 55 intSafeMinInt64 = math.MinInt64 56 intSafeMaxInt64 = math.MaxInt64 57 intSafeMaxUint64 = math.MaxInt64 // int64 can't hold values up to math.MaxUint64 58 } 59 } 60 61 func (c *Converter) registerEnum(pkgName *string, enum *descriptorpb.EnumDescriptorProto) { 62 pkg := globalPkg 63 if pkgName != nil { 64 for _, node := range strings.Split(*pkgName, ".") { 65 if pkg == globalPkg && node == "" { 66 // Skips leading "." 67 continue 68 } 69 child, ok := pkg.children[node] 70 if !ok { 71 child = newProtoPackage(pkg, node) 72 pkg.children[node] = child 73 } 74 pkg = child 75 } 76 } 77 pkg.enums[enum.GetName()] = enum 78 } 79 80 func (c *Converter) registerType(pkgName *string, msg *descriptorpb.DescriptorProto) { 81 pkg := globalPkg 82 if pkgName != nil { 83 for _, node := range strings.Split(*pkgName, ".") { 84 if pkg == globalPkg && node == "" { 85 // Skips leading "." 86 continue 87 } 88 child, ok := pkg.children[node] 89 if !ok { 90 child = newProtoPackage(pkg, node) 91 pkg.children[node] = child 92 } 93 pkg = child 94 } 95 } 96 pkg.types[msg.GetName()] = msg 97 } 98 99 // applyAllowNullValuesOption applies schema changes to schema while handling possibility of use Null values 100 // (if enabled). This is a convenience method for handling the NULL values option. 101 func (c *Converter) applyAllowNullValuesOption(schema *jsonschema.Type, schemaChanges *jsonschema.Type) { 102 if c.AllowNullValues { // insert possibility of using NULL type 103 if len(schemaChanges.OneOf) == 0 { 104 schema.OneOf = []*jsonschema.Type{ 105 { 106 Type: gojsonschema.TYPE_NULL, 107 }, { 108 Type: schemaChanges.Type, 109 Format: schemaChanges.Format, 110 Pattern: schemaChanges.Pattern, 111 Minimum: schemaChanges.Minimum, 112 ExclusiveMinimum: schemaChanges.ExclusiveMinimum, 113 Maximum: schemaChanges.Maximum, 114 ExclusiveMaximum: schemaChanges.ExclusiveMaximum, 115 }, 116 } 117 } else { 118 schema.OneOf = append([]*jsonschema.Type{ 119 {Type: gojsonschema.TYPE_NULL}, 120 }, schemaChanges.OneOf...) 121 } 122 } else { // direct mapping (schema could be already partially built -> need to fill new values into it) 123 schema.Type = schemaChanges.Type 124 schema.Format = schemaChanges.Format 125 schema.Pattern = schemaChanges.Pattern 126 schema.Minimum = schemaChanges.Minimum 127 schema.ExclusiveMinimum = schemaChanges.ExclusiveMinimum 128 schema.Maximum = schemaChanges.Maximum 129 schema.ExclusiveMaximum = schemaChanges.ExclusiveMaximum 130 schema.OneOf = schemaChanges.OneOf 131 } 132 } 133 134 // Convert a proto "field" (essentially a type-switch with some recursion): 135 func (c *Converter) convertField(curPkg *ProtoPackage, desc *descriptorpb.FieldDescriptorProto, msg *descriptorpb.DescriptorProto, duplicatedMessages map[*descriptorpb.DescriptorProto]string) (*jsonschema.Type, error) { 136 // Prepare a new jsonschema.Type for our eventual return value: 137 jsonSchemaType := &jsonschema.Type{} 138 139 // Generate a description from src comments (if available) 140 if src := c.sourceInfo.GetField(desc); src != nil { 141 jsonSchemaType.Description = formatDescription(src) 142 } 143 144 c.logger.Tracef("(PKG: %v) CONVERT FIELD %v", curPkg.name, desc) 145 146 // get field annotations 147 var fieldAnnotations *ligato.LigatoOptions 148 149 if proto.HasExtension(desc.Options, ligato.E_LigatoOptions) { 150 val := proto.GetExtension(desc.Options, ligato.E_LigatoOptions) 151 var ok bool 152 if fieldAnnotations, ok = val.(*ligato.LigatoOptions); !ok { 153 c.logger.Debugf("Field %s.%s have ligato option extension, but its value has "+ 154 "unexpected type (%T)", msg.GetName(), desc.GetName(), val) 155 } 156 } else { 157 c.logger.Debugf("Field %s.%s doesn't have ligato option extension", msg.GetName(), desc.GetName()) 158 } 159 160 // Switch the types, and pick a JSONSchema equivalent: 161 switch desc.GetType() { 162 case descriptorpb.FieldDescriptorProto_TYPE_DOUBLE, 163 descriptorpb.FieldDescriptorProto_TYPE_FLOAT: 164 c.applyAllowNullValuesOption(jsonSchemaType, &jsonschema.Type{Type: gojsonschema.TYPE_NUMBER}) 165 166 case descriptorpb.FieldDescriptorProto_TYPE_INT32, 167 descriptorpb.FieldDescriptorProto_TYPE_SFIXED32, 168 descriptorpb.FieldDescriptorProto_TYPE_SINT32: 169 schema := &jsonschema.Type{ 170 Type: gojsonschema.TYPE_INTEGER, 171 Minimum: math.MinInt32, 172 Maximum: math.MaxInt32, 173 } 174 c.applyIntRangeFieldAnnotation(fieldAnnotations, schema) 175 c.applyAllowNullValuesOption(jsonSchemaType, schema) 176 177 case descriptorpb.FieldDescriptorProto_TYPE_UINT32, 178 descriptorpb.FieldDescriptorProto_TYPE_FIXED32: 179 schema := &jsonschema.Type{ 180 Type: gojsonschema.TYPE_INTEGER, 181 Minimum: -1, 182 ExclusiveMinimum: true, 183 Maximum: intSafeMaxUint32, 184 } 185 c.applyIntRangeFieldAnnotation(fieldAnnotations, schema) 186 c.applyAllowNullValuesOption(jsonSchemaType, schema) 187 188 case descriptorpb.FieldDescriptorProto_TYPE_INT64, 189 descriptorpb.FieldDescriptorProto_TYPE_SFIXED64, 190 descriptorpb.FieldDescriptorProto_TYPE_SINT64: 191 if !c.DisallowBigIntsAsStrings { 192 c.applyAllowNullValuesOption(jsonSchemaType, &jsonschema.Type{Type: gojsonschema.TYPE_STRING}) 193 } else { 194 schema := &jsonschema.Type{ 195 Type: gojsonschema.TYPE_INTEGER, 196 Minimum: intSafeMinInt64, 197 Maximum: intSafeMaxInt64, 198 } 199 c.applyIntRangeFieldAnnotation(fieldAnnotations, schema) 200 c.applyAllowNullValuesOption(jsonSchemaType, schema) 201 } 202 203 case descriptorpb.FieldDescriptorProto_TYPE_UINT64, 204 descriptorpb.FieldDescriptorProto_TYPE_FIXED64: 205 if !c.DisallowBigIntsAsStrings { 206 c.applyAllowNullValuesOption(jsonSchemaType, &jsonschema.Type{Type: gojsonschema.TYPE_STRING}) 207 } else { 208 schema := &jsonschema.Type{ 209 Type: gojsonschema.TYPE_INTEGER, 210 Minimum: -1, 211 ExclusiveMinimum: true, 212 Maximum: intSafeMaxUint64, 213 } 214 c.applyIntRangeFieldAnnotation(fieldAnnotations, schema) 215 c.applyAllowNullValuesOption(jsonSchemaType, schema) 216 } 217 218 case descriptorpb.FieldDescriptorProto_TYPE_STRING: 219 schema := &jsonschema.Type{} 220 switch fieldAnnotations.GetType() { 221 case ligato.LigatoOptions_IPV6: 222 schema.Type = gojsonschema.TYPE_STRING 223 schema.Format = "ipv6" 224 case ligato.LigatoOptions_IPV4: 225 schema.Type = gojsonschema.TYPE_STRING 226 schema.Format = "ipv4" 227 case ligato.LigatoOptions_IP: 228 schema.OneOf = []*jsonschema.Type{ 229 { 230 Type: gojsonschema.TYPE_STRING, 231 Format: "ipv4", 232 }, 233 { 234 Type: gojsonschema.TYPE_STRING, 235 Format: "ipv6", 236 }, 237 } 238 case ligato.LigatoOptions_IPV4_WITH_MASK: 239 schema.Type = gojsonschema.TYPE_STRING 240 schema.Pattern = PatternIpv4WithMask 241 case ligato.LigatoOptions_IPV6_WITH_MASK: 242 schema.Type = gojsonschema.TYPE_STRING 243 schema.Pattern = PatternIpv6WithMask 244 case ligato.LigatoOptions_IP_WITH_MASK: 245 schema.OneOf = []*jsonschema.Type{ 246 { 247 Type: gojsonschema.TYPE_STRING, 248 Pattern: PatternIpv4WithMask, 249 }, 250 { 251 Type: gojsonschema.TYPE_STRING, 252 Pattern: PatternIpv6WithMask, 253 }, 254 } 255 case ligato.LigatoOptions_IPV4_OPTIONAL_MASK: 256 schema.OneOf = []*jsonschema.Type{ 257 { 258 Type: gojsonschema.TYPE_STRING, 259 Format: "ipv4", 260 }, 261 { 262 Type: gojsonschema.TYPE_STRING, 263 Pattern: PatternIpv4WithMask, 264 }, 265 } 266 case ligato.LigatoOptions_IPV6_OPTIONAL_MASK: 267 schema.OneOf = []*jsonschema.Type{ 268 { 269 Type: gojsonschema.TYPE_STRING, 270 Format: "ipv6", 271 }, 272 { 273 Type: gojsonschema.TYPE_STRING, 274 Pattern: PatternIpv6WithMask, 275 }, 276 } 277 case ligato.LigatoOptions_IP_OPTIONAL_MASK: 278 schema.OneOf = []*jsonschema.Type{ 279 { 280 Type: gojsonschema.TYPE_STRING, 281 Format: "ipv4", 282 }, 283 { 284 Type: gojsonschema.TYPE_STRING, 285 Pattern: PatternIpv4WithMask, 286 }, 287 { 288 Type: gojsonschema.TYPE_STRING, 289 Format: "ipv6", 290 }, 291 { 292 Type: gojsonschema.TYPE_STRING, 293 Pattern: PatternIpv6WithMask, 294 }, 295 } 296 default: // no annotations or annotation used are not applicable here 297 schema.Type = gojsonschema.TYPE_STRING 298 } 299 c.applyAllowNullValuesOption(jsonSchemaType, schema) 300 301 case descriptorpb.FieldDescriptorProto_TYPE_BYTES: 302 c.applyAllowNullValuesOption(jsonSchemaType, &jsonschema.Type{Type: gojsonschema.TYPE_STRING}) 303 304 case descriptorpb.FieldDescriptorProto_TYPE_ENUM: 305 // Note: not setting type specification(oneof string and integer), because explicitly saying which 306 // values are valid (and any other is invalid) is enough specification what can be used 307 // (this also overcome bug in example creator https://json-schema-faker.js.org/ that doesn't select 308 // correct type for enum value but rather chooses random type from oneof and cast value to that type) 309 // 310 // jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Type: gojsonschema.TYPE_STRING}) 311 // jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Type: gojsonschema.TYPE_INTEGER}) 312 if c.AllowNullValues { 313 jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Type: gojsonschema.TYPE_NULL}) 314 } 315 316 // Go through all the enums we have, see if we can match any to this field. 317 fullEnumIdentifier := strings.TrimPrefix(desc.GetTypeName(), ".") 318 matchedEnum, _, ok := c.lookupEnum(curPkg, fullEnumIdentifier) 319 if !ok { 320 return nil, fmt.Errorf("unable to resolve enum type: %s", desc.GetType().String()) 321 } 322 323 // We have found an enum, append its values. 324 for _, value := range matchedEnum.Value { 325 jsonSchemaType.Enum = append(jsonSchemaType.Enum, value.Name) 326 jsonSchemaType.Enum = append(jsonSchemaType.Enum, value.Number) 327 } 328 329 case descriptorpb.FieldDescriptorProto_TYPE_BOOL: 330 c.applyAllowNullValuesOption(jsonSchemaType, &jsonschema.Type{Type: gojsonschema.TYPE_BOOLEAN}) 331 332 case descriptorpb.FieldDescriptorProto_TYPE_GROUP, descriptorpb.FieldDescriptorProto_TYPE_MESSAGE: 333 switch desc.GetTypeName() { 334 case ".google.protobuf.Timestamp": 335 jsonSchemaType.Type = gojsonschema.TYPE_STRING 336 jsonSchemaType.Format = "date-time" 337 default: 338 jsonSchemaType.Type = gojsonschema.TYPE_OBJECT 339 // disallowAdditionalProperties will fail validation when this message/group field have value that 340 // have extra fields that are not covered by message/group schema 341 if c.DisallowAdditionalProperties { 342 jsonSchemaType.AdditionalProperties = []byte("false") 343 } else { 344 jsonSchemaType.AdditionalProperties = []byte("true") 345 } 346 } 347 348 default: 349 return nil, fmt.Errorf("unrecognized field type: %s", desc.GetType().String()) 350 } 351 352 // Recurse array of primitive types: 353 if desc.GetLabel() == descriptorpb.FieldDescriptorProto_LABEL_REPEATED && jsonSchemaType.Type != gojsonschema.TYPE_OBJECT { 354 jsonSchemaType.Items = &jsonschema.Type{} 355 356 if len(jsonSchemaType.Enum) > 0 { 357 jsonSchemaType.Items.Enum = jsonSchemaType.Enum 358 jsonSchemaType.Enum = nil 359 jsonSchemaType.Items.OneOf = nil 360 } else { // move schema of primitive type to item schema 361 // copy 362 jsonSchemaType.Items.Type = jsonSchemaType.Type 363 jsonSchemaType.Items.Format = jsonSchemaType.Format 364 jsonSchemaType.Items.Minimum = jsonSchemaType.Minimum 365 jsonSchemaType.Items.Maximum = jsonSchemaType.Maximum 366 jsonSchemaType.Items.ExclusiveMinimum = jsonSchemaType.ExclusiveMinimum 367 jsonSchemaType.Items.OneOf = jsonSchemaType.OneOf 368 369 // cleanup 370 jsonSchemaType.Type = "" 371 jsonSchemaType.Format = "" 372 jsonSchemaType.Minimum = 0 373 jsonSchemaType.Maximum = 0 374 jsonSchemaType.ExclusiveMinimum = false 375 jsonSchemaType.OneOf = nil 376 } 377 378 if c.AllowNullValues { 379 jsonSchemaType.OneOf = []*jsonschema.Type{ 380 {Type: gojsonschema.TYPE_NULL}, 381 {Type: gojsonschema.TYPE_ARRAY}, 382 } 383 } else { 384 jsonSchemaType.Type = gojsonschema.TYPE_ARRAY 385 jsonSchemaType.OneOf = []*jsonschema.Type{} 386 } 387 return jsonSchemaType, nil 388 } 389 390 // Recurse nested objects / arrays of objects (if necessary): 391 if jsonSchemaType.Type == gojsonschema.TYPE_OBJECT { 392 393 recordType, pkgName, ok := c.lookupType(curPkg, desc.GetTypeName()) 394 if !ok { 395 return nil, fmt.Errorf("no such message type named %s", desc.GetTypeName()) 396 } 397 398 // Recurse the recordType: 399 recursedJSONSchemaType, err := c.recursiveConvertMessageType(curPkg, recordType, pkgName, duplicatedMessages, false) 400 if err != nil { 401 return nil, err 402 } 403 404 // Maps, arrays, and objects are structured in different ways: 405 switch { 406 407 // Maps: 408 case recordType.Options.GetMapEntry(): 409 c.logger. 410 WithField("field_name", recordType.GetName()). 411 WithField("msg_name", *msg.Name). 412 Tracef("Is a map") 413 414 // Make sure we have a "value": 415 value, valuePresent := recursedJSONSchemaType.Properties.Get("value") 416 if !valuePresent { 417 return nil, fmt.Errorf("Unable to find 'value' property of MAP type") 418 } 419 420 // Marshal the "value" properties to JSON (because that's how we can pass on AdditionalProperties): 421 additionalPropertiesJSON, err := json.Marshal(value) 422 if err != nil { 423 return nil, err 424 } 425 jsonSchemaType.AdditionalProperties = additionalPropertiesJSON 426 427 // Arrays: 428 case desc.GetLabel() == descriptorpb.FieldDescriptorProto_LABEL_REPEATED: 429 jsonSchemaType.Items = recursedJSONSchemaType 430 jsonSchemaType.Type = gojsonschema.TYPE_ARRAY 431 432 // Build up the list of required fields: 433 if c.AllFieldsRequired && recursedJSONSchemaType.Properties != nil { 434 jsonSchemaType.Items.Required = append(jsonSchemaType.Items.Required, recursedJSONSchemaType.Properties.Keys()...) 435 } 436 437 // Not maps, not arrays: 438 default: 439 440 // If we've got optional types then just take those: 441 if recursedJSONSchemaType.OneOf != nil { 442 return recursedJSONSchemaType, nil 443 } 444 445 // If we're not an object then set the type from whatever we recursed: 446 if recursedJSONSchemaType.Type != gojsonschema.TYPE_OBJECT { 447 jsonSchemaType.Type = recursedJSONSchemaType.Type 448 } 449 450 // Assume the attrbutes of the recursed value: 451 jsonSchemaType.Properties = recursedJSONSchemaType.Properties 452 jsonSchemaType.Ref = recursedJSONSchemaType.Ref 453 if jsonSchemaType.Ref != "" { 454 // clean some fields because usage of REF makes them unnecessary (and in some validator 455 // implementation it cause problems/warnings) 456 jsonSchemaType.AdditionalProperties = []byte{} 457 } 458 jsonSchemaType.Required = recursedJSONSchemaType.Required 459 460 // Build up the list of required fields: 461 if c.AllFieldsRequired && recursedJSONSchemaType.Properties != nil { 462 jsonSchemaType.Required = append(jsonSchemaType.Required, recursedJSONSchemaType.Properties.Keys()...) 463 } 464 } 465 466 // Optionally allow NULL values: 467 if c.AllowNullValues { 468 jsonSchemaType.OneOf = []*jsonschema.Type{ 469 {Type: gojsonschema.TYPE_NULL}, 470 {Type: jsonSchemaType.Type}, 471 } 472 jsonSchemaType.Type = "" 473 } 474 } 475 476 jsonSchemaType.Required = dedupe(jsonSchemaType.Required) 477 478 return jsonSchemaType, nil 479 } 480 481 // applyIntRangeFieldAnnotation applies new int range for int schema (if the annotation is present) 482 func (c *Converter) applyIntRangeFieldAnnotation(fieldAnnotations *ligato.LigatoOptions, schema *jsonschema.Type) { 483 if fieldAnnotations.GetIntRange() != nil { 484 // correct value due for "exclusive" boundary usage 485 correctedMinimum := schema.Minimum 486 correctedMaximum := schema.Maximum 487 if schema.ExclusiveMinimum { 488 correctedMinimum = schema.Minimum + 1 489 } 490 if schema.ExclusiveMaximum { 491 correctedMaximum = schema.Maximum - 1 492 } 493 494 // compute new range 495 schema.Minimum = int(math.Max(float64(fieldAnnotations.GetIntRange().Minimum), float64(correctedMinimum))) 496 schema.Maximum = int(math.Min(float64(fieldAnnotations.GetIntRange().Maximum), float64(correctedMaximum))) 497 schema.ExclusiveMinimum = false 498 schema.ExclusiveMaximum = false 499 500 // apply workaround for 'omitempty' problem (default value is omitted from jsonschema marshaling and 501 // the boundary is missing in generated schema) 502 if schema.Minimum == 0 { 503 schema.Minimum = -1 504 schema.ExclusiveMinimum = true 505 } 506 if schema.Maximum == 0 { 507 schema.Maximum = 1 508 schema.ExclusiveMaximum = true 509 } 510 } 511 } 512 513 // Converts a proto "MESSAGE" into a JSON-Schema: 514 func (c *Converter) convertMessageType(curPkg *ProtoPackage, msg *descriptorpb.DescriptorProto) (*jsonschema.Schema, error) { 515 516 // first, recursively find messages that appear more than once - in particular, that will break cycles 517 duplicatedMessages, err := c.findDuplicatedNestedMessages(curPkg, msg) 518 if err != nil { 519 return nil, err 520 } 521 522 // main schema for the message 523 rootType, err := c.recursiveConvertMessageType(curPkg, msg, "", duplicatedMessages, false) 524 if err != nil { 525 return nil, err 526 } 527 528 // and then generate the sub-schema for each duplicated message 529 definitions := jsonschema.Definitions{} 530 for refMsg, name := range duplicatedMessages { 531 refType, err := c.recursiveConvertMessageType(curPkg, refMsg, "", duplicatedMessages, true) 532 if err != nil { 533 return nil, err 534 } 535 536 // need to give that schema an ID 537 if refType.Extras == nil { 538 refType.Extras = make(map[string]interface{}) 539 } 540 refType.Extras["id"] = name 541 definitions[name] = refType 542 } 543 544 newJSONSchema := &jsonschema.Schema{ 545 Type: rootType, 546 Definitions: definitions, 547 } 548 549 // Look for required fields (either by proto required flag, or the AllFieldsRequired option): 550 for _, fieldDesc := range msg.GetField() { 551 if c.AllFieldsRequired || fieldDesc.GetLabel() == descriptorpb.FieldDescriptorProto_LABEL_REQUIRED { 552 newJSONSchema.Required = append(newJSONSchema.Required, fieldDesc.GetName()) 553 } 554 } 555 556 newJSONSchema.Required = dedupe(newJSONSchema.Required) 557 558 return newJSONSchema, nil 559 } 560 561 // findDuplicatedNestedMessages takes a message, and returns a map mapping pointers to messages that appear more than once 562 // (typically because they're part of a reference cycle) to the sub-schema name that we give them. 563 func (c *Converter) findDuplicatedNestedMessages(curPkg *ProtoPackage, msg *descriptorpb.DescriptorProto) (map[*descriptorpb.DescriptorProto]string, error) { 564 all := make(map[*descriptorpb.DescriptorProto]*nameAndCounter) 565 if err := c.recursiveFindDuplicatedNestedMessages(curPkg, msg, msg.GetName(), all); err != nil { 566 return nil, err 567 } 568 569 result := make(map[*descriptorpb.DescriptorProto]string) 570 for m, nameAndCounter := range all { 571 if nameAndCounter.counter > 1 && !strings.HasPrefix(nameAndCounter.name, ".google.protobuf.") { 572 result[m] = strings.TrimLeft(nameAndCounter.name, ".") 573 } 574 } 575 576 return result, nil 577 } 578 579 type nameAndCounter struct { 580 name string 581 counter int 582 } 583 584 func (c *Converter) recursiveFindDuplicatedNestedMessages(curPkg *ProtoPackage, msg *descriptorpb.DescriptorProto, typeName string, alreadySeen map[*descriptorpb.DescriptorProto]*nameAndCounter) error { 585 if nameAndCounter, present := alreadySeen[msg]; present { 586 nameAndCounter.counter++ 587 return nil 588 } 589 alreadySeen[msg] = &nameAndCounter{ 590 name: typeName, 591 counter: 1, 592 } 593 594 for _, desc := range msg.GetField() { 595 descType := desc.GetType() 596 if descType != descriptorpb.FieldDescriptorProto_TYPE_MESSAGE && descType != descriptorpb.FieldDescriptorProto_TYPE_GROUP { 597 // no nested messages 598 continue 599 } 600 601 typeName := desc.GetTypeName() 602 recordType, _, ok := c.lookupType(curPkg, typeName) 603 if !ok { 604 return fmt.Errorf("no such message type named %s", typeName) 605 } 606 if err := c.recursiveFindDuplicatedNestedMessages(curPkg, recordType, typeName, alreadySeen); err != nil { 607 return err 608 } 609 } 610 611 return nil 612 } 613 614 func (c *Converter) recursiveConvertMessageType(curPkg *ProtoPackage, msg *descriptorpb.DescriptorProto, pkgName string, duplicatedMessages map[*descriptorpb.DescriptorProto]string, ignoreDuplicatedMessages bool) (*jsonschema.Type, error) { 615 // Handle google's well-known types: 616 if msg.Name != nil && wellKnownTypes[*msg.Name] && pkgName == ".google.protobuf" { 617 var typeSchema *jsonschema.Type 618 switch *msg.Name { 619 case "DoubleValue", "FloatValue": 620 typeSchema = &jsonschema.Type{Type: gojsonschema.TYPE_NUMBER} 621 case "Int32Value": 622 typeSchema = &jsonschema.Type{ 623 Type: gojsonschema.TYPE_INTEGER, 624 Minimum: math.MinInt32, 625 Maximum: math.MaxInt32, 626 } 627 case "UInt32Value": 628 typeSchema = &jsonschema.Type{ 629 Type: gojsonschema.TYPE_INTEGER, 630 Minimum: -1, 631 ExclusiveMinimum: true, 632 Maximum: intSafeMaxUint32, 633 } 634 case "Int64Value": 635 typeSchema = &jsonschema.Type{ 636 Type: gojsonschema.TYPE_INTEGER, 637 Minimum: intSafeMinInt64, 638 Maximum: intSafeMaxInt64, 639 } 640 case "UInt64Value": 641 typeSchema = &jsonschema.Type{ 642 Type: gojsonschema.TYPE_INTEGER, 643 Minimum: -1, 644 ExclusiveMinimum: true, 645 Maximum: intSafeMaxUint64, 646 } 647 case "BoolValue": 648 typeSchema = &jsonschema.Type{Type: gojsonschema.TYPE_BOOLEAN} 649 case "BytesValue", "StringValue": 650 typeSchema = &jsonschema.Type{Type: gojsonschema.TYPE_STRING} 651 case "Value": 652 typeSchema = &jsonschema.Type{Type: gojsonschema.TYPE_OBJECT} 653 } 654 655 // If we're allowing nulls then prepare a OneOf: 656 if c.AllowNullValues { 657 return &jsonschema.Type{ 658 OneOf: []*jsonschema.Type{ 659 {Type: gojsonschema.TYPE_NULL}, 660 typeSchema, 661 }, 662 }, nil 663 } 664 665 // Otherwise just return this simple type: 666 return typeSchema, nil 667 } 668 669 if refName, ok := duplicatedMessages[msg]; ok && !ignoreDuplicatedMessages { 670 return &jsonschema.Type{ 671 Version: jsonschema.Version, 672 Ref: refName, 673 }, nil 674 } 675 676 // Prepare a new jsonschema: 677 jsonSchemaType := &jsonschema.Type{ 678 Properties: orderedmap.New(), 679 Version: jsonschema.Version, 680 } 681 682 // Generate a description from src comments (if available) 683 if src := c.sourceInfo.GetMessage(msg); src != nil { 684 jsonSchemaType.Description = formatDescription(src) 685 } 686 687 // Optionally allow NULL values: 688 if c.AllowNullValues { 689 jsonSchemaType.OneOf = []*jsonschema.Type{ 690 {Type: gojsonschema.TYPE_NULL}, 691 {Type: gojsonschema.TYPE_OBJECT}, 692 } 693 } else { 694 jsonSchemaType.Type = gojsonschema.TYPE_OBJECT 695 } 696 697 // disallowAdditionalProperties will prevent validation where extra fields are found (outside of the schema): 698 if c.DisallowAdditionalProperties { 699 jsonSchemaType.AdditionalProperties = []byte("false") 700 } else { 701 jsonSchemaType.AdditionalProperties = []byte("true") 702 } 703 704 // create support jsonchema.Type structures for proto oneof fields 705 protoOneOfJsonOneOfType := make(map[int32]*jsonschema.Type) 706 if len(msg.OneofDecl) == 1 { // single proto oneof in proto message 707 jsonSchemaType.PatternProperties = make(map[string]*jsonschema.Type) 708 protoOneOfJsonOneOfType[0] = jsonSchemaType 709 } else if len(msg.OneofDecl) > 1 { // multiple proto oneof in proto message 710 jsonSchemaType.PatternProperties = make(map[string]*jsonschema.Type) 711 for i := range msg.OneofDecl { 712 jsonOneOfType := &jsonschema.Type{} 713 jsonSchemaType.AllOf = append(jsonSchemaType.AllOf, jsonOneOfType) 714 protoOneOfJsonOneOfType[int32(i)] = jsonOneOfType 715 } 716 } 717 718 c.logger.WithField("message_str", prototext.Format(msg)).Trace("Converting message") 719 for _, fieldDesc := range msg.GetField() { 720 // get field schema 721 recursedJSONSchemaType, err := c.convertField(curPkg, fieldDesc, msg, duplicatedMessages) 722 if err != nil { 723 c.logger.WithError(err).WithField("field_name", fieldDesc.GetName()).WithField("message_name", msg.GetName()).Error("Failed to convert field") 724 return nil, err 725 } 726 c.logger.WithField("field_name", fieldDesc.GetName()).WithField("type", recursedJSONSchemaType.Type).Trace("Converted field") 727 728 // Figure out which field names we want to use: 729 var fieldNames []string 730 switch { 731 case c.UseJSONFieldnamesOnly: 732 fieldNames = append(fieldNames, fieldDesc.GetJsonName()) 733 case c.UseProtoAndJSONFieldnames: 734 fieldNames = append(fieldNames, fieldDesc.GetName()) 735 fieldNames = append(fieldNames, fieldDesc.GetJsonName()) 736 default: 737 fieldNames = append(fieldNames, fieldDesc.GetName()) 738 } 739 740 if fieldDesc.OneofIndex != nil { // field is part of proto oneof structure 741 for _, fieldName := range fieldNames { 742 // allow usage of all proto oneof possible fields without sacrifice of enabling additional properties 743 // (additionalProperties to true would allow also other random names fields and that would cause 744 // external example generator to create for-vpp-agent-unknown fields that will cause problems 745 // in proto parsing) 746 jsonSchemaType.PatternProperties[fmt.Sprintf("^%s$", fieldName)] = &jsonschema.Type{} 747 748 // adding additional restriction that allow to use only one of the proto oneof fields 749 properties := orderedmap.New() 750 properties.Set(fieldName, recursedJSONSchemaType) // apply field schema 751 singleOneofUsageCase := &jsonschema.Type{ 752 Type: "object", 753 Required: []string{fieldName}, 754 Properties: properties, 755 } 756 jsonOneOfType := protoOneOfJsonOneOfType[*fieldDesc.OneofIndex] 757 jsonOneOfType.OneOf = append(jsonOneOfType.OneOf, singleOneofUsageCase) 758 } 759 } else { // normal field 760 // apply field schemas 761 for _, fieldName := range fieldNames { 762 jsonSchemaType.Properties.Set(fieldName, recursedJSONSchemaType) 763 } 764 765 // Look for required fields (either by proto required flag, or the AllFieldsRequired option): 766 if fieldDesc.GetLabel() == descriptorpb.FieldDescriptorProto_LABEL_REQUIRED { 767 jsonSchemaType.Required = append(jsonSchemaType.Required, fieldDesc.GetName()) 768 } 769 } 770 } 771 772 // Remove empty properties to keep the final output as clean as possible: 773 if len(jsonSchemaType.Properties.Keys()) == 0 { 774 jsonSchemaType.Properties = nil 775 } 776 777 return jsonSchemaType, nil 778 } 779 780 func formatDescription(sl *descriptorpb.SourceCodeInfo_Location) string { 781 var lines []string 782 for _, str := range sl.GetLeadingDetachedComments() { 783 if s := strings.TrimSpace(str); s != "" { 784 lines = append(lines, s) 785 } 786 } 787 if s := strings.TrimSpace(sl.GetLeadingComments()); s != "" { 788 lines = append(lines, s) 789 } 790 if s := strings.TrimSpace(sl.GetTrailingComments()); s != "" { 791 lines = append(lines, s) 792 } 793 return strings.Join(lines, "\n\n") 794 } 795 796 func dedupe(inputStrings []string) []string { 797 appended := make(map[string]bool) 798 outputStrings := []string{} 799 800 for _, inputString := range inputStrings { 801 if !appended[inputString] { 802 outputStrings = append(outputStrings, inputString) 803 appended[inputString] = true 804 } 805 } 806 return outputStrings 807 }