From db0f942eb53f31485516abf3e62952d3e06a1d7a Mon Sep 17 00:00:00 2001 From: Andrew Wilkins Date: Mon, 30 Nov 2020 15:56:40 +0800 Subject: [PATCH] modeldecoder/generator: enable additionalProperties subschema (#4464) Handle map value rules without requiring patternProperties in JSON Schema. When value rules (e.g. inputTypesVals) are specified, but no patternKeys rule is specified, then nest the generated schema under additionalProperties. This will enable us to remove the patternKeys rules from labels, tags, etc. when moving label key sanitization into the server, without dropping label value validation. --- docs/spec/v2/metricset.json | 4 ++ model/modeldecoder/generator/jsonschema.go | 6 +- model/modeldecoder/generator/map.go | 80 ++++++++++------------ 3 files changed, 41 insertions(+), 49 deletions(-) diff --git a/docs/spec/v2/metricset.json b/docs/spec/v2/metricset.json index 9bfc90d1564..bbd73837086 100644 --- a/docs/spec/v2/metricset.json +++ b/docs/spec/v2/metricset.json @@ -8,6 +8,10 @@ "additionalProperties": false, "patternProperties": { "^[^*\"]*$": { + "type": [ + "null", + "object" + ], "properties": { "value": { "description": "Value holds the value of a single metric sample.", diff --git a/model/modeldecoder/generator/jsonschema.go b/model/modeldecoder/generator/jsonschema.go index 16f7ae7714f..65e25ea8aa2 100644 --- a/model/modeldecoder/generator/jsonschema.go +++ b/model/modeldecoder/generator/jsonschema.go @@ -121,10 +121,6 @@ func (g *JSONSchemaGenerator) generate(st structType, key string, prop *property default: switch t := f.Type().Underlying().(type) { case *types.Map: - if _, ok := tags[tagPatternKeys]; !ok && name == "" { - // ignore the field in case no json name and no patternProperties are given - continue - } nestedProp := property{Properties: make(map[string]*property)} if err = generateJSONPropertyMap(&info, prop, &childProp, &nestedProp); err != nil { break @@ -240,7 +236,7 @@ type property struct { Type *propertyType `json:"type,omitempty"` // AdditionalProperties should default to `true` and be set to `false` // in case PatternProperties are set - AdditionalProperties *bool `json:"additionalProperties,omitempty"` + AdditionalProperties interface{} `json:"additionalProperties,omitempty"` PatternProperties map[string]*property `json:"patternProperties,omitempty"` Properties map[string]*property `json:"properties,omitempty"` Items *property `json:"items,omitempty"` diff --git a/model/modeldecoder/generator/map.go b/model/modeldecoder/generator/map.go index ab417ed8709..40bc9616ef3 100644 --- a/model/modeldecoder/generator/map.go +++ b/model/modeldecoder/generator/map.go @@ -159,19 +159,23 @@ func generateJSONPropertyMap(info *fieldInfo, parent *property, child *property, child.Type.add(TypeNameObject) patternName, isPatternProp := info.tags[tagPatternKeys] delete(info.tags, tagPatternKeys) - if !isPatternProp && name == "" { - // the object (map) can be places as a property with a defined key, - // or as a patternProperty with a defined key pattern - // if both are missing the field cannot be added - return fmt.Errorf("invalid combination: either json name or tag %s must be given", tagPatternKeys) - } - if !isPatternProp { - // if no pattern property is given, the child map will be nested directly as - // property inside the parent property, identified by it's json name - // e.g. {"parent":{"properties":{"jsonNameXY":{..}}}} + + nestedParent := child + if name == "" { + // The map does not have a json name defined, in which case it is nested directly + // inside the parent object's patternProperties/additionalProperties + // e.g. {"parent":{"patternProperties":{"patternXY":{..}}}} *nested = *child - parent.Properties[name] = nested + nestedParent = parent + } else { + // The map does have a json name defined, in which case it is nested as + // patternProperties/additionalProperties inside an object, which itself is nested + // inside the parent property, identified by its json name + // e.g. {"parent":{"properties":{"jsonNameXY":{"patternProperties":{"patternXY":{..}}}}}} + parent.Properties[name] = child } + + haveValueSchema := len(info.tags) > 0 if maxLen, ok := info.tags[tagMaxLengthVals]; ok { nested.MaxLength = json.Number(maxLen) delete(info.tags, tagMaxLengthVals) @@ -183,42 +187,30 @@ func generateJSONPropertyMap(info *fieldInfo, parent *property, child *property, } delete(info.tags, tagInputTypesVals) nested.Type = &propertyType{names: names} - } - if !isPatternProp { - // nothing more to do when no key pattern is given - return nil - } - pattern, ok := info.parsed.patternVariables[patternName] - if !ok { - return fmt.Errorf("unhandled %s tag value %s", tagPatternKeys, pattern) - } - // for map key patterns two options are supported: - // - the map does not have a json name defined, in which case it is nested directly - // inside the parent object's patternProperty - // e.g. {"parent":{"patternProperties":{"patternXY":{..}}}} - // - the map does have a json name defined, in which case it is nested as - // patternProperty inside an object, which itself is nested - // inside the parent property, identified by it's json name - // e.g. {"parent":{"properties":{"jsonNameXY":{"patternProperties":{"patternXY":{..}}}}}} - if name == "" { - if parent.PatternProperties == nil { - parent.PatternProperties = make(map[string]*property) - } + } else { valueType := info.field.Type().Underlying().(*types.Map).Elem() - typeName, ok := propertyTypes[valueType.String()] - if !ok { - typeName = TypeNameObject + if !types.IsInterface(valueType) { + haveValueSchema = true + typeName, ok := propertyTypes[valueType.String()] + if !ok { + typeName = TypeNameObject + } + nested.Type = &propertyType{names: []propertyTypeName{typeName}} } - nested.Type = &propertyType{names: []propertyTypeName{typeName}} - parent.PatternProperties[pattern] = nested - parent.AdditionalProperties = new(bool) - return nil } - if child.PatternProperties == nil { - child.PatternProperties = make(map[string]*property) + + if isPatternProp { + pattern, ok := info.parsed.patternVariables[patternName] + if !ok { + return fmt.Errorf("unhandled %s tag value %s", tagPatternKeys, pattern) + } + if nestedParent.PatternProperties == nil { + nestedParent.PatternProperties = make(map[string]*property) + } + nestedParent.PatternProperties[pattern] = nested + nestedParent.AdditionalProperties = false + } else if haveValueSchema { + nestedParent.AdditionalProperties = nested } - child.PatternProperties[pattern] = nested - child.AdditionalProperties = new(bool) - parent.Properties[name] = child return nil }