From 6c88d34057041ca133195018ae45c5ae3f9165e0 Mon Sep 17 00:00:00 2001 From: Heiko Henning Date: Wed, 18 Dec 2024 08:27:55 +0100 Subject: [PATCH 1/2] Add protoc-gen-validate support --- src/google-types.ts | 1 + src/protoc-gen-validate.ts | 427 ++++ src/protoj2jsonSchema.ts | 13 + .../documents/protoc-gen-validate.result.json | 2027 +++++++++++++++++ test/documents/protoc-gen-validate.yaml | 425 ++++ test/parser.spec.ts | 17 + 6 files changed, 2910 insertions(+) create mode 100644 src/protoc-gen-validate.ts create mode 100644 test/documents/protoc-gen-validate.result.json create mode 100644 test/documents/protoc-gen-validate.yaml diff --git a/src/google-types.ts b/src/google-types.ts index 67a78ef..08e0c5f 100644 --- a/src/google-types.ts +++ b/src/google-types.ts @@ -852,4 +852,5 @@ export const googleProtoTypes: { [key: string]: { [k: string]: any } } = { } } }, + 'validate/validate.proto': {}, }; diff --git a/src/protoc-gen-validate.ts b/src/protoc-gen-validate.ts new file mode 100644 index 0000000..8decdad --- /dev/null +++ b/src/protoc-gen-validate.ts @@ -0,0 +1,427 @@ +/* eslint-disable sonarjs/cognitive-complexity */ +import {AsyncAPISchemaDefinition} from '@asyncapi/parser/esm/spec-types/v3'; +import {Field} from 'protobufjs'; + +const OPTION_PREFIX = '(validate.rules)'; + +type HashMap = { [k: string]: any }; + +export function visit(obj: AsyncAPISchemaDefinition, field: Field) { + const parsedOption = findRootOption(field); + + if (parsedOption !== null) { + protocGenValidate(parsedOption, obj); + } +} + +function findRootOption(field: Field): null | HashMap { + if (field.parsedOptions && field.parsedOptions[OPTION_PREFIX]) { + return field.parsedOptions[OPTION_PREFIX] as any; + } else if (field.parsedOptions && Array.isArray(field.parsedOptions)) { + for (const parsedOption of field.parsedOptions) { + if (parsedOption[OPTION_PREFIX]) { + return parsedOption[OPTION_PREFIX]; + } + } + } + return null; +} + +function protocGenValidate(option: { [key: string]: any }, obj: AsyncAPISchemaDefinition) { + for (const [optionKey, value] of Object.entries(option)) { + switch (optionKey) { + case 'float': + case 'double': + case 'int32': + case 'int64': + case 'uint32': + case 'uint64': + case 'sint32': + case 'sint64': + case 'fixed32': + case 'fixed64': + case 'sfixed32': + case 'sfixed64': + ProtocGenNumeric.handle(obj, value); + break; + + case 'bool': + ProtocGenBool.handle(obj, value); + break; + + case 'string': + case 'bytes': + ProtocGenString.handle(obj, value); + break; + + case 'repeated': + ProtocGenRepeated.handle(obj, value); + break; + } + } +} + +// https://github.com/bufbuild/protoc-gen-validate/blob/main/tests/harness/cases/numbers.proto +class ProtocGenNumeric { + public static handle(obj: AsyncAPISchemaDefinition, option: { [key: string]: number }) { + for (const [optionKey, value] of Object.entries(option)) { + switch (optionKey) { + case 'const': + ProtocGenGeneric.constValue(obj, value); + break; + case 'lt': + ProtocGenNumeric.lessThan(obj, value); + break; + case 'lte': + ProtocGenNumeric.lessEqualThan(obj, value); + break; + case 'gt': + ProtocGenNumeric.greaterThan(obj, value); + break; + case 'gte': + ProtocGenNumeric.greaterEqualThan(obj, value); + break; + case 'ignore_empty': + // implemented via isOptional + break; + case 'in': + ProtocGenGeneric.inArray(obj, value as any); + break; + case 'not_in': + ProtocGenGeneric.notInArray(obj, value as any); + break; + } + } + } + + // x must equal `value` less than + static lessThan(obj: AsyncAPISchemaDefinition, value: number) { + delete obj.maximum; + obj.exclusiveMaximum = value; + } + + // x must be greater less or equal to `value` + static lessEqualThan(obj: AsyncAPISchemaDefinition, value: number) { + obj.maximum = value; + } + + // x must equal `value` greater than + static greaterThan(obj: AsyncAPISchemaDefinition, value: number) { + delete obj.minimum; + obj.exclusiveMinimum = value; + } + + // x must be greater than or equal to `value` + static greaterEqualThan(obj: AsyncAPISchemaDefinition, value: number) { + obj.minimum = value; + } +} + +// https://github.com/bufbuild/protoc-gen-validate/blob/main/tests/harness/cases/bool.proto +class ProtocGenBool { + public static handle(obj: AsyncAPISchemaDefinition, option: { [key: string]: boolean }) { + for (const [optionKey, value] of Object.entries(option)) { + switch (optionKey) { + case 'const': + ProtocGenGeneric.constValue(obj, value); + break; + } + } + } +} + +// https://github.com/bufbuild/protoc-gen-validate/blob/main/tests/harness/cases/strings.proto +class ProtocGenString { + public static handle(obj: AsyncAPISchemaDefinition, option: { [key: string]: string }) { + for (const [optionKey, value] of Object.entries(option)) { + switch (optionKey) { + case 'const': + ProtocGenGeneric.constValue(obj, value); + break; + + case 'in': + ProtocGenGeneric.inArray(obj, value as any); + break; + case 'not_in': + ProtocGenGeneric.notInArray(obj, value as any); + break; + case 'len': + ProtocGenString.len(obj, value as any); + break; + case 'min_len': + ProtocGenString.minLen(obj, value as any); + break; + case 'max_len': + ProtocGenString.maxLen(obj, value as any); + break; + case 'len_bytes': + ProtocGenString.len(obj, value as any); + break; + case 'min_bytes': + ProtocGenString.minLen(obj, value as any); + break; + case 'max_bytes': + ProtocGenString.maxLen(obj, value as any); + break; + case 'pattern': + ProtocGenString.pattern(obj, value); + break; + case 'prefix': + ProtocGenString.prefix(obj, value); + break; + case 'contains': + ProtocGenString.contains(obj, value); + break; + case 'not_contains': + ProtocGenString.notContains(obj, value); + break; + case 'suffix': + ProtocGenString.suffix(obj, value); + break; + case 'email': + if (value) { + ProtocGenString.email(obj); + } + break; + case 'address': + if (value) { + ProtocGenString.address(obj); + } + break; + case 'hostname': + if (value) { + ProtocGenString.hostname(obj); + } + break; + case 'ip': + if (value) { + ProtocGenString.ip(obj); + } + break; + case 'ipv4': + if (value) { + ProtocGenString.ipv4(obj); + } + break; + case 'ipv6': + if (value) { + ProtocGenString.ipv6(obj); + } + break; + case 'uri': + if (value) { + ProtocGenString.uri(obj); + } + break; + case 'uri_ref': + if (value) { + ProtocGenString.uriRef(obj); + } + break; + case 'uuid': + if (value) { + ProtocGenString.uuid(obj); + } + break; + case 'well_known_regex': + ProtocGenString.wellKnownRegex(obj, value); + break; + case 'ignore_empty': + // implemented via isOptional + break; + } + } + } + + private static len(obj: AsyncAPISchemaDefinition, value: number) { + obj.minLength = value; + obj.maxLength = value; + } + + private static minLen(obj: AsyncAPISchemaDefinition, value: number) { + obj.minLength = value; + } + + private static maxLen(obj: AsyncAPISchemaDefinition, value: number) { + obj.maxLength = value; + } + + private static pattern(obj: AsyncAPISchemaDefinition, value: string) { + obj.pattern = value; + } + + private static prefix(obj: AsyncAPISchemaDefinition, value: string) { + obj.pattern = `^${escapeRegExp(value)}.*`; + } + + private static contains(obj: AsyncAPISchemaDefinition, value: string) { + obj.pattern = `.*${escapeRegExp(value)}.*`; + } + + private static notContains(obj: AsyncAPISchemaDefinition, value: string) { + obj.pattern = `^((?!${escapeRegExp(value)}).)*$`; + } + + private static suffix(obj: AsyncAPISchemaDefinition, value: string) { + obj.pattern = `.*${escapeRegExp(value)}$`; + } + + private static email(obj: AsyncAPISchemaDefinition) { + obj.format = 'email'; + } + + private static hostname(obj: AsyncAPISchemaDefinition) { + obj.format = 'hostname'; + } + + private static address(obj: AsyncAPISchemaDefinition) { + obj.anyOf = [ + {format: 'hostname'}, + {format: 'ipv4'}, + {format: 'ipv6'}, + ]; + } + + private static ip(obj: AsyncAPISchemaDefinition) { + obj.anyOf = [ + {format: 'ipv4'}, + {format: 'ipv6'}, + ]; + } + + private static ipv4(obj: AsyncAPISchemaDefinition) { + obj.format = 'ipv4'; + } + + private static ipv6(obj: AsyncAPISchemaDefinition) { + obj.format = 'ipv6'; + } + + private static uri(obj: AsyncAPISchemaDefinition) { + obj.format = 'uri'; + } + + private static uriRef(obj: AsyncAPISchemaDefinition) { + obj.format = 'uri-reference'; + } + + private static uuid(obj: AsyncAPISchemaDefinition) { + obj.format = 'uuid'; + } + + private static wellKnownRegex(obj: AsyncAPISchemaDefinition, value: string) { + switch (value) { + case 'HTTP_HEADER_NAME': + obj.pattern = '^:?[0-9a-zA-Z!#$%&\'*+-.^_|~\x60]+$'; + break; + case 'HTTP_HEADER_VALUE': + obj.pattern = '^[^\u0000-\u0008\u000A-\u001F\u007F]*$'; + break; + } + } +} + +// https://github.com/bufbuild/protoc-gen-validate/blob/main/tests/harness/cases/repeated.proto +class ProtocGenRepeated { + public static handle(obj: AsyncAPISchemaDefinition, option: { [key: string]: string }) { + for (const [optionKey, value] of Object.entries(option)) { + switch (optionKey) { + case 'min_items': + ProtocGenRepeated.minLen(obj, value as any); + break; + case 'max_items': + ProtocGenRepeated.maxLen(obj, value as any); + break; + case 'unique': + if (value) { + ProtocGenRepeated.unique(obj); + } + break; + case 'items': + if (obj.items) { // avoid null pointer + protocGenValidate(value as any, obj.items as AsyncAPISchemaDefinition); + } + break; + } + } + } + + private static minLen(obj: AsyncAPISchemaDefinition, value: number) { + obj.minItems = value; + } + + private static maxLen(obj: AsyncAPISchemaDefinition, value: number) { + obj.maxItems = value; + } + + private static unique(obj: AsyncAPISchemaDefinition) { + obj.uniqueItems = true; + } +} + +class ProtocGenGeneric { +// x must equal `value` exactly + public static constValue(obj: AsyncAPISchemaDefinition, value: string | number | boolean) { + obj.const = value; + delete obj.maximum; + delete obj.minimum; + } + + public static inArray(obj: AsyncAPISchemaDefinition, value: number[] | string[]) { + if (!Array.isArray(value)) { + throw new Error(`Expect value to be an array: ${value}`); + } + + obj.oneOf = value.map(val => { + const subSchema: AsyncAPISchemaDefinition = { + const: val + }; + return subSchema; + }); + } + + public static notInArray(obj: AsyncAPISchemaDefinition, value: number[] | string[]) { + if (!Array.isArray(value)) { + throw new Error(`Expect value to be an array: ${value}`); + } + + obj.not = { + oneOf: value.map(val => { + const subSchema: AsyncAPISchemaDefinition = { + const: val + }; + return subSchema; + }) + }; + } +} + +export function isOptional(field: Field) { + const parsedOption = findRootOption(field); + + if (parsedOption !== null) { + for (const [dataType, options] of Object.entries(parsedOption)) { + if (dataType === 'repeated') { + if (options.items) { + for (const [key, val] of Object.entries(options.items)) { + if (key === 'ignore_empty' && val) { + return true; + } + } + } + } else { + for (const [key, val] of Object.entries(options)) { + if (key === 'ignore_empty' && val) { + return true; + } + } + } + } + } + + return false; +} + +function escapeRegExp(str: string): string { + return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); +} diff --git a/src/protoj2jsonSchema.ts b/src/protoj2jsonSchema.ts index 0832c2b..9b29d0a 100644 --- a/src/protoj2jsonSchema.ts +++ b/src/protoj2jsonSchema.ts @@ -5,6 +5,7 @@ import {Path} from './pathUtils'; import type {v3} from '@asyncapi/parser/esm/spec-types'; import {PrimitiveTypes} from './primitive-types'; import {AsyncAPISchemaDefinition} from '@asyncapi/parser/esm/spec-types/v3'; +import {isOptional as protocGenValidateIsOptional, visit as protocGenValidateVisit} from './protoc-gen-validate'; const ROOT_FILENAME = 'root'; const COMMENT_ROOT_NODE = '@RootNode'; @@ -78,6 +79,10 @@ class Proto2JsonSchema { // Bundled definition existence checking private getBundledFileName(filename: string): string | null { + if (filename === 'validate/validate.proto' || filename === '/validate/validate.proto') { + return 'validate/validate.proto'; + } + let idx = filename.lastIndexOf('google/protobuf/'); if (idx > -1) { const shortName = filename.substring(idx); @@ -203,6 +208,9 @@ class Proto2JsonSchema { } private isProto3Required(field: protobuf.Field) { + if (protocGenValidateIsOptional(field)) { + return false; + } return (field.options?.proto3_optional !== true && this.isProto3()); } @@ -267,6 +275,8 @@ class Proto2JsonSchema { obj.maxItems = parseFloat(m[1]); } } + + protocGenValidateVisit(properties[field.name], field); } else { properties[field.name] = this.compileField(field, item, stack.slice()); } @@ -344,6 +354,9 @@ class Proto2JsonSchema { this.addValidatorFromCommentAnnotations(obj, field.comment); this.addDefaultFromCommentAnnotations(obj, field.comment); + if (!field.repeated) { + protocGenValidateVisit(obj, field); + } const desc = this.extractDescription(field.comment); if (desc !== null && desc.length > 0) { diff --git a/test/documents/protoc-gen-validate.result.json b/test/documents/protoc-gen-validate.result.json new file mode 100644 index 0000000..c09e8ed --- /dev/null +++ b/test/documents/protoc-gen-validate.result.json @@ -0,0 +1,2027 @@ +{ + "asyncapi": "2.0.0", + "info": { + "title": "Example using ProtoBuff and https://github.com/bufbuild/protoc-gen-validate/blob/main/validate/validate.proto", + "version": "1.0.0" + }, + "channels": { + "mychannel": { + "publish": { + "message": { + "schemaFormat": "application/vnd.google.protobuf;version=3", + "payload": { + "title": "RootNode", + "type": "object", + "required": [ + "n", + "s", + "b", + "o", + "r", + "m", + "g" + ], + "properties": { + "n": { + "title": "NumericExamples", + "type": "object", + "required": [ + "na", + "nb", + "nc", + "nd", + "ne", + "nf", + "ng", + "nh", + "nk", + "nl" + ], + "properties": { + "na": { + "type": "number", + "x-primitive": "float", + "const": 1.23, + "description": "x must equal 1.23 exactly" + }, + "nb": { + "type": "integer", + "minimum": -2147483648, + "x-primitive": "int32", + "exclusiveMaximum": 10, + "description": "x must be less than 10" + }, + "nc": { + "type": "integer", + "minimum": 20, + "maximum": 9007199254740991, + "x-primitive": "uint64", + "description": "x must be greater than or equal to 20" + }, + "nd": { + "type": "number", + "x-primitive": "fixed32", + "minimum": 30, + "exclusiveMaximum": 40, + "description": "x must be in the range [30, 40)" + }, + "ne": { + "type": "number", + "x-primitive": "double", + "exclusiveMaximum": 30, + "minimum": 40, + "description": "x must be outside the range [30, 40)" + }, + "nf": { + "type": "number", + "x-primitive": "double", + "exclusiveMaximum": 30, + "minimum": 40, + "description": "x must be outside the range [30, 40)" + }, + "ng": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295, + "x-primitive": "uint32", + "oneOf": [ + { + "const": 1, + "x-parser-schema-id": "" + }, + { + "const": 2, + "x-parser-schema-id": "" + }, + { + "const": 3, + "x-parser-schema-id": "" + } + ], + "description": "x must be either 1, 2, or 3" + }, + "nh": { + "type": "number", + "x-primitive": "float", + "not": { + "oneOf": [ + { + "const": 0, + "x-parser-schema-id": "" + }, + { + "const": 0.99, + "x-parser-schema-id": "" + } + ] + }, + "description": "x cannot be 0 nor 0.99" + }, + "ni": { + "type": "integer", + "minimum": 200, + "maximum": 4294967295, + "x-primitive": "uint32", + "description": "x must but greater or equal 200 and being optional" + }, + "nk": { + "type": "boolean", + "x-primitive": "bool", + "const": true, + "description": "x must be set to true" + }, + "nl": { + "type": "boolean", + "x-primitive": "bool", + "const": false, + "description": "x cannot be set to true" + } + } + }, + "s": { + "title": "StringExamples", + "type": "object", + "required": [ + "sa", + "sb", + "sc", + "sd", + "se", + "sf", + "sg", + "sh", + "si", + "sj", + "sk", + "sl", + "sm", + "sn", + "so", + "sp", + "sq", + "sr", + "ss", + "ip4", + "ip6", + "uri", + "absuri", + "uuid", + "httpheadname", + "httpheadvalue" + ], + "properties": { + "sa": { + "type": "string", + "x-primitive": "string", + "const": "foo", + "description": "x must be set to \"foo\"" + }, + "sb": { + "type": "string", + "x-primitive": "string", + "minLength": 5, + "maxLength": 5, + "description": "x must be exactly 5 characters long" + }, + "sc": { + "type": "string", + "x-primitive": "string", + "minLength": 3, + "description": "x must be at least 3 characters long" + }, + "sd": { + "type": "string", + "x-primitive": "string", + "minLength": 5, + "maxLength": 10, + "description": "x must be between 5 and 10 characters, inclusive" + }, + "se": { + "type": "string", + "x-primitive": "string", + "maxLength": 15, + "description": "x must be at most 15 bytes long" + }, + "sf": { + "type": "string", + "x-primitive": "string", + "minLength": 128, + "maxLength": 1024, + "description": "x must be between 128 and 1024 bytes long" + }, + "sg": { + "type": "string", + "x-primitive": "string", + "pattern": "(?i)^[0-9a-f]+$", + "description": "x must be a non-empty, case-insensitive hexadecimal string" + }, + "sh": { + "type": "string", + "x-primitive": "string", + "pattern": "^foo.*", + "description": "x must begin with \"foo\"" + }, + "si": { + "type": "string", + "x-primitive": "string", + "pattern": ".*bar$", + "description": "x must end with \"bar\"" + }, + "sj": { + "type": "string", + "x-primitive": "string", + "pattern": ".*baz.*", + "description": "x must contain \"baz\" anywhere inside it" + }, + "sk": { + "type": "string", + "x-primitive": "string", + "pattern": "^((?!baz).)*$", + "description": "x cannot contain \"baz\" anywhere inside it" + }, + "sl": { + "type": "string", + "x-primitive": "string", + "pattern": ".*buzz$", + "description": "x must begin with \"fizz\" and end with \"buzz\"" + }, + "sm": { + "type": "string", + "x-primitive": "string", + "pattern": ".*\\.proto$", + "maxLength": 64, + "description": "x must end with \".proto\" and be less than 64 characters" + }, + "sn": { + "type": "string", + "x-primitive": "string", + "oneOf": [ + { + "const": "foo", + "x-parser-schema-id": "" + }, + { + "const": "bar", + "x-parser-schema-id": "" + }, + { + "const": "baz", + "x-parser-schema-id": "" + } + ], + "description": "x must be either \"foo\", \"bar\", or \"baz\"" + }, + "so": { + "type": "string", + "x-primitive": "string", + "not": { + "oneOf": [ + { + "const": "fizz", + "x-parser-schema-id": "" + }, + { + "const": "buzz", + "x-parser-schema-id": "" + } + ] + }, + "description": "x cannot be \"fizz\" nor \"buzz\"" + }, + "sp": { + "type": "string", + "x-primitive": "string", + "format": "email", + "description": "x must be a valid email address (via RFC 5322)" + }, + "sq": { + "type": "string", + "x-primitive": "string", + "anyOf": [ + { + "format": "hostname", + "x-parser-schema-id": "" + }, + { + "format": "ipv4", + "x-parser-schema-id": "" + }, + { + "format": "ipv6", + "x-parser-schema-id": "" + } + ], + "description": "x must be a valid address (IP or Hostname)." + }, + "sr": { + "type": "string", + "x-primitive": "string", + "format": "hostname", + "description": "x must be a valid hostname (via RFC 1034)" + }, + "ss": { + "type": "string", + "x-primitive": "string", + "anyOf": [ + { + "format": "ipv4", + "x-parser-schema-id": "" + }, + { + "format": "ipv6", + "x-parser-schema-id": "" + } + ], + "description": "x must be a valid IP address (either v4 or v6)" + }, + "ip4": { + "type": "string", + "x-primitive": "string", + "format": "ipv4", + "description": "x must be a valid IPv4 address\neg: \"192.168.0.1\"" + }, + "ip6": { + "type": "string", + "x-primitive": "string", + "format": "ipv6", + "description": "x must be a valid IPv6 address\neg: \"fe80::3\"" + }, + "uri": { + "type": "string", + "x-primitive": "string", + "format": "uri", + "description": "x must be a valid absolute URI (via RFC 3986)" + }, + "absuri": { + "type": "string", + "x-primitive": "string", + "format": "uri-reference", + "description": "x must be a valid URI reference (either absolute or relative)" + }, + "uuid": { + "type": "string", + "x-primitive": "string", + "format": "uuid", + "description": "x must be a valid UUID (via RFC 4122)" + }, + "httpheadname": { + "type": "string", + "x-primitive": "string", + "pattern": "^:?[0-9a-zA-Z!#$%&'*+-.^_|~`]+$", + "description": "x must conform to a well known regex for HTTP header names (via RFC 7230)" + }, + "httpheadvalue": { + "type": "string", + "x-primitive": "string", + "pattern": "^[^\u0000-\b\n-\u001f]*$", + "description": "x must conform to a well known regex for HTTP header values (via RFC 7230)" + } + } + }, + "b": { + "title": "ByteExamples", + "type": "object", + "required": [ + "ba", + "bb", + "bc", + "bd", + "be", + "bf", + "bg", + "bh", + "bi", + "bj", + "bk", + "bm", + "bn", + "bo" + ], + "properties": { + "ba": { + "type": "string", + "x-primitive": "bytes", + "const": "foo", + "description": "x must be set to \"foo\" (\"\\x66\\x6f\\x6f\")" + }, + "bb": { + "type": "string", + "x-primitive": "bytes", + "const": "f09028bc", + "description": "x must be set to \"\\xf0\\x90\\x28\\xbc\"" + }, + "bc": { + "type": "string", + "x-primitive": "bytes", + "minLength": 3, + "maxLength": 3, + "description": "x must be exactly 3 bytes" + }, + "bd": { + "type": "string", + "x-primitive": "bytes", + "minLength": 3, + "description": "x must be at least 3 bytes long" + }, + "be": { + "type": "string", + "x-primitive": "bytes", + "minLength": 5, + "maxLength": 10, + "description": "x must be between 5 and 10 bytes, inclusive" + }, + "bf": { + "type": "string", + "x-primitive": "bytes", + "pattern": "^[00-7F]+$", + "description": "x must be a non-empty, ASCII byte sequence" + }, + "bg": { + "type": "string", + "x-primitive": "bytes", + "pattern": "^99.*", + "description": "x must begin with \"\\x99\"" + }, + "bh": { + "type": "string", + "x-primitive": "bytes", + "pattern": ".*buz7a$", + "description": "x must end with \"buz\\x7a\"" + }, + "bi": { + "type": "string", + "x-primitive": "bytes", + "pattern": ".*baz.*", + "description": "x must contain \"baz\" anywhere inside it" + }, + "bj": { + "type": "string", + "x-primitive": "bytes", + "oneOf": [ + { + "const": "foo", + "x-parser-schema-id": "" + }, + { + "const": "bar", + "x-parser-schema-id": "" + }, + { + "const": "baz", + "x-parser-schema-id": "" + } + ], + "description": "x must be either \"foo\", \"bar\", or \"baz\"" + }, + "bk": { + "type": "string", + "x-primitive": "bytes", + "not": { + "oneOf": [ + { + "const": "fizz", + "x-parser-schema-id": "" + }, + { + "const": "buzz", + "x-parser-schema-id": "" + } + ] + }, + "description": "x cannot be \"fizz\" nor \"buzz\"" + }, + "bl": { + "type": "string", + "x-primitive": "bytes", + "oneOf": [ + { + "const": "foo", + "x-parser-schema-id": "" + }, + { + "const": "bar", + "x-parser-schema-id": "" + }, + { + "const": "baz", + "x-parser-schema-id": "" + } + ] + }, + "bm": { + "type": "string", + "x-primitive": "bytes", + "anyOf": [ + { + "format": "ipv4", + "x-parser-schema-id": "" + }, + { + "format": "ipv6", + "x-parser-schema-id": "" + } + ], + "description": "x must be a valid IP address (either v4 or v6) in byte format" + }, + "bn": { + "type": "string", + "x-primitive": "bytes", + "format": "ipv4", + "description": "x must be a valid IPv4 address in byte format\neg: \"\\xC0\\xA8\\x00\\x01\"" + }, + "bo": { + "type": "string", + "x-primitive": "bytes", + "format": "ipv6", + "description": "x must be a valid IPv6 address in byte format\neg: \"\\x20\\x01\\x0D\\xB8\\x85\\xA3\\x00\\x00\\x00\\x00\\x8A\\x2E\\x03\\x70\\x73\\x34\"" + } + } + }, + "o": { + "title": "ObjectExamples", + "type": "object", + "properties": {}, + "description": "Dont have an idea how to represent those in JsonSchema" + }, + "r": { + "title": "RepeatedExamples", + "type": "object", + "required": [ + "ra", + "rb", + "rc", + "rd", + "re", + "rf", + "rk", + "rl" + ], + "properties": { + "ra": { + "type": "array", + "items": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "x-primitive": "int32", + "description": "x must contain at least 3 elements" + }, + "description": "x must contain at least 3 elements", + "minItems": 3 + }, + "rb": { + "type": "array", + "items": { + "title": "Person", + "type": "object", + "required": [ + "id", + "email", + "name", + "home" + ], + "properties": { + "id": { + "type": "integer", + "maximum": 9007199254740991, + "x-primitive": "uint64", + "exclusiveMinimum": 999 + }, + "email": { + "type": "string", + "x-primitive": "string", + "format": "email" + }, + "name": { + "type": "string", + "x-primitive": "string", + "pattern": "^[A-Za-z]+( [A-Za-z]+)*$", + "maxLength": 256 + }, + "home": { + "title": "Location", + "type": "object", + "required": [ + "lat", + "lng" + ], + "properties": { + "lat": { + "type": "number", + "x-primitive": "double", + "minimum": -90, + "maximum": 90 + }, + "lng": { + "type": "number", + "x-primitive": "double", + "minimum": -180, + "maximum": 180 + } + } + } + }, + "description": "x must contain between 5 and 10 Persons, inclusive" + }, + "description": "x must contain between 5 and 10 Persons, inclusive", + "minItems": 5, + "maxItems": 10 + }, + "rc": { + "type": "array", + "items": { + "type": "number", + "x-primitive": "double", + "description": "x must contain exactly 7 elements" + }, + "description": "x must contain exactly 7 elements", + "minItems": 7, + "maxItems": 7 + }, + "rd": { + "type": "array", + "items": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "x-primitive": "int64", + "description": "x must contain unique int64 values" + }, + "description": "x must contain unique int64 values", + "uniqueItems": true + }, + "re": { + "type": "array", + "items": { + "type": "number", + "x-primitive": "float", + "description": "x must contain positive float values", + "exclusiveMinimum": 0 + }, + "description": "x must contain positive float values" + }, + "rf": { + "type": "array", + "items": { + "title": "Person", + "type": "object", + "required": [ + "id", + "email", + "name", + "home" + ], + "properties": { + "id": { + "type": "integer", + "maximum": 9007199254740991, + "x-primitive": "uint64", + "exclusiveMinimum": 999 + }, + "email": { + "type": "string", + "x-primitive": "string", + "format": "email" + }, + "name": { + "type": "string", + "x-primitive": "string", + "pattern": "^[A-Za-z]+( [A-Za-z]+)*$", + "maxLength": 256 + }, + "home": { + "title": "Location", + "type": "object", + "required": [ + "lat", + "lng" + ], + "properties": { + "lat": { + "type": "number", + "x-primitive": "double", + "minimum": -90, + "maximum": 90 + }, + "lng": { + "type": "number", + "x-primitive": "double", + "minimum": -180, + "maximum": 180 + } + } + } + }, + "description": "x must contain Persons but don't validate them" + }, + "description": "x must contain Persons but don't validate them" + }, + "rk": { + "type": "array", + "items": { + "type": "number", + "x-primitive": "float", + "description": "x must contain positive float values", + "exclusiveMinimum": 0 + }, + "description": "x must contain positive float values" + }, + "rl": { + "type": "array", + "items": { + "title": "Person", + "type": "object", + "required": [ + "id", + "email", + "name", + "home" + ], + "properties": { + "id": { + "type": "integer", + "maximum": 9007199254740991, + "x-primitive": "uint64", + "exclusiveMinimum": 999 + }, + "email": { + "type": "string", + "x-primitive": "string", + "format": "email" + }, + "name": { + "type": "string", + "x-primitive": "string", + "pattern": "^[A-Za-z]+( [A-Za-z]+)*$", + "maxLength": 256 + }, + "home": { + "title": "Location", + "type": "object", + "required": [ + "lat", + "lng" + ], + "properties": { + "lat": { + "type": "number", + "x-primitive": "double", + "minimum": -90, + "maximum": 90 + }, + "lng": { + "type": "number", + "x-primitive": "double", + "minimum": -180, + "maximum": 180 + } + } + } + }, + "description": "x must contain Persons but don't validate them" + }, + "description": "x must contain Persons but don't validate them" + } + } + }, + "m": { + "title": "MapExamples", + "type": "object", + "properties": {}, + "description": "Dont have an idea how to represent those in JsonSchema" + }, + "g": { + "title": "GoogleExamples", + "type": "object", + "required": [ + "ga", + "gb", + "gd", + "ge", + "gj", + "gl", + "gm", + "gp", + "gq" + ], + "properties": { + "ga": { + "title": "Int32Value", + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "x-primitive": "int32" + } + }, + "exclusiveMinimum": 3, + "description": "if it is set, x must be greater than 3" + }, + "gb": { + "title": "Duration", + "type": "object", + "required": [ + "seconds", + "nanos" + ], + "properties": { + "seconds": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "x-primitive": "int64" + }, + "nanos": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "x-primitive": "int32" + } + }, + "description": "x cannot be unset" + }, + "gd": { + "title": "Duration", + "type": "object", + "required": [ + "seconds", + "nanos" + ], + "properties": { + "seconds": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "x-primitive": "int64" + }, + "nanos": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "x-primitive": "int32" + } + }, + "description": "x must be less than 10s" + }, + "ge": { + "title": "Duration", + "type": "object", + "required": [ + "seconds", + "nanos" + ], + "properties": { + "seconds": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "x-primitive": "int64" + }, + "nanos": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "x-primitive": "int32" + } + }, + "description": "x must be greater than or equal to 20ns" + }, + "gj": { + "title": "Timestamp", + "type": "object", + "required": [ + "seconds", + "nanos" + ], + "properties": { + "seconds": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "x-primitive": "int64" + }, + "nanos": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "x-primitive": "int32" + } + }, + "description": "x cannot be unset" + }, + "gl": { + "title": "Timestamp", + "type": "object", + "required": [ + "seconds", + "nanos" + ], + "properties": { + "seconds": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "x-primitive": "int64" + }, + "nanos": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "x-primitive": "int32" + } + }, + "description": "x must be less than the Unix Epoch" + }, + "gm": { + "title": "Timestamp", + "type": "object", + "required": [ + "seconds", + "nanos" + ], + "properties": { + "seconds": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "x-primitive": "int64" + }, + "nanos": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "x-primitive": "int32" + } + }, + "description": "x must be greater than or equal to 2009/11/10T23:00:00Z" + }, + "gp": { + "title": "Timestamp", + "type": "object", + "required": [ + "seconds", + "nanos" + ], + "properties": { + "seconds": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "x-primitive": "int64" + }, + "nanos": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "x-primitive": "int32" + } + }, + "description": "x must be less than the current timestamp" + }, + "gq": { + "title": "Timestamp", + "type": "object", + "required": [ + "seconds", + "nanos" + ], + "properties": { + "seconds": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "x-primitive": "int64" + }, + "nanos": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "x-primitive": "int32" + } + }, + "description": "x must be within ±1s of the current time" + } + } + } + }, + "description": "Root" + } + } + } + } + }, + "components": { + "messages": { + "testMessage": { + "schemaFormat": "application/vnd.google.protobuf;version=3", + "payload": { + "title": "RootNode", + "type": "object", + "required": [ + "n", + "s", + "b", + "o", + "r", + "m", + "g" + ], + "properties": { + "n": { + "title": "NumericExamples", + "type": "object", + "required": [ + "na", + "nb", + "nc", + "nd", + "ne", + "nf", + "ng", + "nh", + "nk", + "nl" + ], + "properties": { + "na": { + "type": "number", + "x-primitive": "float", + "const": 1.23, + "description": "x must equal 1.23 exactly" + }, + "nb": { + "type": "integer", + "minimum": -2147483648, + "x-primitive": "int32", + "exclusiveMaximum": 10, + "description": "x must be less than 10" + }, + "nc": { + "type": "integer", + "minimum": 20, + "maximum": 9007199254740991, + "x-primitive": "uint64", + "description": "x must be greater than or equal to 20" + }, + "nd": { + "type": "number", + "x-primitive": "fixed32", + "minimum": 30, + "exclusiveMaximum": 40, + "description": "x must be in the range [30, 40)" + }, + "ne": { + "type": "number", + "x-primitive": "double", + "exclusiveMaximum": 30, + "minimum": 40, + "description": "x must be outside the range [30, 40)" + }, + "nf": { + "type": "number", + "x-primitive": "double", + "exclusiveMaximum": 30, + "minimum": 40, + "description": "x must be outside the range [30, 40)" + }, + "ng": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295, + "x-primitive": "uint32", + "oneOf": [ + { + "const": 1, + "x-parser-schema-id": "" + }, + { + "const": 2, + "x-parser-schema-id": "" + }, + { + "const": 3, + "x-parser-schema-id": "" + } + ], + "description": "x must be either 1, 2, or 3" + }, + "nh": { + "type": "number", + "x-primitive": "float", + "not": { + "oneOf": [ + { + "const": 0, + "x-parser-schema-id": "" + }, + { + "const": 0.99, + "x-parser-schema-id": "" + } + ] + }, + "description": "x cannot be 0 nor 0.99" + }, + "ni": { + "type": "integer", + "minimum": 200, + "maximum": 4294967295, + "x-primitive": "uint32", + "description": "x must but greater or equal 200 and being optional" + }, + "nk": { + "type": "boolean", + "x-primitive": "bool", + "const": true, + "description": "x must be set to true" + }, + "nl": { + "type": "boolean", + "x-primitive": "bool", + "const": false, + "description": "x cannot be set to true" + } + } + }, + "s": { + "title": "StringExamples", + "type": "object", + "required": [ + "sa", + "sb", + "sc", + "sd", + "se", + "sf", + "sg", + "sh", + "si", + "sj", + "sk", + "sl", + "sm", + "sn", + "so", + "sp", + "sq", + "sr", + "ss", + "ip4", + "ip6", + "uri", + "absuri", + "uuid", + "httpheadname", + "httpheadvalue" + ], + "properties": { + "sa": { + "type": "string", + "x-primitive": "string", + "const": "foo", + "description": "x must be set to \"foo\"" + }, + "sb": { + "type": "string", + "x-primitive": "string", + "minLength": 5, + "maxLength": 5, + "description": "x must be exactly 5 characters long" + }, + "sc": { + "type": "string", + "x-primitive": "string", + "minLength": 3, + "description": "x must be at least 3 characters long" + }, + "sd": { + "type": "string", + "x-primitive": "string", + "minLength": 5, + "maxLength": 10, + "description": "x must be between 5 and 10 characters, inclusive" + }, + "se": { + "type": "string", + "x-primitive": "string", + "maxLength": 15, + "description": "x must be at most 15 bytes long" + }, + "sf": { + "type": "string", + "x-primitive": "string", + "minLength": 128, + "maxLength": 1024, + "description": "x must be between 128 and 1024 bytes long" + }, + "sg": { + "type": "string", + "x-primitive": "string", + "pattern": "(?i)^[0-9a-f]+$", + "description": "x must be a non-empty, case-insensitive hexadecimal string" + }, + "sh": { + "type": "string", + "x-primitive": "string", + "pattern": "^foo.*", + "description": "x must begin with \"foo\"" + }, + "si": { + "type": "string", + "x-primitive": "string", + "pattern": ".*bar$", + "description": "x must end with \"bar\"" + }, + "sj": { + "type": "string", + "x-primitive": "string", + "pattern": ".*baz.*", + "description": "x must contain \"baz\" anywhere inside it" + }, + "sk": { + "type": "string", + "x-primitive": "string", + "pattern": "^((?!baz).)*$", + "description": "x cannot contain \"baz\" anywhere inside it" + }, + "sl": { + "type": "string", + "x-primitive": "string", + "pattern": ".*buzz$", + "description": "x must begin with \"fizz\" and end with \"buzz\"" + }, + "sm": { + "type": "string", + "x-primitive": "string", + "pattern": ".*\\.proto$", + "maxLength": 64, + "description": "x must end with \".proto\" and be less than 64 characters" + }, + "sn": { + "type": "string", + "x-primitive": "string", + "oneOf": [ + { + "const": "foo", + "x-parser-schema-id": "" + }, + { + "const": "bar", + "x-parser-schema-id": "" + }, + { + "const": "baz", + "x-parser-schema-id": "" + } + ], + "description": "x must be either \"foo\", \"bar\", or \"baz\"" + }, + "so": { + "type": "string", + "x-primitive": "string", + "not": { + "oneOf": [ + { + "const": "fizz", + "x-parser-schema-id": "" + }, + { + "const": "buzz", + "x-parser-schema-id": "" + } + ] + }, + "description": "x cannot be \"fizz\" nor \"buzz\"" + }, + "sp": { + "type": "string", + "x-primitive": "string", + "format": "email", + "description": "x must be a valid email address (via RFC 5322)" + }, + "sq": { + "type": "string", + "x-primitive": "string", + "anyOf": [ + { + "format": "hostname", + "x-parser-schema-id": "" + }, + { + "format": "ipv4", + "x-parser-schema-id": "" + }, + { + "format": "ipv6", + "x-parser-schema-id": "" + } + ], + "description": "x must be a valid address (IP or Hostname)." + }, + "sr": { + "type": "string", + "x-primitive": "string", + "format": "hostname", + "description": "x must be a valid hostname (via RFC 1034)" + }, + "ss": { + "type": "string", + "x-primitive": "string", + "anyOf": [ + { + "format": "ipv4", + "x-parser-schema-id": "" + }, + { + "format": "ipv6", + "x-parser-schema-id": "" + } + ], + "description": "x must be a valid IP address (either v4 or v6)" + }, + "ip4": { + "type": "string", + "x-primitive": "string", + "format": "ipv4", + "description": "x must be a valid IPv4 address\neg: \"192.168.0.1\"" + }, + "ip6": { + "type": "string", + "x-primitive": "string", + "format": "ipv6", + "description": "x must be a valid IPv6 address\neg: \"fe80::3\"" + }, + "uri": { + "type": "string", + "x-primitive": "string", + "format": "uri", + "description": "x must be a valid absolute URI (via RFC 3986)" + }, + "absuri": { + "type": "string", + "x-primitive": "string", + "format": "uri-reference", + "description": "x must be a valid URI reference (either absolute or relative)" + }, + "uuid": { + "type": "string", + "x-primitive": "string", + "format": "uuid", + "description": "x must be a valid UUID (via RFC 4122)" + }, + "httpheadname": { + "type": "string", + "x-primitive": "string", + "pattern": "^:?[0-9a-zA-Z!#$%&'*+-.^_|~`]+$", + "description": "x must conform to a well known regex for HTTP header names (via RFC 7230)" + }, + "httpheadvalue": { + "type": "string", + "x-primitive": "string", + "pattern": "^[^\u0000-\b\n-\u001f]*$", + "description": "x must conform to a well known regex for HTTP header values (via RFC 7230)" + } + } + }, + "b": { + "title": "ByteExamples", + "type": "object", + "required": [ + "ba", + "bb", + "bc", + "bd", + "be", + "bf", + "bg", + "bh", + "bi", + "bj", + "bk", + "bm", + "bn", + "bo" + ], + "properties": { + "ba": { + "type": "string", + "x-primitive": "bytes", + "const": "foo", + "description": "x must be set to \"foo\" (\"\\x66\\x6f\\x6f\")" + }, + "bb": { + "type": "string", + "x-primitive": "bytes", + "const": "f09028bc", + "description": "x must be set to \"\\xf0\\x90\\x28\\xbc\"" + }, + "bc": { + "type": "string", + "x-primitive": "bytes", + "minLength": 3, + "maxLength": 3, + "description": "x must be exactly 3 bytes" + }, + "bd": { + "type": "string", + "x-primitive": "bytes", + "minLength": 3, + "description": "x must be at least 3 bytes long" + }, + "be": { + "type": "string", + "x-primitive": "bytes", + "minLength": 5, + "maxLength": 10, + "description": "x must be between 5 and 10 bytes, inclusive" + }, + "bf": { + "type": "string", + "x-primitive": "bytes", + "pattern": "^[00-7F]+$", + "description": "x must be a non-empty, ASCII byte sequence" + }, + "bg": { + "type": "string", + "x-primitive": "bytes", + "pattern": "^99.*", + "description": "x must begin with \"\\x99\"" + }, + "bh": { + "type": "string", + "x-primitive": "bytes", + "pattern": ".*buz7a$", + "description": "x must end with \"buz\\x7a\"" + }, + "bi": { + "type": "string", + "x-primitive": "bytes", + "pattern": ".*baz.*", + "description": "x must contain \"baz\" anywhere inside it" + }, + "bj": { + "type": "string", + "x-primitive": "bytes", + "oneOf": [ + { + "const": "foo", + "x-parser-schema-id": "" + }, + { + "const": "bar", + "x-parser-schema-id": "" + }, + { + "const": "baz", + "x-parser-schema-id": "" + } + ], + "description": "x must be either \"foo\", \"bar\", or \"baz\"" + }, + "bk": { + "type": "string", + "x-primitive": "bytes", + "not": { + "oneOf": [ + { + "const": "fizz", + "x-parser-schema-id": "" + }, + { + "const": "buzz", + "x-parser-schema-id": "" + } + ] + }, + "description": "x cannot be \"fizz\" nor \"buzz\"" + }, + "bl": { + "type": "string", + "x-primitive": "bytes", + "oneOf": [ + { + "const": "foo", + "x-parser-schema-id": "" + }, + { + "const": "bar", + "x-parser-schema-id": "" + }, + { + "const": "baz", + "x-parser-schema-id": "" + } + ] + }, + "bm": { + "type": "string", + "x-primitive": "bytes", + "anyOf": [ + { + "format": "ipv4", + "x-parser-schema-id": "" + }, + { + "format": "ipv6", + "x-parser-schema-id": "" + } + ], + "description": "x must be a valid IP address (either v4 or v6) in byte format" + }, + "bn": { + "type": "string", + "x-primitive": "bytes", + "format": "ipv4", + "description": "x must be a valid IPv4 address in byte format\neg: \"\\xC0\\xA8\\x00\\x01\"" + }, + "bo": { + "type": "string", + "x-primitive": "bytes", + "format": "ipv6", + "description": "x must be a valid IPv6 address in byte format\neg: \"\\x20\\x01\\x0D\\xB8\\x85\\xA3\\x00\\x00\\x00\\x00\\x8A\\x2E\\x03\\x70\\x73\\x34\"" + } + } + }, + "o": { + "title": "ObjectExamples", + "type": "object", + "properties": {}, + "description": "Dont have an idea how to represent those in JsonSchema" + }, + "r": { + "title": "RepeatedExamples", + "type": "object", + "required": [ + "ra", + "rb", + "rc", + "rd", + "re", + "rf", + "rk", + "rl" + ], + "properties": { + "ra": { + "type": "array", + "items": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "x-primitive": "int32", + "description": "x must contain at least 3 elements" + }, + "description": "x must contain at least 3 elements", + "minItems": 3 + }, + "rb": { + "type": "array", + "items": { + "title": "Person", + "type": "object", + "required": [ + "id", + "email", + "name", + "home" + ], + "properties": { + "id": { + "type": "integer", + "maximum": 9007199254740991, + "x-primitive": "uint64", + "exclusiveMinimum": 999 + }, + "email": { + "type": "string", + "x-primitive": "string", + "format": "email" + }, + "name": { + "type": "string", + "x-primitive": "string", + "pattern": "^[A-Za-z]+( [A-Za-z]+)*$", + "maxLength": 256 + }, + "home": { + "title": "Location", + "type": "object", + "required": [ + "lat", + "lng" + ], + "properties": { + "lat": { + "type": "number", + "x-primitive": "double", + "minimum": -90, + "maximum": 90 + }, + "lng": { + "type": "number", + "x-primitive": "double", + "minimum": -180, + "maximum": 180 + } + } + } + }, + "description": "x must contain between 5 and 10 Persons, inclusive" + }, + "description": "x must contain between 5 and 10 Persons, inclusive", + "minItems": 5, + "maxItems": 10 + }, + "rc": { + "type": "array", + "items": { + "type": "number", + "x-primitive": "double", + "description": "x must contain exactly 7 elements" + }, + "description": "x must contain exactly 7 elements", + "minItems": 7, + "maxItems": 7 + }, + "rd": { + "type": "array", + "items": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "x-primitive": "int64", + "description": "x must contain unique int64 values" + }, + "description": "x must contain unique int64 values", + "uniqueItems": true + }, + "re": { + "type": "array", + "items": { + "type": "number", + "x-primitive": "float", + "description": "x must contain positive float values", + "exclusiveMinimum": 0 + }, + "description": "x must contain positive float values" + }, + "rf": { + "type": "array", + "items": { + "title": "Person", + "type": "object", + "required": [ + "id", + "email", + "name", + "home" + ], + "properties": { + "id": { + "type": "integer", + "maximum": 9007199254740991, + "x-primitive": "uint64", + "exclusiveMinimum": 999 + }, + "email": { + "type": "string", + "x-primitive": "string", + "format": "email" + }, + "name": { + "type": "string", + "x-primitive": "string", + "pattern": "^[A-Za-z]+( [A-Za-z]+)*$", + "maxLength": 256 + }, + "home": { + "title": "Location", + "type": "object", + "required": [ + "lat", + "lng" + ], + "properties": { + "lat": { + "type": "number", + "x-primitive": "double", + "minimum": -90, + "maximum": 90 + }, + "lng": { + "type": "number", + "x-primitive": "double", + "minimum": -180, + "maximum": 180 + } + } + } + }, + "description": "x must contain Persons but don't validate them" + }, + "description": "x must contain Persons but don't validate them" + }, + "rk": { + "type": "array", + "items": { + "type": "number", + "x-primitive": "float", + "description": "x must contain positive float values", + "exclusiveMinimum": 0 + }, + "description": "x must contain positive float values" + }, + "rl": { + "type": "array", + "items": { + "title": "Person", + "type": "object", + "required": [ + "id", + "email", + "name", + "home" + ], + "properties": { + "id": { + "type": "integer", + "maximum": 9007199254740991, + "x-primitive": "uint64", + "exclusiveMinimum": 999 + }, + "email": { + "type": "string", + "x-primitive": "string", + "format": "email" + }, + "name": { + "type": "string", + "x-primitive": "string", + "pattern": "^[A-Za-z]+( [A-Za-z]+)*$", + "maxLength": 256 + }, + "home": { + "title": "Location", + "type": "object", + "required": [ + "lat", + "lng" + ], + "properties": { + "lat": { + "type": "number", + "x-primitive": "double", + "minimum": -90, + "maximum": 90 + }, + "lng": { + "type": "number", + "x-primitive": "double", + "minimum": -180, + "maximum": 180 + } + } + } + }, + "description": "x must contain Persons but don't validate them" + }, + "description": "x must contain Persons but don't validate them" + } + } + }, + "m": { + "title": "MapExamples", + "type": "object", + "properties": {}, + "description": "Dont have an idea how to represent those in JsonSchema" + }, + "g": { + "title": "GoogleExamples", + "type": "object", + "required": [ + "ga", + "gb", + "gd", + "ge", + "gj", + "gl", + "gm", + "gp", + "gq" + ], + "properties": { + "ga": { + "title": "Int32Value", + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "x-primitive": "int32" + } + }, + "exclusiveMinimum": 3, + "description": "if it is set, x must be greater than 3" + }, + "gb": { + "title": "Duration", + "type": "object", + "required": [ + "seconds", + "nanos" + ], + "properties": { + "seconds": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "x-primitive": "int64" + }, + "nanos": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "x-primitive": "int32" + } + }, + "description": "x cannot be unset" + }, + "gd": { + "title": "Duration", + "type": "object", + "required": [ + "seconds", + "nanos" + ], + "properties": { + "seconds": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "x-primitive": "int64" + }, + "nanos": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "x-primitive": "int32" + } + }, + "description": "x must be less than 10s" + }, + "ge": { + "title": "Duration", + "type": "object", + "required": [ + "seconds", + "nanos" + ], + "properties": { + "seconds": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "x-primitive": "int64" + }, + "nanos": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "x-primitive": "int32" + } + }, + "description": "x must be greater than or equal to 20ns" + }, + "gj": { + "title": "Timestamp", + "type": "object", + "required": [ + "seconds", + "nanos" + ], + "properties": { + "seconds": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "x-primitive": "int64" + }, + "nanos": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "x-primitive": "int32" + } + }, + "description": "x cannot be unset" + }, + "gl": { + "title": "Timestamp", + "type": "object", + "required": [ + "seconds", + "nanos" + ], + "properties": { + "seconds": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "x-primitive": "int64" + }, + "nanos": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "x-primitive": "int32" + } + }, + "description": "x must be less than the Unix Epoch" + }, + "gm": { + "title": "Timestamp", + "type": "object", + "required": [ + "seconds", + "nanos" + ], + "properties": { + "seconds": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "x-primitive": "int64" + }, + "nanos": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "x-primitive": "int32" + } + }, + "description": "x must be greater than or equal to 2009/11/10T23:00:00Z" + }, + "gp": { + "title": "Timestamp", + "type": "object", + "required": [ + "seconds", + "nanos" + ], + "properties": { + "seconds": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "x-primitive": "int64" + }, + "nanos": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "x-primitive": "int32" + } + }, + "description": "x must be less than the current timestamp" + }, + "gq": { + "title": "Timestamp", + "type": "object", + "required": [ + "seconds", + "nanos" + ], + "properties": { + "seconds": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "x-primitive": "int64" + }, + "nanos": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "x-primitive": "int32" + } + }, + "description": "x must be within ±1s of the current time" + } + } + } + }, + "description": "Root" + } + } + } + } +} \ No newline at end of file diff --git a/test/documents/protoc-gen-validate.yaml b/test/documents/protoc-gen-validate.yaml new file mode 100644 index 0000000..5cffb90 --- /dev/null +++ b/test/documents/protoc-gen-validate.yaml @@ -0,0 +1,425 @@ +asyncapi: 2.0.0 +info: + title: Example using ProtoBuff and https://github.com/bufbuild/protoc-gen-validate/blob/main/validate/validate.proto + version: '1.0.0' +channels: + mychannel: + publish: + message: + $ref: '#/components/messages/testMessage' + +components: + messages: + testMessage: + schemaFormat: 'application/vnd.google.protobuf;version=3' + payload: | + syntax = "proto3"; + + package events; + + import "google/type/date.proto"; + import "google/type/datetime.proto"; + import "google/type/decimal.proto"; + import "google/protobuf/duration.proto"; + import "google/protobuf/timestamp.proto"; + import "google/protobuf/wrappers.proto"; + + import "validate/validate.proto"; + + enum State { + INACTIVE = 0; + PENDING = 1; + ACTIVE = 2; + } + + message Person { + uint64 id = 1 [(validate.rules).uint64.gt = 999]; + + string email = 2 [(validate.rules).string.email = true]; + + string name = 3 [(validate.rules).string = { + pattern: "^[A-Za-z]+( [A-Za-z]+)*$", + max_bytes: 256, + }]; + + Location home = 4 [(validate.rules).message.required = true]; + + message Location { + double lat = 1 [(validate.rules).double = {gte: -90, lte: 90}]; + double lng = 2 [(validate.rules).double = {gte: -180, lte: 180}]; + } + } + + message NumericExamples { + // x must equal 1.23 exactly + float na = 1 [(validate.rules).float.const = 1.23]; + + + // x must be less than 10 + int32 nb = 2 [(validate.rules).int32.lt = 10]; + + // x must be greater than or equal to 20 + uint64 nc = 3 [(validate.rules).uint64.gte = 20]; + + // x must be in the range [30, 40) + fixed32 nd = 4 [(validate.rules).fixed32 = {gte:30, lt: 40}]; + + + // x must be outside the range [30, 40) + double ne = 5 [(validate.rules).double = {lt:30, gte:40}]; + + + // x must be outside the range [30, 40) + double nf = 6 [(validate.rules).double = {lt:30, gte:40}]; + + + // x must be either 1, 2, or 3 + uint32 ng = 7 [(validate.rules).uint32 = {in: [1,2,3]}]; + + // x cannot be 0 nor 0.99 + float nh = 8 [(validate.rules).float = {not_in: [0, 0.99]}]; + + // x must but greater or equal 200 and being optional + uint32 ni = 9 [(validate.rules).uint32 = {ignore_empty: true, gte: 200}]; + + + // x must be set to true + bool nk = 10 [(validate.rules).bool.const = true]; + + // x cannot be set to true + bool nl = 111 [(validate.rules).bool.const = false]; + } + + message StringExamples { + // x must be set to "foo" + string sa = 12 [(validate.rules).string.const = "foo"]; + + + // x must be exactly 5 characters long + string sb = 13 [(validate.rules).string.len = 5]; + + // x must be at least 3 characters long + string sc = 14 [(validate.rules).string.min_len = 3]; + + // x must be between 5 and 10 characters, inclusive + string sd = 15 [(validate.rules).string = {min_len: 5, max_len: 10}]; + + + // x must be at most 15 bytes long + string se = 16 [(validate.rules).string.max_bytes = 15]; + + // x must be between 128 and 1024 bytes long + string sf = 17 [(validate.rules).string = {min_bytes: 128, max_bytes: 1024}]; + + + // x must be a non-empty, case-insensitive hexadecimal string + string sg = 18 [(validate.rules).string.pattern = "(?i)^[0-9a-f]+$"]; + + + // x must begin with "foo" + string sh = 19 [(validate.rules).string.prefix = "foo"]; + + // x must end with "bar" + string si = 20 [(validate.rules).string.suffix = "bar"]; + + // x must contain "baz" anywhere inside it + string sj = 21 [(validate.rules).string.contains = "baz"]; + + // x cannot contain "baz" anywhere inside it + string sk = 22 [(validate.rules).string.not_contains = "baz"]; + + // x must begin with "fizz" and end with "buzz" + string sl = 23 [(validate.rules).string = {prefix: "fizz", suffix: "buzz"}]; + + // x must end with ".proto" and be less than 64 characters + string sm = 24 [(validate.rules).string = {suffix: ".proto", max_len:64}]; + + + // x must be either "foo", "bar", or "baz" + string sn = 25 [(validate.rules).string = {in: ["foo", "bar", "baz"]}]; + + // x cannot be "fizz" nor "buzz" + string so = 26 [(validate.rules).string = {not_in: ["fizz", "buzz"]}]; + + // x must be a valid email address (via RFC 5322) + string sp = 27 [(validate.rules).string.email = true]; + + // x must be a valid address (IP or Hostname). + string sq = 28 [(validate.rules).string.address = true]; + + // x must be a valid hostname (via RFC 1034) + string sr = 29 [(validate.rules).string.hostname = true]; + + // x must be a valid IP address (either v4 or v6) + string ss = 30 [(validate.rules).string.ip = true]; + + // x must be a valid IPv4 address + // eg: "192.168.0.1" + string ip4 = 31 [(validate.rules).string.ipv4 = true]; + + // x must be a valid IPv6 address + // eg: "fe80::3" + string ip6 = 32 [(validate.rules).string.ipv6 = true]; + + // x must be a valid absolute URI (via RFC 3986) + string uri = 33 [(validate.rules).string.uri = true]; + + // x must be a valid URI reference (either absolute or relative) + string absuri = 34 [(validate.rules).string.uri_ref = true]; + + // x must be a valid UUID (via RFC 4122) + string uuid = 35 [(validate.rules).string.uuid = true]; + + // x must conform to a well known regex for HTTP header names (via RFC 7230) + string httpheadname = 36 [(validate.rules).string.well_known_regex = HTTP_HEADER_NAME]; + + // x must conform to a well known regex for HTTP header values (via RFC 7230) + string httpheadvalue = 37 [(validate.rules).string.well_known_regex = HTTP_HEADER_VALUE]; + + // To complex for protobufjs parser + // x must conform to a well known regex for headers, disallowing \r\n\0 characters. + // string httpheadvaluestrict = 38 [(validate.rules).string {well_known_regex: HTTP_HEADER_VALUE, strict: false}]; + } + + message ByteExamples { + // x must be set to "foo" ("\x66\x6f\x6f") + bytes ba = 39 [(validate.rules).bytes.const = "foo"]; + + // x must be set to "\xf0\x90\x28\xbc" + bytes bb = 40 [(validate.rules).bytes.const = "\xf0\x90\x28\xbc"]; + + + // x must be exactly 3 bytes + bytes bc = 41 [(validate.rules).bytes.len = 3]; + + // x must be at least 3 bytes long + bytes bd = 42 [(validate.rules).bytes.min_len = 3]; + + // x must be between 5 and 10 bytes, inclusive + bytes be = 43 [(validate.rules).bytes = {min_len: 5, max_len: 10}]; + + + // x must be a non-empty, ASCII byte sequence + bytes bf = 44 [(validate.rules).bytes.pattern = "^[\x00-\x7F]+$"]; + + + + // x must begin with "\x99" + bytes bg = 45 [(validate.rules).bytes.prefix = "\x99"]; + + // x must end with "buz\x7a" + bytes bh = 46 [(validate.rules).bytes.suffix = "buz\x7a"]; + + // x must contain "baz" anywhere inside it + bytes bi = 47 [(validate.rules).bytes.contains = "baz"]; + + + // x must be either "foo", "bar", or "baz" + bytes bj = 48 [(validate.rules).bytes = {in: ["foo", "bar", "baz"]}]; + + // x cannot be "fizz" nor "buzz" + bytes bk = 49 [(validate.rules).bytes = {not_in: ["fizz", "buzz"]}]; + + + bytes bl = 50 [(validate.rules).bytes = {ignore_empty: true, in: ["foo", "bar", "baz"]}]; + + + // x must be a valid IP address (either v4 or v6) in byte format + bytes bm = 51 [(validate.rules).bytes.ip = true]; + + // x must be a valid IPv4 address in byte format + // eg: "\xC0\xA8\x00\x01" + bytes bn = 52 [(validate.rules).bytes.ipv4 = true]; + + // x must be a valid IPv6 address in byte format + // eg: "\x20\x01\x0D\xB8\x85\xA3\x00\x00\x00\x00\x8A\x2E\x03\x70\x73\x34" + bytes bo = 53 [(validate.rules).bytes.ipv6 = true]; + } + + // Dont have an idea how to represent those in JsonSchema + message ObjectExamples { + // x must be set to ACTIVE (2) + //State oa = 54 [(validate.rules).enum.const = 2]; + + + // x can only be INACTIVE, PENDING, or ACTIVE + //State ob = 55 [(validate.rules).enum.defined_only = true]; + + + // x must be either INACTIVE (0) or ACTIVE (2) + //State oc = 56 [(validate.rules).enum = {in: [0,2]}]; + + // x cannot be PENDING (1) + //State od = 57 [(validate.rules).enum = {not_in: [1]}]; + + + // The fields on Person x will not be validated. + //Person oe = 58 [(validate.rules).message.skip = true]; + + + // x cannot be unset + //Person of = 59 [(validate.rules).message.required = true]; + + // x cannot be unset, but the validations on x will not be performed + //Person og = 60 [(validate.rules).message = {required: true, skip: true}]; + } + + message RepeatedExamples { + // x must contain at least 3 elements + repeated int32 ra = 61 [(validate.rules).repeated.min_items = 3]; + + // x must contain between 5 and 10 Persons, inclusive + repeated Person rb = 62 [(validate.rules).repeated = {min_items: 5, max_items: 10}]; + + // x must contain exactly 7 elements + repeated double rc = 63 [(validate.rules).repeated = {min_items: 7, max_items: 7}]; + + + // x must contain unique int64 values + repeated int64 rd = 64 [(validate.rules).repeated.unique = true]; + + + // x must contain positive float values + repeated float re = 65 [(validate.rules).repeated.items.float.gt = 0]; + + // x must contain Persons but don't validate them + repeated Person rf = 66 [(validate.rules).repeated.items.message.skip = true]; + + // To complex for protobufjs parser + // repeated int64 rg = 67 [(validate.rules).repeated = {ignore_empty: true, items: {int64: {gt: 200}}}]; + + // x must contain positive float values + repeated float rk = 71 [(validate.rules).repeated.items.float.gt = 0]; + + // x must contain Persons but don't validate them + repeated Person rl = 72 [(validate.rules).repeated.items.message.skip = true]; + } + + // Dont have an idea how to represent those in JsonSchema + message MapExamples { + // x must contain at least 3 KV pairs + // map rh = 68 [(validate.rules).map.min_pairs = 3]; + + // x must contain between 5 and 10 KV pairs + //map ri = 69 [(validate.rules).map = {min_pairs: 5, max_pairs: 10}]; + + // x must contain exactly 7 KV pairs + // map rj = 70 [(validate.rules).map = {min_pairs: 7, max_pairs: 7}]; + + // x's keys must all be negative + //map rm = 73 [(validate.rules).map.keys.sint32.lt = 0]; + + + // x must contain strings of at least 3 characters + //map rn = 74 [(validate.rules).map.values.string.min_len = 3]; + + // x must contain Persons but doesn't validate them + //map ro = 75 [(validate.rules).map.values.message.skip = true]; + + + //map rp = 76 [(validate.rules).map = {ignore_empty: true, values: {string: {min_len: 3}}}]; + } + + message GoogleExamples { + // if it is set, x must be greater than 3 + google.protobuf.Int32Value ga = 77 [(validate.rules).int32.gt = 3]; + + + // x cannot be unset + google.protobuf.Duration gb = 78 [(validate.rules).duration.required = true]; + + + // To complex for protobufjs parser + // x must equal 1.5s exactly + // google.protobuf.Duration gc = 79 [(validate.rules).duration.const = { + // seconds: 1, + // nanos: 500000000 + // }]; + + + // x must be less than 10s + google.protobuf.Duration gd = 80 [(validate.rules).duration.lt.seconds = 10]; + + // x must be greater than or equal to 20ns + google.protobuf.Duration ge = 81 [(validate.rules).duration.gte.nanos = 20]; + + // x must be in the range [0s, 1s) + // To complex for protobufjs parser + // google.protobuf.Duration gf = 82 [(validate.rules).duration = { + // gte: {}, + // lt: {seconds: 1} + // }]; + + + // x must be outside the range [0s, 1s) + // google.protobuf.Duration gg = 83 [(validate.rules).duration = { + // lt: {}, + // gte: {seconds: 1} + // }]; + + + // x must be either 0s or 1s + // google.protobuf.Duration gh = 84 [(validate.rules).duration = {in: [ + // {}, + // {seconds: 1} + // ]}]; + + // x cannot be 20s nor 500ns + // google.protobuf.Duration gi = 85 [(validate.rules).duration = {not_in: [ + // {seconds: 20}, + // {nanos: 500} + // ]}]; + + + // x cannot be unset + google.protobuf.Timestamp gj = 86 [(validate.rules).timestamp.required = true]; + + + // x must equal 2009/11/10T23:00:00.500Z exactly + // google.protobuf.Timestamp gk = 87 [(validate.rules).timestamp.const = { + // seconds: 63393490800, + // nanos: 500000000 + // }]; + + // x must be less than the Unix Epoch + google.protobuf.Timestamp gl = 88 [(validate.rules).timestamp.lt.seconds = 0]; + + // x must be greater than or equal to 2009/11/10T23:00:00Z + google.protobuf.Timestamp gm = 89 [(validate.rules).timestamp.gte.seconds = 63393490800]; + + // x must be in the range [epoch, 2009/11/10T23:00:00Z) + // google.protobuf.Timestamp gn = 90 [(validate.rules).timestamp = { + // gte: {}, + // lt: {seconds: 63393490800} + // }]; + + + // x must be outside the range [epoch, 2009/11/10T23:00:00Z) + // google.protobuf.Timestamp go = 91 [(validate.rules).timestamp = { + // lt: {}, + // gte: {seconds: 63393490800} + // }]; + + // x must be less than the current timestamp + google.protobuf.Timestamp gp = 92 [(validate.rules).timestamp.lt_now = true]; + + // x must be within ±1s of the current time + google.protobuf.Timestamp gq = 93 [(validate.rules).timestamp.within.seconds = 1]; + + // x must be within the range (now, now+1h) + // google.protobuf.Timestamp gr = 94 [(validate.rules).timestamp = { + // gt_now: true, + // within: {seconds: 3600} + // }]; + } + + // Root + message RootNode { + NumericExamples n = 1; + StringExamples s = 2; + ByteExamples b = 3; + ObjectExamples o = 4; + RepeatedExamples r = 5; + MapExamples m = 6; + GoogleExamples g = 7; + } diff --git a/test/parser.spec.ts b/test/parser.spec.ts index 22361c0..77e8e0f 100644 --- a/test/parser.spec.ts +++ b/test/parser.spec.ts @@ -182,6 +182,23 @@ describe('parse()', function () { ); }); + it('protoc-gen-validate should added as correct validations', async function () { + const document = await parseSpec('./documents/protoc-gen-validate.yaml'); + + if (UPDATE_RESULTS) { + writeResults( + document, + './documents/protoc-gen-validate.result.json' + ); + } + + expect( + stripParserExtraInfos(document?.json()) + ).toEqual( + readResultFile('./documents/protoc-gen-validate.result.json') + ); + }); + function filterDiagnostics(diagnostics: Diagnostic[], code: string) { return diagnostics.filter((d) => d.code === code); } From 43e8c6ae5b16b1fad7cc643016a875247c4b11b1 Mon Sep 17 00:00:00 2001 From: Heiko Henning Date: Wed, 18 Dec 2024 08:30:53 +0100 Subject: [PATCH 2/2] Fix sonar issue --- src/protoj2jsonSchema.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/protoj2jsonSchema.ts b/src/protoj2jsonSchema.ts index 9b29d0a..f8cb8c1 100644 --- a/src/protoj2jsonSchema.ts +++ b/src/protoj2jsonSchema.ts @@ -330,6 +330,7 @@ class Proto2JsonSchema { return obj; } + // eslint-disable-next-line sonarjs/cognitive-complexity private compileField(field: protobuf.Field, parentItem: protobuf.Type, stack: string[]): v3.AsyncAPISchemaDefinition { let obj: v3.AsyncAPISchemaDefinition = {};