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  }