diff --git a/Makefile b/Makefile index f3f5cbad0..7215e91af 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ build: validate/validate.pb.go .PHONY: bazel bazel: # generate the PGV plugin with Bazel - bazel build //tests/... + bazel build //tests/... --incompatible_new_actions_api=false .PHONY: build_generation_tests build_generation_tests: @@ -72,7 +72,7 @@ harness: testcases tests/harness/go/harness.pb.go tests/harness/gogo/harness.pb. .PHONY: bazel-harness bazel-harness: # runs the test harness via bazel - bazel run //tests/harness/executor:executor -- -go -gogo -cc -java + bazel run //tests/harness/executor:executor --incompatible_new_actions_api=false -- -go -gogo -cc -java -python .PHONY: testcases testcases: gogofast diff --git a/WORKSPACE b/WORKSPACE index 08d08ab24..5dcae3d50 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -73,3 +73,31 @@ maven_jar( name = "org_apache_commons_validator", artifact = "commons-validator:commons-validator:1.6" ) + +load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") + +git_repository( + name = "io_bazel_rules_python", + remote = "https://github.com/bazelbuild/rules_python.git", + commit = "fdbb17a4118a1728d19e638a5291b4c4266ea5b8", + shallow_since = "1557865590 -0400", +) + +# Only needed for PIP support: +load("@io_bazel_rules_python//python:pip.bzl", "pip_repositories") + +pip_repositories() + +load("@io_bazel_rules_python//python:pip.bzl", "pip_import") + +# This rule translates the specified requirements.txt into +# @my_deps//:requirements.bzl, which itself exposes a pip_install method. +pip_import( + name = "my_deps", + requirements = "//:requirements.txt", +) + +# Load the pip_install symbol for my_deps, and create the dependencies' +# repositories. +load("@my_deps//:requirements.bzl", "pip_install") +pip_install() diff --git a/bazel/pgv_proto_library.bzl b/bazel/pgv_proto_library.bzl index e1619a3ff..8787228ca 100644 --- a/bazel/pgv_proto_library.bzl +++ b/bazel/pgv_proto_library.bzl @@ -1,6 +1,6 @@ load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") load("@io_bazel_rules_go//proto:compiler.bzl", "go_proto_compiler") -load(":protobuf.bzl", "cc_proto_gen_validate", "java_proto_gen_validate") +load(":protobuf.bzl", "cc_proto_gen_validate", "java_proto_gen_validate", "python_proto_gen_validate") def pgv_go_proto_library(name, proto = None, deps = [], **kwargs): go_proto_compiler( @@ -74,4 +74,31 @@ def pgv_cc_proto_library( **kargs ) +def pgv_python_proto_library( + name, + deps = [], + python_deps = [], + **kwargs): + """Bazel rule to create a Python protobuf validation library from proto sources files. + Args: + name: the name of the pgv_python_proto_library + deps: proto_library rules that contain the necessary .proto files + python_deps: Python dependencies of the protos being compiled. Likely py_proto_library or pgv_python_proto_library. + """ + + python_proto_gen_validate( + name = name + "_validate", + deps = deps, + ) + + native.py_library( + name = name, + srcs = [name + "_validate"], + deps = python_deps + [ + "@com_envoyproxy_protoc_gen_validate//validate:validate_py", + ], + **kwargs + ) + + pgv_java_proto_library = java_proto_gen_validate diff --git a/bazel/protobuf.bzl b/bazel/protobuf.bzl index c77e3b387..f58130df7 100644 --- a/bazel/protobuf.bzl +++ b/bazel/protobuf.bzl @@ -69,6 +69,38 @@ def _protoc_gen_validate_cc_impl(ctx): package_command = "true", ) +def _protoc_python_output_files(proto_file_sources): + python_srcs = [] + + for p in proto_file_sources: + basename = p.basename[:-len(".proto")] + + python_srcs.append(basename + "_pb2.py") + return python_srcs + +def _protoc_gen_validate_python_impl(ctx): + """Generate Python protos using protoc-gen-validate plugin""" + protos = _proto_sources(ctx) + + python_files = _protoc_python_output_files(protos) + out_files = [ctx.actions.declare_file(out) for out in python_files] + + dir_out = _output_dir(ctx) + + args = [ + "--python_out=" + dir_out, + ] + + return _protoc_gen_validate_impl( + ctx = ctx, + lang = "python", + protos = protos, + out_files = out_files, + protoc_args = args, + package_command = "true", + ) + + def _protoc_gen_validate_impl(ctx, lang, protos, out_files, protoc_args, package_command): protoc_args.append("--plugin=protoc-gen-validate=" + ctx.executable._plugin.path) @@ -241,3 +273,26 @@ java_proto_gen_validate = rule( }, implementation = _java_proto_gen_validate_impl, ) + +python_proto_gen_validate = rule( + attrs = { + "deps": attr.label_list( + mandatory = True, + providers = ["proto"], + ), + "_protoc": attr.label( + cfg = "host", + default = Label("@com_google_protobuf//:protoc"), + executable = True, + allow_single_file = True, + ), + "_plugin": attr.label( + cfg = "host", + default = Label("@com_envoyproxy_protoc_gen_validate//:protoc-gen-validate"), + allow_files = True, + executable = True, + ), + }, + output_to_genfiles = True, + implementation = _protoc_gen_validate_python_impl, +) diff --git a/java/pgv-java-validation/src/main/java/io/envoyproxy/pgv/validation/JavaHarness.java b/java/pgv-java-validation/src/main/java/io/envoyproxy/pgv/validation/JavaHarness.java index 52149a8b7..48953e6fd 100644 --- a/java/pgv-java-validation/src/main/java/io/envoyproxy/pgv/validation/JavaHarness.java +++ b/java/pgv-java-validation/src/main/java/io/envoyproxy/pgv/validation/JavaHarness.java @@ -23,6 +23,7 @@ public static void main(String[] args) { Bool.getDescriptor().toProto(), Bytes.getDescriptor().toProto(), Enums.getDescriptor().toProto(), + KitchenSink.getDescriptor().toProto(), Maps.getDescriptor().toProto(), Messages.getDescriptor().toProto(), Numbers.getDescriptor().toProto(), diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..f42889c8c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +ipaddress==1.0.22 +validate-email==1.3 +Jinja2==2.10.1 +MarkupSafe==1.1.1 diff --git a/rule_comparison.md b/rule_comparison.md index dd47aa598..e27887f3b 100644 --- a/rule_comparison.md +++ b/rule_comparison.md @@ -1,109 +1,109 @@ # Constraint Rule Comparison ## Global -| Constraint Rule | Go | GoGo | C++ | Java | -| ---| :---: | :---: | :---: | :---: | -| disabled |✅|✅|✅|✅| +| Constraint Rule | Go | GoGo | C++ | Java | Python | +| ---| :---: | :---: | :---: | :---: | :---: | +| disabled |✅|✅|✅|✅|✅| ## Numerics -| Constraint Rule | Go | GoGo | C++ | Java | -| ---| :---: | :---: | :---: | :---: | -| const |✅|✅|✅|✅| -| lt/lte/gt/gte |✅|✅|✅|✅| -| in/not_in |✅|✅|✅|✅| +| Constraint Rule | Go | GoGo | C++ | Java | Python | +| ---| :---: | :---: | :---: | :---: | :---: | +| const |✅|✅|✅|✅|✅| +| lt/lte/gt/gte |✅|✅|✅|✅|✅| +| in/not_in |✅|✅|✅|✅|✅| ## Bools -| Constraint Rule | Go | GoGo | C++ | Java | -| ---| :---: | :---: | :---: | :---: | -| const |✅|✅|✅|✅| +| Constraint Rule | Go | GoGo | C++ | Java | Python | +| ---| :---: | :---: | :---: | :---: | :---: | +| const |✅|✅|✅|✅|✅| ## Strings -| Constraint Rule | Go | GoGo | C++ | Java | -| ---| :---: | :---: | :---: | :---: | -| const |✅|✅|✅|✅| -| len/min\_len/max_len |✅|✅|❌|✅| -| min\_bytes/max\_bytes |✅|✅|✅|✅| -| pattern |✅|✅|❌|✅| -| prefix/suffix/contains |✅|✅|✅|✅| -| in/not_in |✅|✅|✅|✅| -| email |✅|✅|❌|✅| -| hostname |✅|✅|✅|✅| -| address |✅|✅|✅|✅| -| ip |✅|✅|✅|✅| -| ipv4 |✅|✅|✅|✅| -| ipv6 |✅|✅|✅|✅| -| uri |✅|✅|✅|✅| -| uri_ref |✅|✅|✅|✅| -| uuid |✅|✅|❌|✅| +| Constraint Rule | Go | GoGo | C++ | Java | Python | +| ---| :---: | :---: | :---: | :---: | :---: | +| const |✅|✅|✅|✅|✅| +| len/min\_len/max_len |✅|✅|❌|✅|✅| +| min\_bytes/max\_bytes |✅|✅|✅|✅|✅| +| pattern |✅|✅|❌|✅|✅| +| prefix/suffix/contains |✅|✅|✅|✅|✅| +| in/not_in |✅|✅|✅|✅|✅| +| email |✅|✅|❌|✅|✅| +| hostname |✅|✅|✅|✅|✅| +| address |✅|✅|✅|✅|✅| +| ip |✅|✅|✅|✅|✅| +| ipv4 |✅|✅|✅|✅|✅| +| ipv6 |✅|✅|✅|✅|✅| +| uri |✅|✅|✅|✅|✅| +| uri_ref |✅|✅|✅|✅|✅| +| uuid |✅|✅|❌|✅|✅| ## Bytes -| Constraint Rule | Go | GoGo | C++ | Java | -| ---| :---: | :---: | :---: | :---: | -| const |✅|✅|✅|✅| -| len/min\_len/max_len |✅|✅|✅|✅| -| pattern |✅|✅|✅|✅| -| prefix/suffix/contains |✅|✅|✅|✅| -| in/not_in |✅|✅|✅|✅| -| ip |✅|✅|❌|✅| -| ipv4 |✅|✅|❌|✅| -| ipv6 |✅|✅|❌|✅| +| Constraint Rule | Go | GoGo | C++ | Java | Python | +| ---| :---: | :---: | :---: | :---: | :---: | +| const |✅|✅|✅|✅|❌| +| len/min\_len/max_len |✅|✅|✅|✅|❌| +| pattern |✅|✅|✅|✅|❌| +| prefix/suffix/contains |✅|✅|✅|✅|❌| +| in/not_in |✅|✅|✅|✅|❌| +| ip |✅|✅|❌|✅|❌| +| ipv4 |✅|✅|❌|✅|❌| +| ipv6 |✅|✅|❌|✅|❌| ## Enums -| Constraint Rule | Go | GoGo | C++ | Java | -| ---| :---: | :---: | :---: | :---: | -| const |✅|✅|✅|✅| -| defined_only |✅|✅|✅|✅| -| in/not_in |✅|✅|✅|✅| +| Constraint Rule | Go | GoGo | C++ | Java | Python | +| ---| :---: | :---: | :---: | :---: | :---: | +| const |✅|✅|✅|✅|✅| +| defined_only |✅|✅|✅|✅|✅| +| in/not_in |✅|✅|✅|✅|✅| ## Messages -| Constraint Rule | Go | GoGo | C++ | Java | -| ---| :---: | :---: | :---: | :---: | -| skip |✅|✅|✅|✅| -| required |✅|✅|✅|✅| +| Constraint Rule | Go | GoGo | C++ | Java | Python | +| ---| :---: | :---: | :---: | :---: | :---: | +| skip |✅|✅|✅|✅|✅| +| required |✅|✅|✅|✅|✅| ## Repeated -| Constraint Rule | Go | GoGo | C++ | Java | -| ---| :---: | :---: | :---: | :---: | -| min\_items/max_items |✅|✅|✅|✅| -| unique |✅|✅|✅|✅| -| items |✅|✅|❌|✅| +| Constraint Rule | Go | GoGo | C++ | Java | Python | +| ---| :---: | :---: | :---: | :---: | :---: | +| min\_items/max_items |✅|✅|✅|✅|❌| +| unique |✅|✅|✅|✅|❌| +| items |✅|✅|❌|✅|❌| ## Maps -| Constraint Rule | Go | GoGo | C++ | Java | -| ---| :---: | :---: | :---: | :---: | -| min\_pairs/max_pairs |✅|✅|❌|✅| -| no_sparse |✅|✅|❌|❌| -| keys |✅|✅|❌|✅| -| values |✅|✅|❌|✅| +| Constraint Rule | Go | GoGo | C++ | Java | Python | +| ---| :---: | :---: | :---: | :---: | :---: | +| min\_pairs/max_pairs |✅|✅|❌|✅|❌| +| no_sparse |✅|✅|❌|❌|❌| +| keys |✅|✅|❌|✅|❌| +| values |✅|✅|❌|✅|❌| ## OneOf -| Constraint Rule | Go | GoGo | C++ | Java | -| ---| :---: | :---: | :---: | :---: | -| required |✅|✅|✅|✅| +| Constraint Rule | Go | GoGo | C++ | Java | Python | +| ---| :---: | :---: | :---: | :---: | :---: | +| required |✅|✅|✅|✅|❌| ## WKT Scalar Value Wrappers -| Constraint Rule | Go | GoGo | C++ | Java | -| ---| :---: | :---: | :---: | :---: | -| wrapper validation |✅|✅|✅|✅| +| Constraint Rule | Go | GoGo | C++ | Java | Python | +| ---| :---: | :---: | :---: | :---: | :---: | +| wrapper validation |✅|✅|✅|✅|✅| ## WKT Any -| Constraint Rule | Go | GoGo | C++ | Java | -| ---| :---: | :---: | :---: | :---: | -| required |✅|✅|✅|✅| -| in/not_in |✅|✅|✅|✅| +| Constraint Rule | Go | GoGo | C++ | Java | Python | +| ---| :---: | :---: | :---: | :---: | :---: | +| required |✅|✅|✅|✅|✅| +| in/not_in |✅|✅|✅|✅|✅| ## WKT Duration -| Constraint Rule | Go | GoGo | C++ | Java | -| ---| :---: | :---: | :---: | :---: | -| required |✅|✅|✅|✅| -| const |✅|✅|✅|✅| -| lt/lte/gt/gte |✅|✅|✅|✅| -| in/not_in |✅|✅|✅|✅| +| Constraint Rule | Go | GoGo | C++ | Java | Python | +| ---| :---: | :---: | :---: | :---: | :---: | +| required |✅|✅|✅|✅|✅| +| const |✅|✅|✅|✅|✅| +| lt/lte/gt/gte |✅|✅|✅|✅|✅| +| in/not_in |✅|✅|✅|✅|✅| ## WKT Timestamp -| Constraint Rule | Go | GoGo | C++ | Java | -| ---| :---: | :---: | :---: | :---: | -| required |✅|✅|❌|✅| -| const |✅|✅|❌|✅| -| lt/lte/gt/gte |✅|✅|❌|✅| -| lt_now/gt_now |✅|✅|❌|✅| -| within |✅|✅|❌|✅| +| Constraint Rule | Go | GoGo | C++ | Java | Python | +| ---| :---: | :---: | :---: | :---: | :---: | +| required |✅|✅|❌|✅|✅| +| const |✅|✅|❌|✅|✅| +| lt/lte/gt/gte |✅|✅|❌|✅|✅| +| lt_now/gt_now |✅|✅|❌|✅|✅| +| within |✅|✅|❌|✅|✅| diff --git a/tests/harness/BUILD b/tests/harness/BUILD index 443593c77..9edc4bf57 100644 --- a/tests/harness/BUILD +++ b/tests/harness/BUILD @@ -3,6 +3,8 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") # gazelle:exclude harness.pb.go load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") +load("@com_google_protobuf//:protobuf.bzl", "py_proto_library") + proto_library( name = "harness_proto", @@ -54,3 +56,12 @@ go_library( importpath = "github.com/envoyproxy/protoc-gen-validate/tests/harness", visibility = ["//visibility:public"], ) + +py_proto_library( + name = "python-harness-proto", + srcs = ["harness.proto"], + protoc = "@com_google_protobuf//:protoc", + default_runtime = "@com_google_protobuf//:protobuf_python", + deps = ["@com_google_protobuf//:protobuf_python"], + visibility = ["//visibility:public"], +) diff --git a/tests/harness/cases/BUILD b/tests/harness/cases/BUILD index a7c2766dd..9a7cf3ac6 100644 --- a/tests/harness/cases/BUILD +++ b/tests/harness/cases/BUILD @@ -8,6 +8,7 @@ load( "pgv_go_proto_library", "pgv_cc_proto_library", "pgv_java_proto_library", + "pgv_python_proto_library", ) proto_library( @@ -16,6 +17,7 @@ proto_library( "bool.proto", "bytes.proto", "enums.proto", + "kitchen_sink.proto", "maps.proto", "messages.proto", "numbers.proto", @@ -90,3 +92,12 @@ pgv_java_proto_library( "//tests/harness/cases/other_package:java", ], ) + +pgv_python_proto_library( + name = "python", + deps = [":cases_proto"], + visibility = ["//visibility:public"], + python_deps = [ + "//tests/harness/cases/other_package:python", + ], +) diff --git a/tests/harness/cases/kitchen_sink.proto b/tests/harness/cases/kitchen_sink.proto new file mode 100644 index 000000000..3d5da0907 --- /dev/null +++ b/tests/harness/cases/kitchen_sink.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +package tests.harness.cases; +option go_package = "cases"; + +import "validate/validate.proto"; + +import "google/protobuf/wrappers.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; +import "google/protobuf/any.proto"; + +enum ComplexTestEnum { + ComplexZero = 0; + ComplexONE = 1; + ComplexTWO = 2; +} + +message ComplexTestMsg { + string const = 1 [(validate.rules).string.const = "abcd"]; + ComplexTestMsg nested = 2; + int32 int_const = 3 [(validate.rules).int32.const = 5]; + bool bool_const = 4 [(validate.rules).bool.const = false]; + google.protobuf.FloatValue float_val = 5 [(validate.rules).float.gt = 0]; + google.protobuf.Duration dur_val = 6 [(validate.rules).duration.lt = {seconds: 17}, (validate.rules).duration.required = true]; + google.protobuf.Timestamp ts_val = 7 [(validate.rules).timestamp.gt = {seconds: 7}]; + ComplexTestMsg another = 8; + float float_const = 9 [(validate.rules).float.lt = 8]; + double double_in = 10 [(validate.rules).double = {in: [456.789, 123]}]; + ComplexTestEnum enum_const = 11 [(validate.rules).enum.const = 2]; + google.protobuf.Any any_val = 12 [(validate.rules).any = {in: ["type.googleapis.com/google.protobuf.Duration"]}]; +} + +message KitchenSinkMessage { ComplexTestMsg val = 1; } diff --git a/tests/harness/cases/other_package/BUILD b/tests/harness/cases/other_package/BUILD index 58a959b80..86d66269b 100644 --- a/tests/harness/cases/other_package/BUILD +++ b/tests/harness/cases/other_package/BUILD @@ -8,6 +8,7 @@ load( "pgv_gogo_proto_library", "pgv_cc_proto_library", "pgv_java_proto_library", + "pgv_python_proto_library", ) proto_library( @@ -77,3 +78,9 @@ pgv_java_proto_library( visibility = ["//visibility:public"], deps = [":embed_proto"], ) + +pgv_python_proto_library( + name = "python", + visibility = ["//visibility:public"], + deps = [":embed_proto"], +) diff --git a/tests/harness/executor/BUILD b/tests/harness/executor/BUILD index fac763f9a..b6888a7a6 100644 --- a/tests/harness/executor/BUILD +++ b/tests/harness/executor/BUILD @@ -40,6 +40,7 @@ go_binary( "//tests/harness/go/main:go-harness-bin", "//tests/harness/gogo/main:go-harness-bin", "//tests/harness/java:java-harness", + "//tests/harness/python:python-harness", ], }), embed = [":go_default_library"], diff --git a/tests/harness/executor/cases.go b/tests/harness/executor/cases.go index 2a91f1f7d..2c4f7d500 100644 --- a/tests/harness/executor/cases.go +++ b/tests/harness/executor/cases.go @@ -53,6 +53,7 @@ func init() { durationCases, timestampCases, anyCases, + kitchenSink, } for _, set := range sets { @@ -1302,3 +1303,11 @@ var anyCases = []TestCase{ {"any - not in - valid (empty)", &cases.AnyNotIn{}, true}, {"any - not in - invalid", &cases.AnyNotIn{Val: &any.Any{TypeUrl: "type.googleapis.com/google.protobuf.Timestamp"}}, false}, } + +var kitchenSink = []TestCase{ + {"kitchensink - field - valid", &cases.KitchenSinkMessage{Val: &cases.ComplexTestMsg{Const: "abcd", IntConst: 5, BoolConst: false, FloatVal: &wrappers.FloatValue{Value: 1}, DurVal: &duration.Duration{Seconds: 3}, TsVal: ×tamp.Timestamp{Seconds: 17}, FloatConst: 7, DoubleIn: 123, EnumConst: cases.ComplexTestEnum_ComplexTWO, AnyVal: &any.Any{TypeUrl: "type.googleapis.com/google.protobuf.Duration"}}}, true}, + {"kitchensink - valid (unset)", &cases.KitchenSinkMessage{}, true}, + {"kitchensink - field - invalid", &cases.KitchenSinkMessage{Val: &cases.ComplexTestMsg{}}, false}, + {"kitchensink - field - embedded - invalid", &cases.KitchenSinkMessage{Val: &cases.ComplexTestMsg{Another: &cases.ComplexTestMsg{}}}, false}, + {"kitchensink - field - invalid (transitive)", &cases.KitchenSinkMessage{Val: &cases.ComplexTestMsg{Const: "abcd", BoolConst: true, Nested: &cases.ComplexTestMsg{}}}, false}, +} diff --git a/tests/harness/executor/executor.go b/tests/harness/executor/executor.go index f502271b6..bb9f28616 100644 --- a/tests/harness/executor/executor.go +++ b/tests/harness/executor/executor.go @@ -20,10 +20,11 @@ func main() { gogoFlag := flag.Bool("gogo", false, "Run gogo test harness") ccFlag := flag.Bool("cc", false, "Run c++ test harness") javaFlag := flag.Bool("java", false, "Run java test harness") + pythonFlag := flag.Bool("python", false, "Run python test harness") flag.Parse() start := time.Now() - successes, failures, skips := run(*parallelism, *goFlag, *gogoFlag, *ccFlag, *javaFlag) + successes, failures, skips := run(*parallelism, *goFlag, *gogoFlag, *ccFlag, *javaFlag, *pythonFlag) log.Printf("Successes: %d | Failures: %d | Skips: %d (%v)", successes, failures, skips, time.Since(start)) @@ -33,12 +34,12 @@ func main() { } } -func run(parallelism int, goFlag bool, gogoFlag bool, ccFlag bool, javaFlag bool) (successes, failures, skips uint64) { +func run(parallelism int, goFlag bool, gogoFlag bool, ccFlag bool, javaFlag bool, pythonFlag bool) (successes, failures, skips uint64) { wg := new(sync.WaitGroup) if parallelism <= 0 { panic("Parallelism must be > 0") } - if !(goFlag || gogoFlag || ccFlag || javaFlag) { + if !(goFlag || gogoFlag || ccFlag || javaFlag || pythonFlag) { panic("At least one harness must be selected with a flag") } wg.Add(parallelism) @@ -48,7 +49,7 @@ func run(parallelism int, goFlag bool, gogoFlag bool, ccFlag bool, javaFlag bool done := make(chan struct{}) for i := 0; i < parallelism; i++ { - go Work(wg, in, out, goFlag, gogoFlag, ccFlag, javaFlag) + go Work(wg, in, out, goFlag, gogoFlag, ccFlag, javaFlag, pythonFlag) } go func() { diff --git a/tests/harness/executor/harness.go b/tests/harness/executor/harness.go index 936839216..b936faf5c 100644 --- a/tests/harness/executor/harness.go +++ b/tests/harness/executor/harness.go @@ -17,7 +17,7 @@ import ( "golang.org/x/net/context" ) -func Harnesses(goFlag bool, gogoFlag bool, ccFlag bool, javaFlag bool) []Harness { +func Harnesses(goFlag bool, gogoFlag bool, ccFlag bool, javaFlag bool, pythonFlag bool) []Harness { harnesses := make([]Harness, 0) if goFlag { harnesses = append(harnesses, InitHarness("tests/harness/go/main/go-harness")) @@ -31,6 +31,9 @@ func Harnesses(goFlag bool, gogoFlag bool, ccFlag bool, javaFlag bool) []Harness if javaFlag { harnesses = append(harnesses, InitHarness("tests/harness/java/java-harness")) } + if pythonFlag { + harnesses = append(harnesses, InitHarness("tests/harness/python/python-harness")) + } return harnesses } diff --git a/tests/harness/executor/worker.go b/tests/harness/executor/worker.go index a09a9be2a..d9c69de46 100644 --- a/tests/harness/executor/worker.go +++ b/tests/harness/executor/worker.go @@ -13,15 +13,15 @@ import ( harness "github.com/envoyproxy/protoc-gen-validate/tests/harness/go" ) -func Work(wg *sync.WaitGroup, in <-chan TestCase, out chan<- TestResult, goFlag bool, gogoFlag bool, ccFlag bool, javaFlag bool) { +func Work(wg *sync.WaitGroup, in <-chan TestCase, out chan<- TestResult, goFlag bool, gogoFlag bool, ccFlag bool, javaFlag bool, pythonFlag bool) { for tc := range in { - ok, skip := execTestCase(tc, goFlag, gogoFlag, ccFlag, javaFlag) + ok, skip := execTestCase(tc, goFlag, gogoFlag, ccFlag, javaFlag, pythonFlag) out <- TestResult{ok, skip} } wg.Done() } -func execTestCase(tc TestCase, goFlag bool, gogoFlag bool, ccFlag bool, javaFlag bool) (ok, skip bool) { +func execTestCase(tc TestCase, goFlag bool, gogoFlag bool, ccFlag bool, javaFlag bool, pythonFlag bool) (ok, skip bool) { any, err := ptypes.MarshalAny(tc.Message) if err != nil { log.Printf("unable to convert test case %q to Any - %v", tc.Name, err) @@ -37,7 +37,7 @@ func execTestCase(tc TestCase, goFlag bool, gogoFlag bool, ccFlag bool, javaFlag ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() - harnesses := Harnesses(goFlag, gogoFlag, ccFlag, javaFlag) + harnesses := Harnesses(goFlag, gogoFlag, ccFlag, javaFlag, pythonFlag) wg := new(sync.WaitGroup) wg.Add(len(harnesses)) diff --git a/tests/harness/python/BUILD b/tests/harness/python/BUILD new file mode 100644 index 000000000..936f51572 --- /dev/null +++ b/tests/harness/python/BUILD @@ -0,0 +1,18 @@ +load("@my_deps//:requirements.bzl", "requirement") +load("@io_bazel_rules_python//python:python.bzl", "py_binary") + + +py_binary( + name = "python-harness", + srcs = ["harness.py"], + deps = [ + "//tests/harness:python-harness-proto", + "//tests/harness/cases:python", + requirement("validate-email"), + requirement("ipaddress"), + requirement("Jinja2"), + requirement("MarkupSafe"),], + main = "harness.py", + visibility = ["//tests/harness:__subpackages__"], +# Python Version is set by default to PY2AND3. +) diff --git a/tests/harness/python/harness.py b/tests/harness/python/harness.py new file mode 100644 index 000000000..41449a338 --- /dev/null +++ b/tests/harness/python/harness.py @@ -0,0 +1,58 @@ +import sys +import inspect +import os + +from tests.harness.harness_pb2 import TestCase, TestResult +from tests.harness.cases.bool_pb2 import * +from tests.harness.cases.bytes_pb2 import * +from tests.harness.cases.enums_pb2 import * +from tests.harness.cases.messages_pb2 import * +from tests.harness.cases.numbers_pb2 import * +from tests.harness.cases.oneofs_pb2 import * +from tests.harness.cases.repeated_pb2 import * +from tests.harness.cases.strings_pb2 import * +from tests.harness.cases.maps_pb2 import * +from tests.harness.cases.wkt_any_pb2 import * +from tests.harness.cases.wkt_duration_pb2 import * +from tests.harness.cases.wkt_wrappers_pb2 import * +from tests.harness.cases.wkt_timestamp_pb2 import * +from tests.harness.cases.kitchen_sink_pb2 import * + +sys.path.append(os.environ['GOPATH']+'/src/github.com/envoyproxy/protoc-gen-validate/validate') +from validator import * + +class_list = [] +for k, v in inspect.getmembers(sys.modules[__name__], inspect.isclass): + if 'DESCRIPTOR' in dir(v): + class_list.append(v) + +def unpack(message): + for cls in class_list: + if message.Is(cls.DESCRIPTOR): + test_class = cls() + message.Unpack(test_class) + return test_class + +if __name__ == "__main__": + message = sys.stdin.read() + testcase = TestCase() + try: + testcase.ParseFromString(message) + except TypeError: + testcase.ParseFromString(message.encode(errors='surrogateescape')) + test_class = unpack(testcase.message) + try: + result = TestResult() + validate = generate_validate(test_class) + valid = validate(test_class) + result.Valid = True + except ValidationFailed as e: + result.Valid = False + result.Reason = str(e) + except UnimplementedException as e: + result.Error = False + result.AllowFailure = True + try: + sys.stdout.write(result.SerializeToString()) + except TypeError: + sys.stdout.write(result.SerializeToString().decode(errors='surrogateescape')) diff --git a/validate/validator.py b/validate/validator.py new file mode 100644 index 000000000..5a8e95f04 --- /dev/null +++ b/validate/validator.py @@ -0,0 +1,655 @@ +import re +from validate_email import validate_email +import ipaddress +try: + import urlparse +except ImportError: + import urllib.parse as urlparse +import uuid +import struct +from jinja2 import Template +import time + +printer = "" + +def generate_validate(proto_message): + func = file_template(proto_message) + global printer + printer += func + "\n" + exec(func) + try: + return validate + except NameError: + return locals()['validate'] + +def print_validate(proto_message): + return printer + +def has_validate(field): + if field.GetOptions() is None: + return False + for option_descriptor, option_value in field.GetOptions().ListFields(): + if option_descriptor.full_name == "validate.rules": + return True + return False + +def byte_len(s): + try: + return len(s.encode('utf-8')) + except: + return len(s) + +def _validateHostName(host): + if len(host) > 253: + return False + + s = host.rsplit(".",1)[0].lower() + + for part in s.split("."): + if len(part) == 0 or len(part) > 63: + return False + + # Host names cannot begin or end with hyphens + if s[0] == "-" or s[len(s)-1] == '-': + return False + for r in part: + if (r < 'A' or r > 'Z') and (r < 'a' or r > 'z') and (r < '0' or r > '9') and r != '-': + return False + return True + + +def _validateEmail(addr): + if '<' in addr and '>' in addr: addr = addr.split("<")[1].split(">")[0] + + if not validate_email(addr): + return False + + if len(addr) > 254: + return False + + parts = addr.split("@") + if len(parts[0]) > 64: + return False + return _validateHostName(parts[1]) + +def _has_field(message_pb, property_name): + # NOTE: As of proto3, HasField() only works for message fields, not for + # singular (non-message) fields. First try to use HasField and + # if it fails (with a ValueError) we manually consult the fields. + try: + return message_pb.HasField(property_name) + except: + all_fields = set([field.name for field in message_pb.DESCRIPTOR.fields]) + return property_name in all_fields + +def const_template(option_value, name): + const_tmpl = """{%- if str(o.string) and o.string.HasField('const') -%} + if p.{{ name }} != \"{{ o.string['const'] }}\": + raise ValidationFailed(\"{{ name }} not equal to {{ o.string['const'] }}\") + {%- elif str(o.bool) and o.bool['const'] != "" -%} + if p.{{ name }} != {{ o.bool['const'] }}: + raise ValidationFailed(\"{{ name }} not equal to {{ o.bool['const'] }}\") + {%- elif str(o.enum) and o.enum['const'] -%} + if p.{{ name }} != {{ o.enum['const'] }}: + raise ValidationFailed(\"{{ name }} not equal to {{ o.enum['const'] }}\") + {%- endif -%} + """ + return Template(const_tmpl).render(o = option_value, name = name, str = str) + +def in_template(value, name): + in_tmpl = """ + {%- if value['in'] %} + if p.{{ name }} not in {{ value['in'] }}: + raise ValidationFailed(\"{{ name }} not in {{ value['in'] }}\") + {%- endif -%} + {%- if value['not_in'] %} + if p.{{ name }} in {{ value['not_in'] }}: + raise ValidationFailed(\"{{ name }} in {{ value['not_in'] }}\") + {%- endif -%} + """ + return Template(in_tmpl).render(value = value, name = name) + +def string_template(option_value, name): + str_templ = """ + {{ const_template(o, name) -}} + {{ in_template(o.string, name) -}} + {%- set s = o.string -%} + {%- if s['len'] %} + if len(p.{{ name }}) != {{ s['len'] }}: + raise ValidationFailed(\"{{ name }} length does not equal {{ s['len'] }}\") + {%- endif -%} + {%- if s['min_len'] %} + if len(p.{{ name }}) < {{ s['min_len'] }}: + raise ValidationFailed(\"{{ name }} length is less than {{ s['min_len'] }}\") + {%- endif -%} + {%- if s['max_len'] %} + if len(p.{{ name }}) > {{ s['max_len'] }}: + raise ValidationFailed(\"{{ name }} length is more than {{ s['max_len'] }}\") + {%- endif -%} + {%- if s['len_bytes'] %} + if byte_len(p.{{ name }}) != {{ s['len_bytes'] }}: + raise ValidationFailed(\"{{ name }} length does not equal {{ s['len_bytes'] }}\") + {%- endif -%} + {%- if s['min_bytes'] %} + if byte_len(p.{{ name }}) < {{ s['min_bytes'] }}: + raise ValidationFailed(\"{{ name }} length is less than {{ s['min_bytes'] }}\") + {%- endif -%} + {%- if s['max_bytes'] %} + if byte_len(p.{{ name }}) > {{ s['max_bytes'] }}: + raise ValidationFailed(\"{{ name }} length is greater than {{ s['max_bytes'] }}\") + {%- endif -%} + {%- if s['pattern'] %} + if re.search(r\'{{ s['pattern'] }}\', p.{{ name }}) is None: + raise ValidationFailed(\"{{ name }} pattern does not match {{ s['pattern'] }}\") + {%- endif -%} + {%- if s['prefix'] %} + if not p.{{ name }}.startswith(\"{{ s['prefix'] }}\"): + raise ValidationFailed(\"{{ name }} does not start with prefix {{ s['prefix'] }}\") + {%- endif -%} + {%- if s['suffix'] %} + if not p.{{ name }}.endswith(\"{{ s['suffix'] }}\"): + raise ValidationFailed(\"{{ name }} does not end with suffix {{ s['suffix'] }}\") + {%- endif -%} + {%- if s['contains'] %} + if not \"{{ s['contains'] }}\" in p.{{ name }}: + raise ValidationFailed(\"{{ name }} does not contain {{ s['contains'] }}\") + {%- endif -%} + {%- if s['email'] %} + if not _validateEmail(p.{{ name }}): + raise ValidationFailed(\"{{ name }} is not a valid email\") + {%- endif -%} + {%- if s['hostname'] %} + if not _validateHostName(p.{{ name }}): + raise ValidationFailed(\"{{ name }} is not a valid email\") + {%- endif -%} + {%- if s['address'] %} + try: + ipaddress.ip_address(p.{{ name }}) + except ValueError: + if not _validateHostName(p.{{ name }}): + raise ValidationFailed(\"{{ name }} is not a valid address\") + {%- endif -%} + {%- if s['ip'] %} + try: + ipaddress.ip_address(p.{{ name }}) + except ValueError: + raise ValidationFailed(\"{{ name }} is not a valid ip\") + {%- endif -%} + {%- if s['ipv4'] %} + try: + ipaddress.IPv4Address(p.{{ name }}) + except ValueError: + raise ValidationFailed(\"{{ name }} is not a valid ipv4\") + {%- endif -%} + {%- if s['ipv6'] %} + try: + ipaddress.IPv6Address(p.{{ name }}) + except ValueError: + raise ValidationFailed(\"{{ name }} is not a valid ipv6\") + {%- endif %} + {%- if s['uri'] %} + url = urlparse.urlparse(p.{{ name }}) + if not all([url.scheme, url.netloc, url.path]): + raise ValidationFailed(\"{{ name }} is not a valid uri\") + {%- endif %} + {%- if s['uri_ref'] %} + url = urlparse.urlparse(p.{{ name }}) + if not all([url.scheme, url.path]) and url.fragment: + raise ValidationFailed(\"{{ name }} is not a valid uri ref\") + {%- endif -%} + {%- if s['uuid'] %} + try: + uuid.UUID(p.{{ name }}) + except ValueError: + raise ValidationFailed(\"{{ name }} is not a valid UUID\") + {%- endif -%} + """ + return Template(str_templ).render(o = option_value, name = name, const_template = const_template, in_template = in_template) + +def required_template(value, name): + req_tmpl = """{%- if value['required'] -%} + if not _has_field(p, \"{{ name }}\"): + raise ValidationFailed(\"{{ name }} is required.\") + {%- endif -%} + """ + return Template(req_tmpl).render(value = value, name = name) + +def message_template(option_value, name): + message_tmpl = """{%- if m.message %} + {{- required_template(m.message, name) }} + {%- endif -%} + {%- if m.message and m.message['skip'] %} + # Skipping validation for {{ name }} + {%- else %} + if _has_field(p, \"{{ name }}\"): + embedded = generate_validate(p.{{ name }})(p.{{ name }}) + if embedded is not None: + return embedded + {%- endif -%} + """ + return Template(message_tmpl).render(m = option_value, name = name, required_template = required_template) + +def bool_template(option_value, name): + bool_tmpl = """ + {{ const_template(o, name) -}} + """ + return Template(bool_tmpl).render(o = option_value, name = name, const_template = const_template) + +def num_template(option_value, name, num): + num_tmpl = """{%- if num.HasField('const') and str(o.float) == "" -%} + if p.{{ name }} != {{ num['const'] }}: + raise ValidationFailed(\"{{ name }} not equal to {{ num['const'] }}\") + {%- endif -%} + {%- if num.HasField('const') and str(o.float) != "" %} + if p.{{ name }} != struct.unpack(\"f\", struct.pack(\"f\", ({{ num['const'] }})))[0]: + raise ValidationFailed(\"{{ name }} not equal to {{ num['const'] }}\") + {%- endif -%} + {{ in_template(num, name) }} + {%- if num.HasField('lt') %} + {%- if num.HasField('gt') %} + {%- if num['lt'] > num['gt'] %} + if p.{{ name }} <= {{ num['gt'] }} or p.{{ name }} >= {{ num ['lt'] }}: + raise ValidationFailed(\"{{ name }} is not in range {{ num['lt'], num['gt'] }}\") + {%- else %} + if p.{{ name }} >= {{ num['lt'] }} and p.{{ name }} <= {{ num['gt'] }}: + raise ValidationFailed(\"{{ name }} is not in range {{ num['gt'], num['lt'] }}\") + {%- endif -%} + {%- elif num.HasField('gte') %} + {%- if num['lt'] > num['gte'] %} + if p.{{ name }} < {{ num['gte'] }} or p.{{ name }} >= {{ num ['lt'] }}: + raise ValidationFailed(\"{{ name }} is not in range {{ num['lt'], num['gte'] }}\") + {%- else %} + if p.{{ name }} >= {{ num['lt'] }} and p.{{ name }} < {{ num['gte'] }}: + raise ValidationFailed(\"{{ name }} is not in range {{ num['gte'], num['lt'] }}\") + {%- endif -%} + {%- else %} + if p.{{ name }} >= {{ num['lt'] }}: + raise ValidationFailed(\"{{ name }} is not lesser than {{ num['lt'] }}\") + {%- endif -%} + {%- elif num.HasField('lte') %} + {%- if num.HasField('gt') %} + {%- if num['lte'] > num['gt'] %} + if p.{{ name }} <= {{ num['gt'] }} or p.{{ name }} > {{ num ['lte'] }}: + raise ValidationFailed(\"{{ name }} is not in range {{ num['lte'], num['gt'] }}\") + {%- else %} + if p.{{ name }} > {{ num['lte'] }} and p.{{ name }} <= {{ num['gt'] }}: + raise ValidationFailed(\"{{ name }} is not in range {{ num['gt'], num['lte'] }}\") + {%- endif -%} + {%- elif num.HasField('gte') %} + {%- if num['lte'] > num['gte'] %} + if p.{{ name }} < {{ num['gte'] }} or p.{{ name }} > {{ num ['lte'] }}: + raise ValidationFailed(\"{{ name }} is not in range {{ num['lte'], num['gte'] }}\") + {%- else %} + if p.{{ name }} > {{ num['lte'] }} and p.{{ name }} < {{ num['gte'] }}: + raise ValidationFailed(\"{{ name }} is not in range {{ num['gte'], num['lte'] }}\") + {%- endif -%} + {%- else %} + if p.{{ name }} > {{ num['lte'] }}: + raise ValidationFailed(\"{{ name }} is not lesser than or equal to {{ num['lte'] }}\") + {%- endif -%} + {%- elif num.HasField('gt') %} + if p.{{ name }} <= {{ num['gt'] }}: + raise ValidationFailed(\"{{ name }} is not greater than {{ num['gt'] }}\") + {%- elif num.HasField('gte') %} + if p.{{ name }} < {{ num['gte'] }}: + raise ValidationFailed(\"{{ name }} is not greater than or equal to {{ num['gte'] }}\") + {%- endif -%} + """ + return Template(num_tmpl).render(o = option_value, name = name, num = num, in_template = in_template, str = str) + +def dur_arr(dur): + value = 0 + arr = [] + for val in dur: + value += val.seconds + value += (10**-9 * val.nanos) + arr.append(value) + value = 0 + return arr + +def dur_lit(dur): + value = dur.seconds + (10**-9 * dur.nanos) + return value + +def duration_template(option_value, name): + dur_tmpl = """ + {{- required_template(o.duration, name) }} + if _has_field(p, \"{{ name }}\"): + dur = p.{{ name }}.seconds + round((10**-9 * p.{{ name }}.nanos), 9) + {%- set dur = o.duration -%} + {%- if dur.HasField('lt') %} + lt = {{ dur_lit(dur['lt']) }} + {% endif %} + {%- if dur.HasField('lte') %} + lte = {{ dur_lit(dur['lte']) }} + {% endif %} + {%- if dur.HasField('gt') %} + gt = {{ dur_lit(dur['gt']) }} + {% endif %} + {%- if dur.HasField('gte') %} + gte = {{ dur_lit(dur['gte']) }} + {% endif %} + {%- if dur.HasField('const') %} + if dur != {{ dur_lit(dur['const']) }}: + raise ValidationFailed(\"{{ name }} is not equal to {{ dur_lit(dur['const']) }}\") + {%- endif -%} + {%- if dur['in'] %} + if dur not in {{ dur_arr(dur['in']) }}: + raise ValidationFailed(\"{{ name }} is not in {{ dur_arr(dur['in']) }}\") + {%- endif -%} + {%- if dur['not_in'] %} + if dur in {{ dur_arr(dur['not_in']) }}: + raise ValidationFailed(\"{{ name }} is not in {{ dur_arr(dur['not_in']) }}\") + {%- endif -%} + {%- if dur.HasField('lt') %} + {%- if dur.HasField('gt') %} + {%- if dur_lit(dur['lt']) > dur_lit(dur['gt']) %} + if dur <= gt or dur >= lt: + raise ValidationFailed(\"{{ name }} is not in range {{ dur_lit(dur['lt']), dur_lit(dur['gt']) }}\") + {%- else -%} + if dur >= lt and dur <= gt: + raise ValidationFailed(\"{{ name }} is not in range {{ dur_lit(dur['gt']), dur_lit(dur['lt']) }}\") + {%- endif -%} + {%- elif dur.HasField('gte') %} + {%- if dur_lit(dur['lt']) > dur_lit(dur['gte']) %} + if dur < gte or dur >= lt: + raise ValidationFailed(\"{{ name }} is not in range {{ dur_lit(dur['lt']), dur_lit(dur['gte']) }}\") + {%- else -%} + if dur >= lt and dur < gte: + raise ValidationFailed(\"{{ name }} is not in range {{ dur_lit(dur['gte']), dur_lit(dur['lt']) }}\") + {%- endif -%} + {%- else -%} + if dur >= lt: + raise ValidationFailed(\"{{ name }} is not lesser than {{ dur_lit(dur['lt']) }}\") + {%- endif -%} + {%- elif dur.HasField('lte') %} + {%- if dur.HasField('gt') %} + {%- if dur_lit(dur['lte']) > dur_lit(dur['gt']) %} + if dur <= gt or dur > lte: + raise ValidationFailed(\"{{ name }} is not in range {{ dur_lit(dur['lte']), dur_lit(dur['gt']) }}\") + {%- else -%} + if dur > lte and dur <= gt: + raise ValidationFailed(\"{{ name }} is not in range {{ dur_lit(dur['gt']), dur_lit(dur['lte']) }}\") + {%- endif -%} + {%- elif dur.HasField('gte') %} + {%- if dur_lit(dur['lte']) > dur_lit(dur['gte']) %} + if dur < gte or dur > lte: + raise ValidationFailed(\"{{ name }} is not in range {{ dur_lit(dur['lte']), dur_lit(dur['gte']) }}\") + {%- else -%} + if dur > lte and dur < gte: + raise ValidationFailed(\"{{ name }} is not in range {{ dur_lit(dur['gte']), dur_lit(dur['lte']) }}\") + {%- endif -%} + {%- else -%} + if dur > lte: + raise ValidationFailed(\"{{ name }} is not lesser than or equal to {{ dur_lit(dur['lte']) }}\") + {%- endif -%} + {%- elif dur.HasField('gt') %} + if dur <= gt: + raise ValidationFailed(\"{{ name }} is not greater than {{ dur_lit(dur['gt']) }}\") + {%- elif dur.HasField('gte') %} + if dur < gte: + raise ValidationFailed(\"{{ name }} is not greater than or equal to {{ dur_lit(dur['gte']) }}\") + {%- endif -%} + """ + return Template(dur_tmpl).render(o = option_value, name = name, required_template = required_template, dur_lit = dur_lit, dur_arr = dur_arr) + +def timestamp_template(option_value, name): + timestamp_tmpl = """ + {{- required_template(o.timestamp, name) }} + if _has_field(p, \"{{ name }}\"): + ts = p.{{ name }}.seconds + round((10**-9 * p.{{ name }}.nanos), 9) + {%- set ts = o.timestamp -%} + {%- if ts.HasField('lt') %} + lt = {{ dur_lit(ts['lt']) }} + {% endif -%} + {%- if ts.HasField('lte') %} + lte = {{ dur_lit(ts['lte']) }} + {% endif -%} + {%- if ts.HasField('gt') %} + gt = {{ dur_lit(ts['gt']) }} + {% endif -%} + {%- if ts.HasField('gte') %} + gte = {{ dur_lit(ts['gte']) }} + {% endif -%} + {%- if ts.HasField('const') %} + if ts != {{ dur_lit(ts['const']) }}: + raise ValidationFailed(\"{{ name }} is not equal to {{ dur_lit(ts['const']) }}\") + {% endif %} + {%- if ts['in'] %} + if ts not in {{ dur_arr(ts['in']) }}: + raise ValidationFailed(\"{{ name }} is not in {{ dur_arr(ts['in']) }}\") + {%- endif %} + {%- if ts['not_in'] %} + if ts in {{ dur_arr(ts['not_in']) }}: + raise ValidationFailed(\"{{ name }} is not in {{ dur_arr(ts['not_in']) }}\") + {%- endif %} + {%- if ts.HasField('lt') %} + {%- if ts.HasField('gt') %} + {%- if dur_lit(ts['lt']) > dur_lit(ts['gt']) %} + if ts <= gt or ts >= lt: + raise ValidationFailed(\"{{ name }} is not in range {{ dur_lit(ts['lt']), dur_lit(ts['gt']) }}\") + {%- else -%} + if ts >= lt and ts <= gt: + raise ValidationFailed(\"{{ name }} is not in range {{ dur_lit(ts['gt']), dur_lit(ts['lt']) }}\") + {%- endif -%} + {%- elif ts.HasField('gte') %} + {%- if dur_lit(ts['lt']) > dur_lit(ts['gte']) %} + if ts < gte or ts >= lt: + raise ValidationFailed(\"{{ name }} is not in range {{ dur_lit(ts['lt']), dur_lit(ts['gte']) }}\") + {%- else -%} + if ts >= lt and ts < gte: + raise ValidationFailed(\"{{ name }} is not in range {{ dur_lit(ts['gte']), dur_lit(ts['lt']) }}\") + {%- endif -%} + {%- else -%} + if ts >= lt: + raise ValidationFailed(\"{{ name }} is not lesser than {{ dur_lit(ts['lt']) }}\") + {%- endif -%} + {%- elif ts.HasField('lte') %} + {%- if ts.HasField('gt') %} + {%- if dur_lit(ts['lte']) > dur_lit(ts['gt']) %} + if ts <= gt or ts > lte: + raise ValidationFailed(\"{{ name }} is not in range {{ dur_lit(ts['lte']), dur_lit(ts['gt']) }}\") + {%- else -%} + if ts > lte and ts <= gt: + raise ValidationFailed(\"{{ name }} is not in range {{ dur_lit(ts['gt']), dur_lit(ts['lte']) }}\") + {%- endif -%} + {%- elif ts.HasField('gte') %} + {%- if dur_lit(ts['lte']) > dur_lit(ts['gte']) %} + if ts < gte or ts > lte: + raise ValidationFailed(\"{{ name }} is not in range {{ dur_lit(ts['lte']), dur_lit(ts['gte']) }}\") + {%- else -%} + if ts > lte and ts < gte: + raise ValidationFailed(\"{{ name }} is not in range {{ dur_lit(ts['gte']), dur_lit(ts['lte']) }}\") + {%- endif -%} + {%- else -%} + if ts > lte: + raise ValidationFailed(\"{{ name }} is not lesser than or equal to {{ dur_lit(ts['lte']) }}\") + {%- endif -%} + {%- elif ts.HasField('gt') %} + if ts <= gt: + raise ValidationFailed(\"{{ name }} is not greater than {{ dur_lit(ts['gt']) }}\") + {%- elif ts.HasField('gte') %} + if ts < gte: + raise ValidationFailed(\"{{ name }} is not greater than or equal to {{ dur_lit(ts['gte']) }}\") + {%- elif ts.HasField('lt_now') %} + now = time.time() + {%- if ts.HasField('within') %} + within = {{ dur_lit(ts['within']) }} + if ts >= now or ts >= now - within: + raise ValidationFailed(\"{{ name }} is not within range {{ dur_lit(ts['within']) }}\") + {%- else %} + if ts >= now: + raise ValidationFailed(\"{{ name }} is not lesser than now\") + {%- endif -%} + {%- elif ts.HasField('gt_now') %} + now = time.time() + {%- if ts.HasField('within') %} + within = {{ dur_lit(ts['within']) }} + if ts <= now or ts <= now + within: + raise ValidationFailed(\"{{ name }} is not within range {{ dur_lit(ts['within']) }}\") + {%- else %} + if ts <= now: + raise ValidationFailed(\"{{ name }} is not greater than now\") + {%- endif -%} + {%- elif ts.HasField('within') %} + now = time.time() + within = {{ dur_lit(ts['within']) }} + if ts >= now + within or ts <= now - within: + raise ValidationFailed(\"{{ name }} is not within range {{ dur_lit(ts['within']) }}\") + {%- endif -%} + """ + return Template(timestamp_tmpl).render(o = option_value, name = name, required_template = required_template, dur_lit = dur_lit, dur_arr = dur_arr) + +def wrapper_template(option_value, name): + wrapper_tmpl = """ + if p.HasField(\"{{ name }}\"): + {%- if str(option_value.float) %} + {{- num_template(option_value, name + ".value", option_value.float)|indent(8,True) -}} + {% endif -%} + {%- if str(option_value.double) %} + {{- num_template(option_value, name + ".value", option_value.double)|indent(8,True) -}} + {% endif -%} + {%- if str(option_value.int32) %} + {{- num_template(option_value, name + ".value", option_value.int32)|indent(8,True) -}} + {% endif -%} + {%- if str(option_value.int64) %} + {{- num_template(option_value, name + ".value", option_value.int64)|indent(8,True) -}} + {% endif -%} + {%- if str(option_value.uint32) %} + {{- num_template(option_value, name + ".value", option_value.uint32)|indent(8,True) -}} + {% endif -%} + {%- if str(option_value.uint64) %} + {{- num_template(option_value, name + ".value", option_value.uint64)|indent(8,True) -}} + {% endif -%} + {%- if str(option_value.bool) %} + {{- bool_template(option_value, name + ".value")|indent(8,True) -}} + {% endif -%} + {%- if str(option_value.string) %} + {{- string_template(option_value, name + ".value")|indent(8,True) -}} + {% endif -%} + {%- if str(option_value.message) and option_value.message['required'] %} + else: + raise ValidationFailed(\"{{ name }} is required.\") + {%- endif %} + """ + return Template(wrapper_tmpl).render(option_value = option_value, name = name, str = str, num_template = num_template, bool_template = bool_template, string_template = string_template) + +def enum_values(field): + return [x.number for x in field.enum_type.values] + +def enum_template(option_value, name, field): + enum_tmpl = """ + {{ const_template(option_value, name) -}} + {{ in_template(option_value.enum, name) -}} + {% if option_value.enum['defined_only'] %} + if p.{{ name }} not in {{ enum_values(field) }}: + raise ValidationFailed(\"{{ name }} is not defined\") + {% endif %} + """ + return Template(enum_tmpl).render(option_value = option_value, name = name, const_template = const_template, in_template = in_template, field = field, enum_values = enum_values) + + + +def any_template(option_value, name): + any_tmpl = """ + {{- required_template(o, name) }} + {% if o['in'] %} + if _has_field(p, \"{{ name }}\"): + if p.{{ name }}.type_url not in {{ o['in'] }}: + raise ValidationFailed(\"{{ name }} not in {{ o['in'] }}\") + {% endif %} + {% if o['not_in'] %} + if _has_field(p, \"{{ name }}\"): + if p.{{ name }}.type_url in {{ o['not_in'] }}: + raise ValidationFailed(\"{{ name }} in {{ o['not_in'] }}\") + {% endif %} + """ + return Template(any_tmpl).render(name = name, required_template = required_template, o = option_value.any) + +def rule_type(field): + if has_validate(field) and field.message_type is None and not field.containing_oneof: + for option_descriptor, option_value in field.GetOptions().ListFields(): + if option_descriptor.full_name == "validate.rules": + if str(option_value.string): + return string_template(option_value, field.name) + elif str(option_value.message): + return message_template(option_value, field.name) + elif str(option_value.bool): + return bool_template(option_value, field.name) + elif str(option_value.float): + return num_template(option_value, field.name, option_value.float) + elif str(option_value.double): + return num_template(option_value, field.name, option_value.double) + elif str(option_value.int32): + return num_template(option_value, field.name, option_value.int32) + elif str(option_value.int64): + return num_template(option_value, field.name, option_value.int64) + elif str(option_value.uint32): + return num_template(option_value, field.name, option_value.uint32) + elif str(option_value.uint64): + return num_template(option_value, field.name, option_value.uint64) + elif str(option_value.sint32): + return num_template(option_value, field.name, option_value.sint32) + elif str(option_value.sint64): + return num_template(option_value, field.name, option_value.sint64) + elif str(option_value.fixed32): + return num_template(option_value, field.name, option_value.fixed32) + elif str(option_value.fixed64): + return num_template(option_value, field.name, option_value.fixed64) + elif str(option_value.sfixed32): + return num_template(option_value, field.name, option_value.sfixed32) + elif str(option_value.sfixed64): + return num_template(option_value, field.name, option_value.sfixed64) + elif str(option_value.enum): + return enum_template(option_value, field.name, field) + else: + return "raise UnimplementedException()" + if field.message_type and not field.containing_oneof: + for option_descriptor, option_value in field.GetOptions().ListFields(): + if option_descriptor.full_name == "validate.rules": + if str(option_value.duration): + return duration_template(option_value, field.name) + elif str(option_value.timestamp): + return timestamp_template(option_value, field.name) + elif str(option_value.float) or str(option_value.int32) or str(option_value.int64) or \ + str(option_value.double) or str(option_value.uint32) or str(option_value.uint64) or \ + str(option_value.bool) or str(option_value.string): + return wrapper_template(option_value, field.name) + elif str(option_value.bytes): + return "raise UnimplementedException()" + elif str(option_value.message) is not "": + return message_template(option_value, field.name) + elif str(option_value.any): + return any_template(option_value, field.name) + else: + return "raise UnimplementedException()" + if field.message_type.full_name.startswith("google.protobuf"): + return "" + else: + return message_template(None, field.name) + return "" + +def file_template(proto_message): + file_tmp = """ +# Validates {{ p.DESCRIPTOR.name }} +def validate(p): + {%- for option_descriptor, option_value in p.DESCRIPTOR.GetOptions().ListFields() %} + {%- if option_descriptor.full_name == "validate.disabled" and option_value %} + return None + {%- endif -%} + {%- endfor -%} + {%- for field in p.DESCRIPTOR.fields -%} + {%- if field.label == 3 or field.containing_oneof %} + raise UnimplementedException() + {%- else %} + {{ rule_type(field) -}} + {%- endif -%} + {%- endfor %} + return None""" + return Template(file_tmp).render(rule_type = rule_type, p = proto_message) + +class UnimplementedException(Exception): + pass + +class ValidationFailed(Exception): + pass